2026-01-01 10:48 AM
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!
2026-01-02 5:26 AM
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.
2026-01-06 7:03 AM
Hi @HardFaultHero ,
Could you please share your Rust project so I can try debugging it on my side?
2026-01-06 7:27 AM
@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:
2026-01-06 2:54 PM - edited 2026-01-06 2:55 PM
> 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?
2026-01-08 9:43 PM
> 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.
2026-01-09 3:37 AM - edited 2026-01-09 3:40 AM
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.tomlMEMORY
{
/* 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