ESP8266 - UART Fun

14 May 2016

This is the second in a series of posts regarding my experiences with the ESP8266 microcontroller. The first blog post is available for your reading as an introduction.

Typically on a microcontroller, reading and writing data via serial is one of the easiest things to do. Because of this, I decided to make this the next topic in my ESP8266 journey.

Unfortunately, things turned out to be a little harder than I expected. And by a little, I mean that I was stumped for several days trying to work things out. Reading through the SDK documentation (available [here] [sdk]), functions such as “uart_init” are mentioned as being available for our usage. Trying to use this function will give you nothing but errors and grief.

I’ll save you the journey I took to get to to here, so I’ll just summarise some of the things I had to do:

  • Copy the “examples/driver_lib/driver/uart.c” and “examples/driver_lib/include/driver/uart*.h” files from the SDK to the local project (under the “driver” and “include/driver” directories respectively.)
  • Alter the “include/espmissingincludes.h” file to include the “os_type.h” file.
  • Make the following changes to the “uart.c” file:
    • Add include for “espmissingincludes.h”
    • Add include for “user_interface.h”
    • Create a forward definition to a function called “uart_rx_task”
    • Remove the “uart_recv_task” function
    • Alter the “system_os_task” call to create the “uart_recvTaskPrio” task to point to the “uart_rx_task”
  • Add the “uart_rx_task” function to the “user_main.c” file. Here is where we process messages received from the UART
  • Alter the makefile to look for the “uart.c” file under the “driver” directory

About serial connections

In an ESP8266, as in many other microcontrollers, a bit of the chip’s hardware is dedicated to sending and receiving serial data. This is called a UART, or Universal Asynchronous Receiver / Transmitter. The UART handles the problems of serial transmission (things like detecting when a bit is low or high, where the start/stop bits are, if any, and ensuring the parity calculations are correct, if required). It is entirely possible (and often done) to handle serial transmissions in software, but if the hardware’s available, why not use it? It does reduce your code complexity, as well as the run-time overhead.

Serial connections are great as they are the ultimate lowest common denominator for inter-chip or chip-computer connections. At its simplest, all you need are two wires, one for transmitting data, one for receiving data. As long as each end agree on the format of the connection (i.e. speed, start/stop bits and parity) and are able to keep up (so flow control is not required) that’s it!

To talk to a modern computer over serial, a converter, such as an FTDI chip is required to convert between the serial lines and the computer’s USB port. Some modules, such as the NodeMCU module include a serial/USB converter chip on it, so nothing more than a USB cable is required. Underneath though, it’s all still simple old serial, harking back to the earliest days of the PC revolution, if not before.

In “the olden days”, there were two main forms of physical communications fighting for supremacy, serial and parallel. Serial communications were “slow”, as they sent the data one bit at a time over one or two wires, while parallel connections could have eight or more wires, so a whole byte could be transmitted in a single go. Parallel was obviously better and faster because of this, but more expensive due to the additional wires and circuitry required. If you look at a modern computer, however, you’ll see very few to no parallel connections left. Ethernet networks are serial, the “S” in “SATA” connections for hard disks and the “S” in “USB” stands for serial, even PCI has gone serial with PCIe, with each “lane” sending data serially. So why, if parallel is so superior, why is serial everywhere? Speed. Yes, it looks like I’m contradicting myself, but the problem with parallel connections is as you make your clock faster to send data faster, the “cross-talk” between the wires increases. This means that a “1” in one wire may incorrectly induce a “1” in a neighbouring wire, corrupting your data. While good old serial can counteract cross-talk via differential signalling so the clock can be increased ever higher.

Initialising the UART

The UART is initialised by calling the “uart_init” function from within the “user_init” function. There are two parameters to pass through, the speed of the two UARTs. The following speeds are available via constants in the code:

  • BIT_RATE_300
  • BIT_RATE_600
  • BIT_RATE_1200
  • BIT_RATE_2400
  • BIT_RATE_4800
  • BIT_RATE_9600
  • BIT_RATE_19200
  • BIT_RATE_38400
  • BIT_RATE_57600
  • BIT_RATE_74880
  • BIT_RATE_115200
  • BIT_RATE_230400
  • BIT_RATE_460800
  • BIT_RATE_921600
  • BIT_RATE_1843200
  • BIT_RATE_3686400

I personally tend to use 115,200 bps, as this is a fast enough speed for most usage, while still being very reliable on pretty much all hardware. You can try faster speeds, but the tolerances for errors from timing and noise reduce as you go faster.

