Skip to main content
Associate
January 1, 2026
Question

USART stm32h5 rust

  • January 1, 2026
  • 4 replies
  • 603 views

Hi everyone,

I’m bringing up USART2 on an STM32 using bare-metal Rust (#![no_std]), direct register access (no HAL, no CubeMX).

The CODE looks correctly

However, the data itself is wrong.

Instead of transmitting 'A' (0x41), the terminal consistently receives values like:

  • 0xCE

  • 0x05

(always stable, but never 0x41).

#![no_std]
#![no_main]

use core::ptr::{read_volatile, write_volatile};
use core::panic::PanicInfo;
use cortex_m_rt::entry;

#[panic_handler]
fn panic(_: &PanicInfo) -> ! {
loop {}
}

#[inline(always)]
fn r(addr: u32) -> u32 {
unsafe { read_volatile(addr as *const u32) }
}

#[inline(always)]
fn w(addr: u32, val: u32) {
unsafe { write_volatile(addr as *mut u32, val) }
}

/* ================= BASE ================= */
const RCC: u32 = 0x4402_0C00;
const GPIOA: u32 = 0x4202_0000;
const USART2: u32 = 0x4000_4400;

/* ================= RCC ================= */
const RCC_AHB2ENR: u32 = RCC + 0x08C;
const RCC_APB1LENR: u32 = RCC + 0x09C;
const RCC_CCIPR2: u32 = RCC + 0x0DC;

/* ================= GPIO ================= */
const GPIOA_MODER: u32 = GPIOA + 0x00;
const GPIOA_AFRL: u32 = GPIOA + 0x20;

/* ================= USART ================= */
const USART_CR1: u32 = USART2 + 0x00;
const USART_BRR: u32 = USART2 + 0x0C;
const USART_ISR: u32 = USART2 + 0x1C;
const USART_TDR: u32 = USART2 + 0x28;
const USART_PRESC: u32 = USART2 + 0x2C;

/* ================= BITS ================= */
const USART_CR1_UE: u32 = 1 << 0;
const USART_CR1_RE: u32 = 1 << 2;
const USART_CR1_TE: u32 = 1 << 3;

const USART_ISR_TXFNF: u32 = 1 << 7;
const USART_ISR_TEACK: u32 = 1 << 21;
const USART_ISR_REACK: u32 = 1 << 22;

#[entry]
fn main() -> ! {

/* -------- CLOCK ENABLE -------- */

// GPIOA enable
w(RCC_AHB2ENR, r(RCC_AHB2ENR) | (1 << 0));

// USART2 enable
w(RCC_APB1LENR, r(RCC_APB1LENR) | (1 << 17));

// USART2 clock = PCLK1
w(RCC_CCIPR2, r(RCC_CCIPR2) & !(0b111 << 3));

/* -------- GPIO -------- */

// PA2, PA3 → AF
w(GPIOA_MODER,
(r(GPIOA_MODER) & !(0b1111 << 4)) | (0b1010 << 4)
);

// AF7
w(GPIOA_AFRL,
(r(GPIOA_AFRL) & !(0xFF << 8)) | (0x77 << 8)
);

/* -------- USART -------- */

// Disable
w(USART_CR1, 0);
while (r(USART_ISR) & (USART_ISR_TEACK | USART_ISR_REACK)) != 0 {}

// Prescaler /1
w(USART_PRESC, 0);

// 115200 @ 16 MHz
w(USART_BRR, 16_000_000 / 115_200);

// Enable TX RX
w(USART_CR1, USART_CR1_TE | USART_CR1_RE);

// Enable USART
w(USART_CR1, r(USART_CR1) | USART_CR1_UE);

while (r(USART_ISR) & USART_ISR_TEACK) == 0 {}

/* -------- TX LOOP -------- */
loop {
while (r(USART_ISR) & USART_ISR_TXFNF) == 0 {}
w(USART_TDR, b'A' as u32);
}
}

If anyone has run into “stable baud but wrong characters” on STM32 USART, I’d really appreciate pointers on what to double-check.

Thanks!



4 replies

Bob S
Super User
January 2, 2026

How do you know the baud rate is correct?  Have you looked at the TX line with a scope?

I don't know specifically for the H5 for sure, but with other STM32 families, if you want to send 1 byte you need to write 1 byte, not a 32-bit value (which will actually send 2 bytes - in your code that would be 0x41 0x00, or the other way around, I don't recall).

How do you have USART2 connected to to your PC?  Is this a Nucleo board and USART2 is the port connected through the on-board STLink?

You can always use CubeMX to generate a simple program, or use one of the sample UART projects, run that and see what the register values are, then compare to your register values.

Andrew Neil
Super User
January 6, 2026

@Bob S wrote:

if you want to send 1 byte you need to write 1 byte, not a 32-bit value


Well, the USART TDR register is a 32-bit register:

AndrewNeil_0-1767713136779.png

https://www.st.com/resource/en/reference_manual/rm0492-stm32h503-line-armbased-32bit-mcus-stmicroelectronics.pdf#page=1421

 


A complex system that works is invariably found to have evolved from a simple system that worked.A complex system designed from scratch never works and cannot be patched up to make it work.
Bob S
Super User
January 9, 2026

> Well, the USART TDR register is a 32-bit register:

Right-o, I was thinking of the SPI data registers, that can take 8/16/32 bit writes.

Duc
Senior
January 6, 2026

Hi @HardFaultHero ,

Could you please share your Rust project so I can try debugging it on my side?

Pavel A.
Super User
January 6, 2026

 the terminal consistently receives values like:

Which terminal? Does it expect specific parity (odd or even)? Number of bits?

