Getting closer to the hardware, LPC17xx.h is your friend
.
This notebook was inspired by forum thread http://mbed.org/forum/mbed/topic/1828
Introduction
Sometimes you find yourself in a situation where you want or need to write code that's more "native" to the hardware rather than using an existing library. The above forum post seems like a possible candidate to write some examples.
Note, in this article we have no ides what's in the data packets or how to logically consume them. All we are interested in demonstrating is how to get the into buffers that a "back end" process can comsume. For the sake of simplicity, our "back end" consumer is simply going to write the captued packet to Serial(USBTX, USBRX).
Since the following uses interrupts it's a good idea to read my other article about interrupt context before continuing.
Specification
Before going "native" it's important to have to hand the specification before you start writing any code. Without that, you are either a) thrashing around in the dark or b) going to end up with a "generic library"! (An example is MODSERIAL which does something specific but in a general reusable way). When we go "native" you are effectively writing non-generic code that's highly specific to the task at hand.
So, to begin with, lets write a very simple specification:-
- Data arrives on Serial(p9, p10). From the Mbed schematic that's the LPC1768's UART3.
- The serial baud rate/format is "9600,8,n,1"
- Data packets are always 8 bytes long. The first byte is denoted by bit7 set, the remaining 7 bytes have bit7 clear.
- Data packets arrive in bursts, ie 8 bytes then a delay or wait followed by another burst of 8 bytes.
This is our simplistic starting point. We'll grow/alter this specification as we look at more advanced techniques later on.
The Mbed libraries
Before diving head first into writing interrupt handlers one has to consider the Mbed libraries and what they do. As a first up example, the Mbed wait_api uses TIMER3. So if you were writing a handler that uses TIMER3_IRQn then you are heading for potential troble later if you used Tickers, Timeouts, waits, etc. So avoid TIMER3 unless you know what you're doing!
Other Mbed library objects will use interrupt handlers too. CAN and Ethernet are two obvious complex peripherals that Mbed libraries make easier to use. So again, if you take control of an interrupt vector that an Mbed library is expecting to control you can expect trouble.
In our example we are going to be using UART3_IRQn. So declaring Serial(p9, p10) in your program is going to cause problems as the Mbed Serial library component would be expecting to use that vector.
When using any library, read it's manual page. Sometime's the manual won't mention what interrupts it's using. But "by peripheral" you don't have to be Einstein to figure out what interrupt vectors it's going to use.
To C or not to C
By their very nature, interrupt handlers are C functions. There's no notion at this point about C++ and class objects. In a later article I will describe binding an interrupt service routine (ISR) to an instance of a class/method. But for the remainder of this article we'll stay in the world of C.
Defining the ISR
There are two basic ways to get your ISR into the LPC1768's vector table. One is compile time and the other is run time.
Compile time
This is done by creating your ISR with a specific format of function prototype. The Mbed cloud compiler weakly defines all interrupt vectors at link time. By crafting your function prototype to follow a specific format you can override this default and get your own function into the table. Since we are using UART3 in our example, lets take a look at how one would define our ISR
extern "C" void UART3_IRQHandler(void) __irq { // Our function code goes here. }
A full list of "special names" can be found on Igor Skochinsky's useful forum post.
Runtime vector replacment
We can insert our ISR function at runtime using the CMSIS standard function NVIC_SetVector() function. Here's an example:-
extern "C" void MyUart3Handler(void) { // our ISR code here. } int main() { NVIC_SetVector(UART3_IRQn, (uint32_t)MyUart3Handler); }
Notice that in both cases we defined the ISR with extern "C". This is used to prevent any C++ name mangling.
In our examples to follow we will use the compile time technique to get our ISR into the interrupt vector table.
Divide and conquer
Beginners often start with main.cpp and just don't stop! It grows and grows until it's really hard to see what's going on. So, in our example, we will create two cpp code files and two matching .h header files. That way we can keep all the ISR and "protocol" bits seperate from our back end consumer. Try where possible to break your projects up into "easy to see" components. I normally break them up based on functionality.
The consumer, main.cpp
Let's begin by writing main.cpp that defines the buffers, flags, etc and "consumes" the data. In our example we are just going to print the buffer to Serial(USBTX,USBRX). We will start with the standard "Mbed new project main.cpp" and modify it to our needs.
This is a standard new project "main.cpp":-
main.cpp
#include "mbed.h" DigitalOut myled(LED1); int main() { while(1) { myled = 1; wait(0.2); myled = 0; wait(0.2); } }
Now, lets modify this to add the bits we need and get rid of those damn wait() calls (if you read my other article you'll know I have a dislike of wait() used in "main while(1) loop" code, there's a good reason for this as you will see).
main.cpp
#include "mbed.h" #include "myheader.h" Ticker flipLed; DigitalOut myled(LED1); Serial pc(USBTX, USBRX); char myBuffer[SIZEOF_MY_PACKET]; volatile bool myBufferReady; void cbFlipLed(void) { myled = !myled; } // Declare the function prototype for our initialiser which we will // define later on in this article. void myUart3_init(void); int main() { pc.baud(115200); myUart3_init(); // This will be defined in myUart3.cpp later. // To mimic that flashing LED use a Ticker, never wait() in the main while(1) loop. myled = 1; flipLed.attach(&cbFlipLed, 0.2); while(1) { if (myBufferReady) { myBufferReady = false; myBuffer[0] &= 0x7F; // Make sure bit7 start marker is zero for (int i = 0; i < SIZEOF_MY_PACKET; i++) { pc.putc(myBuffer[i]); } } } }
First thing to notice is that we keep that flashing LED functionality. We didn't have to, it wasn't part of the specification to flash the LED. But I did this to show you have to do it without the wait() in the while(1) loop. It should be obvious why we didn't want wait() in there. In that loop we test the myBufferReady flag to see if there's a packet in the buffer. The last thing we want to do is introduce any wait()ing around. We want the while(1) loop to cycle as fast as possible to ensure we keep testing that flag. No wait()s please!
The next thing you will see is that the flag was declared volatile. This is done to ensure that the compiler doesn't try to use an internal CPU register to hold the variable's value. Volatile forces it to use a memory location. When sharing simple variables between an ISR and main code, always declare them as volatile.
Also, we #include "myheader.h". Why? Well, lets create that now:-
myheader.h
#ifndef MYHEADER_H #define MYHEADER_H #define SIZEOF_MY_PACKET 8 #define PROTOCOL_IDLE 0 #define PROTOCOL_IN_RX 1 #define UART_ISSET_RDA 0x0004 #endif
We use a header so that we can share our constants between code files.
Having reached this point our main.cpp "consumer" should be obvious. All it does is examine the flag and if set it knows the buffer conatins the 8 bytes of interest and just prints them to the USB virtual com port. In your own application you would handle the buffer as needed.
Note, at this point it won't compile. That's because we are yet to define myUart3_init() function, which we are about to do!
myUart3.cpp
It's now time to create a new code file, we'll call it myUart3.cpp. One of the first things we need to do is define the myUart3_init() function. So, lets begin with that. The thing to remember here is that this article is about getting close to the hardware. So it's time to bring to the party the LPC17xx manual and LPC17xx.h
myUart3.cpp
function void myUart3_init(void) { // Power up the UART3 it's disabled on powerup. LPC_SC->PCONP |= (1UL << 25); // Setup the PCLK for UART3 LPC_SC->PCLKSEL1 &= ~(3UL << 18); LPC_SC->PCLKSEL1 |= (1UL << 18); // PCLK = CCLK // Enable the pins on the device to use UART3 LPC_PINCON->PINSEL0 &= ~0x3; LPC_PINCON->PINSEL0 |= ((2UL << 0) | (2UL << 2)); // Setup the baud rate for 9600 LPC_UART3->LCR = 0x80; LPC_UART3->DLM = 0x2; LPC_UART3->DLL = 0x71; LPC_UART3->LCR = 0x3; // Enable and reset UART3 FIFOs. LPC_UART3->FCR = 0x7; // Enable the interrupt NVIC_EnableIRQ(UART3_IRQn); // Init the UART3 RX interrupt LPC_UART3->IER = 0x01; }
In the above code we knew we wanted 9600 baud so the values in DLM and DLL can be precalculated. If you need to set the baud (and format) at runtime you will need to provide functions to do this. You can see examples here that can assist you in this. However, that's an exercise left for the reader. For this example, we'll directly set the baud rate in the init() function.
Now, it's onto creating the ISR. In the following snippets I assume you leave in the definition for myUart3_init() above. I will omit it from the following snippets so as not to bloat this article. The following is described in detail in the comments of the snippet.
#include "mbed.h" #include "myheader.h" // Define the variables decalred in main.cpp so we can use them. extern char myBuffer[]; extern bool myBufferReady; // Declare a variable to hold a pointer into the buffer. volatile int myBufferIn = 0; int myUart3State = PROTOCOL_IDLE; // See myheader.h extern "C" void UART3_IRQHandler(void) __irq { // We only set IER = 1 to interupt on receive // Note, it's important to ensure peripherals clear the // reason for the interrupt within the ISR. For UART3 // this is done simple by reading the IIR register. uint32_t iir = LPC_UART3->IIR; // Unknown interrupt, this should never happen according // to the datasheet but handle anyway. if (iir & 0x1) return; /* Do we have a serial character(s) in the fifo? */ if (iir & UART_ISSET_RDA) { char c = LPC_UART3->RBR; // Are we idle waiting for a packet and is char start of packet? switch (myUart3State) { case PROTOCOL_IDLE: if (c & 0x80) { myBufferIn = 0; myBuffer[myBufferIn++] = c; myUart3State = PROTOCOL_IN_RX; } else { // Do nothing, drop the byte as we need to align // the incoming data stream with our state machine. // This can happen if we power up while the remote // is part way through sending a packet. Nothing we // can do here as we missed the start of the packet // so must wait for the next packet. } break; case PROTOCOL_IN_RX: if (myBufferIn < SIZEOF_MY_PACKET) { myBuffer[myBufferIn++] = c; } if (myBufferIn == SIZEOF_MY_PACKET) { // If here buffer has eight bytes, // set the flag so that main() knows. myBufferReady = true; myUart3State = PROTOCOL_IDLE; } break; default: // This should never happen as the state machine should // only ever toggle between IDLE and IN_RX break; } } }
Now the above is a start. It demonstrates how to fill the buffer with 8 bytes aligned to a known "protocol" in that the first byte has bit7 set. However, there's more than one way to "skin a cat" as they say and in this regard there's an even better way.
The basic problem with the above is that the ISR will always write the buffer. If your "consumer" doesn't handle it in time it wil start over-writing the buffer and you could end up with corruption. Now, it goes without saying that your consumer really needs to be able to consume the packet faster than packets arrive but there is room to give yourself some "room for maneuver". And that's to use double buffering. Our specification says packets are 8 bytes long. Th eLPC17xx UARTS all have a 16 byte TX and RX FIFO. So that means we can accumulate incoming bytes in the FIFO and only copy them to the buffer when we have all 8 bytes. So we can rewrite the ISR thus:-
#include "mbed.h" #include "myheader.h" // Define the variables decalred in main.cpp so we can use them. extern char myBuffer[]; extern bool myBufferReady; // Declare a variable to hold a pointer into the buffer. volatile int myBufferIn = 0; int myUart3State = PROTOCOL_IDLE; // See myheader.h char firstByte; extern "C" void UART3_IRQHandler(void) __irq { // We only set IER = 1 to interupt on receive // Note, it's important to ensure peripherals clear the // reason for the interrupt within the ISR. For UART3 // this is done simple by reading the IIR register. uint32_t iir = LPC_UART3->IIR; // Unknown interrupt, this should never happen according // to the datasheet but handle anyway. if (iir & 0x1) return; /* Do we have a serial character(s) in the fifo? */ if (iir & UART_ISSET_RDA) { char c; // Are we idle waiting for a packet and is char start of packet? switch (myUart3State) { case PROTOCOL_IDLE: c = LPC_UART3->RBR; if (c & 0x80) { firstByte = c; // Store for later on. myBufferIn = 1; myUart3State = PROTOCOL_IN_RX; } else { // Do nothing, drop the byte as we need to align // the incoming data stream with our state machine. // This can happen if we power up while the remote // is part way through sending a packet. Nothing we // can do here as we missed the start of the packet // so must wait for the next packet. } break; case PROTOCOL_IN_RX: if (myBufferIn < SIZEOF_MY_PACKET) { myBufferIn++; } if (myBufferIn == SIZEOF_MY_PACKET) { myBuffer[0] = firstByte; // Copy the RX FIFO contents to the buffer. for (int i = 1; i < SIZEOF_MY_PACKET; i++) { myBuffer[i] = LPC_UART3->RBR; } myBufferReady = true; myUart3State = PROTOCOL_IDLE; } break; default: // This should never happen as the state machine should // only ever toggle between IDLE and IN_RX. break; } } }
But what to do if our specification defines packets longer than the 16 byte FIFO? Well, it's fairly straight forward to add double buffering in the ISR using two (or more) buffers. This is the technique used by MODGPS which has two buffers. The ISR fills a buffer. When it receives the NEMA string terminator '\n' it flags a buffer full and then switches reception to an alternate buffer. This allows the consumer to handle/process one buffer while the ISR receives bytes to the alternate buffer.
So, at this point we should have learned enough to implement an ISR for UART3 (or any UART using the LPC17xx manual and LPC17xx.h).
A further improvement can be made by using the GPDMA module. This case works when you know the size of the packet. Either it could be a fixed length packet or a packet that defines the packet length from a predefined header field so we know how many bytes to transfer using GPDMA. However, this doesn't work when you have variable length packets and you only know you have a full packet by a terminating token (as in the GPS NEMA sentaence which ends with a '\n' character. In that you have no idea how long the packet is until you have received all of it ruling out effective GPDMA use).
3 comments on Getting closer to the hardware, LPC17xx.h is your friend:
Please log in to post comments.
This is really cool, thanks.