STM32F103通过USART串口指令实现多LED状态控制

📅 2026/7/15 20:48:23 👁️ 阅读次数 📝 编程学习
STM32F103通过USART串口指令实现多LED状态控制

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波特率对应的USARTDIV=39.0625。配置时直接填整数部分39,小数部分0.0625×16=1,所以BRR寄存器应设置为0x271。

3. 指令集设计与状态控制

3.1 指令格式定义

为了控制多个LED,我设计了这样的指令格式:

  • 单灯控制:LEDx_ON / LEDx_OFF (x=1~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 实用字符串处理技巧

在嵌入式环境处理字符串要特别注意效率,我总结了几个技巧:

  1. 用strncmp代替strstr做前缀匹配更高效
  2. 环形缓冲区比线性缓冲区更适合高频数据
  3. 使用查表法解析指令比if-else更快
const CmdEntry cmd_table[] = { {"LED1_ON", CMD_LED1_ON}, {"LED1_OFF", CMD_LED1_OFF}, // ...其他指令 }; CommandType fast_parse(char* cmd) { for(int i=0; i<sizeof(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 调试经验分享

调试时踩过几个坑:

  1. 波特率不匹配:电脑端和STM32的波特率必须完全一致
  2. 中断优先级:如果系统有其他中断,要合理设置USART中断优先级
  3. 缓冲区溢出:一定要做长度检查,我有次就因为没检查导致系统崩溃
  4. 电气干扰:长距离传输时建议加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 i=1; i<strlen(cmd)-1; i++) { checksum ^= cmd[i]; } return (checksum == cmd[strlen(cmd)-1]); }

6.2 使用DMA提高效率

当需要控制大量LED或高频通信时,可以用DMA:

void 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. 最终效果演示

烧录程序后,打开串口助手测试:

  1. 发送LED1_ON→ PB5 LED点亮
  2. 发送ALL_BLINK→ 所有LED闪烁三次
  3. 发送STATUS?→ 返回LED1:ON,LED2:OFF,...

为了更直观,可以用Python写个测试脚本:

import serial import time ser = serial.Serial('COM3', 115200, timeout=1) 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调光功能,我只用了半小时就实现了。