6 ways to communicate with stm32, part 2. UART and GDB


This post is part of a series: how to talk to the stm32 in 6 different ways as well as some interesting things to do with each method. The code mentioned in this post, a working example, can be found in my github.
In my last post, I talked about how to start from zero to the point where you can blink a light on the cheap stm32f103c8t6 “blue pill” board.

This image came from here: https://developer.mbed.org/users/hudakz/code/STM32F103C8T6_Hello/

In that post (and its associated video), I showed how to use:

  • STMCubeMX to get the basic register settings,
  • the additional code needed to blink an LED,
  • how to compile (including where to get the compiler),
  • and how to upload to the module via GDB/OpenOCD

In this post, I build on that to show how to talk to the module via the UART from a terminal window on your computer. Topics/steps I’ll cover:

  • Needed changes in STMCubeMX
  • Some tips on reading the HAL manual published by ST corp.
  • How to send UART messages using two available methods. (polling and interrupt)
  • How to receive these messages using the minicom terminal
  • Bonus: how to unbrick an unresponsive module using the stm32flash utility

This video shows many of the concepts of this post, more details below.

GDB is an important communication channel

Before I go into UART, I want to emphasize the importance of GDB as a communication channel1 GDB is an important, perhaps the main, reason that I don’t just do everything in the Arduino environment.
Throughout my career, it seems that a large portion of software developers I worked with were content to debug only with printfs. I don’t know how they do it. Some/many of them are even very good, but without the use of a debugger, I’d feel crippled. It’s such an important tool. Writing code is one thing, but what about getting it to work? What if you need to understand old or someone else’s code? Nothing beats stepping through it.
In my last post about stm32, I use gdb as my programming utility. Once it’s done reprogramming the module, I have a debug prompt. It’s all done in a .gdbinit file, which can be found on my github.
To recap the gdb commands I use, to connect to openocd:
[code]
#export GDBEXEC=/bubba/electronicsDS/stm32/gcc/gcc-arm-none-eabi-6-2017-q1-update/bin/arm-none-eabi-gdb-py
#export OPENOCD=/bubba/electronicsDS/stm32/openocd-0.10.0/install
file build/basic_uart.elf
target remote | $OPENOCD/bin/openocd -f $OPENOCD/share/openocd/scripts/interface/stlink-v2.cfg -f $OPENOCD/share/openocd/scripts/target/stm32f1x.cfg -c "gdb_port pipe; log_output openocd.log"
[/code]
To reprogram:
[code]
define reload
monitor reset halt
monitor stm32f1x mass_erase 0
monitor program build/basic_uart.elf verify
monitor reset halt
end
[/code]
To reboot/reset:
[code]
define restart
monitor reset halt
end
[/code]
And that’s what I have to say about that.

Needed hardware

Beyond the stm32f103c8t6 module and stlink-v2 programmer (each <$2 on AliExpress/Ebay), to replicate this post you will need an FTDI usb adapter and some dupont cables (also <$2 each)

Getting additional boilerplate using STMCubeMX

Beyond the blinky project, you’ll want to make a couple changes:

  • In the pinout tab, activate the USART1 feature by setting it to asynchronous.2
  • (optional but recommended) If you want to use the interrupt based RX/TX features, in the configuration tab, click USART1 and enable the USART1 global interupt in the NVIC sub-tab.
  • (optional) also in the tab, under USART1 the baud rate and related can be selected under the parameters sub-tab.
  • (optional but recommended) in project settings->code generator select “generate peripheral initialization as a pair of .c/.h files.
  • If using DMA based RX. under USART1 -> DMA Settings -> Add -> DMA request = USART1_RX. Also set mode to circular

Click “generate source” icon. The results can be found here.

Understanding the HAL documentation

Section 41.2 of the HAL documentation (UM1850) describes how to use ST’s HAL to use the UART. The documentation is mostly pretty good. The main thing I wish they’d provide is the specific code needed to do what’s described. STMCubeMX gives you this code, but it’s sometimes confusing to know which parts you still need to add.
In the case of UART, everything described in the reference manuals “how to use this driver” (page 599) is provided by the STMCubeMX output. You only need to add calls to the polling/interrupt IO functions.
I’ve found UM1850 to be the most useful document on understanding my devices.

Sending UART messages without interrupts

You have the option of sending messages in polling mode via a blocking call; it doesn’t return until the message is sent.
[code]
HAL_UART_StateTypeDef state = HAL_UART_GetState(&huart1);
if (state == HAL_UART_STATE_READY ||
state == HAL_UART_STATE_BUSY_RX) {
HAL_StatusTypeDef tstate;
tstate = HAL_UART_Transmit(&huart1, themessage, sizeof(themessage), HAL_MAX_DELAY);
state = HAL_UART_GetState(&huart1);
}
[/code]

Sending UART message with interrupts

The other way of sending UART messages it to send them and forget them, optionally giving an interrupt callback to the system to let you know when transmision is complete. Note that for HAL_UART_Transmit_IT to work, UART global interrupts must be enabled. This is true, even if you don’t care about message completion interrupts. Even if you just want to send the message and forget about it.
[code]
HAL_UART_StateTypeDef state = HAL_UART_GetState(&huart1);
if (state == HAL_UART_STATE_READY ||
state == HAL_UART_STATE_BUSY_RX) {
HAL_StatusTypeDef tstate;
tstate = HAL_UART_Transmit_IT(&huart1, theothermessage, sizeof(theothermessage));
state = HAL_UART_GetState(&huart1);
}
[/code]
If you want to be notified when the message is finished
[code]
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
HAL_UART_StateTypeDef state = HAL_UART_GetState(huart);
if (state == HAL_UART_STATE_READY ||
state == HAL_UART_STATE_BUSY_RX) {
} else {
}
}
[/code]

