STM32 & OpenCM3 Part 1: Alternate Functions and USART

Companion code for this post available on Github

In the previous section, we covered the basics of compiling for, and uploading to, an STM32F0 series MCU using libopencm3 to make an LED blink. This time, we’ll take a look at alternate pin functions, and use one of the four USARTs on our chip to send information back to our host machine. As before, this is all based on a small breakout board for the STM32F070CBT6, but can be applied to other boards and MCUs.

Alternate functions

In addition to acting as General Purpose I/Os, many of the pins on the STM32 microcontrollers have one or more alternate functions. These alternate functions are tied to subsystems inside the MCU, such as one or more SPI, I2C, USART, Timer, DMA or other peripherals. If we take a look at the datasheet for the STM32F070, we see that there are up to 8 possible alternate functions per pin on ports A and B.

Alternate function tables for STM32F070

Note that some peripherals are accessible on multiple different sets of pins as alternate functions - this can help with routing your designs later, since you can to some degree shuffle your pins around to move them closer to the other components to which they connect. An example would be SPI1, which can be accessed either as alternate function 0 on port A pins 5-7, or as alternate function 0 on port B, pins 3-5. But for this example, we will be looking at USART1, which from the tables above we can see is AF1 on PA9 (TX) and PA10 (RX).

Universal Synchronous/Asynchronous Receiver/Transmitter

To quickly recap - USARTs allow for sending relatively low-speed data using a simple 1-wire per direction protocol. Each line is kept high, until a transmission begins and the sender pulls the line low for a predefined time to signal a start condition (start bit). The sender then, using it’s own clock, pulls the line low for logic 1 and high for logic 0 to transmit a configurable number of bits to the receiver, followed by an optional parity bit and stop bit. The receiver calculates the time elapsed after the start condition using it’s own clock, and recovers the data. While simple to implement, they have a drawback in that they lack a separate clock line, and must rely on both sides keeping close enough time to understand each other. For our case, they work great for sending back debug information to our host computer. So let’s update our example from last time, to include a section that initializes USART1 with a baudrate of 115200, and the transmit pin connected to Port A Pin 9.

static void usart_setup() {
    // For the peripheral to work, we need to enable it's clock
    rcc_periph_clock_enable(RCC_USART1);
    // From the datasheet for the STM32F0 series of chips (Page 30, Table 11)
    // we know that the USART1 peripheral has it's TX line connected as
    // alternate function 1 on port A pin 9.
    // In order to use this pin for the alternate function, we need to set the
    // mode to GPIO_MODE_AF (alternate function). We also do not need a pullup
    // or pulldown resistor on this pin, since the peripheral will handle
    // keeping the line high when nothing is being transmitted.
    gpio_mode_setup(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO9);
    // Now that we have put the pin into alternate function mode, we need to
    // select which alternate function to use. PA9 can be used for several
    // alternate functions - Timer 15, USART1 TX, Timer 1, and on some devices
    // I2C. Here, we want alternate function 1 (USART1_TX)
    gpio_set_af(GPIOA, GPIO_AF1, GPIO9);
    // Now that the pins are configured, we can configure the USART itself.
    // First, let's set the baud rate at 115200
    usart_set_baudrate(USART1, 115200);
    // Each datum is 8 bits
    usart_set_databits(USART1, 8);
    // No parity bit
    usart_set_parity(USART1, USART_PARITY_NONE);
    // One stop bit
    usart_set_stopbits(USART1, USART_CR2_STOPBITS_1);
    // For a debug console, we only need unidirectional transmit
    usart_set_mode(USART1, USART_MODE_TX);
    // No flow control
    usart_set_flow_control(USART1, USART_FLOWCONTROL_NONE);
    // Enable the peripheral
    usart_enable(USART1);

    // Optional extra - disable buffering on stdout.
    // Buffering doesn't save us any syscall overhead on embedded, and
    // can be the source of what seem like bugs.
    setbuf(stdout, NULL);
}

Now that we have this, we can write some helper functions for logging strings to the serial console:

void uart_puts(char *string) {
    while (*string) {
        usart_send_blocking(USART1, *string);
        string++;
    }
}

void uart_putln(char *string) {
    uart_puts(string);
    uart_puts("\r\n");
}

With this, let’s update our main loop from last time to also log every time we turn the LED either on or off:

int main() {
    // Previously defined clock, GPIO and SysTick setup elided
    // [...]

    // Initialize our UART
    usart_setup();

    while (true) {
        uart_putln("LED on");
        gpio_set(GPIOA, GPIO11);
        delay(1000);
        uart_putln("LED off");
        gpio_clear(GPIOA, GPIO11);
        delay(1000);
    }
}

Once again, run make flash to compile and upload to your target. Now, take the ground and VCC lines of the serial interface on the bottom of your Black Magic probe, and connect them to the ground / positive rails of your test board. Then connect the RX line on the probe (purple wire) to the TX pin on your board. You can then start displaying the serial output by running

$ screen /dev/ttyACM1 115200

After a couple seconds, you should have a similarly riveting console output:

Basic serial console logs, using screen

This is ok, but what if we want to actually format data into our console logs? If we have size constraints we may roll our own integer/floating point serialization logic, but printf already exists and provides a nice interface - so why not take printf and allow it to write to the serial console?

Replacing POSIX calls

When targeting embedded systems, one tends to compile without linking against a full standard library - since there is no operating system, syscalls such as open, read and exit don’t really make sense. This is part of what is done by linking with -lnosys - we replace these syscalls with stub functions, that do nothing. For example, the POSIX write call eventually calls through to a function with the prototype:

ssize_t _write      (int file, const char *ptr, ssize_t len);

(I believe that this list of prototypes covers the syscalls that can be implemented in this manner). So, if printf will eventually call write, if we re-implement the backing _write method to instead push that data to the serial console, we can effectively redirect stdout and stderr somewhere we can see them - we could even redirect stdout to one USART and stderr to another! But for simplicity, let’s just pipe both to USART1 which we set up earlier:

// Don't forget to allow external linkage if this is C++ code
extern "C" {
    ssize_t _write(int file, const char *ptr, ssize_t len);
}

int _write(int file, const char *ptr, ssize_t len) {
    // If the target file isn't stdout/stderr, then return an error
    // since we don't _actually_ support file handles
    if (file != STDOUT_FILENO && file != STDERR_FILENO) {
        // Set the errno code (requires errno.h)
        errno = EIO;
        return -1;
    }

    // Keep i defined outside the loop so we can return it
    int i;
    for (i = 0; i < len; i++) {
        // If we get a newline character, also be sure to send the carriage
        // return character first, otherwise the serial console may not
        // actually return to the left.
        if (ptr[i] == '\n') {
            usart_send_blocking(USART1, '\r');
        }

        // Write the character to send to the USART1 transmit buffer, and block
        // until it has been sent.
        usart_send_blocking(USART1, ptr[i]);
    }

    // Return the number of bytes we sent
    return i;
}

Now, we could jazz up that print output from before by prefixing all of our log messages with the current monotonic time:

int main() {
    // Previously defined clock, USART, etc... setup elided
    // [...]

    while (true) {
        printf("[%lld] LED on\n", millis());
        gpio_set(GPIOA, GPIO11);
        delay(1000);
        printf("[%lld] LED off\n", millis());
        gpio_clear(GPIOA, GPIO11);
        delay(1000);
    }
}

Serial console logs, with formatting

As before, the final source code for this post is available on Github.

In the next post, we will go over SPI and memory-to-peripheral DMA.

Comments