Embedded C primer
This document covers a few C programming techniques that apply to embedded systems. Writing programs is largely the same as a completely software-based c program, but coding for embedded hardware has a few extra quirks. Make sure you review your c programming basics, things like standard library print functions won’t be availible, but soon you will develop your own code that allows you to send text through a serial connection.
Multi-file format is even more important in embedded systems, as you will usually want to keep code related to specific peripheral devices in their own header and source file pairs. We recommend you save your peripheral code libraries from each project to use in subsequent tasks. If you do, you will build a usable set of device drivers by the time you finish the course!
Accessing Memory Mapped Registers
In assembly, memory addresses were acessed through loading an address into a register then acessing the memory using that register. You could load any number into the register, making it easy to put in an arbitrary address, were it needed to access some peripheral. C abstracts the idea of registers, so memory must be accessed differently.
Pointers
Pointers are C types that allow indirect access of data. A pointer holds an address, which when dereferenced allows access to the data at the address. Pointers are often used to access variables outside of a function’s scope, but they can also be used to access arbitrary addresses.
Code Example
The code below illustrates loading a pointer with addresses, then accessing the data. You’re encouraged to run these code examples, as well as play around with c code yourself. Pull up the debugger and step through lines of code and see their individual effects to gain an understanding of how the programs interact with the peripherals.
//make the uint32_t type availible
#include <stdint.h>
#define LED_DATA_ADDR 0x41210000
int main(void)
{
uint32_t *datap;
uint32_t val;
datap = (uint32_t *)LED_DATA_ADDR; //set the address of datap to led control
*datap = 0xF; //write 0xF to the LED control register
*datap = 0b1001; //write '9' to the LED's
val = *datap; //read from the LED data reg into val
val++;
*datap = val; //write '9+1' to the register
for(;;); //stop at this point
return 1;
}
stdint.h
The first preprocessor directive of the program includes stdint.h
this includes the type definition of uint32_t
. uint32_t are unsigned 32-bit wide integers, which should be used for access to 32-bit registers.
Macros
The next 2 statements are macro definitions for the blackboard’s LED control and data registers. Macros replace the string on the left with the string on the right. The string on the right can be anything, a number, an expression, or just text. Macros can be very powerful but also can be difficult to debug, since they replace text in your code BEFORE it compiles. Anywhere in the program LED_DATA
is found, it is replaced with the given hexidecimal number before compilation.
Macros are often used for defining a constant in one place, so if it needs to be changed it only needs to be edited at that one place. They are also very useful for defining addresses of memory mapped registers.
The next program uses macros in a different way to access memory mapped registers
#include <stdint.h>
#define LED_DATA (*((uint32_t *) 0x41210000))
int main(void)
{
uint32_t val;
LED_DATA = 0b1001;
val = LED_DATA;
val++;
LED_DATA = val;
LED_DATA++; //increment LED data register
for(;;);
return 1;
}
This program, rather than use an explicit pointer variable to hold an address, just dereferences the defined macro to write and read from memory.
The #define statments define LED_DATA
as a dereferenced constant cast as a 32-bit pointer. This allows access to registers using the macro as if the data was always dereferenced. This can make for much more readable code but take care so you know when you’re writing to a memory mapped address, as opposed to a standard variable. A common convention when using macros is to write the defined symbol in all caps, as practiced above.
You may encounter code such as this:
#define LED_DATA 0x41210000
void do_something(void)
{
*((uint32_t *) LED_DATA) = 12;
}
This is another way of writing to a memory mapped register. The number must be manually dereferenced (and cast to avoid warnings) before writing or reading. Both ways of accessing 32-bit addresses work the same, but using a macro to always dereference address increases readability of code.
Often times in embedded programming, you will deal with seperate modules in a system, such as the LED module in the zynq. It’s a good idea to collect register definitions and address macros in a single place. If you write macro definitions in a header file, they can be included in other source files and used. General rule of thumb is to write a header file for each module.
Below is an example RGB_LED.h
that provides macros for the blackboard’s RGB LED PWM registers. The register names and addresses are taken out of the blackboard’s programmer reference manual (addresses are for blackboard revision D). In this case the names were taken verbatim from the reference, but the definition in code of registers is up to the programmer. In the prior examples, for instance, ‘RGB_EN’ and ‘RGB_PERIOD’ were used. When defining macros you can set the name to anything that might help you remember what the register is, this might make more readable code.
#define RGB_EN0 (*( (uint32_t *) 0x43C00000)))
#define RGB_EN1 (*( (uint32_t *) 0x43C00010)))
#define RGB_EN2 (*( (uint32_t *) 0x43C00020)))
#define RGB_EN3 (*( (uint32_t *) 0x43C00030)))
#define RGB_EN4 (*( (uint32_t *) 0x43C00040)))
#define RGB_EN5 (*( (uint32_t *) 0x43C00050)))
#define RGB_PERIOD0 (*( (uint32_t *) 0x43C00004)))
#define RGB_PERIOD1 (*( (uint32_t *) 0x43C00014)))
#define RGB_PERIOD2 (*( (uint32_t *) 0x43C00024)))
#define RGB_PERIOD3 (*( (uint32_t *) 0x43C00034)))
#define RGB_PERIOD4 (*( (uint32_t *) 0x43C00044)))
#define RGB_PERIOD5 (*( (uint32_t *) 0x43C00054)))
#define RGB_WIDTH0 (*( (uint32_t *) 0x43C00008)))
#define RGB_WIDTH0 (*( (uint32_t *) 0x43C00018)))
//etc
Notice that each RGB Channel (0, 1, 2, etc.) has each an enable register, a period register, and a width register. You will also notice all of the enable registers have an address with 0 as the least significant nibble, all period registers have 4, and all width registers have 8. It can also be noted that for all the registers in this module, the 2nd least-significant hex digit always corresponds to the channel number for enable, period, and width registers. The regularity in address offsets can allow us to take a shortcut when defining our address macros:
#define RGB_EN(n) (*( (uint32_t *) (0x43C00000 +(16*n))))
#define RGB_PERIOD(n) (*( (uint32_t *) (0x43C00004 +(16*n))))
#define RGB_WIDTH(n) (*( (uint32_t *) (0x43C00008 +(16*n))))
The new header has only 3 lines, with all of each type of register wrapped up in a single statements. This is a macro that uses a parameter which in this case is used to calculate the offset for the different channels for each registertype. Since each channel’s registers are found at constant offsets from one another in memory (in this case 16), we can use our parameter to calculate the offset for a specific channel. Note the extra set of parenthesis, which make sure the n is added before the cast; If you add to a number the result is the sum of the 2, howevber if you add an integer to a pointer, it offsets the address based on the data type (1 for bytes, 2 for 16-bit words, 4 for 32-bit long words). For example (((uint32_t *)0x4) +3)
results in an effective address of 0x10
as (4+3*4 == 16). The expression ((uint32_t *)(0x4+0xC))
gives the same effective address. When using a macro with a parameter to set the effective address, be careful that you only access registers that are defined, there is no bounds checking in the example provided (For example RGB_EN(200) would access memory far outside of the RGB LED Module).
A program that uses the above header is written below. The program enables 4 RGB_LED pwm channels, 2 from each module and sets different window width and pulse widths for all of them. As the ratio between pulse width to window width is the same for each channel, 50% duty, all diodes will have the same intensity. As the Green and Blue channels are enabled in each module both LEDs will show the same intensity and color of teal.
#include <stdint.h>
#include "RGB_LED.h"
int main(void)
{
uint32_t i=0;
for(i=0;i<=1;i++)
RGB_EN(i) = 1; //enable RGB LED 0 Channels G and B)
for(i=3;i<=4;i++)
RGB_EN(i) = 1; //enable RGB LED 1 Channels G and B)
//set the window for each channel double of the last
RGB_PERIOD(0) = 256;
RGB_PERIOD(1) = 512;
RGB_PERIOD(3) = 1024;
RGB_PERIOD(4) = 2048;
//set the pulse width half of each window
RGB_WIDTH(0) = 128;
RGB_WIDTH(1) = 256;
RGB_WIDTH(3) = 512;
RGB_WIDTH(4) = 1024;
for(;;);
return 1;
}
Bitwise Operators
The last code example above used a bitshift operator to shift the bits in the variable i
. Due to the nature of memory mapped registers, knowing how to properly use bitwise operators is invaluable to embedded programming.
There are 6 bitwise operators in the C programming language. They are listed below with their function and symbol.
Operator | Symbol | |
---|---|---|
bitwise AND | & | |
bitwise OR | ||
bitwise XOR | ^ | |
bitwise inversion(NOT) | ~ | |
left shift | << | |
right shift | >> |
Bitwise operators are very useful for manipulating data from and to memory mapped registers. Usage examples are shown below. Note the use of augmented assignments, these operators use the assigned variable as an operand in the expression, then saves the result to the variable. For example A+=4
is equivalent to A=A+4
.
#include "LED.h"
#include <stdint.h>
int main(void)
{
LED_DATA = 0x200; //set bit 9 in the register
LED_DATA |= 3; //set the lowest 2 bits (bits 1 and 0), without modifying other bits
LED_DATA &= ~1; //clear only the lowest bit (don't modify other bits
LED_DATA = (LED_DATA & (~0x1FF)) | 0x17; //set the lower 9 bits to 0x17, don't change any upper bits
for(;;);
return 1;
}
Bitwise Operators
A very common task in embedded programing is configuring specific bits or groups of bits in device registers, referred to as bitfields.
For an example, the bitfields found in the seven segment controller’s data register is shown below:
When enabled and in hex mode, each digit on the display will reflect the hex value found in each of the four ‘BCD_n Data’ bitfields. Configuring and reading data from peripheral registers is where bitwise operations become their most useful.
Take the example program below. The program writes a value to all four digits of the display data register. It then reads the register, masks out the third digit’s value, increments it, and writes it back. This illustrates initializing a register, reading a specific bitfield from it, and modifying some of the registers bits without touching others.
#include <stdint.h>
#include "seg_disp.h"
int main(void)
{
uint32_t read_data;
uint32_t digit;
//function not provided
seg_disp_en();
//BCD data bitfields are 0-3,8-11,16-19,24-27
//(every other nibble;
//init display to show AAAA
SEG_DATA =0x0A0A0A0A;
for(;;)
{
//read from register into temp variable
read_data = SEG_DATA;
digit = read_data;
//extract digit 3 (bits 16-19)
digit &= 0x000F0000; //mask out all but 16-19
digit >>=16; //shift so in the lowest nibble
digit +=1; //inc value by 1
//keep only the digit 4 bits
digit &=0xF;
//read data still holds the current data reg value
//now place digit back in it's original bitfield
//mask out (16-19), keep other bits same
read_data &= 0xFFF0FFFF;
//we know digit is only 4-bits
//shift it into place and or it with the read data
read_data |= digit<<16;
//write back to segment data reg
SEG_DATA = read_data;
//an empty for loop with large value
//acts as a software delay
for(int i=0;i<20000000;i++);
}
}