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.