You seem to use the FIFO mode (TXFNF) - have you enabled it?

Associate
January 9, 2026

STM32H533-NUCLEO
After debugging, the baud rate and clock configuration were correct.
The real issue I had was in my RX code, not in the hardware or clock tree.

Below is a minimal working Rust project that continuously transmits the character 'A' at 115200 baud.

Hopefully this helps someone else.

 

.
├── .cargo
│ └── config.toml
├── Cargo.toml
├── memory.x
└── src
 └── main.rs

this is the directory configuration
[package]
name = "stm32h5tls"
version = "0.1.0"
edition = "2024"

[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"

this is the Cargo.toml
#![no_std]
#![no_main]

use core::ptr::{read_volatile, write_volatile};
use core::panic::PanicInfo;
use cortex_m_rt::entry;

#[panic_handler]
fn panic(_: &PanicInfo) -> ! {
 loop {}
}

#[inline(always)]
fn r(addr: u32) -> u32 {
 unsafe { read_volatile(addr as *const u32) }
}

#[inline(always)]
fn w(addr: u32, val: u32) {
 unsafe { write_volatile(addr as *mut u32, val) }
}

/* ================= BASE ================= */
const RCC: u32 = 0x4402_0C00;
const GPIOA: u32 = 0x4202_0000;
const USART2: u32 = 0x4000_4400;

/* ================= RCC ================= */
const RCC_CR: u32 = RCC + 0x000;
const RCC_CFGR1: u32 = RCC + 0x01C;
const RCC_CFGR2: u32 = RCC + 0x020;
const RCC_AHB2ENR: u32 = RCC + 0x08C;
const RCC_APB1LENR: u32 = RCC + 0x09C;
const RCC_CCIPR1: u32 = RCC + 0x0D8;

/* ================= GPIO ================= */
const GPIOA_MODER: u32 = GPIOA + 0x00;
const GPIOA_AFRL: u32 = GPIOA + 0x20;

/* ================= USART ================= */
const USART_CR1: u32 = USART2 + 0x00;
const USART_BRR: u32 = USART2 + 0x0C;
const USART_ISR: u32 = USART2 + 0x1C;
const USART_TDR: u32 = USART2 + 0x28;
const USART_PRESC: u32 = USART2 + 0x2C;

/* ================= BITS ================= */
const USART_ISR_TXFNF: u32 = 1 << 7;
const USART_ISR_TEACK: u32 = 1 << 21;

#[entry]
fn main() -> ! {

 /* ============================================================
 * CLOCK — HSI 64 MHz / 4 = 16 MHz
 * ============================================================ */

 /* HSI ON + HSIDIV=/4 */
 w(
 RCC_CR,
 (r(RCC_CR) & !(0b11 << 3)) |
 (0b10 << 3) | // HSIDIV = /4
 (1 << 0) // HSION
 );

 /* wait HSIRDY */
 while r(RCC_CR) & (1 << 1) == 0 {}

 /* SYSCLK = HSI */
 w(RCC_CFGR1, r(RCC_CFGR1) & !0b11);

 /* wait SWS = HSI */
 while (r(RCC_CFGR1) >> 3) & 0b11 != 0 {}

 /* CFGR2: bus ON, no prescaler */
 w(RCC_CFGR2, 0x0000_0000);

 /* ============================================================
 * PERIPHERAL CLOCK ENABLE
 * ============================================================ */

 /* GPIOA clock */
 w(RCC_AHB2ENR, r(RCC_AHB2ENR) | (1 << 0));

 /* USART2 clock */
 w(RCC_APB1LENR, r(RCC_APB1LENR) | (1 << 17));

 /* USART2 kernel clock = HSI */
 w(
 RCC_CCIPR1,
 (r(RCC_CCIPR1) & !(0b111 << 3)) |
 (0b011 << 3)
 );

 /* ============================================================
 * GPIO — PA2 / PA3 AF7
 * ============================================================ */

 /* PA2, PA3 → Alternate Function */
 w(
 GPIOA_MODER,
 (r(GPIOA_MODER) & !((0b11 << 4) | (0b11 << 6))) |
 ((0b10 << 4) | (0b10 << 6))
 );

 /* PA2, PA3 → AF7 */
 w(
 GPIOA_AFRL,
 (r(GPIOA_AFRL) & !((0b1111 << 8) | (0b1111 << 12))) |
 ((0b0111 << 8) | (0b0111 << 12))
 );

 /* ============================================================
 * USART2 INIT — TX attivo, RX pronto
 * ============================================================ */

 /* Disable USART */
 w(USART_CR1, 0x0000_0000);

 /* Prescaler /1 */
 w(USART_PRESC, 0x0000_0000);

 /* BRR = 16 MHz / 115200 ≈ 139 */
 w(USART_BRR, 139);

 /* UE + TE + RE */
 w(USART_CR1, (1 << 3) | (1 << 2) | (1 << 0));

 /* wait TEACK */
 while r(USART_ISR) & USART_ISR_TEACK == 0 {}

 /* ============================================================
 * TX LOOP — transmit only 'A'
 * ============================================================ */
 loop {
 while r(USART_ISR) & USART_ISR_TXFNF == 0 {}
 w(USART_TDR, b'A' as u32);
 }
}
[build]
target = "thumbv8m.main-none-eabi"

[target.thumbv8m.main-none-eabi]
rustflags = [
 "-C", "link-arg=-Tlink.x",
]
this is the config.toml
MEMORY
{
 /* Flash non-secure: Bank1 + Bank2 = 512 KB */
 FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K

 /* SRAM1 non-secure = 128 KB */
 RAM (rwx): ORIGIN = 0x20000000, LENGTH = 128K
}
this is the memory.x