当前位置: 首页 > news >正文

从 0 到 1!Keil uVision5 驱动 STM32 串口通信实战(附工程源码与调试心法)

1. 从点亮LED到串口通信:你的STM32进阶之路

上次我们一起用Keil uVision5点亮了STM32的LED,算是成功迈出了第一步。但说实话,点亮LED就像是学会了开灯,而真正的嵌入式世界,是让设备“开口说话”。今天,我们就来聊聊怎么让STM32这块“小石头”开口——也就是实现串口通信。这绝对是嵌入式开发中最实用、最核心的技能之一,没有它,你的单片机就像个哑巴,你根本不知道它在想什么,程序跑得对不对,数据算得准不准。

我刚开始玩STM32那会儿,串口调试是我花时间最多的地方。从最开始的乱码,到数据丢包,再到中断卡死,踩过的坑一个接一个。但一旦打通了串口,整个开发体验就完全不一样了。你可以把传感器数据发到电脑上分析,可以接收电脑的指令控制设备,甚至可以用手机APP通过蓝牙模块(底层也是串口)和STM32对话。所以,今天这篇实战文,我会把我这些年调试串口的心得,掰开了揉碎了讲给你听。我们不只讲怎么配通,更要讲明白为什么这么配,出了问题怎么用Keil自带的工具快速定位。文章最后会提供完整的工程源码,你可以直接拿来用,也可以跟着我一步步搭建,彻底吃透。

2. 实战前的“粮草”准备:硬件连接与原理扫盲

工欲善其事,必先利其器。在敲代码之前,咱们得先把硬件和原理搞清楚,不然出了问题你连从哪儿下手都不知道。

2.1 硬件清单与接线图

硬件和点亮LED时差不多,但多了一个“串口通信”的环节。你需要准备:

  • 核心:STM32F103C8T6最小系统板(还是我们熟悉的那块蓝色小板子)。
  • 调试与下载:ST-Link调试器(用于下载程序和硬件调试)。
  • 串口通信桥梁:一个USB转TTL串口模块。这是关键!因为电脑的USB口是USB协议,而STM32的串口是TTL电平(0V和3.3V),需要这个模块来转换。常用的有CH340、CP2102等芯片的模块,都很便宜。
  • 软件:Keil uVision5(MDK-ARM)已安装激活,并安装了STM32F1的器件包。此外,你还需要一个串口调试助手软件在电脑上,用来收发数据。像XCOM、SSCOM、AccessPort这类都行,选一个你顺手的。

接线是关键,接错了肯定没反应:

  1. ST-Link连接(用于下载程序):ST-Link的SWDIO接板子的PA13SWCLKPA14GNDGND3.3V3.3V(如果板子能自己供电,只接GND也行)。
  2. USB转TTL连接(用于串口通信):这是新步骤。模块的TX引脚接STM32的PA10(USART1的RX),模块的RX引脚接STM32的PA9(USART1的TX)。务必注意:交叉连接!即发(TX)对收(RX),收(RX)对发(TX)。最后,把模块的GND和板子的GND接在一起,共地是通信的基础。

注意:很多新手在这里栽跟头,直接TX接TX,RX接RX,结果互相“聋哑”。记住口诀:“发对收,收对发”。

2.2 串口通信原理极简说

你可能听过波特率、数据位、停止位这些词,觉得头大。我用打电话来类比一下:

  • 波特率:好比我们说话的速度。我俩必须约定用同样的语速(比如9600字/分钟)说话,才能听清。9600、115200这些数字就是每秒传输的比特数。速度不一致,收到的就是乱码。
  • 数据位:就是你每个字说几个字母。通常是8位(一个字节),正好能表示一个ASCII字符。
  • 停止位:相当于每句话说完后的停顿。告诉对方:“我这句话说完了”,通常是1位。
  • 校验位:一种简单的纠错机制,比如约定一句话里“的”字必须是奇数个,用来检查传输过程中有没有出错,简单通信可以不用(None)。

STM32内部有叫USART(通用同步异步收发器)的硬件外设,它专门干串口通信这个活儿。我们编程要做的,就是配置好这个硬件外设的工作模式(波特率等),然后它就会自动帮我们把数据一位一位地发出去,或者收进来。我们只需要关心“发什么数据”和“收到数据后怎么办”,底层繁琐的时序它全包了。理解了这个,你就知道代码其实是在配置和驱动这个硬件模块。