Wait? Did I write that there are two UARTs? Yes, the ESP8266 actually comes with two UARTs for your usage - well, closer to 1.5 UARTs, actually. The first UART (UART 0) has full Tx (transmit) and Rx (receive) functionality, while the second UART (UART 1) only has a transmit line. Why have a UART that can send but not receive? I believe that the reason is to allow for debug information to be printed out a different port than one used to communicate with other chips/devices.

Receiving serial data

After all of the changes listed at the start of this have been made, receiving serial data is actually very easy - the “uart_rx_task” task is executed whenever serial data is received from the UART. Inside this function, the data can be received by reading from the appropriate ESP8266 registers. Below is an example of a receive task that I have written:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*
 * Receives the characters from the serial port.
 */
void ICACHE_FLASH_ATTR uart_rx_task(os_event_t *events) {
    if (events->sig == 0) {
        // Sig 0 is a normal receive. Get how many bytes have been received.
        uint8_t rx_len = (READ_PERI_REG(UART_STATUS(UART0)) >> UART_RXFIFO_CNT_S) & UART_RXFIFO_CNT;

        // Parse the characters, taking any digits as the new timer interval.
        char rx_char;
        for (uint8_t ii=0; ii < rx_len; ii++) {
            rx_char = READ_PERI_REG(UART_FIFO(UART0)) & 0xFF;
            if ((rx_char >= '0') && (rx_char <= '9')) {
                // This is a digit, update the interval.
                tmp_interval = (tmp_interval * 10) + (rx_char - '0');
            } else {
                // We have finished receiving digits.
                if (tmp_interval > 0) {
                    set_blink_timer(tmp_interval);
                    tmp_interval = 0;
                }
            }
        }

        // Clear the interrupt condition flags and re-enable the receive interrupt.
        WRITE_PERI_REG(UART_INT_CLR(UART0), UART_RXFIFO_FULL_INT_CLR | UART_RXFIFO_TOUT_INT_CLR);
        uart_rx_intr_enable(UART0);
    }
}

If you choose to copy the above task, don’t forget to clear the interrupt and re-enable the interrupt at the end, or things will grind to a halt!

Sending serial data

Sending data is thankfully easier than receiving it. There are two choices:

  • Send a string via the “os_printf” function
  • Send binary data via the “uart0_tx_buffer” function

At first glance, there is no big difference between the two, as they both take an array of 8-bit values to be sent. However, the “os_printf” function uses C strings, and parses the contents of the string to substitute vavlues with other values passed in. A string in C is unlike that in newer languages, as it is not an object, but an array. The string simply ends when a NULL character (binary 0x00) is present. To find the length of a C string is performed by looping through the string until a NULL is found. If you forget to put a NULL in for any reason, most code will simply keep running through whatever memory randomly comes after the string until it hits a NULL. This is a cause of many bugs (including security flaws) in C code, so be careful!

The “os_printf” function will look for values in the string such as “%s” or “%d” and replace them with a string or decimal number respectively that is passed in as a subsequent parameter. Look at the “printf” function in C for other flag values that may be used instead of “%s”.

The “uart0_tx_buffer” function does not take a string, rather than an array of bytes, and the number of them to be transmitted. It does not parse the array in any way, and will not stop if a NULL value is present. This makes it very handy for use when communicating via binary protocols.

For both functions, try and limit the number of characters sent in any one call to < 125, to avoid overflowing the transmit buffer, which would cause the ESP8266 to discard extra characters. Remember, a microcontroller will always have much more severe limits on its usage when compared to a normal PC.

The blink code from the previous release has been changed to include the sending of some fixed binary data and some textual data each time the LED changes state, as well as to change the delay between changing the LED’s state based on numbers sent to it serially. The receive code was included above, while the transmit code is contained in the new “blink_cb” function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
 * Call-back for when the blink timer expires. This simply toggles the GPIO 4 state.
 */
LOCAL void ICACHE_FLASH_ATTR blink_cb(void *arg) {
    // Update the LED's status.
    led_state = !led_state;
    GPIO_OUTPUT_SET(4, led_state);

    // Write a binary message (the 0x00 would terminate any string to os_printf).
    uint8_t message[] = {0x03, 0x02, 0x01, 0x00, 0x01, 0x02, 0x03, '\r', '\n'};
    uart0_tx_buffer(message, 9);

    // Write the LED status as a string.
    os_printf("LED state - %d.\n", led_state);
}

The full source code for the “uart-blink” demonstration project is available in GitHub here.

comments powered by Disqus