C/C++ I/O Register Names

Normally you would always use mbed's I/O APIs from the Handbook to write programs for mbed since they are easier to use and you will be more productive working at a higher level of abstraction. Communication with I/O devices is done using special I/O registers that control the I/O device hardware. On a RISC processor, these I/O registers are typically mapped into memory locations at a fixed (absolute) address. A memory address space map in the figure below shows the areas used for I/O registers. In the rare case that it is necessary to directly communicate with I/O hardware or you want to experiment and write your own I/O drivers, there are already predefined I/O register names for mbed's I/O registers. C/C++ I/O register names appear in LPC17xx.h and it uses 32-bit hex constants to setup all of the correct addresses for the registers. Each I/O hardware unit has a name "LPC_hardwareunit". For each hardware unit, register names have been setup in a C structure at the correct address. In most cases, these are the register names used in the LPC1768 Users manual.

To use these names to read and write I/O registers, the syntax is "LPC_hardwareunit->registername". This approach takes a lot more time since it is necessary to understand all of the various things that need to be done to initialize, control, and use the I/O hardware. In general, you will need to read and study the 840 page LPC1768 Users Manual to figure out the I/O devices and how to use the registers. 840 pages sounds like too much, but at times you will wish it was a few thousand pages longer. Quite a few I/O examples with source code are also provided with the demo version of the Keil tools compiler and available from NXP. Commercial quality I/O drivers can be quite complex since they attempt to support every feature and option on the I/O device through APIs even including interrupts, DMA, error checking , and perhaps timeout errors. The demo examples here will use only simple polling and not include the code for device initialization or check for any errors.

Warning!

When using other mbed I/O APIs it is very easy to break mbed's runtime code since there can be unintended interactions between the APIs and code writing directly to I/O registers on anything using the same hardware units or even sharing pins or ports. If anything stops working, it is probably problems with the new code that directly writes to registers. With that said, here are a couple demos of how it works.

memmap

Example Blink LED

This code blinks two LEDs, myled4 using mbed's DigitalOut, and myled1 by writing directly to the GPIO I/O registers that drive the LED. In the C API DigitalOut the compiler automatically determines the port address and bit number using the pin names. From pinnames.h or the LPC1768 Pin functions table, LED1 is shown as being connected to GPIO port 1 bit 18. This could also be determined by checking the mbed LPC1768 module schematics to find the chip pin number connected to LED1. This information can then be used to find the names of the I/O registers used to control the pin by consulting Chapter 9 General Purpose Input/Output (GPIO) of the LPC1768 User Manual. For other pins, the pin's function may also need to be set to GPIO first as described in Chapter 8 Section 3.

The code below writes the new bit out using GPIO Port 1’s (i.e., register name LPC_GPIO1) FIOSET set or FIOCLR clear register along with a mask value used to select the correct bit. The mask value could also be setup as a constant. Most pins have several functions. The function is selected by a pin function register, and a GPIO pin's direction (in or out) must also be setup in the FIODIR direction register. The C/C++ statement DigitalOut myled1(LED1); automatically sets up the pin's function register and the pin's direction register. If this statement was removed, these registers would need to be initialized by the routine.

#include "mbed.h"
// This program will blink LED1 and LED4
// using register names for LED1 and
// API functions for LED4
// declare LED outputs – let C set them up as output bits
DigitalOut myled1(LED1);
DigitalOut myled4(LED4);

int main() {
    int value = 0;
    uint32_t myled1_bit_mask=0;
    // loop forever
    while (1) {
        //control LED1 using C/C++ hardware I/O register names
        //from LPC1768 data manual the LED1 pin goes to GPIO1 port bit 18
        myled1_bit_mask = 0x00040000; // 0x040000 = 1<<18 all "0"s with a "1" in bit 18
        if (value==0) {
            LPC_GPIO1->FIOCLR = myled1_bit_mask; //Write to register that clears bits
        } else {
            LPC_GPIO1->FIOSET =  myled1_bit_mask;//Write to register that sets bits
        }
        //API function to control LED4
        myled4 = value;
        // flip value and wait
        value = ~ value;
        wait(0.2);
    }
}

Example Serial I/O

This code sends and receives data using the serial port by directly writing and reading the UARTs I/O registers instead of using "putc" and "getc". It loops through the printable ASCII character set, sending out a character, reading it back, and sending it to the PC. This example is more typical of what needs to be done in I/O drivers. Most I/O devices have registers for data transfers, and additional registers to check device status and control device options. From the pin table or manual, P9 and P10 are found to be connected to UART3, so hardware unit, LPC_UART3, is used for register names. I/O devices are very slow compared to instruction execution times on a processor and things will happen asynchronously, so handshake or ready bits in status registers must be checked prior to most data transfers. From the LPC1768 Users manual, bit 0 in the UART's LSR register must be a 1 before sending it a new character to the THR register, and bit 5 must be a 1 before reading in a new character from the RBR register. In this case, waiting for 1 on the status ready bits is done in do..while loops and a logical AND (i.e., "&") with a hex mask value is used to test only the correct bit. A state machine in the UART's hardware automatically changes these status bits.

There are also several registers that need to be initialized at power up to select the UART features needed. In this case, the UARTs baud rate clock, number of data bits, and several other options involving other I/O registers. These were automatically setup by using mbed's Serial API on P9 and P10 (UART3). It can get very complex and at some point the I/O emulator in Keil tools would probably help debug code since it shows the contents of registers and what I/O pins are doing. Chapter 14 of the LPC1768 Users Manual has information on the UART and was consulted to determine the register names and the specific bits used to indicate device ready.

//Serial port driver demo using register names and loopback
#include "mbed.h"

Serial pc(USBTX, USBRX); // tx, rx
Serial device(p9, p10);  // tx, rx
//Add jumper from p9 to p10 for the serial TX/RX loopback in this code
int main() {
    char i=0;
    while (1) {
        //loop sending out a stream of printable ASCII characters
        for (i='0'; i<='z'; i++) {

// "putc" send a character using the UART - check status register transmit ready bit first
// must wait until transmitter character register is empty to write
            do {
            } while ((LPC_UART3->LSR & 0x20)==0) ;
//write character to UART
            LPC_UART3->THR = i ;

// UART hardware transfers a character here asynchronously over the loopback jumper wire
// time depends on baud rate

// "getc" receive a character using the UART - check status register receive ready bit first
// must wait until receiver has a new character to read
            do {
            } while ((LPC_UART3->LSR & 0x01)==0) ;
//read character from UART and send to PC
            pc.putc(LPC_UART3->RBR);
        }
    }
}



Running the Serial Demo Code

The jumper wire from p9 to p10 is added to connect TX to RX, a terminal application is started on the PC and connected to mbed's virtual com port. After sending out and reading each character back in correctly using the serial port, the pc.putc() call should send each character to the PC as seen below in the screen capture.

ST
Output on PC Terminal Application while code is running


2 comments on C/C++ I/O Register Names:

01 Feb 2013

Thanks for taking the time to explain this. I now have a much better understanding of how to access registers direct using C.

Much appreciated. Andrew

15 Apr 2013

Thanks.

Could someone expand this to include an example for the KL25Z?

Please log in to post comments.