3. 在Keil中从零构建串口通信工程

好了,理论说完,我们动手。这里我会用STM32标准库来操作,因为它寄存器封装得好,理解起来直观。后续你想玩HAL库或者LL库,原理都是相通的。

3.1 创建工程与RTE环境配置

打开Keil,点击Project -> New uVision Project,新建一个工程,取名USART_Example,选择我们的老伙计芯片STM32F103C8

在弹出的“Manage Run-Time Environment”窗口,这是Keil5非常方便的功能。我们需要勾选:

  • CMSIS -> CORE:必须勾,ARM内核支持。
  • Device -> Startup:必须勾,启动代码。
  • Device -> STM32Cube Framework (API) -> Classic:这里我们暂时不勾,因为我们用标准库。但如果你找不到标准库,可以在这里勾选STM32F1xx系列,它会使用HAL库,我们这次以标准库为例。 由于我们用标准库,我们直接点OK,然后手动添加标准库文件到工程。你需要提前准备好STM32F10x标准库的文件夹。在工程目录下新建一个Library文件夹,把标准库的inc(头文件)和src(源文件)拷贝进去。然后回到Keil,在项目浏览器里右键Target 1,选择Add Group,创建Library组,再把src里的关键文件(如stm32f10x_gpio.cstm32f10x_rcc.cstm32f10x_usart.c)添加进来。

接下来是关键一步:告诉编译器头文件在哪。点击魔术棒图标(Options for Target),选择C/C++选项卡。在Include Paths里,添加我们刚才的Library/inc文件夹路径。同时,在Define框里,根据你的芯片型号添加全局宏定义。对于STM32F103C8,属于中等容量,需要添加:USE_STDPERIPH_DRIVER,STM32F10X_MD。这个STM32F10X_MD中的MD就是指Medium Density(中等容量),非常重要,决定了编译器使用哪个型号的启动文件和外设定义。

3.2 编写串口初始化函数:配置USART1

我们来创建main.c,并编写串口初始化的核心代码。初始化一个外设,通常遵循“时钟->引脚->模式->使能”的套路。

#include "stm32f10x.h" #include <stdio.h> // 为了后面可以用printf void USART1_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; // 1. 开启时钟:USART1和GPIOA的时钟都必须开 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); // 2. 配置USART1的TX引脚(PA9)为复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // TX GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 3. 配置USART1的RX引脚(PA10)为浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // RX GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); // 4. 配置USART1的工作参数 USART_InitStructure.USART_BaudRate = 115200; // 波特率,和电脑端调试助手设成一样 USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 8位数据 USART_InitStructure.USART_StopBits = USART_StopBits_1; // 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); // 5. 使能USART1 USART_Cmd(USART1, ENABLE); }

这段代码是串口通信的基石。我解释几个容易迷糊的点:GPIO_Mode_AF_PP(复用推挽输出)是指这个引脚现在不是普通的IO口了,它的控制权交给了内部的USART外设,由USART硬件来控制它什么时候输出高电平或低电平,以产生串行波形。GPIO_Mode_IN_FLOATING(浮空输入)则是因为RX引脚只需要安静地读取外部传来的电平信号,不需要内部上拉或下拉电阻。

3.3 实现数据发送与接收:轮询与中断两种方式

配置好硬件,接下来就是怎么用了。发送数据相对简单,接收数据则有“轮询”和“中断”两种主流方式,它们区别很大。

发送一个字符和字符串:

// 发送一个字符 void USART1_SendByte(uint8_t data) { USART_SendData(USART1, data); // 把数据塞进发送数据寄存器 while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); // 等待发送完成 // TXE标志位为1表示“发送数据寄存器空”,可以写入新数据了 } // 发送一个字符串 void USART1_SendString(char *str) { while (*str) { USART1_SendByte(*str++); } }

main函数里初始化后,调用USART1_SendString("Hello STM32!\r\n");,电脑端的串口调试助手就能收到这句话了。\r\n是回车换行,让显示更整齐。

接收数据——轮询方式:这是最简单粗暴的方式,就是不停地去问USART:“你有数据收到吗?”

uint8_t USART1_ReceiveByte(void) { while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET); // 死等,直到收到数据 // RXNE标志位为1表示“接收数据寄存器非空”,有数据可读了 return USART_ReceiveData(USART1); }

