Lecture 6 - The Memory-Mapped Register
- Introduction
- The MSP-430 peripheral bus
- The Memory-mapped register
- Enhancing the Access Mechanism
- Adding interrupts
- Finding space for memory-mapped registers
- Runnning the examples
- Conclusions
Introduction
Today we take a first step towards building customized MSP-430 based systems derived from the openmsp430. We’ll introduce a crucially important link between the world of hardware and software: the memory-mapped register.
A memory-mapped register is exactly what it says it is. It’a hardware register, typically as wide as the data word-width in the system, that can be written to and read from in software. The register is directly accessible from software because it is mapped into a memory location of the memory-map, such that writing to and reading from that memory location corresponds to writing to and reading from the hardware register.
Our objective is to design such registers in Verilog and integrate them into the openmsp430 architecture, as well as into software. We will deal with a variety of design issues - finding a proper location in the memory map; finding the currect access method from C; detecting software register access from hardware and vice versa.
The MSP-430 memory space for peripheral access extends from byte address 0 to address 0x1FF (511). The openmsp430 implementation supports an extended and configurable) peripheral range up to 32KB (0x7FFF). However, we will stick to the default peripheral address range.
The MSP-430 peripheral bus
Section 6 of the OpenMSP430 User Guide describes the busses used to realize system integration. One of these busses is the peripheral bus. It defines 5 signals used to connect peripherals.
Signal | Direction | Width | Function |
---|---|---|---|
PER_EN |
output | 1 | Active high. Indicates an active read or write access |
PER_ADDR |
output | 14 | Peripheral word address |
PER_DOUT |
input | 16 | Peripheral to MSP430 data bus |
PER_DIN |
output | 16 | MSP430 to Peripheral data bus |
PER_WE |
output | 2 | Indicates an active-write on a byte of a word |
Bus transfers on the peripheral bus always complete within one clock cycle.
That means that the address PER_ADDR
and the control signals PER_EN
and PER_WE
must be decoded by the peripheral within one clock cycle. For a read operation, data
and data must be returned to PER_DOUT
immediately (before the next clock edge).
For a write operation, data must be accepted from PER_DIN
at the next clock edge.
Due to system interconnect constraints, PER_DOUT
must be kept at 0 if no
data is being returned from the peripheral to the MSP-430.
The address delivered from the MSP-430 is always a 14-bit address, even though
only 8 bits are needed to deliver a word address within the 0-FF range.
The Memory-mapped register
We are now ready to design, integrate and access memory-mapped registers for the peripheral bus.
Verilog Design
Here is a 16-bit memory-mapped register attached to the peripheral bus,
and mapped to address 0x110
. We develop the register in Verilog.
The decoded address within the module is a word-level address, and therefore we
decode the value 0x88 = 0x110/2. Also, this particular register will only respond
to word-level operations that use the full databus. Byte-level writes, which assert
only per_we[0]
or per_we[1]
, will not affect the register.
It’s easy to see how to build variants of such a register, such as write-only registers (keep per_dout
always at 0), or registers that are aliased at multiple address locations (introduce don’t cares in the address decoder).
module myreg (
output [15:0] per_dout, // data output
input mclk, // system clock
input [13:0] per_addr, // address bus
input per_din, // data input
input per_en, // active bus cycle enable
input [1:0] per_we, // write control
input puc_rst // power-up clear reset
);
reg [15:0] r1;
wire [15:0] r1_next;
wire valid_write;
wire valid_read;
always @(posedge mclk or posedge puc_rst)
r1 <= puc_rst ? 16'b0 : r1_next;
assign valid_write = per_en & (per_addr == 14'h88) & per_we[0] & per_we[1];
assign valid_read = per_en & (per_addr == 14'h88) & ~per_we[0] & ~per_we[1];
assign r1_next = valid_write ? per_din : r1;
assign per_dout = valid_read ? r1 : 16'h0;
endmodule
When there are multple registers within a single module, each register
has to be decoded separately. For example, the following design uses
one word-level register r1
, and two byte-level registers r2
and r3
.
These registers are mapped at address 0x110, 0x112 and 0x115 respectively.
Here’s a visualization of this mapping. In practice, it is uncommon to leave such holes
in the address space. This example is only for demonstration purposes.
Word Address | Byte Address Hi | Byte Address Lo | Byte Data Hi | Byte Address Lo |
---|---|---|---|---|
0x88 | 0x111 | 0x110 | R1 (high) | R1 (low) |
0x89 | 0x113 | 0x112 | unused | R2 |
0x8A | 0x115 | 0x114 | R3 | unused |
The Verilog design is shown next. We used an alternate modeling style with two
always blocks and a case statement, which emphasizes the address decoding.
module demoreg (output [15:0] per_dout, // data output
input mclk, // system clock
input [13:0] per_addr, // address bus
input [15:0] per_din, // data input
input per_en, // active bus cycle enable
input [1:0] per_we, // write control
input puc_rst // power-up clear reset
);
reg [15:0] r1; // mapped to 0x110
reg [15:0] r1_next;
reg [ 7:0] r2; // mapped to 0x112
reg [ 7:0] r2_next;
reg [ 7:0] r3; // mapped to 0x115
reg [ 7:0] r3_next;
reg [15:0] dmux;
always @(posedge mclk or posedge puc_rst)
begin
r1 <= puc_rst ? 16'b0 : r1_next;
r2 <= puc_rst ? 8'b0 : r2_next;
r3 <= puc_rst ? 8'b0 : r3_next;
end
always @*
begin
r1_next = r1;
r2_next = r2;
r3_next = r3;
dmux = 16'h0;
if (per_en)
begin
// write
case (per_addr)
14'h88 : r1_next = ( per_we[0] & per_we[1] ) ? per_din : r1;
14'h89 : r2_next = ( per_we[0] & ~per_we[1] ) ? per_din : r2;
14'h8a : r3_next = (~per_we[0] & per_we[1] ) ? per_din : r3;
endcase
// read
case (per_addr)
14'h88 : dmux = ( ~per_we[0] & ~per_we[1] ) ? r1 : 16'h0;
14'h89 : dmux = ( ~per_we[0] & ~per_we[1] ) ? {8'h0,r2} : 16'h0;
14'h8a : dmux = ( ~per_we[0] & ~per_we[1] ) ? {r3,8'h0} : 16'h0;
endcase
end
end
assign per_dout = dmux;
endmodule
Integrating the Memory-mapped Register
To add this register to a Verilog design for the openmspde430, the following steps have to be completed.
First, the top-level interconnect must be extended with this new module. We’ll add
the Verilog file in the same directory as toplevel.v, and integrate the module
as follows. The module shares per_addr
, per_we
, per_en
and per_din
with
all other peripheral modules, but creates its own per_dout_demoreg0
. The
module also uses the system clock mclk
and system reset puc_rst
.
...
wire [15:0] per_dout_demoreg0;
demoreg demoreg0 (.per_dout(per_dout_demoreg0),
.mclk(mclk),
.per_addr(per_addr),
.per_din(per_din),
.per_en(per_en),
.per_we(per_we),
.puc_rst(puc_rst));
assign per_dout = per_dout_dio |
per_dout_tA |
per_dout_demoreg0 | // added this per_dout bus
per_dout_uart;
...
Next, the new file must be added to the list of files in the design.
This can be done through the Quartus GUI or else by manually adding
a reference to the file in the msp430de1soc.qsf
system constraints file.
set_global_assignment -name VERILOG_FILE msp430/demoreg.v
Accessing the Memory-mapped Register
Finally, we can demonstrate read/write access to the newly added memory-mapped
register using software. Here is a small demonstration program in C.
When accessing memory-mapped registers, it’s crucial to use the proper data
type in C, such that the correct peripheral bus transfer is generated.
That is, writing an unsigned char
uses a different instruction than writing
an unsigned int
. In the program, we handle this type sensitivity in the
definition of peripheral registers.
When this program is loaded onto the board, you will see the HEX display cycle between three different states that display the value stored in REG1, REG2, REG3 respectively.
#include "omsp_de1soc.h"
#define REG1 (*(volatile unsigned *) 0x110)
#define REG2 (*(volatile unsigned char *) 0x112)
#define REG3 (*(volatile unsigned char *) 0x115)
int main(void) {
de1soc_init();
while (1) {
REG1 = 0x1234;
REG2 = 0x56;
REG3 = 0x78;
de1soc_hexhi(1);
de1soc_hexlo(REG1);
long_delay(2000);
de1soc_hexhi(2);
de1soc_hexlo(REG2);
long_delay(2000);
de1soc_hexhi(3);
de1soc_hexlo(REG3);
long_delay(2000);
}
LPM0;
return 0;
}
In the assembly listing, the difference between a word-level access and a byte-level access is identified by the MSP-430 instruction. The instructions that use byte-level access have a .b
suffix, while instructions that use word-level access do not.
Here are the instructions that correspond to the assignments in the beginning of the while-loop.
0000fc30 <.L2>:
fc30: b2 40 34 12 mov #4660, &0x0110 ;#0x1234
fc34: 10 01
fc36: f2 40 56 00 mov.b #86, &0x0112 ;#0x0056
fc3a: 12 01
fc3c: f2 40 78 00 mov.b #120, &0x0115 ;#0x0078
fc40: 15 01
Enhancing the Access Mechanism
Memory-mapped registers are implemented using direct read and write instructions. This method allocates a dedicated address for each register. We discuss a few cases where the one-address-per-register method is not as convenient, and we discuss possible enhancements.
Accessing many registers
When the register address space is tight, and there are many different registers to access, then we may run out of address space. In that case, an index register may be helpful. The idea of an index register is to implement a level of indirect adressing. A group of N registers is replaced by an index register and a data-access location. To write into any of the N registers, we first write the index into the index register, and next write the data to be written into the data-access location. The data-access location is not a register by itself, but simple an address that is shared among the N registers. Indexed register access can also be used to implement a read access.
Accessing wide registers
When the register is wider than the bus width, then several addresses may have to be allocated for one logical register. For example, a 128-bit value will require 8 address locations in a 16-bit address space. Since this is a 128-bit value, there is little benefit in maintaining random-access capability for these 8 address locations. Instead, we may implement a wide shift register that automatically shifts upon each write access. Then, we use a single data access location to write into the 128-bit register, and 8 subsequent writes will result in the 128-bit value being build in the wide shift register. The same principle applies for reading a wide register. A wide shift register can be read in smaller chunks, and each read of the register shifts the chunks one position down.
Adding interrupts
Memory-mapped registers are often used in combination with an interrupt mechanism. This is used as follows. When the hardware module completes a task, it issues an interrupt request. When the software serves the interrupt request, it accesses the hardware through memory-mapped registers. This is a commonly used mechanism, that is found in the design of UARTs, Timers, ADC peripherals, and so on.
The interrupt interface of the MSP-430 is one-hot encoded and has as many bits as there are interrupt vectors. The interrupt interface contains an interrupt request (input to MSP430) and a hardware-level interrupt acknowledge (output from MSP430).
The hardware implementation of the interrupt is as follows. The peripheral requests
interrupt service by asserting irq
. When the MSP430 serves the interrupt, it asserts the corresponding irqacc
. The interrupt request has to be removed before another interrupt can be generated.
Verilog design for a seconds counter
Here is an example design of a module that issues an interrupt every second.
The timing is done by increment a 50MHz counter until it reaches 50,000,000.
The interrupt level is recorded in a register pending
, that is set
by the counter overflow and reset by the interrupt acknowledge.
A second counter, sec
is incremented for every timer overflow so that
it will count seconds. The sec
is then memory-mapped to word location 0x8b
(byte location 0x116).
module demo2reg (output [15:0] per_dout, // data output
input mclk, // system clock
input [13:0] per_addr, // address bus
input per_din, // data input
input per_en, // active bus cycle enable
input [1:0] per_we, // write control
input puc_rst, // power-up clear reset
output irq, // interrupt request
input irqacc // interrupt acknowledge
);
reg [7:0] sec;
wire [7:0] sec_next;
reg [27:0] cnt50;
wire [27:0] cnt50_next;
reg pending;
wire pending_next;
wire valid_read;
wire tick;
always @(posedge mclk or posedge puc_rst)
begin
cnt50 <= puc_rst ? 16'b0 : cnt50_next;
sec <= puc_rst ? 16'b0 : sec_next;
pending <= puc_rst ? 1'b0 : pending_next;
end
// counting logic
assign tick = (cnt50 == 28'h2FAF080);
assign cnt50_next = tick ? 28'h0 : cnt50 + 28'b1;
assign sec_next = tick ? sec + 1'b1 : sec;
// interrupt logic
assign pending_next = tick ? 1'b1 :
irqacc ? 1'b0 :
pending;
assign irq = pending;
// memory mapped interface
assign valid_read = per_en & (per_addr == 14'h8b) & ~per_we[0] & ~per_we[1];
assign per_dout = valid_read ? {8'b0,sec} : 16'h0;
endmodule
Integration of the seconds counter
The integration of this module is similar to the first design. First, it must be instantiated in the top-level interconnect, and attached to the peripheral bus as well as to the interrupt interface. Then, it must be added to the project. Here is the integration in toplevel.v.
wire [15:0] per_dout_demo2reg0;
wire irq_demo2reg0;
// instantiation
demo2reg demo2reg0 (.per_dout(per_dout_demo2reg0),
.mclk(mclk),
.per_addr(per_addr),
.per_din(per_din),
.per_en(per_en),
.per_we(per_we),
.puc_rst(puc_rst),
.irq(irq_demo2reg0),
.irqacc(irq_acc[1]),
);
// connection of the per_dout bus to the system per_dout bus
assign per_dout = per_dout_dio |
per_dout_tA |
per_dout_demoreg0 |
per_dout_demo2reg0 |
per_dout_uart;
// connection of the interrupt request. We use Vector 1.
assign nmi = 1'b0;
assign irq_bus = {1'b0, // Vector 13 (0xFFFA)
1'b0, // Vector 12 (0xFFF8)
1'b0, // Vector 11 (0xFFF6)
1'b0, // Vector 10 (0xFFF4) - Watchdog -
irq_ta0, // Vector 9 (0xFFF2)
irq_ta1, // Vector 8 (0xFFF0)
irq_uart_rx, // Vector 7 (0xFFEE)
irq_uart_tx, // Vector 6 (0xFFEC)
1'b0, // Vector 5 (0xFFEA)
1'b0, // Vector 4 (0xFFE8)
irq_port2, // Vector 3 (0xFFE6)
irq_port1, // Vector 2 (0xFFE4)
irq_demo2reg0,// Vector 1 (0xFFE2) our new interrupt
1'b0}; // Vector 0 (0xFFE0)
Running the seconds counter
The software driver for this module displays the memory-mapped seconds counter on the HEX displays. It also defines an interrupt service routine that catches the interrupt requests from the hardware. The interrupt service routine is attached the vector 2 which, for this particular compiler, corresponds to vector 0xFFE2 (i.e., vector #1 in hardware corresponds to interrupt(2) in software).
#include "omsp_de1soc.h"
#define SEC (*(volatile unsigned char *) 0x116)
void __attribute__ ((interrupt(2))) demo2regisr (void) {
de1soc_hexlo(SEC);
}
int main(void) {
de1soc_init();
_enable_interrupts();
while (1) ;
_disable_interrupts();
return 0;
}
Since the interrupt request is automatically reset by the hardware, no software-level interrupt acknowledge is needed. Often, however, the interrupt acknowledge flag is set from within the interrupt service routine. Doing so prevents the hardware from issuing another interrupt while the service of the previous request is still processing.
Finding space for memory-mapped registers
Within the lower 512 bytes, not every memory address is available. Before using a memory address for a new memory-mapped register, we have to verify if the chosen location is available or not.
That information is available in an ad-hoc manner: some locations are already occupied because the openmsp430 is compatible with the baseline device MSP430C1111, so that we may refer to the MSP430C1111 instead to find the addresses in use. Other locations, though, may be the result of configuring new peripherals within a specific openmsp430 design.
We will look at two different ways in which we can extract memory-mapped allocation data from an openmsp430. In an ideal world, this information may be well documented and identified.
-
First, check the compiler include files and linker symbol files for MSP430C1111 to see what locations are already defined by default, for an MSP430C1111. This saves the effort of exhaustively working through all verilog contained within openmsp430.
-
Second, check the system-level toplevel verilog file, to see if there are any peripherals in the openmsp430 that are not included by default in the MSP430C1111.
Finding reserved locations through MSP430C1111 Linker Symbols
In one of the earlier lectures, we used a macro to define an absolute memory address access as follows. Assume that we have a byte-wide register at decimal address 124, then we would use a macro as follows. The symbol MYREG
can be used as a ‘pseudo-variable’ in C expressions.
#define MYREG (*((volatile unsigned char *) 124))
The msp430-elf-gcc
compiler uses a different technique. Instead of creating a macro that contains a hard-coded memory address, the compiler
introduces an external variable as follows.
#define sfr_b(x) extern volatile unsigned char x
sfr_b(MYREG);
This creates a byte-level variable that is compiled in the code as an external (global) variable. This means that the linker will have to know the proper memory address for MYREG. This is done using the linker description file by adding a link as follows.
PROVIDE(MYREG = 124);
To see the fill linker description file
for an MSP430C1111 device, consult msp430c1111.ld
which is located in c:/ti/msp430-gcc/include (assuming you have followed the software installation guidelines used for this class).
Thus, the two techniques to define globally visible and reserved memory locations are to use macro’s of absolute memory locations, and external symbols which can be resolved by the linker.
Consulting the linker symbols defined for the MSP430C1111, we conclude the following memory locations are reserved. Keep in mind that this table is for one of the smallest devices in the MSP-430 series. Modern microcontrollers have thousands of such reserved memory locations.
Symbol | Width | Address | Purpose |
---|---|---|---|
IE1 | byte | 0x0000 | Interrupt Enable 1 |
IFG1 | byte | 0x0002 | Interrupt Flag 1 |
WDTCTL | word | 0x0120 | Watchdog Timer Control |
P1IN | byte | 0x0020 | Port 1 Input |
P1OUT | byte | 0x0021 | Port 1 Output |
P1DIR | byte | 0x0022 | Port 1 Direction |
P1IFG | byte | 0x0023 | Port 1 Interrupt Flag |
P1IES | byte | 0x0024 | Port 1 Interrupt Edge Select |
P1IE | byte | 0x0025 | Port 1 Interrupt Enable |
P1SEL | byte | 0x0026 | Port 1 Selection |
P2IN | byte | 0x0028 | Port 2 Input |
P2OUT | byte | 0x0029 | Port 2 Output |
P2DIR | byte | 0x002A | Port 2 Direction |
P2IFG | byte | 0x002B | Port 2 Interrupt Flag |
P2IES | byte | 0x002C | Port 2 Interrupt Edge Select |
P2IE | byte | 0x002D | Port 2 Interrupt Enable |
P2SEL | byte | 0x002E | Port 2 Selection |
TAIV | word | 0x012E | Timer A Interrupt Vector Word |
TACTL | word | 0x0160 | Timer A Control |
TACCTL0 | word | 0x0162 | Timer A Capture/Compare Control 0 |
TACCTL1 | word | 0x0164 | Timer A Capture/Compare Control 1 |
TACCTL2 | word | 0x0166 | Timer A Capture/Compare Control 2 |
TAR | word | 0x0170 | Timer A Counter Register |
TACCR0 | word | 0x0172 | Timer A Capture/Compare 0 |
TACCR1 | word | 0x0174 | Timer A Capture/Compare 1 |
TACCR2 | word | 0x0176 | Timer A Capture/Compare 2 |
DCOCTL | byte | 0x0056 | DCO Clock Frequency Control |
BCSCTL1 | byte | 0x0057 | Basic Clock System Control 1 |
BCSCTL2 | byte | 0x0058 | Basic Clock System Control 2 |
FCTL1 | word | 0x0128 | FLASH Control 1 |
FCTL2 | word | 0x012A | FLASH Control 2 |
FCTL3 | word | 0x012C | FLASH Control 3 |
CACTL1 | byte | 0x0059 | Comparator A Control 1 |
CACTL2 | byte | 0x005A | Comparator A Control 2 |
CAPD | byte | 0x005B | Comparator A Port Disable |
Finding reserved locations through the openmsp430 Verilog
The second method of finding occupied memory locations in the peripheral space is
by studying toplevel.v
in the openmsp430 design. The toplevel module includes
the system-level interconnect for the openmsp430 core, and includes all the peripherals
used in the design. It is also the place where one would add a new memory-mapped register.
├── hardware
│ ├── msp430de1nano
│ │ ├── msp430
│ │ │ ├── toplevel.v
│ │ └── mspconnect
│ └── msp430de0nano
├── loader
└── software
The standard configuration of openmsp430 includes a UART, which is not present
in the MSP430C1111. Here is the instantiation of that module. There are two parameters
that hint at the number of address locations occupied by the module. The first is the
base address, which is 0x80
. The second is the number of memory-mapped registers
used by the UART.
omsp_uart #(.BASE_ADDR(15'h0080))
uart_0 (
.irq_uart_rx (irq_uart_rx), // UART receive interrupt
.irq_uart_tx (irq_uart_tx), // UART transmit interrupt
.per_dout (per_dout_uart), // Peripheral data output
.uart_txd (user_uart_tx), // UART Data Transmit (TXD)
.mclk (mclk), // Main system clock
.per_addr (per_addr), // Peripheral address
.per_din (per_din), // Peripheral data input
.per_en (per_en), // Peripheral enable (high active)
.per_we (per_we), // Peripheral write enable (high active)
.puc_rst (puc_rst), // Main system reset
.smclk_en (smclk_en), // SMCLK enable (from CPU)
.uart_rxd (user_uart_rx) // UART Data Receive (RXD)
);
If all else fails, look even further
Ironically, the two methods just explained still do not guarantee that you will identify all possible memory-mapped registers. The openmsp430 internally defines additional memory-mapped locations that may interfere with the operation of peripherals. One of these is the hardware multiplier, which occupies address locations 0x130 to 0x13F. You can only identify the multiplier by studying the Verilog source code! The reason why it does not show up in the MSP430C1111 linker description file, is that the MSP430C1111 does not have a multiplier. The reason why the multiplier does not show up on the system interconnect, is that it is defined inside of the openmsp430 module. And yes, sometimes you can only find out the hard way.
Runnning the examples
There are two examples with this lecture, corresponding to the first and second memory-mapped register design discussed above. To download, open a Cygwin shell and a Nios-II Command Shell. Clone the example repository.
git clone https://github.com/vt-ece4530-f19/example-memmap430
Then, complete the following commands.
Example 1: Memory-mapped Register Design
In the Nios-II Command Shell, compile the hardware
cd example-memmap430/hardware/msp430de1soc
quartus_sh --flow compile msp430de1soc
In the Cygwin shell, compile the software
cd example-memmap430/software
make compile
In the Nios-II Command Shell, download the bitstream and the software binary
cd example-memmap430/loader
system-console --script connect.tcl \
-sof ../hardware/msp430de1soc/msp430de1soc.sof \
-bin ../software/demoreg/demoreg.bin
You will observe a circular sequence of the following numbers on the HEX displays. Consult the lecture notes, the Verilog demoreg.v and the C driver main.c in demoreg/ to understand what is going on.
01 12 34
02 00 56
03 00 78
Example 2: Memory-mapped Register Design with Interrupts
In the Nios-II Command Shell, compile the hardware. This needs to be done only once. If you completed Example 1, you can skip this step.
cd example-memmap430/hardware/msp430de1soc
quartus_sh --flow compile msp430de1soc
In the Cygwin shell, compile the software
cd example-memmap430/software
make compile
In the Nios-II Command Shell, download the bitstream and the software binary
cd example-memmap430/loader
system-console --script connect.tcl \
-sof ../hardware/msp430de1soc/msp430de1soc.sof \
-bin ../software/demoreg/demo2reg.bin
You will observe a counting sequence on the HEX displays. Consult the lecture notes, the Verilog demo2reg.v and the C driver main.c in demo2reg/ to understand what is going on.
00
01
02
03
..
Conclusions
We studied the concept of a memory-mapped register for the case of openmsp430. These registers appear as memory locations to the software, but they are present as physical registers in the hardware. Memory-mapped registers are attached to the MSP-430 peripheral bus, and follow the bus access protocol of the peripheral bus.
We discussed all aspects of adding a memory-mapped register to openmsp430, including finding room in the address space, developing the hardware, integrating the hardware, and writing the software driver. We also discussed the combination of interrupts and memory-mapped register. Interrupts allow the hardware to grab the attention of software, and the memory-mapped communication channel allows the hardware to exchange data with the software.