Receiving UART

Receiving is a little trickier. The APIs want you to tell them how many bytes you want to receive. If you’re getting typed input, then

  1. you don’t know how long the message will be and
  2. you want to process data as it’s received

One way of doing this is by having the incoming data stored in a circular/ring buffer and regularly checking the read position via polling.
I’ll show two ways of implementing this buffer3.

  1. Direct interrupt based UART and
  2. DMA4 based UART

For both methods, I define a global to hold the buffer as well as a circle count. The latter is to help you know if the data has wrapped since you last looked.
[code]
uint8_t received[] = " \n\r";
uint8_t num_rx_rounds = 0;
[/code]
In the beginning of main, I initiate receiving. Note that I reduce the size by three. This is to maintain the printability of the string when debugging. \r,\n, and null termination.
[code]
// -3 because of \n \r and 0x0 to end the string
if (use_dma) {
HAL_UART_Receive_DMA(&huart1, received, sizeof(received)-3);
} else {
HAL_UART_Receive_IT(&huart1, received, sizeof(received)-3);
}
[/code]
And the interrupt callback. In DMA mode, just increment the counter. If using Interrupt based, I restart the buffer whenever it’s full.
[code]
void HAL_UART_RxCpltCallback (UART_HandleTypeDef *huart)
{
num_rx_rounds++;
if (use_dma) {
// don’t need to do anything. DMA is circular
} else {
__attribute__((unused)) HAL_StatusTypeDef state;
// -3 because of \n \r and 0x0 to end the string
state = HAL_UART_Receive_IT(&uart1, received, sizeof(received)-3);
}
}
[/code]
Also, when transmitting, you’ll need to check for RX in addition to ready. For example:
[code]
if (state == HAL_UART_STATE_READY || state == HAL_UART_STATE_BUSY_RX) {
[/code]

Knowing how far along you are

With both DMA and interrupt based receive, unless you’re only giving a single byte buffer, you’ll want to know what’s been received so far. That information is accessed by reading the CNDTR control register. You can read more about it in RM0008 section 13.4.4:
[code]
// this register tells you how many bytes the receive function
// is still waiting for.
huart1.hdmarx->Instance->CNDTR;
[/code]

Send/receiving from your computer

Now that the device is reading/writing (we hope), how do we connect to it from a computer? Simple

  • connect the stm32 to an ftdi adapter,
  • plug the usb part of the adapter to your computer
  • use a terminal program like minicom, screen, or picocom to talk.

Connecting the module to ftdi adapter

this image comes from http://www.arduinesp.com/getting-started

You only need two wire connections:

  • stm32 pin A9 to FTDI RX
  • stm32 pin A10 to FTDI TX
  • GND to GND
  • optionally, you can connect Vcc. It’s safest to only power the stm32 module in one way. If it’s also connected to st-link, it’s probably getting power from it. If not, the ftdi adapter can provide power. pick just one to be safest.

Another good how-to can be found here. It’s a good link if you want to try using these modules in the arduino environment. 5

Minicom and hardware flow control

[code]
minicom –baudrate 115200 –device /dev/ttyUSB0
[/code]
By default, minicom has hardware flow control turned on. In this mode, you’ll find that letters typed in the terminal don’t reach your module. You can turn it off.
ctrl-A -> O -> serial port setup -> F
Credit goes to this stackoverflow answer: https://stackoverflow.com/a/7876053/23630

Bonus item – unbricking

I’ve had it happen that my modules can become unresponsive to reprogramming. The STLink just couldn’t talk to it. Most of the time, this was due to bugs in STMCubeMX generated code. As of late, it hasn’t happened much; the bugs seem to have been fixed. In any case, if you find that your stm32 module is not responding to anything, the solution may be pretty easy.
Built into the STM32 is a hardware bootloader which you can access via UART (one of the reasons I’m talking about it in this post). There are two steps required

  • change the header option on the module to active the bootloader
  • use the stm32flash utility

To use the bootloader, move the header jumper next to the pin header. See red circle in the picture. Click to get a larger version of this image.

[code]
sudo apt install stm32flash
// the -o option says to erase everything.
stm32flash /dev/ttyUSB0 -o
[/code]
 
I hope you found this post useful, see my other stm32 videos here.


  1. GDB is the second of the six ways I’m covering. Blinking is the first and UART is the third. I summarize all six on this page.

  2. there are several other options, but I haven’t used them and don’t know why one would choose them

  3. The HAL also provides the function HAL_UART_Receive, but I don’t see it as usable. This function only returns when data is received and the processor does nothing else in the mean time. It’s a blocking function.

  4. Don’t forget to add DMA in STMCubeMX above

  5. Again, the main thing I don’t like about arduino env is the inability to use a proper debugger like GDB. To be fair, this is more of a criticism of AVR. You can also use the arduino env on the stm32 module I’m using and then debug. That would be outside of arduino. Since arduino has more tutorials and demo code, I find it’s a useful place to steal code.


Leave a Reply

Your email address will not be published. Required fields are marked *