mainwhile(1)循环里,你可以调用这个函数。但它的致命缺点是阻塞。如果一直没数据来,程序就卡死在这里了,什么都干不了。只适合非常简单的场景。

接收数据——中断方式(推荐!):这才是工程中实际用的方法。原理是:当USART收到一个数据后,硬件会自动置位一个标志,并触发一个中断。CPU会暂停当前的工作,跳转到我们预先写好的“中断服务函数”里,把数据读走,处理完再回去继续工作。这样主程序while(1)就可以安心做其他事(比如闪烁LED、读取传感器),完全不用操心串口数据什么时候来。

// 1. 在初始化函数最后,开启接收中断 void USART1_Config(void) { // ... 前面的配置代码不变 USART_Init(USART1, &USART_InitStructure); // 使能USART1的接收中断 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 配置NVIC(嵌套向量中断控制器),给USART1中断分配优先级 NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 先设置优先级分组,常用分组2 NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; // 中断通道是USART1 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 抢占优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 子优先级 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); USART_Cmd(USART1, ENABLE); } // 2. 编写中断服务函数(函数名是固定的,在启动文件startup_stm32f10x_md.s里能找到) void USART1_IRQHandler(void) { uint8_t received_data; if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { // 判断是否是接收中断 received_data = USART_ReceiveData(USART1); // 读取数据,这个操作会自动清除RXNE标志 // 在这里处理接收到的数据,比如存到数组里,或者立刻回发 USART1_SendByte(received_data); // 示例:把收到的数据原样发回去(echo) } }

中断方式理解起来稍复杂,但用起来非常高效。上面例子实现了“回声”功能:你从电脑发一个字符,STM32收到后立刻发回给电脑。

4. Keil调试心法:用Serial Window和Memory窗口洞悉数据流

代码写完了,编译通过,下载进去,但串口调试助手没反应?别慌,这才是学习的开始。Keil自带的调试器是强大的侦探工具,能帮你看到芯片内部的真实情况。

4.1 活用Debug模式与Serial Window

点击Debug -> Start/Stop Debug Session(或按Ctrl+F5)进入调试模式。在调试界面,点击View -> Serial Windows -> UART #1(或者UART #2,取决于你用的哪个串口)。这个Serial Window(串口窗口)是神器!它可以直接模拟一个串口终端,和你代码里的printf输出对接。

但默认情况下,printf函数并不会输出到USART。我们需要“重定向”它。在工程里添加以下代码:

#include <stdio.h> // 重写fputc函数,让printf的输出指向USART1 int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); return ch; }

然后在main函数里,初始化串口后,你就可以直接使用printf("系统启动成功,当前温度:%d\r\n", temperature);了。在调试时,这些打印信息不会真的从硬件串口发出去(因为可能没接USB转TTL),而是会显示在Keil的Serial Window里!这对于调试程序逻辑、查看变量值无比方便,无需依赖外部硬件。

4.2 通过Memory窗口和Peripheral寄存器视图深层次排错

如果Serial Window都没数据,说明问题更底层。这时就要祭出Memory窗口Peripheral视图了。

  • Peripheral视图:点击View -> Peripheral Registers,然后找到USART1并展开。这里以最直观的方式显示了USART1所有寄存器的当前值。你可以一边单步执行你的初始化代码(F10/F11),一边观察这里的寄存器位是如何被改变的。重点看CR1(控制寄存器1)里的UE(USART使能)、TE(发送使能)、RE(接收使能)位是否变成了1。再看BRR(波特率寄存器)的值,计算一下是否和你设置的115200相符(在72MHz系统时钟下,BRR应该是0x1D4C)。如果不对,说明你的时钟配置或初始化代码有问题。

  • Memory窗口:点击View -> Memory Windows -> Memory 1。在地址栏输入0x40013800,这是USART1外设的基地址。这里显示的是原始的内存数据,和Peripheral视图是对应的,但更“硬核”。你可以在这里直接修改内存值来操控外设,不过新手用Peripheral视图更友好。

  • 逻辑分析仪(Logic Analyzer):这是大招。在调试模式下,点击View -> Analysis Windows -> Logic Analyzer。点击左上角的Setup,新建一个信号,输入USART1_TXUSART1_RX。然后全速运行程序。你就能看到一个真实的、波形图形式的串口数据流!你可以测量每一位的宽度,精确计算实际波特率,看数据位、停止位是否正确。这是解决复杂时序问题的终极武器。

