Polling UART byte by byte wastes CPU cycles and drops characters under load. In this lesson you will build a proper serial command shell that receives data through DMA into a circular buffer, detects complete commands using UART idle line detection, and processes them without blocking. The shell gives you a live debugging interface: type a command and the STM32 responds instantly with register values, pin states, or confirmation of your PWM changes. #STM32 #UART #DMA
What We Are Building
Interactive Command Shell over Serial
A command-line interface running on the Blue Pill that accepts typed commands over UART and executes them in real time. The receive path uses DMA with a circular buffer and idle line detection so no bytes are ever lost, even when the CPU is busy processing a previous command. The transmit path also uses DMA to send long responses without blocking.
Project specifications:
Parameter
Value
Board
Blue Pill (STM32F103C8T6)
UART
USART1 at 115200 baud
TX pin
PA9 (USART1_TX)
RX pin
PA10 (USART1_RX)
RX DMA channel
DMA1 Channel 5 (USART1_RX)
TX DMA channel
DMA1 Channel 4 (USART1_TX)
RX buffer size
256 bytes (circular)
Commands
pwm, read, dump, help, reset
Parts needed
No new parts (reuse Blue Pill, ST-Link, serial adapter)
Supported Commands
Command
Example
Description
help
help
List all available commands
pwm <ch> <duty>
pwm 1 75
Set TIM2 channel 1 to 75% duty
read <port><pin>
read A5
Read the digital state of PA5
dump <periph>
dump RCC
Print all registers of a peripheral
reset
reset
Software reset the MCU
NVIC and Interrupt Priorities
The Nested Vectored Interrupt Controller (NVIC) manages all interrupts on the Cortex-M3. It supports up to 240 external interrupts with programmable priority levels. On the STM32F103, there are 4 bits of priority, but by default only the upper 4 bits are used, giving 16 priority levels (0 being the highest priority). Interrupts with lower priority numbers preempt those with higher numbers.
Priority Grouping
The NVIC processes interrupts based on their priority number. Lower numbers mean higher urgency. When a higher-priority interrupt fires during a lower-priority ISR, it preempts immediately.
NVIC interrupt nesting:
Priority 0 (highest)
Priority 1 | +--[USART1 IDLE]--+
Priority 2 | | +--[DMA TX]--+ |
Priority 3 | | | | |
| | | | |
main() ----+--+--+ +-+---
^ ^ ^ ^
USART1 DMA_TX DMA USART1
fires preempts done returns
USART1
The 4 priority bits can be split between preemption priority (which interrupts can preempt others) and sub-priority (tie-breaking when two interrupts fire simultaneously):
Group Setting
Preemption Bits
Sub-priority Bits
Preemption Levels
Group 4 (default)
4
0
16 levels, no sub-priority
Group 3
3
1
8 levels, 2 sub-priorities
Group 2
2
2
4 levels, 4 sub-priorities
Setting Priorities for This Project
voidnvic_config(void) {
/* Use priority group 2: 4 preemption levels, 4 sub-priority levels */
NVIC_SetPriorityGrouping(5); /* 5 means group 2 on Cortex-M3 */
DMA (Direct Memory Access) transfers data between peripherals and memory without CPU involvement. The DMA controller reads from one address and writes to another, incrementing pointers and decrementing the count automatically.
DMA transfer: USART1 RX to memory
+----------+ +-----------+ +--------+
| USART1 | | DMA1 | | SRAM |
| DR reg |---->| Channel 5 |---->| rx_buf |
| (CPAR) | | | | (CMAR) |
+----------+ | CNDTR=256 | +--------+
| (count) |
Peripheral +-----------+ Memory
address fixed, address
no increment increments
In circular mode, CNDTR reloads
automatically and CMAR wraps to start.
The CPU sets up the transfer (source address, destination address, byte count, direction) and the DMA controller handles the rest. On the STM32F103, DMA1 has 7 channels, each hardwired to specific peripheral requests. You cannot freely assign any peripheral to any channel.
CNDTR (Number of Data Register): how many transfers to perform
CPAR (Peripheral Address Register): the peripheral data register address
CMAR (Memory Address Register): the memory buffer address
UART Receive with DMA (Circular Buffer)
Why Circular Mode
In normal DMA mode, the transfer stops when CNDTR reaches zero, and you must reconfigure the channel to start again. In circular mode, the DMA controller automatically reloads CNDTR and wraps back to the start of the buffer. Combined with UART idle line detection, this gives you a continuous receive path that never misses a byte and never needs to be restarted.
Implementation
#defineRX_BUF_SIZE256
uint8_trx_dma_buf[RX_BUF_SIZE];
volatileuint16_t rx_read_pos =0;
voiduart_dma_rx_init(void) {
/* Enable DMA1 clock */
RCC->AHBENR|= RCC_AHBENR_DMA1EN;
/* Configure DMA1 Channel 5 for USART1 RX */
DMA1_Channel5->CCR=0; /* Disable channel first */
DMA1_Channel5->CPAR= (uint32_t)&USART1->DR; /* Source: USART data reg */
while (uart_dma_available()>0&& count < max_len -1) {
char c = (char)uart_dma_read_byte();
if (c =='\n'|| c =='\r') {
if (count >0) break; /* End of line */
continue; /* Skip leading newlines */
}
dest[count++] = c;
}
dest[count] ='\0';
return count;
}
Idle Line Detection
The UART idle line interrupt fires when the RX line has been idle for one frame duration after receiving data. This provides natural message boundary detection.
Idle line detection:
RX line: ____ __ __ __ __ __ ____________
|__| |__| |__|
h e l p \n
<-- data --> <-- idle -->
^
IDLE interrupt
fires here
DMA circular buffer state:
+---+---+---+---+---+---+---+---+
| h | e | l | p |\n | | | |
+---+---+---+---+---+---+---+---+
^ ^
read_pos write_pos
(= 256 - CNDTR)
This is the perfect trigger for “a complete message has arrived” because most serial terminals send commands as a burst of characters followed by silence. You do not need to poll or wait for a specific delimiter; the idle interrupt tells you when the sender has stopped transmitting.
volatileuint8_t rx_data_ready =0;
voidUSART1_IRQHandler(void) {
if (USART1->SR& USART_SR_IDLE) {
/* Clear idle flag by reading SR then DR */
(void)USART1->SR;
(void)USART1->DR;
rx_data_ready =1;
}
}
voidDMA1_Channel5_IRQHandler(void) {
if (DMA1->ISR& DMA_ISR_TCIF5) {
DMA1->IFCR= DMA_IFCR_CTCIF5;
/* Buffer wrapped around; data is still valid in circular mode */
}
if (DMA1->ISR& DMA_ISR_HTIF5) {
DMA1->IFCR= DMA_IFCR_CHTIF5;
/* Half buffer filled; useful for double-buffer processing */
Comments