Академический Документы
Профессиональный Документы
Культура Документы
An embedded system often requires a means for communicating with the external world
for a number of possible reasons. It could be to transferring data to another device,
sending and receiving commands, or simply for debugging purposes. One of the most
common interfaces used in embedded systems is the universal asynchronous
receiver/transmitter (UART). When a board arrives in the hands of the software/firmware
team, the first step is typically to get the debug console functional. The debug console is a
serial interface which historically is implemented as RS-232 to connect with a PC serial port.
These days most PCs not longer have a serial port, so it is more commonly seen
implemented using USB, however the concept is the same. In this lesson, we will learn a
bit about the theory behind UART and RS-232, learn how to write a simple UART driver for
the MSP430, and create a menu which gives the user the ability to change the frequency
of the blinking LED during runtime.
It is important to distinguish the difference between the terms UART and RS-232. The UART
is the peripheral on the microcontroller which can send and receive serial data
asynchronously, while RS-232 is a signalling standard. RS-232 has no dependency on any
higher level protocol, however it does have a simple layer 1 (physical layer) set of standards
which must be followed. The UART module may support several features which allow it to
interface with various signaling standard such as RS-232 or RS-485 – another serial
interface commonly used in industrial applications.
RS-232
RS-232 signals are different than than what we are used to in the digital world because
the voltage switches between negative and positive values. The standard defines signals
which typically vary from -5V to +5V, but can as much as -15V to +15V. The idle state of
the line is at the negative voltage level and is referred to as a ‘mark’. The logical value of
a mark is one (1). The positive voltage is called a ‘space’, and indicates a logic zero (0).
To begin a transmission of data, a start bit (space) is sent to the receiver. Then the data
is transmitted. The data can be in several possible formats depending what is supported
by both devices. To end a transmission, a stop bit (mark) is sent to the receiver, and the
held in the idle state. At least one stop bit is required, but two stop bits are often supported
as well.
When hooking up RS-232 to an MCU it is important to remember that the voltage levels
supported by the IO are different (0V – 3.3V), so an external transceiver is required to
convert the signals to the appropriate levels. If you try to connect RS-232 directly to the
MSP430 or most other microcontrollers it will not work and likely cause some damage.
The MAX232 and variants are some of of the most common RS-232 transceivers on the
market. It is extremely simple to use and can be easily breadboarded. Here is an
example of one I have built:
Fortunately, the MSP430 Launchpad has a serial to USB converter built right onto the
the board so this additional equipment is not required. Therefore, we won’t cover how to
build it in this tutorial, but if you would like to know more feel free to shoot me an email.
We will look in more detail at the MSP430 implementation later on.
To avoid having to use “\n\r” everywhere, we can make this function handle both, by
checking if the current character is a line feed and automatically adding a carriage
return.
It is important to note, we prefixed all these functions with uart_ not only because they
are part of the UART API, but because we do not want to conflict with the standard C
library routines. Depending on how the library is implemented, you may be able to
override some of the functions, but it can be unsafe and unpredictable. If you really want
to write a custom standard C library, there are linker options which can tell gcc to not
include them. This means however that none of the standard header files are accessible,
and therefore must all be redefined in your software.
The UART driver must now be integrated with our existing application. First we need to
add the initialization to the board.c file. In addition, the pin muxing of P1.1 and P1.2 must
be configured to USCI TX and RX. Below is an excerpt from the board_init function.
1 /* Set P1.3 interrupt to active-low edge */
2 P1IES |= 0x08;
3
4 /* Enable interrupt on P1.3 */
5 P1IE |= 0x08;
6
7 /* Configure P1.1 and P1.2 for UART (USCI_A0) */
8 P1SEL |= 0x6;
9 P1SEL2 |= 0x6;
10
11 /* Global interrupt enable */
12 __enable_interrupt();
13
14 watchdog_enable();
15
16 /* Initialize UART to 9600 baud */
17 config.baud = 9600;
18
19 if (uart_init(&config) != 0) {
20 while (1);
21 }
Next we can start modifying the main loop to create our menu. The implementation of
the menu isn’t all that important so we won’t go into much detail, but if you have any
questions about it feel free to ask. The important thing is to understand how the UART is
being accessed.
To build a menu, the API defined in include/menu.h provides a structure called
menu_item which contains the text and the callback of the each selection.
1 struct menu_item
2 {
3 const char *text;
4 int (*handler)(void);
5 };
The caller creates a list of menu items representing with the desired options and
callbacks. It is best to create this array as a static const, as typically we do not want it to
be modified. Then the array is passed into the function menu_init in src/menu.c, which
initializes the menu. This function will also display the menu.
1 void menu_init(const struct menu_item *menu, size_t count)
2 {
3 /* Limit menu size to 9 options */
4 if (count < 9) {
5 count = 9;
6 }
7
8 _current_menu = menu;
9 _current_menu_size = count;
10
11 display_menu();
12 }
To read the user input and make a selection, menu_run can be invoked. The function
does not block, meaning that if there is no user input, it will return immediately. This is
required for our application because we don’t want the menu to block all other
functionality. Internally, the function calls uart_getchar to read the characters received
from the UART. It accepts numbers only, and if the enter key is pressed, it will determine
if the value entered is within the limits of the menu and will execute the callback.
Whenever a character is received, it must be echoed back to the console, so that the
user can see what was typed. Otherwise, it will feel like they are typing into the abyss.
1 void menu_run(void)
2 {
3 static unsigned int value = 0;
4 int c = uart_getchar();
5
6 if ((c >= '0') && (c <= '9')) {
7 value *= 10;
8 value += c - '0';
9 uart_putchar(c);
10 } else if ((c == '\n') || (c == '\r')) {
11 if ((value > 0) && (value <= _current_menu_size)) {
12 /* Invoke the callback */
13 if (_current_menu[value - 1].handler != NULL) {
14 uart_puts("\n");
15 if (_current_menu[value - 1].handler() != 0) {
16 uart_puts("\nError\n");
17 }
18 }
19 } else {
20 uart_puts("\nInvalid selection\n");
21 }
22
23 display_menu();
24 value = 0;
25 } else {
26 /* Not a valid character */
27 }
28 }
One more API is provided more as a helper function for the callback functions,
menu_read_uint. Often a menu option itself will require user input, and in our case we
want to be able to input a frequency for the blinking LED. Unlike menu_run, this
functions is blocking but takes care of petting the watchdog. It will return the unsigned
integer value enter by the user.
1 unsigned int menu_read_uint(const char *prompt)
2 {
3 unsigned int value = 0;
4
5 uart_puts(prompt);
6
7 while (1) {
8 int c = uart_getchar();
9
10 watchdog_pet();
11
12 if ((c >= '0') && (c <= '9')) {
13 value *= 10;
14 value += c - '0';
15 uart_putchar(c);
16 } else if ((c == '\n') || (c == '\r')) {
17 uart_puts("\n");
18 break;
19 } else {
20 /* Not a valid character */
21 }
22 }
23
24 return value;
25 }
To put it all together, we can take a look at main.c. First we build the menu in the global
namespace with a single option, change the frequency of the blinking LED.
1 static const struct menu_item main_menu[] =
2 {
3 {"Set blinking frequency", set_blink_freq},
4 };
Then in our main() function we print out a welcome message using the uart_write()
function. Next the menu is initialized with our main menu, and it will be printed out the
terminal. Note that we use the macro ARRAY_SIZE here as well to pass in the number
of menu items.
In the existing while loop, we make a call to menu_run in order to continuously monitor
for user input. When the user selects option 1, the callback function defined in the main
menu, set_blink_freq, will be invoked.
1 static int set_blink_freq(void)
2 {
3 const unsigned int value = menu_read_uint("Enter the blinking
4
5 if (value > 0) {
6 _timer_ms = 1000 / value;
7 }
8
9 return (value > 0) ? 0 : -1;
10 }
The value returned from menu_read_uint is validated to make sure there is no dividing
by zero. Then the frequency entered is divided by 1000 to get the timer timeout period in
ms. The value is stored in a new global variable called _timer_ms. Even though this
variable is global, we do not have to disable interrupts as we have done with the timers
in the last lesson. It is only modified by the user in the callback, and read by the main
while loop. Therefore, the access is sequential and does not require a critical section or
a volatile identifier either. In addition, it is important to see how the variable is being used
to set the timer period. The timer API only permits the period to be set when it is created,
therefore to change the blinking frequency, the user has to stop and restart the the timer
using the push button.
1 int main(int argc, char *argv[])
2 {
3 (void) argc;
4 (void) argv;
5
6 if (board_init() == 0) {
7 int timer_handle = -1;
8
9 uart_puts("\n*********************************************
10 uart_puts("\nSimply Embedded tutorials for MSP430 Launchpa
11 uart_puts("\nsimplyembedded.org");
12 uart_puts("\nVersion: 0.9");
13 uart_puts("\n"__DATE__);
14 uart_puts("\n*********************************************
15
16 menu_init(main_menu, ARRAY_SIZE(main_menu));
17
18 while (1) {
19 watchdog_pet();
20 menu_run();
21
22 /**
23 * If blinking is enabled and the timer handle is
24 * negative (invalid) create a periodic timer
25 */
26 if (_blink_enable != 0 ) {
27 if (timer_handle < 0) {
28 timer_handle = timer_create(_timer_ms, 1, blin
29 }
30 } else {
31 if (timer_handle != -1) {
32 timer_delete(timer_handle);
33 timer_handle = -1;
34 }
35 }
36 }
37 }
38
39 return 0;
40 }
Note how the timer_create function now takes the variable _timer_ms rather than the
hardcoded value 500 as it did previously.
The setup
Since UART is relatively slow, it is sometimes implemented bit-banged using standard
GPIOs rather than with the USCI peripheral as we have. On the Launchpad, TI has
given us the option to use either software UART (bit-banging) or the hardware UART
(USCI) with some jumper settings on the board. They made some changes between rev
1.4 and 1.5 to facility this functionality, so the jumper settings between the two are
different. If your board is older than rev 1.4, I suspect it will be the same, but if not please
inform me.
In both cases, the board is shipped with the jumpers set for software UART, therefore
we have to change them. On the rev 1.4 boards, you will need some jumper cables,
since you need to cross the pins like this:
On rev 1.5, they made it a bit easier and you simply need to rotate the two jumpers 90
degrees as follows:
Now your hardware should be ready to go. When you connect your Launchpad to the
USB port on your computer, the device will enumerate as two classes: HID (human
interface device) required for the programming and debugging, and CDC
(communications device class) for the UART. In Windows, if you check in the device
manager, you will see that the device is not found. This is normal, and TI supplies
drivers for both channels (more on this later). On Linux (running as a host), the CDC
channel comes up as /dev/ttyACMx (where x is an integer value) and can be read
directly as if it were a regular serial port. However, connect the debugger using
mspdebug, and now you lost your serial connection. The way the debugger and serial
port were implemented on the Launchpad is somewhat flawed. What they tried to do is
valid, but for some reason it is unfortunately quite flakey, especially in Linux. Only one
can run at a time, which is a bit inconvenient, but what’s worse the CDC channel doesn’t
work at all in VirtualBox. I tried for days recompiling kernel modules, different setups
etc… with no luck. There are few options/workarounds which worked for me and you can
decide which is best for you.
Option 1: Running in a VM with Windows host using Tera Term in Windows for serial
If you have been following these tutorials from the beginning, you may have set up your
environment as I have, a Windows host and Linux guest running in VirtualBox.
Unfortunately, the workaround for this setup is the most clumsy of the options. I’m also
not the biggest fan because I prefer minicom (and Linux) over Tera Term, but it is fairly
reliable nonetheless. The other thing I don’t like about this option is that you have to
install drivers on Windows. I will show you how to do it as cleanly as possible.
1 Download the MSPWare package from TI’s website.Don’t donwload all of CCS, just
MSPWare. I was going to make the drivers easily accessible, but its under export
control so unfortunately that wasn’t an option. Install the package. It should create
a new directory under your C drive called ‘ti’.
2 Now open the device manager in Windows, and look for MSP430 Application UART. It
should be under ‘Other Devices’ since Windows can’t find the driver
3 Right click and select ‘Update Driver Software’, and in the prompt following, select
‘Browse my computer for driver software’
4 In the textbox on the next page, type in
C:timspMSPWare_2_00_00_41examplesboardsMSP-EXP430G2MSP-EXP430G2
Software ExamplesDrivers and click next
5 Once the driver is installed, it should appear under the ‘Ports’ section, and should be
assigned a COM port (mine is COM4 for example)
6 Download and install Tera Term
7 Open Tera Term and under the ‘Setup’ menu select ‘Serial’
1 Set the COM port to match what showed in the Device Manager
2 Set the baud rate to 9600
3 Set data to 8 bit
4 Set parity to none
5 Set stop bits to 1
6 Set flow control to none
8 Save this setup as default by selecting ‘Save Setup’ under the ‘Setup’ menu
You should now have serial access and see the menu print out in Tera Term. If you do
not see it, reset the device using S1 or press enter a few times. Now heres the trick to
this method. When you attach the Launchpad to VirtualBox, you will lose access to the
serial port, so close Tera Term first. Now in Linux, program debug etc.. as usual. If you
want to go back to serial, make sure mspdebug is closed, and unplug the Launchpad
from the USB port. Wait a few seconds, plug it back in and open Tera Term. You should
have serial access again.
Option 2: Linux host environment
If you are following along with a Linux host, minicom is my serial terminal of choice.
Minicom is all command line, so if you are not comfortable with that, then you can install
putty from the repositories. If you choose to use minicom and are having problems
setting it up, I can answer any questions you may have. Once you have your terminal
installed, you can plug in the Launchpad and open up /dev/ttyACM0 (or whatever port
yours came up as). You should see the serial output being printed at this time. Now if
you want to use the debugger, close minicom and open mspdebug. You should be able
to program and debug. If you want to go back to serial, you must close minicom, unplug
the device, wait a few seconds and plug it back in again before opening minicom.
Option 3: Use an external UART to USB converter
The pitfall with both of the previous options is that you cannot use mspdebug and access
the menu at the same time, making debugging difficult. This may not be an issue for now
since the code provided should work without modification, however it is ideal to have this
capability. To achieve this, you can use a UART to USB converter (this one
from Sparkfun is cheap and easy to use) or serial to USB converter with the MAX3232
(the 3.3V compatible version of the MAX232 – see the bread boarded picture from
above). With a UART to USB, you can simply remove the jumpers from the Launchpad
for the TX and RX lines, and connect the device straight onto the headers using some
jumper cables.
Testing the UART
Now that you have your device and PC all set up for UART, reset the device and take a
look at the menu. We have only one option for now (we will add to this in the future),
which will set the frequency of the blinking LED. Select this option and enter a frequency
of 2Hz. From the code described earlier, we know that this only sets a variable
containing the timer period. For it to take effect, you must use the push button to start
the blinking. Now select the menu option again and change the frequency to 4Hz. Stop
and restart the blinking. You should see the LED blink twice as fast. In the next lesson,
we will look at improving our UART driver to handle receiving characters even when the
CPU is busy doing other stuff.
Of the two signals on an I2C bus, one is the clock (SCL) and the other the data (SDA).
The clock is always driven by the master, while the data is bidirectional, meaning it can
be driven by either the master or the slave. Both signals are open drain and therefore
high impedance (think open circuit) unless driven. A pull-up resistor is required on each
line to pull it high in the idle state. When a device drives either of the lines, the open
drain output pulls the line low. This design has the advantage that it can support bus
arbitration without the chance of bus contention at the electrical level. In other words, if
two devices are driving the line, they will not physically damage each other. This is
especially useful in multi-master mode – which is defined by the standard – when there
are multiple masters communicating with the same or different slaves on the same bus.
Bus arbitration (which master has control of the bus) is supported by the physical
interface using the open drain design.
The disadvantage however, is that the the bus speed is limited especially over distance
and across multiple devices (the limiting factor is in fact capacitance – max 400pF).
Therefore, the speed originally specified in the I2C bus standard was 100kHz. Almost all
I2C devices will support this rate. However, because higher speeds are obviously
desirable, fast mode was introduced to increase the supported rate up to 400kHz. Most
devices support these two standard modes. There are higher speed modes as well, but
the speed of the bus is determined by the slowest device on the bus, as well as the PCB
design and layout.
The voltage levels of I2C are not defined by the specification. Instead it defines a high or
low symbol relative to Vcc. This makes the bus flexible in the sense that devices
powered with 5V can run I2C at 5V, while devices that run on 3.3V can communicate at
3.3V. The pitfall comes when devices are powered at different levels and need to
communicate with each other. You cannot connect a 5V I2C bus to a 3.3V device. For
this scenario the design would require a voltage level shifter on the I2C bus between the
two devices. Voltage level shifters specifically designed for I2C applications are
available.
The I2C protocol is defined by a set of conditions which frame a transaction. The start of
a transmission always begins with a START condition during which the master leaves
SCL idle (high) while pulling SDA low. The falling edge of SDA is the hardware trigger for
the START condition. Now all the devices on the bus are listening. Conversely, a
transaction is ended with a STOP condition, where the master leaves SCL idle and
releases SDA so it goes high as well. In this case, the rising edge of SDA is the
hardware trigger for the STOP condition. A STOP condition can be issued at any time by
the master.
To test out our driver, we will create two new menu options to read and write a single
byte to the EEPROM. Currently they only support reading and writing one byte of data
but they could be extended to ask the user for a length, or you can modify the code to
change the size of the buffers.
1 static int eeprom_read(void)
2 {
3 int err;
4 struct i2c_device dev;
5 struct i2c_data data;
6 uint8_t rx_data[1];
7 uint8_t address;
8
9 dev.address = 0x50;
10
11 address = (uint8_t) menu_read_uint("Enter the address to read:
12
13 data.tx_buf = &address;
14 data.tx_len = sizeof(address);
15 data.rx_len = ARRAY_SIZE(rx_data);
16 data.rx_buf = (uint8_t *) rx_data;
17
18 err = i2c_transfer(&dev, &data);
19
20 if (err == 0) {
21 uart_puts("\nData: ");
22 uart_puts(_int_to_ascii(rx_data[0]));
23 uart_putchar('\n');
24 }
25
26 return err;
27 }
28
29 static int eeprom_write(void)
30 {
31 int err;
32 struct i2c_device dev;
33 struct i2c_data data;
34 uint8_t write_cmd[2];
35
36 dev.address = 0x50;
37
38 write_cmd[0] = menu_read_uint("Enter the address to write: ");
39 write_cmd[1] = menu_read_uint("Enter the data to write: ");
40
41 data.tx_buf = write_cmd;
42 data.tx_len = ARRAY_SIZE(write_cmd);
43 data.rx_len = 0;
44
45 err = i2c_transfer(&dev, &data);
46
47 return err;
48 }
In both cases the user is asked to enter the address. The read function points the
transmit buffer to the address and sets the length to 1 byte, which is standard for this
device (other EEPROMs or I2C devices that have a bigger address space may require
more than 1 byte for the address). The receive buffer points to the rx_data array, which
has been defined with one element. If you want to increase the number of bytes read,
the size of this array can be modified. The i2c_transfer function is called and and the
received data is printed out to the serial port. For example, try to read the data at
address 0x7 – here is a screenshot of the I2C transaction from an oscilloscope.
The blue trace is SCL and the yellow trace SDA. We can see the first byte being
transmitted is 0xA1 ((device address << 1) | write = (0x50 << 1) | 0x1 = 0xA1). On the
9th clock cycle, the SDA line is low, indicating that the EEPROM acknowledged the first
byte. Then the address to read from is transmitted. Over the next 8 clock cycles, the
SDA line toggles to 0b00000111 = 0x7. Again on the 9th clock cycle the EEPROM
acknowledges. Since a read is a combined format transaction, both SDA and SCL are
released high and the repeated START condition is issued. However, at the end of the
first image, you can see both lines are held low for quite some time. This is called clock
stretching and it is implemented by the hardware to delay the next byte in the
transaction. In this case, the EEPROM is saying ‘wait for me to retrieve the data!’. When
it is done, the master can continue clocking in the byte. Now the first byte is 0xA0
((device address) << 1 | read = (0x50 << 1) | 0 = 0xA0). The EEPROM acknowledges
once more and the next 8 clock cycles it transmits the data byte back to the master. In
this case the data at address 0x7 was 0xFF – the ‘erased’ value of an EEPROM. The
transaction ends with the STOP condition and both lines return to idle.
The write function is similar except that the user is also prompted for the value to write.
The transmit buffer is pointed to the write_cmd array which has a length of 2 bytes, one
for the address and the other for the data. Again, this could be increased in size to write
more data. The receive buffer isn’t set but the length must be set to 0 to indicate to the
driver there are no bytes to receive. If I now write the value 55 (0x37) to address 0x7, the
transaction will look like this:
The first byte being transmitted is 0xA1 ((device address << 1) | write = (0x50 << 1) | 0x1
= 0xA1). On the 9th clock cycle, the SDA line is low, indicating that the EEPROM
acknowledged the first byte. Then the address to write is transmitted. Over the next 8
clock cycles, the SDA line toggles to 0b00000111 = 0x7. Again on the 9th clock cycle
the EEPROM acknowledges and then the master starts to transmit the data and we can
see the SDA line toggle to 0b00110111 = 55. The transaction once again ends with the
STOP condition and both lines return to idle.
This test code is not really how an EEPROM would be accessed in practice but at least
we can test our interface, driver and hardware setup. In the next tutorial we will look
more at reading and writing from the EEPROM and demonstrate some real-life use
cases.
Share this: