简介:一份面向嵌入式初学者的STM32串口通信实验与Proteus仿真完整工程包,能够帮助开发者快速完成串口协议配置、数据收发测试以及硬件电路逻辑验证。工程采用标准外设库编写,涵盖串口初始化、波特率设定、中断处理及数据收发函数调用,代码注释清晰,适合直接借鉴或在此基础上扩展功能。
压缩包内共有一百三十六个文件,总体积十九点九兆字节,以C语言源程序与头文件为核心,同时包含Proteus仿真工程文件、Keil工程配置文件、可烧录的HEX固件以及编译生成的中间文件,既能在仿真软件中直观查看运行效果,也能在Keil中重新编译调试。
目前已有一千七百四十四人学习使用。借助这套资料,使用者可以拿到可运行的全套串口实验代码与配套仿真电路,系统掌握STM32串口调试流程,并学会处理Proteus模型连接、虚拟串口匹配等常见问题,为后续嵌入式项目开发积累扎实的实践经验。
1. 串口实验为什么值得用 Proteus 仿真先跑一遍
很多人学 STM32 串口时,第一反应是直接买块开发板点灯、回显,结果卡在“板子为什么没反应”“数据为什么不进中断”上,最后发现是杜邦线松了或者电平没对上。这个实验包把 USART 串口通信和 Proteus 仿真放在一起,先用虚拟环境把收发链路完整调通,再移植到真实板子——适合刚接触 STM32 的初学者,也适合想快速验证串口协议逻辑、不想反复拔线的开发者。它解决的核心问题是:串口实验的“黑匣子”到底长什么样,数据从发送端到接收端经历了什么,以及程序烧进 Proteus 虚拟芯片后如何观察每一步的现象。
2. 工程与仿真环境:Keil + Proteus 的基本盘
2.1 资源包里的硬件选型与工具链
这个实验包默认基于 STM32F103 系列,具体的串口外设是 USART1,这是 STM32 里最常用的异步串口,支持全双工通信,波特率可配。Keil MDK 负责编译下载固件,Proteus 负责虚拟硬件仿真,两者通过 HEX 文件对接:Keil 编译生成 .hex 后,加载到 Proteus 的 STM32F103 芯片里运行。
我一般检查一个 Proteus 串口仿真项目时,先看三样东西:一是虚拟终端(VIRTUAL TERMINAL)有没有接对引脚,二是单片机的时钟配置有没有和代码里匹配,三是波特率有没有“看起来一致但实际上差很远”。这个实验包里如果按标准做法,USART1 对应 PA9(TX)和 PA10(RX),虚拟终端的 RX 接 PA9,TX 接 PA10,交叉连接,这是串口仿真里最容易搞反的一步。
2.2 文件结构与代码组织的预期
解压后会看到一个典型的 Keil 工程目录:核心代码在 User 目录下,包含 main.c、stm32f10x_it.c、usart.c 或类似文件;系统文件由标准外设库提供,包括 stm32f10x_rcc.c、stm32f10x_usart.c、stm32f10x_gpio.c;仿真文件是 Proteus 的 .pdsprj 工程,双击就能打开。实际运行时还需要确认固件库版本是 V3.5 标准外设库还是 HAL 库,这决定初始化代码的风格。
这个选型有一个很实际的理由:F103 内部有 64KB 到 128KB 的 Flash,跑一个串口收发加中断处理,资源绰绰有余。Proteus 仿真时不需要接外部晶振也可以跑内部 RC 振荡器,但如果你在代码里配了外部晶振启动,Proteus 里也得放一个 8MHz 晶振,否则串口波特率会偏得离谱。这里推荐的做法是仿真阶段直接用 HSI 内部时钟配合 PLL 倍频到 72MHz,减少外部器件变量。
2.3 工程初始化:时钟、GPIO 与 USART 外设
串口实验的第一步不是写串口代码,而是把时钟和引脚先理清楚。STM32F103 的 USART1 挂在 APB2 总线上,时钟频率最高 72MHz,它的 GPIO 引脚 PA9、PA10 同时也要打开 GPIOA 的时钟。这个顺序错了,后面全白搭。
void USART1_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; /* TX 推挽复用输出 */ GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; /* RX 浮空输入 */ GPIO_Init(GPIOA, &GPIO_InitStructure); }PA9 必须配置成复用推挽输出,因为 TX 信号由 USART1 外设驱动,不是普通 GPIO 输出;PA10 作为输入引脚,理论上可以配成浮空输入或上拉输入,但仿真时浮空输入最简单,而且 Proteus 里虚拟终端会自身驱动电平,不用担心悬空抖动。GPIO_Speed_50MHz在这里是合理的,USART 通信速率相对 GPIO 翻转速度不算高,但 F103 标准库代码里习惯性配成 50MHz,不会有副作用。
2.4 串口参数的代码配置与参数意义
串口参数配置是一个结构体集中设置的过程,标准外设库把波特率、数据位、停止位、校验位都封装在一个USART_InitTypeDef里。仿真时通常选 9600 或 115200,配合 Proteus 虚拟终端的波特率设置,两边必须一致。
void USART1_Init(uint32_t baudrate) { USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate = baudrate; 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_Cmd(USART1, ENABLE); }USART_Mode_Rx | USART_Mode_Tx同时使能收发,保证仿真环境里既能发也能收。USART_HardwareFlowControl_None是 Proteus 仿真一个重要选择:如果开启硬件流控,需要额外接 RTS 和 CTS 引脚到虚拟终端,而且 Proteus 里 VIRTUAL TERMINAL 默认不模拟流控,开了反而会把数据卡住。这个细节在真实板子上没关系,但在仿真里就是血泪经验:程序一直在等 CTS 信号,数据显示不出来。参数里没有涉及校验位,这是串口通信里最简单、最容易调试通的配置。熟悉这个配置以后,再去扩展奇偶校验、9 位数据模式,思路是一样的。
3. 核心代码拆解:一个串口数据从进入到返回的完整链路
3.1 中断接收的实现方式
串口接收最常见的做法是“中断 + 缓冲区”:每收到一个字节就进一次 USART 中断,把数据存到内存数组里,主循环再从数组里取出来处理。这个实验包里最核心的一段也就是这个,它决定了串口能不能可靠接收一串数据,而不是只收单字节。
#define RX_BUFFER_SIZE 64 uint8_t rx_buffer[RX_BUFFER_SIZE]; volatile uint8_t rx_index = 0; volatile uint8_t rx_complete = 0; void USART1_IRQHandler(void) { uint8_t data; if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { data = USART_ReceiveData(USART1); rx_buffer[rx_index++] = data; if (rx_index >= RX_BUFFER_SIZE) /* 防止缓冲区溢出 */ { rx_index = 0; } if (data == '\n') /* 以换行符作为一帧结束 */ { rx_buffer[rx_index - 1] = '\0'; rx_complete = 1; } } }USART_GetITStatus检查接收中断标志 RXNE,即接收数据寄存器非空。进入中断后必须立即调用USART_ReceiveData把数据读走,否则 RXNE 标志不会自动清除,会一直触发中断,导致程序卡死在中断里。这个实验包的设计里缓冲区大小为 64 字节,一次接收一帧短命令足够用。
3.2 中断配置的细节:使能与优先级
有了中断处理函数,还要在初始化阶段使能 USART 的接收中断,并设置 NVIC 优先级,否则中断永远不会进入。这是一个容易被忽略的步骤,尤其刚学的时候总觉得为什么收不到数据,其实中断没有打开。
void USART1_NVIC_Config(void) { NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); }这里抢占优先级设为 1,子优先级为 0。在 Proteus 仿真环境里,多个外设中断同时触发的情况比较少,优先级设置主要为了以后扩展时留余地。如果后面加了定时器中断并且频率很高,建议把串口中断的抢占优先级设置得高一些,避免收发过程中被定时器打断导致丢字节。USART_ITConfig的第三个参数是 ENABLE,这一步相当于给串口接收装上了“触发器”。
3.3 主循环里的数据加工与回显
中断负责接收数据,主循环负责处理数据。这个实验包的核心逻辑是回显——把收到的内容原封不动地送回去,一个最简单但能完整走通链路的功能。
int main(void) { uint8_t i; USART1_GPIO_Config(); USART1_Init(9600); USART1_NVIC_Config(); USART_SendString("STM32 USART Test Ready\r\n"); while (1) { if (rx_complete) { USART_SendString("Received: "); for (i = 0; i < rx_index; i++) { USART_SendByte(rx_buffer[i]); } USART_SendString("\r\n"); rx_index = 0; rx_complete = 0; } } }主循环不断检查rx_complete标志位,当一帧数据接收完成时,把缓冲区里的内容逐个发送回去。USART_SendByte是串口开发里最基础的发送函数,它的实现方式是等待发送数据寄存器为空,然后把数据写入 DR 寄存器。这个轮询发送的方式在波特率不高、数据量不大的场景下完全没有问题。
void USART_SendByte(uint8_t ch) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, ch); } void USART_SendString(char* str) { while (*str) { USART_SendByte((uint8_t)*str); str++; } }发送函数的核心是USART_FLAG_TXE:这个标志位表示发送数据寄存器已空,可以写入下一条数据。在执行USART_SendData之前先循环等待,防止数据丢失或覆盖。这是串口发送的常见做法,熟练以后如果需要高速发送大量数据,再升级到 DMA 方式。
3.4 接收协议设计的节奏
这个实验包从回显 Demo 出发,但中断 + 缓冲区的框架本身已经具备协议处理的基础形态。如果你尝试修改成“按键发送”“定时发送”“收到特定指令再回传一组数据”,不需要动中断部分,只需要在主循环里对rx_buffer做字符串匹配。常见的做法是定义几个指令宏,比如:
if (rx_complete) { if (strcmp((char*)rx_buffer, "LED_ON") == 0) { GPIO_SetBits(GPIOC, GPIO_Pin_13); USART_SendString("LED ON\r\n"); } else if (strcmp((char*)rx_buffer, "LED_OFF") == 0) { GPIO_ResetBits(GPIOC, GPIO_Pin_13); USART_SendString("LED OFF\r\n"); } else { USART_SendString("Unknown Command\r\n"); } rx_index = 0; rx_complete = 0; }这样的设计让串口实验从“会收发”进入“能控制外设”的阶段。核心思想是:中断只负责收数据,主循环负责解析和响应,两者解耦。这个结构以后不管换到哪个 STM32 系列,甚至换到别的单片机,思路都能直接用。
4. 完整运行流程:从 Keil 编译到 Proteus 联调
4.1 Keil 工程编译与 HEX 文件生成
拿到实验包后,第一步是安装 Keil MDK,版本建议 5.x 以上,打开工程文件后先确认一下 Target 选项里的 Device 是否选择了正确的 STM32F103 型号。然后点编译,如果代码本身没有问题,会生成对应的 .hex 文件。这里需要注意输出路径,Proteus 加载时默认从工程目录找,建议直接把 Output 选项的路径设为工程根目录下的 Objects 文件夹,方便后续配置。
# 编译前确认三个关键配置 # 1. Options for Target -> Output -> Create HEX File,必须勾选 # 2. Options for Target -> Debug -> Use Simulator,Keil 内部仿真可跳过 # 3. C/C++ -> Define 里是否包含 USE_STDPERIPH_DRIVER编译报错时,先看是语法错误还是头文件路径问题。如果出现stm32f10x.h: No such file,说明 C/C++ 的 Include Paths 里没有添加库头文件目录。这个实验包如果按标准库工程组织,需要添加 Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x 和 Libraries/STM32F10x_StdPeriph_Driver/inc 两个路径。
4.2 Proteus 仿真文件的加载与调整
打开 Proteus 工程后,如果看到的是空白设计,需要手动添加元器件。右键芯片选择 Edit Properties,在 Program File 一栏加载刚才编译生成的 hex 文件。注意单片机型号如果不同,加载方式一样,但外设引脚分布可能不同,比如有的 F103 封装没有开放全部引脚,这时要检查原理图上有没有把 PA9、PA10 引脚连接到虚拟终端。
Proteus 里 USART 仿真的标准连接方式是:
- 单片机 PA9(TX)→ 虚拟终端 RXD 引脚
- 单片机 PA10(RX)→ 虚拟终端 TXD 引脚
- 虚拟终端 VCC、GND 悬空即可,由仿真环境自动供电
配置虚拟终端时,右键选择 Edit Properties,将 Baud Rate 设置为 9600,与代码里的初始化参数保持一致。Data Bits 选 8,Parity 选 None,Stop Bits 选 1,这四项任何一个对不上,显示出来的就是乱码或没有内容。
4.3 仿真运行与串口调试助手的配合
启动仿真后,虚拟终端窗口会弹出,这时在键盘上直接打字,注意不要点终端窗口里的输入区,而是确保虚拟终端获得焦点后直接输入字符。你输入的每个字符会通过 Proteus 的虚拟串口链路传给 STM32 的 RX 引脚,触发接收中断,然后主循环把内容回显到终端上。第一次跑通时,屏幕上出现你输入的字符和回显内容,串口收发链路就算验证成功了。
如果这个实验包自带 COMPIM 物理串口模型,场景会稍微复杂一些:COMPIM 在 Proteus 里模拟一个真实的串口设备,需要设置 COM 口号和波特率,然后用串口调试助手或者 PuTTY 打开对应的 COM 口发送数据。这时数据链路更长,涉及 PC 串口 -> 虚拟串口 -> STM32 -> 回显 -> PC,任何一个环节断开都可能导致看不出现象。
4.4 实验现象的确认与常用验证方法
跑通以后,建议做三组验证,确认这个实验包不是“只能显示一个回显”的玩具:
// 长字符串回显测试:输入超过 20 个字符,观察是否丢字节 // 连续输入测试:每次间隔 50ms 连续发送 5 条消息,观察 rx_buffer 是否被覆盖 // 停止位测试:临时把波特率改为 4800,观察解码结果是乱码还是超时无响应这三组验证分别考验中断接收的稳定性、缓冲区管理和波特率容错。如果长字符串回显时末尾几个字符丢失,通常是主循环来不及处理,rx_buffer被覆盖了。对策是加大缓冲区,或者在主循环里加一个“接收忙”标志位,让发送方等待。
5. Proteus 串口仿真的典型坑:现象、原因与解法
5.1 虚拟终端不显示任何内容
现象:程序编译正常,HEX 文件已加载,Proteus 运行后虚拟终端窗口弹出,但输入字符没有任何响应,终端里也看不到启动消息。
原因:最常见的有三个,一是 HEX 文件没有正确加载到芯片里,芯片实际上在跑空白程序或者之前残留的程序;二是单片机时钟没有配置好,USART 波特率设置了一个值但实际时钟频率不对;三是虚拟终端的 RX、TX 接反了。
解决:先在芯片属性里确认 Program File 路径有值,然后重新加载一次 HEX 文件。看代码里的 RCC 配置,如果用的是外部晶振 8MHz 经过 PLL 到 72MHz,确保 Proteus 原理图里有 8MHz 晶振。最后核对虚拟终端 RXD 接的是单片机 TX 引脚,TXD 接的是 RX 引脚。
5.2 回显出现乱码,但字符数量是对的
现象:收到的内容长度正确,但每个字符都是错乱的符号,比如输入 ABC,显示的可能是 4 位特殊符号。
原因:波特率不匹配。单片机初始化时写的 9600,虚拟终端配置的可能是 2400 或者 19200。Proteus 里虚拟终端默认波特率有时不是 9600,是它在启动时读取配置文件里的预设值。
解决:把虚拟终端属性的 Baud Rate 改为和代码一致的 9600。另一个容易被忽视的是时钟源:如果 Proteus 里单片机的频率设置为 72MHz 外部晶振,但代码里实际用的是内部 8MHz 时钟,算出来的波特率会有接近 9 倍的偏差,表现出来的就不是乱码而是完全无输出。
5.3 输入一个字符回显两个甚至多个字符
现象:在虚拟终端里输入一个字符,屏幕上出现两个相同的字符,有时还会出现回车换行符被重复执行。
原因:这是典型的回显叠加问题。虚拟终端本身可以把输入显示在屏幕上,同时单片机把这同一个字符回显出来,两个来源叠在一起。如果代码里又手工加了回车换行,每输入一个字符,屏幕上就看到两三次内容。
解决:调试的时候可以在虚拟终端属性里关闭本地回显,或者在代码里只在收到完整一行后才做回显处理。实际项目里很少逐字节回显,更多是按帧处理。
5.4 数据能发出去,但收不到串口调试助手的数据
现象:Proteus 里虚拟终端工作正常,说明单片机的 USART 收发链路没问题,但通过 COMPIM 连到 PC 串口调试助手后,PC 发数据给单片机没反应,单片机发数据 PC 也收不到。
原因:COMPIM 的虚拟串口还需要在 PC 端建立一个“虚拟串口对”,让 PC 软件认为这是一个真实串口。操作时还要注意 COMPIM 的缓冲区设置,如果缓冲太小数据会丢。
解决:确认 COMPIM 属性里的物理 COM 口号是存在的,且该 COM 口没有被别的程序占用。将 COMPIM 与单片机的连接检查一下,如果 COMPIM 的 RXD 接到了单片机的 RXD,数据链路就断了。
5.5 程序在 GPIO 初始化后直接跑飞
现象:Proteus 仿真运行时,程序在初始化阶段就卡住,设置断点发现停在 RCC 配置或 GPIO 初始化代码中,但代码逻辑看起来没问题。
原因:Proteus 对 STM32 的仿真并非完全等价于真实芯片。某些型号的 Flash 等待周期和时钟配置不当,会导致程序跑飞。另一个常见问题是没有正确开启 AFIO 时钟。
解决:检查 RCC 配置里是否也打开了RCC_APB2Periph_AFIO,在初始化 GPIO 前先使能复用功能时钟。把外部晶振去掉,直接使用 HSI + PLL 到 36MHz,很多跑飞问题会消失。
6. 二次开发思路:从串口回显到自己的小协议栈
当你把这个实验包跑通以后,它的价值才刚刚开始。回显是基本功,下一步要改成实用的通信逻辑。我建议做三个阶段:第一,把固定长度的回显改成可变长帧解析;第二,把接收方式从中断 + 轮询升级到 DMA + 空闲中断;第三,把串口数据与外部模块联动,比如通过串口控制步进电机、继电器或模拟传感器。
第一个阶段最具性价比,改动量小,收益最大。把'\n'作为帧结束标志的代码保留,但把rx_buffer里的内容按第一个字段当作命令号,后面当作参数。实话说,串口协议里最常用的结构就是“帧头 + 命令字 + 数据长度 + 数据 + 校验”,在中断里收满一帧后做校验,主循环里解析执行。这个骨架搭好以后,后面接蓝牙模块、WiFi 模块、RS485 总线,都是同一套思路。
第二个阶段,如果你研究 DMA 方式,注意一点:STM32F103 的 USART 用 DMA 接收时,没有硬件意义上的“空闲中断”,常见做法是用定时器模拟超时判断帧结束,或者直接用 IDLE 中断。实际开发中,IDLE 中断 + DMA 是最省 CPU 的方案,但 Proteus 对 IDLE 中断的仿真有时不准确,建议先在真实板子上验证后再往回移植。
第三个阶段牵涉到仿真和实物不一致的问题。Proteus 里控制 LED 和继电器很方便,但真实硬件里有驱动电流、隔离、上拉电阻这些 Proteus 不会告诉你的坑。二次开发时,每加一个功能模块,先在 Proteus 里把逻辑验证完,再上真板子,这样踩坑成本最低。
我在调试这个实验包时,最大的教训是不要同时改多处配置。有一次为了测试波特率自适应,同时改了代码里的波特率宏、虚拟终端设置和 COMPIM 属性,最后数据完全收不到,分不清是哪个环节出了问题。从那以后,我每次做串口相关改动都强制走一遍流程:先改代码,初始化确认输出正常,再改仿真里的虚拟终端参数,最后才动物理串口部分,一次只动一个变量。最后也希望这个实验包能帮你在串口这条路上少走一段弯路,少踩几个坑。
本文还有配套的精品资源,点击获取