4.3 串口通信常见问题与排查清单

根据我的经验,问题就出在以下几个地方,按这个清单排查,99%的问题都能解决:

  1. 完全没数据(电脑收不到任何东西)

    • 接线:TX/RX是否交叉连接?USB转TTL模块的GND是否和板子共地?
    • 供电:USB转TTL模块和开发板是否都正常供电?有些模块需要外部5V供电。
    • 驱动:电脑设备管理器里,USB转TTL的COM口驱动是否安装好?端口号是多少?
    • 代码初始化:用Peripheral视图检查USART时钟、GPIO时钟是否开启?UETE位是否使能?波特率寄存器BRR值是否正确?
    • 软件设置:串口调试助手选择的COM口号、波特率、数据位、停止位、校验位是否和代码设置完全一致
  2. 收到乱码

    • 波特率不匹配:这是最常见原因。确认代码和调试助手的波特率一模一样。用Logic Analyzer测量一下实际波特率。
    • 系统时钟不对:你的STM32系统时钟(SystemCoreClock)真的是72MHz吗?如果用了HSE(外部高速晶振),启动文件system_stm32f10x.c里的HSE_VALUE宏定义是否和你板子上的晶振频率一致(通常是8M)?时钟不对,算出来的波特率自然不准。
  3. 只能收不能发,或只能发不能收

    • GPIO模式配置错误:TX引脚必须配置为复用推挽输出GPIO_Mode_AF_PP),RX引脚必须配置为浮空输入或上拉输入GPIO_Mode_IN_FLOATINGGPIO_Mode_IPU)。检查你的GPIO_Init函数。
    • 中断方式接收不到:检查USART_ITConfig是否开启了USART_IT_RXNE中断?NVIC中断控制器是否配置正确?中断服务函数的函数名是否拼写正确(USART1_IRQHandler)?
  4. 数据丢失或错位

    • 接收缓冲区溢出:在中断服务函数里,处理数据的速度太慢,或者没有及时读取USART_ReceiveData。确保中断函数执行时间尽可能短,复杂的处理可以只把数据拷贝到缓冲区,然后设置一个标志,让主循环去处理。
    • 逻辑错误:比如你想用printf发送一个变量,但变量在发送过程中被其他中断修改了。可以考虑在发送关键数据前暂时关闭中断。

5. 工程源码解析与模块化进阶

光说不练假把式,我把一个完整的、模块化的串口工程拆解给你看。这个工程实现了:上电发送欢迎信息,通过中断接收电脑发送的指令(以回车换行\r\n为结束符),并控制一个LED灯的开关。

工程文件结构如下:

USART_Project/ ├── User/ │ ├── main.c │ ├── usart1.c │ └── usart1.h ├── Library/ │ ├── inc/ (标准库头文件) │ └── src/ (标准库源文件) └── Startup/ (启动文件)

usart1.h头文件负责声明对外接口,非常清晰:

#ifndef __USART1_H #define __USART1_H #include "stm32f10x.h" #define USART1_RX_BUFFER_SIZE 128 // 定义接收缓冲区大小 extern uint8_t USART1_RxBuffer[USART1_RX_BUFFER_SIZE]; // 接收缓冲区 extern volatile uint16_t USART1_RxCounter; // 接收数据计数器 extern volatile uint8_t USART1_RxFlag; // 接收完成标志 void USART1_Init(uint32_t baudrate); // 初始化函数,可指定波特率 void USART1_SendByte(uint8_t data); void USART1_SendString(char *str); uint8_t USART1_ReceiveByte(void); void USART1_SendDataBuffer(uint8_t *buffer, uint16_t length); #endif

usart1.c源文件则是具体实现,里面包含了带缓冲区的中断接收逻辑。核心思路是:在USART1_IRQHandler中断函数里,每收到一个字节,就存入USART1_RxBuffer数组,并递增计数器USART1_RxCounter。同时检查收到的字符是否是结束符(比如\r\n),如果是,则置位USART1_RxFlag标志。主循环里只需要不断检查这个RxFlag,一旦发现被置位,就知道收到了一帧完整的数据,然后进行解析和处理(比如判断是否是“LED_ON”命令)。这种方式避免了在中断里做复杂的字符串比较,是嵌入式串口通信的经典范式。

