STM32 & OpenCM3 Part 1: Alternate Functions and USART
Wed, Sep 12, 2018 Companion code for this post available on GithubIn 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.
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:
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);
}
}
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.