STM32开发三件套:sys.c/delay.c/usart.c深度解析

1. STM32开发中的核心三件套:sys.c/delay.c/usart.c深度解析

在STM32嵌入式开发领域,有三个基础文件几乎出现在每个工程中——sys.c、delay.c和usart.c。这三个文件构成了STM32开发的"铁三角",承担着系统初始化、精确延时和串口通信的核心功能。作为从事STM32开发多年的工程师,我发现很多初学者虽然能照搬这些代码使用,但对其中精妙的设计思想和实现细节理解不深。今天我们就以usart.c为重点,深入剖析这三个关键文件的实现原理和最佳实践。

2. USART模块的架构设计与实现原理

2.1 串口通信的基础配置

USART(Universal Synchronous/Asynchronous Receiver/Transmitter)是STM32中最重要的通信接口之一。在usart.c中,uart_init函数完成了所有必要的初始化工作:

void uart_init(u32 bound){ // GPIO端口设置 GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; // 时钟使能 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE); // TX引脚配置(PA9) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_Init(GPIOA, &GPIO_InitStructure); // RX引脚配置(PA10) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); // NVIC配置 NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); // USART参数配置 USART_InitStructure.USART_BaudRate = bound; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, &USART_InitStructure); // 使能中断和串口 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE); }

这段代码有几个关键点需要注意:

  1. GPIO模式配置:TX引脚必须设置为复用推挽输出(GPIO_Mode_AF_PP),而RX引脚应配置为浮空输入(GPIO_Mode_IN_FLOATING)
  2. 中断优先级配置:NVIC_PriorityGroupConfig需要在main函数中先调用,确定优先级分组
  3. 波特率计算:实际波特率=APB时钟/(16*USARTDIV),其中USARTDIV是一个固定点小数

提示:在调试串口通信时,最常见的错误就是GPIO模式配置不正确。务必确认TX和RX引脚的模式设置准确。

2.2 printf重定向的实现机制

在嵌入式开发中,printf调试是极其重要的手段。usart.c中通过重定义fputc函数实现了printf到串口的重定向:

#pragma import(__use_no_semihosting) struct __FILE { int handle; }; FILE __stdout; void _sys_exit(int x) { x = x; } int fputc(int ch, FILE *f) { while((USART1->SR & 0X40) == 0); // 等待发送缓冲区空 USART1->DR = (u8)ch; return ch; }

这段代码的工作原理:

  1. __use_no_semihosting告诉编译器不使用半主机模式
  2. 定义了基本的FILE结构和__stdout
  3. 重写fputc函数,将字符通过USART1发送
  4. 通过轮询USART_SR寄存器的TXE位确保数据发送完成

实际项目中,我建议将这个重定向代码封装成宏,方便在不同串口间切换:

#define PRINTF_USART USART1 int fputc(int ch, FILE *f) { while((PRINTF_USART->SR & USART_SR_TXE) == 0); PRINTF_USART->DR = (u8)ch; return ch; }

2.3 中断接收协议设计

usart.c中实现了一个精巧的接收协议,通过USART_RX_BUF缓冲区和USART_RX_STA状态寄存器协同工作:

#define USART_REC_LEN 200 u8 USART_RX_BUF[USART_REC_LEN]; u16 USART_RX_STA = 0; void USART1_IRQHandler(void) { u8 Res; if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { Res = USART_ReceiveData(USART1); if((USART_RX_STA & 0x8000) == 0) { if(USART_RX_STA & 0x4000) { if(Res != 0x0a) USART_RX_STA = 0; else USART_RX_STA |= 0x8000; } else { if(Res == 0x0d) USART_RX_STA |= 0x4000; else { USART_RX_BUF[USART_RX_STA & 0X3FFF] = Res; USART_RX_STA++; if(USART_RX_STA > (USART_REC_LEN-1)) USART_RX_STA = 0; } } } } }

这个协议的设计特点:

  1. 使用0x0D 0x0A作为帧结束标志(兼容串口助手习惯)
  2. USART_RX_STA的高16位用作状态标志:
    • bit15:接收完成标志
    • bit14:已收到0x0D
    • bit13-0:有效数据长度
  3. 自动处理接收溢出和错误情况

在实际项目中,我通常会做以下改进:

  1. 增加超时机制:当两个字符间隔超过一定时间认为帧结束
  2. 支持二进制数据:避免将0x0D 0x0A作为特殊字符处理
  3. 使用环形缓冲区:提高缓冲区利用率

3. SYS.C系统初始化关键点解析

3.1 时钟树配置与系统初始化

sys.c通常包含系统级的初始化函数,最核心的是系统时钟配置。以STM32F103为例,典型的时钟配置流程:

void SystemInit(void) { // 复位RCC寄存器 RCC->CR |= (uint32_t)0x00000001; RCC->CFGR = 0x00000000; RCC->CR &= (uint32_t)0xFEF6FFFF; RCC->CR &= (uint32_t)0xFFFBFFFF; RCC->CFGR &= (uint32_t)0xFF80FFFF; RCC->CIR = 0x00000000; // 设置外部晶振和PLL RCC->CR |= ((uint32_t)RCC_CR_HSEON); while(!(RCC->CR & RCC_CR_HSERDY)); // 配置FLASH预取指和等待状态 FLASH->ACR = FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY_2; // 配置PLL为72MHz RCC->CFGR |= RCC_CFGR_PLLMULL9; RCC->CFGR |= RCC_CFGR_PLLSRC; // 使能PLL并等待就绪 RCC->CR |= RCC_CR_PLLON; while(!(RCC->CR & RCC_CR_PLLRDY)); // 切换系统时钟到PLL RCC->CFGR |= RCC_CFGR_SW_PLL; while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL); }

关键参数说明:

  1. FLASH等待状态:根据时钟频率设置,72MHz需要2个等待状态
  2. PLL倍频系数:外部8MHz晶振×9=72MHz
  3. 时钟源选择:HSE(外部高速晶振)通常比HSI(内部RC振荡器)更精确

3.2 中断优先级分组配置

NVIC(嵌套向量中断控制器)配置对系统实时性至关重要:

void NVIC_Configuration(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); }

STM32有4种优先级分组方式:

  • NVIC_PriorityGroup_0:0位抢占优先级,4位子优先级
  • NVIC_PriorityGroup_1:1位抢占优先级,3位子优先级
  • NVIC_PriorityGroup_2:2位抢占优先级,2位子优先级
  • NVIC_PriorityGroup_3:3位抢占优先级,1位子优先级
  • NVIC_PriorityGroup_4:4位抢占优先级,0位子优先级

选择优先级分组2(2位抢占,2位响应)是最常用的配置,可以在中断响应速度和嵌套深度间取得平衡。

4. DELAY.C精确延时实现剖析

4.1 SysTick定时器原理

delay.c的核心是利用SysTick定时器实现精确延时。SysTick是Cortex-M内核的一个24位递减计数器,具有以下特点:

  1. 时钟源可选处理器时钟或外部参考时钟
  2. 计数到0时会产生中断并可自动重装载
  3. 提供计数标志位便于查询

初始化代码示例:

void delay_init(u8 SYSCLK) { SysTick->CTRL &= ~(1 << 2); // 选择外部时钟源 fac_us = SYSCLK / 8; // 1us需要的计数 fac_ms = fac_us * 1000; // 1ms需要的计数 }

4.2 微妙级和毫秒级延时实现

基于SysTick的延时函数实现:

void delay_us(u32 nus) { u32 temp; SysTick->LOAD = nus * fac_us; SysTick->VAL = 0x00; SysTick->CTRL |= 1 << 0; do { temp = SysTick->CTRL; } while((temp & 0x01) && !(temp & (1 << 16))); SysTick->CTRL &= ~(1 << 0); SysTick->VAL = 0x00; } void delay_ms(u16 nms) { u32 temp; SysTick->LOAD = nms * fac_ms; SysTick->VAL = 0x00; SysTick->CTRL |= 1 << 0; do { temp = SysTick->CTRL; } while((temp & 0x01) && !(temp & (1 << 16))); SysTick->CTRL &= ~(1 << 0); SysTick->VAL = 0x00; }

使用注意事项:

  1. delay_ms的最大延时受限于24位计数器,在72MHz下最大约186ms
  2. 延时期间不能修改SysTick配置
  3. 在中断中调用时需考虑中断优先级

4.3 精确延时的替代方案

对于需要更长延时的场景,可以采用定时器+软件计数的方式:

volatile u32 timer_count = 0; void TIM2_IRQHandler(void) { if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { timer_count++; TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } } void delay_s(u32 sec) { u32 target = timer_count + sec * 1000; while(timer_count < target); }

5. 三模块协同工作实例分析

5.1 典型main函数流程

一个典型的工程中main函数会依次初始化这三个模块:

#include "sys.h" #include "delay.h" #include "usart.h" int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); delay_init(72); uart_init(115200); while(1) { printf("System running...\r\n"); delay_ms(1000); } }

初始化顺序很重要:

  1. 先配置中断优先级分组
  2. 然后初始化系统时钟和延时
  3. 最后初始化外设如USART

5.2 调试信息输出最佳实践

结合printf和延时函数,可以实现丰富的调试信息输出:

void debug_log(const char *format, ...) { va_list args; va_start(args, format); printf("[%lu] ", timer_count); vprintf(format, args); printf("\r\n"); va_end(args); }

这种封装的好处:

  1. 自动添加时间戳
  2. 保持格式统一
  3. 方便后期添加日志等级过滤

5.3 性能优化技巧

  1. 串口发送优化:使用DMA减少CPU占用
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); DMA_Init(DMA1_Channel4, &DMA_InitStructure);
  1. 延时函数优化:使用RTOS的系统时钟时基
  2. 系统时钟优化:根据实际需求选择适当的时钟频率

6. 常见问题与解决方案

6.1 串口通信异常排查

现象可能原因解决方案
无任何输出波特率不匹配检查两端波特率设置
乱码时钟配置错误确认系统时钟和USART时钟源
数据丢失缓冲区溢出增大接收缓冲区或提高处理速度
只能收不能发TX引脚模式错误检查GPIO_Mode_AF_PP设置

6.2 延时不准问题分析

  1. 时钟源选择错误:确认SysTick使用的是系统时钟还是外部时钟
  2. 时钟频率参数错误:检查delay_init传入的参数是否与实际系统时钟一致
  3. 中断干扰:高优先级中断可能导致延时变长

6.3 系统启动失败排查

  1. 检查启动文件(startup_stm32f10x_xx.s)是否正确
  2. 确认系统初始化函数被调用
  3. 检查堆栈大小设置是否足够
  4. 排查是否有硬件故障或电源问题

7. 进阶应用与扩展思考

7.1 多串口管理策略

在实际项目中,经常需要管理多个串口设备。可以采用面向对象的思想封装串口操作:

typedef struct { USART_TypeDef *USARTx; GPIO_TypeDef *GPIOx; uint16_t tx_pin; uint16_t rx_pin; uint8_t *rx_buf; uint16_t buf_size; uint16_t rx_status; } UART_Device; void UART_Init(UART_Device *dev, uint32_t baudrate); void UART_Send(UART_Device *dev, uint8_t *data, uint16_t len);

7.2 低功耗模式下的延时处理

在低功耗应用中,传统的延时方式可能不适用。可以考虑:

  1. 使用RTC唤醒实现长延时
  2. 在睡眠模式下使用低功耗定时器(LPTIM)
  3. 动态调整系统时钟降低功耗

7.3 跨平台兼容性设计

为了使代码更容易移植到不同型号的STM32或其他ARM芯片,可以:

  1. 使用宏定义抽象硬件差异
  2. 通过函数指针实现硬件抽象层(HAL)
  3. 将硬件相关代码集中管理

通过深入理解sys.c、delay.c和usart.c这三个基础文件的设计原理和实现细节,开发者可以构建更稳定、高效的STM32应用程序。这三个模块虽然看似简单,但其中蕴含着嵌入式系统设计的许多核心思想,值得反复研究和实践。