STM32串口中断与空闲中断高效接收不定长数据实战指南

发布时间:2026/7/30 14:59:54
STM32串口中断与空闲中断高效接收不定长数据实战指南 1. 项目概述从轮询到中断串口数据接收的效率革命在嵌入式开发尤其是基于STM32这类MCU的项目里串口通信几乎是“标配”。无论是打印调试信息、与上位机交互还是连接GPS、蓝牙模块串口都扮演着关键角色。但很多新手甚至一些有经验的开发者在处理串口接收时往往停留在最简单的“轮询”方式——在主循环里不停地检查串口接收寄存器有没有新数据。这种方式在数据量小、实时性要求不高的场合勉强能用一旦数据流变得不规律或数据包较长就会严重占用CPU资源导致系统响应迟钝甚至丢失数据。今天要聊的“STM32F103串口中断接收和空闲中断接收”就是解决这个痛点的核心方案。这不仅仅是两个中断功能的简单使用而是一套提升串口通信可靠性、实时性和系统效率的完整方法论。简单来说串口接收中断USART_IT_RXNE负责在每一个字节数据到达时及时响应并读取确保数据不丢失而串口空闲中断USART_IT_IDLE则像一个聪明的“包尾检测器”当一帧数据发送完毕总线空闲一段时间后它会自动触发告诉我们“这一包数据收齐了可以处理了”。这种组合拳完美解决了如何高效、准确地接收不定长数据包的问题。你不用再费心去计算数据包长度或者设置超时定时器硬件已经为你提供了最优雅的解决方案。无论是接收GPS模块的NMEA语句还是解析自定义的通信协议帧这套机制都能让你的代码更加健壮和高效。接下来我们就深入STM32F103的USART内部拆解这两种中断的工作原理、配置步骤以及如何将它们结合起来打造一个稳定可靠的串口数据接收引擎。2. 核心机制解析RXNE与IDLE中断如何协同工作要玩转这两种中断首先得理解STM32的USART外设是如何“感知”数据的。这不仅仅是配置几个寄存器那么简单你需要明白数据从物理引脚到你的应用程序缓冲区中间经历了什么以及中断在哪个环节介入。2.1 串口接收中断USART_IT_RXNE数据的“门铃”想象一下USART外设内部有一个小的接收缓冲区通常是一个字节的寄存器RDR。当串口接收引脚RX上一个字节的数据被完整地移入这个寄存器后硬件会自动将状态寄存器SR中的RXNERead data register not empty标志位置1。这个标志位的意思就是“接收数据寄存器非空”有数据可以读了。如果我们使能了RXNE中断USART_IT_RXNE那么这个硬件标志就会触发一个中断请求。CPU会暂停当前任务跳转到我们预先写好的中断服务函数ISR中。在这个函数里我们的核心任务就一件事立刻把RDR寄存器里的这个字节读出来。通常我们会把它存到一个自己定义的数组环形缓冲区里。关键点读取RDR寄存器的操作通常通过USART_ReceiveData()函数或直接访问USARTx-DR会自动清除RXNE标志。如果你在中断里忘了读或者读的方式不对这个标志会一直存在导致中断不断被触发形成“中断风暴”系统就卡死在这里了。2.2 串口空闲中断USART_IT_IDLE帧结束的“信号枪”空闲中断的机制比RXNE要稍微“间接”一些。它监测的不是具体的数据而是RX引脚上的电平状态。当一帧数据开始传输时起始位会把总线拉低然后传输数据位和停止位。在数据传输期间总线上是有电平变化的。当最后一个字节的停止位结束后如果发送方不再发送新的数据RX引脚就会恢复到空闲状态通常是高电平。如果这个高电平状态持续超过一个完整字符传输的时间即1个起始位8个数据位1个停止位共10位的时间USART硬件就会认为“总线空闲了”随之将状态寄存器中的IDLE标志位置1。同样如果我们使能了IDLE中断USART_IT_IDLE此时就会触发中断。注意IDLE标志不会被硬件自动清除也不会通过读RDR寄存器清除。这是一个非常容易踩坑的地方。必须在中断服务函数中通过先读SR寄存器再读DR寄存器的序列来清除它。具体操作是USART_GetITStatus(USARTx, USART_IT_IDLE)检测到中断后紧接着调用USART_ReceiveData(USARTx)这个读DR的操作可能无实际数据但为了清除标志必须执行。2.3 协同工作流程一个完整的接收场景假设我们通过串口接收一个不定长的指令“ATCOMMAND\r\n”。第一个字节 ‘A’ 到达硬件接收完毕置位RXNE触发RXNE中断。中断服务函数读取‘A’存入缓冲区buf[0]。后续字节 ‘T’, ‘’, ‘C’… 陆续到达每个字节都会触发一次RXNE中断依次存入缓冲区buf[1],buf[2],buf[3]…最后一个字节 ‘\n’ 到达并处理‘\n’被存入缓冲区。总线空闲发送方发完‘\n’后停止发送。RX线保持高电平超过一个字符时间。触发IDLE中断硬件置位IDLE标志触发空闲中断。处理帧数据在IDLE中断服务函数中我们清除IDLE标志然后根据缓冲区中已存入的字节数可以通过一个计数变量得知知道收到了一帧完整的数据“ATCOMMAND\r\n”。此时我们可以设置一个标志位如rx_frame_complete 1通知主循环或其他任务“有一包新数据准备好了快来处理”。这样主程序完全不用操心数据什么时候来、来了多少。它只需要在rx_frame_complete被置位时去处理缓冲区里的完整数据包即可。CPU的利用率得到了极大提升。3. 基于标准外设库的完整实现与配置理论清楚了我们动手实现它。这里以STM32F103C8T6的USART1为例使用STM32标准外设库StdPeriph_Lib。虽然ST现在主推HAL/LL库但标准库的代码更贴近寄存器对于理解原理非常有帮助。3.1 硬件与引脚初始化首先确保你的硬件连接正确USART1的TXPA9接USB转串口模块的RXRXPA10接USB转串口模块的TX共地。初始化分为两部分GPIO和USART。// 1. 开启时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); // 2. 配置GPIO GPIO_InitTypeDef GPIO_InitStructure; // PA9 作为复用推挽输出 (TX) 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); // PA10 作为浮空输入 (RX) GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; // 浮空输入 GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 配置USART参数 USART_InitTypeDef USART_InitStructure; 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);3.2 中断配置与使能这是核心步骤需要配置NVIC嵌套向量中断控制器和使能USART的特定中断。// 4. 配置NVIC设置中断优先级和使能 NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 设置优先级分组为2 // 配置USART1全局中断通道 NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; // 抢占优先级1 NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; // 子优先级1 NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); // 5. 使能USART的接收中断和空闲中断 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 使能接收中断 USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); // 使能空闲中断 // 6. 最后使能USART USART_Cmd(USART1, ENABLE);注意中断优先级分组NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2)表示有2位用于抢占优先级2位用于子优先级。抢占优先级高的可以打断低的相同抢占优先级时子优先级高的先响应。串口中断一般不需要设置成最高优先级但要避免被其他长时间中断阻塞导致数据丢失。3.3 中断服务函数与缓冲区管理中断服务函数是灵魂所在。我们需要在这里安全、高效地处理数据。// 定义接收缓冲区和相关变量 #define RX_BUF_SIZE 256 uint8_t rx_buffer[RX_BUF_SIZE]; // 环形缓冲区或线性缓冲区 volatile uint16_t rx_index 0; // 当前存储位置必须加volatile volatile uint8_t rx_flag 0; // 帧接收完成标志必须加volatile void USART1_IRQHandler(void) { uint8_t temp; // 1. 处理接收中断 (RXNE) if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { // 读取一个字节这个操作会清除RXNE标志 temp USART_ReceiveData(USART1); // 将数据存入缓冲区这里使用简单的线性缓冲区生产环境建议用环形缓冲区 if(rx_index RX_BUF_SIZE) { rx_buffer[rx_index] temp; } else { // 缓冲区溢出处理可以重置索引或丢弃数据 rx_index 0; // 也可以设置一个错误标志 } // 注意不要在此处进行复杂处理或调用耗时函数 } // 2. 处理空闲中断 (IDLE) if(USART_GetITStatus(USART1, USART_IT_IDLE) ! RESET) { // 重要清除IDLE标志位的序列先读SR再读DR temp USART1-SR; // 读SR寄存器 temp USART1-DR; // 读DR寄存器这个读操作是清除IDLE标志的关键 (void)temp; // 防止编译器警告 // 一帧数据接收完毕设置标志位 rx_flag 1; // 此时rx_index 就是这一帧数据的长度 // rx_buffer[0] 到 rx_buffer[rx_index-1] 是有效数据 // 注意不要在中断里长时间处理数据通常只是置位标志。 } }3.4 主循环中的数据处理中断服务函数只负责“收”和“通知”真正的数据处理应该放在主循环或低优先级任务中。int main(void) { // ... 系统初始化USART初始化包含上述中断配置 while(1) { // 检查帧接收完成标志 if(rx_flag) { rx_flag 0; // 清除标志 // 处理接收到的数据帧 // rx_buffer 中存放了从 0 到 (rx_index-1) 的数据 process_rx_data(rx_buffer, rx_index); // 处理完后重置索引准备接收下一帧 rx_index 0; } // 其他任务... // 例如按键扫描、LED闪烁、传感器读取等 } } void process_rx_data(uint8_t *data, uint16_t len) { // 这里是你的应用层协议解析函数 // 例如判断是否是有效指令解析GPS数据等 // 可以将数据通过串口回传用于调试 for(int i0; ilen; i) { USART_SendData(USART1, data[i]); while(USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); // 等待发送完成 } // 或者加个换行便于观察 USART_SendData(USART1, \r); while(USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); USART_SendData(USART1, \n); while(USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); }4. 进阶优化与环形缓冲区应用上面的例子使用了一个简单的线性缓冲区和索引rx_index。这在单次数据包长度可控且处理速度很快的场景下没问题。但在实际复杂应用中存在两个问题1) 如果数据处理函数process_rx_data比较耗时在新一帧数据到来时可能还没处理完导致缓冲区被覆盖。2) 无法应对数据流非常快或者数据包背靠背发送的情况。解决方案是使用环形缓冲区Ring Buffer。环形缓冲区是一个逻辑上的“环”有读指针和写指针。写指针由RXNE中断移动存数据读指针由主循环处理函数移动取数据。两者互不干扰只要不同时操作同一位置就能安全地在中断和主循环之间共享数据。4.1 环形缓冲区的实现#define RING_BUF_SIZE 512 // 缓冲区大小根据实际需要调整最好是2的幂次方便于取模运算优化 typedef struct { uint8_t buffer[RING_BUF_SIZE]; volatile uint16_t head; // 写指针中断写入指向下一个可写位置 volatile uint16_t tail; // 读指针主循环读取指向下一个可读位置 } ring_buffer_t; ring_buffer_t uart_rx_buf; // 初始化缓冲区 void ring_buf_init(ring_buffer_t *rb) { rb-head 0; rb-tail 0; } // 中断中调用向缓冲区写入一个字节 static inline uint8_t ring_buf_put(ring_buffer_t *rb, uint8_t data) { uint16_t next_head (rb-head 1) % RING_BUF_SIZE; // 判断缓冲区是否已满写指针即将追上读指针 if(next_head rb-tail) { return 0; // 缓冲区满写入失败 } rb-buffer[rb-head] data; rb-head next_head; return 1; // 写入成功 } // 主循环中调用从缓冲区读出一个字节 static inline uint8_t ring_buf_get(ring_buffer_t *rb, uint8_t *data) { // 判断缓冲区是否为空读指针等于写指针 if(rb-head rb-tail) { return 0; // 缓冲区空读取失败 } *data rb-buffer[rb-tail]; rb-tail (rb-tail 1) % RING_BUF_SIZE; return 1; // 读取成功 } // 获取缓冲区中有效数据长度 static inline uint16_t ring_buf_len(ring_buffer_t *rb) { return (rb-head - rb-tail) % RING_BUF_SIZE; }4.2 使用环形缓冲区的中断服务函数修改后的中断服务函数不再直接操作线性数组而是调用ring_buf_put。void USART1_IRQHandler(void) { uint8_t temp; if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { temp USART_ReceiveData(USART1); // 存入环形缓冲区如果满了可以根据需求丢弃或设置错误标志 if(!ring_buf_put(uart_rx_buf, temp)) { // 缓冲区溢出处理 // 例如可以丢弃这个字节或者丢弃最早的数据移动tail // uart_rx_buf.tail (uart_rx_buf.tail 1) % RING_BUF_SIZE; // ring_buf_put(uart_rx_buf, temp); // 再试一次 } } if(USART_GetITStatus(USART1, USART_IT_IDLE) ! RESET) { temp USART1-SR; temp USART1-DR; (void)temp; // 空闲中断发生时设置标志。数据已经在环形缓冲区里了。 rx_flag 1; // 注意此时我们不知道数据在环形缓冲区里的具体起止位置。 // 需要结合协议来处理例如在数据中查找帧头帧尾。 } }4.3 主循环中的数据处理策略使用环形缓冲区后主循环中的处理逻辑也需要改变。因为数据是流式的我们需要在缓冲区中解析出完整的帧。// 假设我们的协议帧格式为帧头(0xAA 0x55) 数据长度(1字节) 数据 校验和 void process_ring_buffer_data(void) { static uint8_t state 0; // 状态机状态 static uint8_t data_len 0; static uint8_t data_cnt 0; static uint8_t checksum 0; static uint8_t packet[256]; uint8_t byte; while(ring_buf_get(uart_rx_buf, byte)) // 只要缓冲区有数据就处理 { switch(state) { case 0: // 寻找第一个帧头 if(byte 0xAA) state 1; break; case 1: // 寻找第二个帧头 if(byte 0x55) state 2; else state 0; // 匹配失败复位 break; case 2: // 获取数据长度 data_len byte; if(data_len sizeof(packet) - 3) // 长度异常保护 { state 0; break; } data_cnt 0; checksum 0xAA ^ 0x55 ^ byte; // 初始化校验和 state 3; break; case 3: // 接收数据体 packet[data_cnt] byte; checksum ^ byte; if(data_cnt data_len) { state 4; } break; case 4: // 接收并验证校验和 if(byte checksum) { // 校验成功得到一个完整的数据包 packet[0..data_len-1] // 调用真正的处理函数 handle_valid_packet(packet, data_len); } // 无论校验是否成功都回到初始状态准备接收下一帧 state 0; break; default: state 0; break; } } } int main(void) { // ... 初始化 ring_buf_init(uart_rx_buf); while(1) { if(rx_flag) // 空闲中断触发了说明可能有一帧数据发完了 { rx_flag 0; // 启动对环形缓冲区的解析 process_ring_buffer_data(); } // 也可以不依赖rx_flag在主循环里持续解析环形缓冲区 // process_ring_buffer_data(); // 其他任务... } }这种“中断环形缓冲区状态机解析”的组合是工业级串口通信程序的标配。它解耦了数据接收和数据处理抗数据冲击能力强稳定性极高。5. 常见问题、调试技巧与避坑指南在实际项目中配置和使用这两种中断时会遇到各种各样的问题。下面是我在多年项目中总结的一些典型坑点和解决技巧。5.1 中断服务函数中的常见错误问题1中断服务函数执行时间过长。这是最忌讳的。中断应该快进快出。如果你在USART1_IRQHandler里进行复杂计算、调用printf、或者等待某个标志很可能会错过后续的中断导致数据丢失或者影响其他中断的响应。解决中断里只做最必要的事——存取数据、设置标志。所有耗时的处理如协议解析、数据转发都放到主循环或低优先级任务中通过标志位来触发。问题2忘记清除中断标志。对于RXNE中断读USARTx-DR会自动清除。但对于IDLE中断必须手动清除。我见过太多人只写了if(USART_GetITStatus(USARTx, USART_IT_IDLE) ! RESET)却没有后续的清标志操作结果IDLE中断只触发一次系统就卡死了因为标志一直存在。解决牢记IDLE中断的清标志“标准操作”volatile uint32_t tmp USARTx-SR; tmp USARTx-DR; (void)tmp;。用库函数的话就是先USART_GetITStatus检测再USART_ReceiveData读一下虽然可能没数据。问题3全局变量未加volatile。在中断里修改的、在主循环里判断的标志位如rx_flag,rx_index必须用volatile修饰。否则编译器可能会进行优化比如把rx_flag的值缓存到寄存器里导致主循环永远看不到中断里修改的新值。解决所有在中断和主程序间共享的变量一律加上volatile关键字。5.2 数据丢失与错乱排查问题4高速率下丢数据。在115200甚至更高波特率下如果系统还有其他高优先级中断如定时器中断长时间执行或者主循环关中断时间太长就可能因为来不及响应串口中断而丢数据。解决优化中断优先级适当提高串口中断的抢占优先级但不要最高避免影响系统关键任务。检查关中断时间排查代码中__disable_irq()或类似关总中断的操作确保其持续时间极短。使用DMA如果数据量真的很大考虑使用“串口空闲中断DMA”的模式让硬件直接把数据搬到内存完全解放CPU。这是更高级的玩法。问题5收到的数据偶尔多一个或少一个字节。这很可能和清除IDLE标志的时机有关。IDLE中断是在总线空闲一个字符时间后触发的。如果你在IDLE中断服务函数里进行了一些处理比如解析数据而这段时间内发送方又紧接着发送了下一帧数据的第一个字节那么这个字节触发的RXNE中断可能会在IDLE中断服务函数结束前就被响应。这会导致第一帧的末尾和第二帧的开头在逻辑上混在一起。解决在IDLE中断服务函数的一开始就暂时关闭RXNE中断USART_ITConfig(USART1, USART_IT_RXNE, DISABLE)等处理完标志、准备好接收新帧后再重新开启RXNE中断。或者确保你的IDLE中断处理极其迅速。5.3 硬件与配置相关陷阱问题6使能了中断但就是不触发。请按以下清单检查USART时钟和GPIO时钟是否已开启RCC_APB2PeriphClockCmdNVIC配置是否正确中断通道如USART1_IRQn和优先级设置了吗NVIC_Init调用了吗USART本身是否使能USART_Cmd(USART1, ENABLE)必须在配置完中断后再使能USART顺序很重要。中断函数名是否正确USART1_IRQHandler不能拼错。检查硬件连接RX/TX是否接反共地了吗问题7使用CubeMX/HAL库时的问题。如果你用的是HAL库流程类似但函数不同。关键点使能中断__HAL_UART_ENABLE_IT(huart1, UART_IT_RXNE);和__HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE);中断服务函数在stm32f1xx_it.c中找到USART1_IRQHandler调用HAL_UART_IRQHandler(huart1);。回调函数HAL库采用回调机制。你需要重写__weak函数HAL_UART_RxCpltCallbackRXNE和HAL_UART_ErrorCallbackIDLE中断在HAL中作为错误处理之一通过huart-ErrorCode判断是否为HAL_UART_ERROR_IDLE。HAL库的坑HAL的IDLE处理有时不够直观且默认可能未开启IDLE中断。需要手动开启并处理好回调。5.4 调试技巧利用发送功能调试接收在RXNE中断里收到什么字节就立刻通过串口发送回环出去。用串口助手看如果发送“123”收到“123”说明RXNE中断基本正常。如果发现数据不对可能是波特率等配置错误。翻转GPIO引脚在中断入口和出口用GPIO_WriteBit翻转一个LED或空闲的IO引脚用示波器或逻辑分析仪观察中断的触发频率和持续时间。这是最直观的调试硬件中断的方法。打印调试信息在IDLE中断里通过串口打印出当前接收到的数据长度。但要注意打印本身是耗时操作可能会影响下一次接收所以仅作临时调试用。模拟发送测试不要总依赖上位机。可以写一个简单的定时器中断每隔一段时间模拟发送一包数据来测试你的接收程序是否稳定。6. 项目实战构建一个简单的AT指令解析器让我们把上面的所有知识整合起来做一个有实际意义的小项目一个通过串口接收并解析简单AT指令的系统。指令格式为ATCMD[PARAM]\r\n。例如ATLEDON\r\n或ATQUERY\r\n。6.1 系统设计硬件STM32F103C8T6最小系统板USART1连接PC串口助手PA0连接一个LED。功能接收不定长的AT指令解析出命令和参数并执行相应操作控制LED或回复状态。技术选型采用“RXNE中断IDLE中断线性缓冲区状态机解析”的方案。考虑到指令不会很长暂不使用环形缓冲区。6.2 代码实现框架// at_parser.h #ifndef __AT_PARSER_H #define __AT_PARSER_H void at_parser_init(void); void at_parser_process(void); // 在主循环中调用 #endif // at_parser.c #include stm32f10x.h #include string.h #include stdio.h #define AT_BUF_SIZE 128 static uint8_t at_rx_buf[AT_BUF_SIZE]; static volatile uint16_t at_rx_index 0; static volatile uint8_t at_rx_flag 0; // 声明USART1初始化函数在uart.c中实现 extern void uart1_init(void); void at_parser_init(void) { uart1_init(); // 初始化串口1包含GPIO、USART、NVIC和中断使能配置 at_rx_index 0; at_rx_flag 0; printf(AT Parser Ready.\r\n); // 假设printf已重定向到串口 } // USART1中断服务函数 (在 stm32f10x_it.c 中或集中放在uart.c) void USART1_IRQHandler(void) { uint8_t temp; if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { temp USART_ReceiveData(USART1); if(at_rx_index AT_BUF_SIZE) { at_rx_buf[at_rx_index] temp; } else { // 缓冲区溢出丢弃最旧的数据类似FIFO memmove(at_rx_buf, at_rx_buf[1], AT_BUF_SIZE - 1); at_rx_index AT_BUF_SIZE - 1; at_rx_buf[at_rx_index] temp; } } if(USART_GetITStatus(USART1, USART_IT_IDLE) ! RESET) { volatile uint32_t tmp USART1-SR; tmp USART1-DR; (void)tmp; at_rx_flag 1; // 设置帧接收完成标志 } } // 解析并执行AT指令 static void execute_at_command(uint8_t *cmd, uint16_t len) { // 确保是以\r\n结尾 if(len 2 || cmd[len-2] ! \r || cmd[len-1] ! \n) { printf(ERROR: Invalid frame ending\r\n); return; } // 去掉结尾的\r\n len - 2; cmd[len] \0; // 方便使用字符串函数 // 检查是否以AT开头 if(len 2 || cmd[0] ! A || cmd[1] ! T) { printf(ERROR: Not an AT command\r\n); return; } // 解析命令 if(strncmp((char*)cmd, ATLEDON, 9) 0) { GPIO_SetBits(GPIOA, GPIO_Pin_0); // 假设LED在PA0高电平点亮 printf(OK: LED ON\r\n); } else if(strncmp((char*)cmd, ATLEDOFF, 10) 0) { GPIO_ResetBits(GPIOA, GPIO_Pin_0); printf(OK: LED OFF\r\n); } else if(strncmp((char*)cmd, ATQUERY, 8) 0) { printf(OK: System is running.\r\n); } else if(strncmp((char*)cmd, ATHELP, 7) 0) { printf(OK: Supported commands:\r\n); printf( ATLEDON/OFF\r\n); printf( ATQUERY\r\n); printf( ATHELP\r\n); } else { printf(ERROR: Unknown command\r\n); } } // 主循环中调用的处理函数 void at_parser_process(void) { if(at_rx_flag) { at_rx_flag 0; // 清除标志 // 复制一份数据到临时缓冲区进行处理避免处理过程中被中断修改 uint8_t temp_buf[AT_BUF_SIZE]; uint16_t temp_len at_rx_index; memcpy(temp_buf, at_rx_buf, temp_len); // 执行解析 execute_at_command(temp_buf, temp_len); // 重置接收索引准备接收下一帧 at_rx_index 0; } } // main.c #include at_parser.h int main(void) { // 系统时钟、GPIO等初始化... GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 初始化LED引脚 at_parser_init(); // 初始化AT解析器包含串口 while(1) { at_parser_process(); // 处理接收到的AT指令 // 这里可以添加其他任务如按键扫描等 // 由于串口接收完全由中断处理这里不会阻塞 } }6.3 测试与验证将代码编译下载到STM32打开串口助手如XCOMPutty设置好波特率115200。发送ATHELP\r\n会收到支持的指令列表。发送ATLEDON\r\n会收到“OK: LED ON”并且板子上的LED应该点亮。发送ATLEDOFF\r\nLED熄灭。发送ATQUERY\r\n收到状态回复。发送错误指令如ATTEST\r\n会收到“ERROR: Unknown command”。尝试快速连续发送多条指令观察系统是否都能正确响应没有数据丢失或粘连。这个项目虽然小但涵盖了串口中断接收、空闲中断、数据帧处理、简单协议解析等多个核心知识点。你可以在此基础上扩展增加更多的AT指令或者将其改造成一个更复杂的通信协议解析引擎。