
1. 项目背景与硬件准备最近在做一个智能家居控制项目需要STM32通过串口接收指令控制多个LED灯的状态。手头正好有块正点原子的精英版STM32F103ZET6开发板板载4个LED和CH340G串口芯片特别适合做这个实验。先说说硬件连接开发板USART1的PA9(TX)和PA10(RX)已通过CH340G与电脑连接LED1~LED4分别连接在PB5、PB0、PB1、PC13引脚使用USB转串口模块时记得把开发板的BOOT0跳线帽接到1下载模式实测中发现个坑如果直接用杜邦线连接USB转TTL模块必须共地GND接GND否则会出现乱码。有次调试两小时才发现是地线没接气得我差点把开发板扔出窗外。2. USART串口初始化配置先上干货初始化代码要配置三个部分// 1. GPIO初始化 void GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; // 开启GPIO时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC, ENABLE); // 配置USART1的TX(PA9)为复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 配置USART1的RX(PA10)为浮空输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); // 配置LED引脚(PB5,PB0,PB1,PC13)为推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_13; GPIO_Init(GPIOC, GPIO_InitStructure); } // 2. USART初始化 void USART1_Config(void) { USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_Init(USART1, USART_InitStructure); USART_Cmd(USART1, ENABLE); }波特率计算有个小技巧当系统时钟72MHz时115200波特率对应的USARTDIV39.0625。配置时直接填整数部分39小数部分0.0625×161所以BRR寄存器应设置为0x271。3. 指令集设计与状态控制3.1 指令格式定义为了控制多个LED我设计了这样的指令格式单灯控制LEDx_ON / LEDx_OFF x1~4群组控制ALL_ON / ALL_OFF特殊效果ALL_BLINK闪烁3次查询状态STATUS?例如发送LED1_ON点亮PB5的LED发送ALL_BLINK让所有LED闪烁发送STATUS?返回当前LED状态3.2 状态机实现用状态机处理指令更可靠下面是核心逻辑typedef enum { CMD_LED1_ON, CMD_LED1_OFF, CMD_LED2_ON, // ...其他指令枚举 CMD_UNKNOWN } CommandType; CommandType parse_command(char* cmd) { if(strstr(cmd, LED1_ON)) return CMD_LED1_ON; if(strstr(cmd, LED1_OFF)) return CMD_LED1_OFF; // ...其他条件判断 return CMD_UNKNOWN; } void execute_command(CommandType cmd) { switch(cmd) { case CMD_LED1_ON: GPIO_ResetBits(GPIOB, GPIO_Pin_5); USART_SendString(USART1, LED1 is ON\r\n); break; case CMD_LED1_OFF: GPIO_SetBits(GPIOB, GPIO_Pin_5); USART_SendString(USART1, LED1 is OFF\r\n); break; // ...其他case } }4. 字符串解析与中断处理4.1 接收缓冲区管理#define MAX_CMD_LEN 32 char rx_buffer[MAX_CMD_LEN]; uint8_t rx_index 0; void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { char ch USART_ReceiveData(USART1); if(ch \n || rx_index MAX_CMD_LEN-1) { rx_buffer[rx_index] \0; process_command(rx_buffer); rx_index 0; } else { rx_buffer[rx_index] ch; } } }4.2 实用字符串处理技巧在嵌入式环境处理字符串要特别注意效率我总结了几个技巧用strncmp代替strstr做前缀匹配更高效环形缓冲区比线性缓冲区更适合高频数据使用查表法解析指令比if-else更快const CmdEntry cmd_table[] { {LED1_ON, CMD_LED1_ON}, {LED1_OFF, CMD_LED1_OFF}, // ...其他指令 }; CommandType fast_parse(char* cmd) { for(int i0; isizeof(cmd_table)/sizeof(CmdEntry); i) { if(strncmp(cmd, cmd_table[i].name, strlen(cmd_table[i].name)) 0) return cmd_table[i].cmd; } return CMD_UNKNOWN; }5. 系统整合与调试5.1 主程序逻辑int main(void) { SystemInit(); GPIO_Config(); USART1_Config(); // 使能接收中断 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); NVIC_EnableIRQ(USART1_IRQn); // 发送欢迎信息 USART_SendString(USART1, LED Control System Ready\r\n); while(1) { // 主循环可以添加其他任务 __WFI(); // 进入低功耗模式 } }5.2 调试经验分享调试时踩过几个坑波特率不匹配电脑端和STM32的波特率必须完全一致中断优先级如果系统有其他中断要合理设置USART中断优先级缓冲区溢出一定要做长度检查我有次就因为没检查导致系统崩溃电气干扰长距离传输时建议加120Ω终端电阻推荐使用SecureCRT或Putty作为串口调试工具比普通的串口助手更稳定。调试时可以添加这样的调试信息#define DEBUG(fmt, ...) \ do { \ char dbg_buf[64]; \ sprintf(dbg_buf, [DEBUG] fmt \r\n, ##__VA_ARGS__); \ USART_SendString(USART1, dbg_buf); \ } while(0)6. 功能扩展与优化6.1 增加指令校验为防止误操作可以增加简单的校验机制指令格式#CMD?问号是校验和校验和计算所有字符异或bool verify_command(char* cmd) { if(cmd[0] ! # || strlen(cmd) 3) return false; char checksum 0; for(int i1; istrlen(cmd)-1; i) { checksum ^ cmd[i]; } return (checksum cmd[strlen(cmd)-1]); }6.2 使用DMA提高效率当需要控制大量LED或高频通信时可以用DMAvoid DMA_Config(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel4); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)tx_buffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize 0; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Normal; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel4, DMA_InitStructure); USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); }7. 最终效果演示烧录程序后打开串口助手测试发送LED1_ON→ PB5 LED点亮发送ALL_BLINK→ 所有LED闪烁三次发送STATUS?→ 返回LED1:ON,LED2:OFF,...为了更直观可以用Python写个测试脚本import serial import time ser serial.Serial(COM3, 115200, timeout1) def send_cmd(cmd): ser.write((cmd \n).encode()) print(ser.readline().decode().strip()) send_cmd(LED1_ON) time.sleep(1) send_cmd(ALL_BLINK) time.sleep(2) send_cmd(STATUS?)这个项目最让我自豪的是指令系统的可扩展性——新增LED或功能只需修改指令表和状态机核心逻辑不用动。后来客户要求增加PWM调光功能我只用了半小时就实现了。