UART TX on STM32L432KC using CMSIS

Let’s do the simplest thing that could possibly work.

I’m using a Nucleo-32 board. It sends serial info on pin PA2, which you can pick up via the USB link that is incorporated on the board. So you don’t need extra hardware to print to the console.

Here’s an example program:

#include "delay.h"
#include "uart.h"

int main()
{
	uart2_init();

	char msg[] = "hello world\n";
	while(1) {
		uart2_send_blocking(msg, sizeof(msg));
		delayish(1000);
	}

}

As you can see, you need two functions. The first is uart2_init():

void uart2_init(void)
{
	RCC->APB1ENR1 |= RCC_APB1ENR1_USART2EN;
	gpio_pullup(GPIOA, 2);
	gpio_alt(GPIOA, 2, 7);
	uint16_t uartdiv = SystemCoreClock / 115200;
	USART2->BRR = ( ( ( uartdiv / 16 ) << USART_BRR_DIV_MANTISSA_Pos ) |
			( ( uartdiv % 16 ) << USART_BRR_DIV_FRACTION_Pos ) );
	USART2->CR1 |= USART_CR1_TE; // enable transmission
	USART2->CR1 |= USART_CR1_UE; // enable uart itself
}

The CubeIDE generates a lot of code, but when you break it down, you get something that is actually quite simple, shown above.

Although I’m not a fan of multiple layers of abstraction, a little bit of abstraction is useful. gpio_pullup() sets the pin to be a pullup, but I don’t think it’s strictly necessary.

gpio_alt() sets PA2 into its alternate function mode. If you look at the datasheet, table 15, you’ll see that PA2 UART2_TX has alternate function number 7. That’s what the “7” in the gpio_alt() function call is about. Here is the implementation of gpio_alt():

void gpio_alt(GPIO_TypeDef *port, uint32_t pin, uint8_t alt_fn)
{
	gpio_enable_rcc(port);
	gpio_set_moder(port, pin, 0b10);

	int pos = pin*4;
	uint8_t idx = 0;
	if(pin>7) { // not tested properly
		pos -= 32;
		idx = 1;
	}
	port->AFR[idx] &= ~(0b1111  << pos); // mask out
	port->AFR[idx] |= (alt_fn  << pos);
}

The function is admittedly a bit busy. We enable the port, and set the MODER for the pin to 0b10, which means “alternate function”. We then need to set an Alternate Function Register to the function number. Things are a bit fiddly, because each pin requires 4 bits to accommodate the function number. This means that to accommodate 16 pins in a port, two registers are required: a “low” port, and a “high” port. The reference sheet refers to these as AFRL and AFRH, although CMSIS actually implements it as an array of 2 registers. So the complicated-looking calculations are trying to determine which array element to use and the offset within it.

Returning to uart2_init(), after we have set up the pins, we need to set up the baud rate in the USART->BRR (Baud Rate Register). We choose 115200. Of course, things are never that easy, We don’t just slot the value into the register. We have to do a bit of jiggery-pokery by adjusting for the system core clock, working our a divisor and remainder, and putting them in the offset positions that the register is expecting.

Fortunately it’s pretty much downhill from thereon in.

Then we set the control register CR1. We enable the transmission enable bit, and then enable the uart itself. You might want to enable receiving as well, in which case you would need to set USART_CR1_RE bit.

To send a message:

void uart2_send_blocking(const char* str, int n)
{
	USART2->CR1 |= USART_CR1_TE; //send an idle frame
	while(n--) {
		while ((USART2->ISR & USART_ISR_TXE) == 0); // wait until transmission buffer is empty
		USART2->TDR = *str++; // send a char
	}
	while((USART2->ISR & USART_ISR_TC) == 0); // wait until transmission complete
}

The code is fairly self-explanatory. We send an idle frame to say that we are transmitting data (the ref manual says to do this, although I’m not certain it is necessary).

Then, for each byte we want to transmit we wait for the TX buffer to be available, and then write our byte into the TDR register.

At the end of the transmission we poll the ISR register until we are notified that transmission is complete.

You can find the code here if you are interested. The folder “jagan” contains the “library” code. I am slowly accreting functionality. I have tried to arrange, as much as possible, that the code can be used as a piecemeal replacement for HAL libraries. The goal is to completely ditch CubeIDE completely.

The code I presented above could also be a basis for working with other STM32 processors. Register names will have to change, for sure, but there should at least some commonality as to how the different mcus work.

One thing I haven’t covered here is how to do formatted output. It is typical for mcu projects to use the newlib library. I have done so in the past, but I found it a little fiddly. I prefer to create my own library for this. I will report back when I have something suitable.

About mcturra2000

Computer programmer living in Scotland.
This entry was posted in Uncategorized. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s