main.c中,代码变得非常简洁和专注于业务逻辑:

int main(void) { LED_Init(); // 初始化LED GPIO USART1_Init(115200); // 初始化串口1,波特率115200 printf("System Ready.\r\n"); // 使用重定向后的printf while(1) { if(USART1_RxFlag == 1) { // 如果收到一帧完整数据 USART1_RxFlag = 0; // 清除标志 // 处理接收缓冲区 USART1_RxBuffer 中的数据 if(strcmp((char*)USART1_RxBuffer, "LED_ON") == 0) { GPIO_ResetBits(GPIOC, GPIO_Pin_13); // 开灯 printf("LED is ON.\r\n"); } else if(strcmp((char*)USART1_RxBuffer, "LED_OFF") == 0) { GPIO_SetBits(GPIOC, GPIO_Pin_13); // 关灯 printf("LED is OFF.\r\n"); } else { printf("Unknown Command: %s\r\n", USART1_RxBuffer); } // 清空接收缓冲区,准备接收下一帧 memset(USART1_RxBuffer, 0, USART1_RX_BUFFER_SIZE); USART1_RxCounter = 0; } // 这里可以放心地做其他事情,比如读取传感器 // Delay_ms(100); } }

这样的模块化设计,让串口功能成为一个独立的、可靠的“黑盒子”。你在其他任何项目中,只需要把usart1.c/h文件拷贝过去,简单配置一下引脚和中断优先级,就能直接使用USART1_SendString和查询USART1_RxFlag,完全不用再操心底层细节。这才是工程化的思维,也是从新手迈向熟练的关键一步。代码的健壮性和可维护性大大提升。当你下次需要添加一个无线模块(比如ESP8266,也是通过串口AT指令通信)时,你会发现一切都变得如此简单。

http://www.cnnetsun.cn/news/1252009.html

相关文章:

  • 利用快马平台快速搭建旗博士口播智能体交互原型
  • 通义千问2.5-7B部署避坑指南:常见问题全解析,一次部署成功
  • 构建AI绘画助手:集成Lingbot-Depth-Pretrain-ViTL-14与Stable Diffusion进行可控图像生成
  • MGeo地址要素解析完整指南:支持省/市/区/街道/门牌/楼栋号
  • 长尾关键词在SEO策略中的有效应用及其优化指南
  • 3个核心功能:机器学习可视化资源的高效应用指南
  • 一套开源电商系统的架构到底能扛多大流量?
  • 解决pyecharts在JupyterLab中图形空白问题的终极指南
  • 基于天空星STM32F407的MQ-6丙烷传感器驱动移植与浓度检测实战
  • GitHub界面本地化方案:提升中文开发者效率的开源工具指南
  • 告别复杂配置,用快马ai一键生成flask待办应用原型
  • 造相-Z-Image性能优化:利用OpenVINO加速Python推理流程
  • VLC媒体播放器跨平台界面架构深度解析:从技术选型到实现策略
  • 零门槛GUI自动化:UI-TARS-desktop掀起桌面操作效率革命
  • AI+Yapi:构建接口自动化测试用例生成引擎的实践与架构解析
  • Ollama一键部署EmbeddingGemma-300m:打造个人知识库搜索引擎
  • STM32 SAI驱动PDM麦克风阵列:TDM配置、延迟校准与DMA实时优化
  • 3步解锁你的音乐自由:NCMconverter全方位技术解析
  • 突破BT下载瓶颈:公共Tracker优化配置核心方案
  • Alibaba DASD-4B Thinking 对话工具产业应用:SolidWorks设计文档的智能问答助手
  • PlotJuggler实战指南:从零安装到ROS2实时数据可视化
  • 从零开始学AI修图:InstructPix2Pix环境配置与调用全指南
  • 3个步骤永久保存QQ空间历史记录,让青春回忆不褪色
  • 智能车竞赛利器:用快马平台快速生成嵌入式控制原型代码
  • AHK脚本迁移自动化工具:AHK-v2-script-converter高效升级指南
  • Compose图片加载实战:本地与网络资源的优雅处理
  • SAM3快速部署指南:一键启动Web界面,上传图片即用
  • 利用快马平台快速构建c++面试题智能练习系统原型
  • E900V22C电视盒子的CoreELEC媒体中心配置指南
  • 突破电子书阅读限制:AnyFlip Downloader让在线内容轻松离线化