Keil串口调试实战:从零点亮“开发者之眼”
你有没有过这样的经历?代码烧进STM32,板子上电,LED不闪、屏幕无显,程序像掉进了黑洞——完全不知道它跑到了哪里。这时候,最朴素也最有效的救星是什么?串口打印一行Hello, World!。
在嵌入式开发中,没有比串口更忠实的“眼睛”了。而对国内大多数初学者来说,Keil MDK就是他们第一次点亮这双眼睛的工具箱。本文不讲空泛理论,也不堆砌术语,而是带你一步步亲手实现一个能在PC上看到输出的完整串口调试系统——用Keil + STM32 + 串口助手,从工程创建到printf打印,全程实操,拒绝“已解决”。
为什么是串口?因为它够“笨”,但也够可靠
别看现在I2C、SPI、USB、以太网五花八门,但调试阶段,工程师第一反应永远是:“先打个串口看看。”
UART之所以成为调试标配,就因为它简单到极致:
- 只要两根线:TX(发)、RX(收);
- 不需要共同时钟,靠双方约定好波特率就能通信;
- 协议清晰:起始位 → 数据位 → 停止位,一帧数据清清楚楚;
- 几乎所有MCU都自带至少一个UART外设;
- PC端通过一块几块钱的USB转TTL模块(如CH340、CP2102)就能接入。
更重要的是,你可以用它输出任何你想知道的信息:变量值、函数进入标志、状态机跳转……它是你和单片机之间的“摩斯电码”。
🔧 典型应用场景:
- 系统启动自检信息输出
- 传感器原始数据实时回传
- 故障日志记录与分析
- 按键/中断触发事件追踪
Keil不是万能的,但它足够“接地气”
说到ARM开发,IAR、GCC、VS Code都各有拥趸,但在中国高校实验室和中小企业里,Keil依然是那个绕不开的名字。
原因很简单:
- 安装简单,界面直观,适合新手快速上手;
- 中文资料铺天盖地,百度一搜就有答案;
- 和ST官方库(如STM32F1标准外设库)配合默契;
- 支持J-Link、ST-Link等主流下载器,调试体验流畅。
当然,它也有短板——免费版限制32KB代码大小,商业授权价格偏高。但对于学习阶段而言,这个“门槛低+生态熟”的组合,依然是绝佳起点。
而且,Keil有个隐藏神技叫semihosting,允许你在不用串口的情况下,直接把printf输出显示在IDE控制台。不过今天我们不玩虚的,我们要走硬核路线:让数据真正通过TX引脚发出去,经USB模块传到电脑屏幕。
动手前准备:硬件与软件清单
别急着敲代码,先把环境搭起来:
✅ 硬件部分
| 名称 | 示例型号 | 备注 |
|---|---|---|
| 开发板 | STM32F103C8T6(蓝丸) | 最常见入门板 |
| 下载器 | ST-Link V2 | 或使用板载DAP |
| USB-TTL模块 | CH340G / CP2102 | 用于串口通信 |
| 杜邦线若干 | —— | 连接PA2(TX) → RXD |
📌 接线重点:
- MCU的TX 引脚接 USB模块的 RXD(发送对接接收)
- 不需要接RTS/CTS流控线(默认关闭)
- GND必须共地!
✅ 软件部分
| 工具 | 版本建议 |
|---|---|
| Keil MDK | 5.37及以上 |
| STM32F1xx标准外设库 | V3.5.0 |
| 串口助手 | XCOM、SSCOM、PuTTY任选 |
第一步:建工程,别再复制别人的模板
很多人学嵌入式第一步就是找“例程压缩包”,解压→打开.uvprojx→改几行代码→下载。结果一旦换个芯片就不会了。
我们要做的,是从头新建一个工程。
1. 启动 μVision,新建 Project
- File → New uVision Project
- 选择目标芯片:
STM32F103C8 - Keil会自动加载对应的启动文件(startup_stm32f10x_md.s)
2. 添加必要的源文件
右键“Source Group 1” → Add Existing Files:
-system_stm32f10x.c—— 系统时钟初始化
-stm32f10x_usart.c,stm32f10x_gpio.c,stm32f10x_rcc.c—— 外设驱动
- 自己写的main.c
💡 提示:这些
.c文件来自标准外设库的Libraries/STM32F10x_StdPeriph_Driver/src/目录。
3. 包含头文件路径
Project → Options → C/C++ → Include Paths:
.\Inc .\Libraries\CMSIS\CM3\CoreSupport .\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x .\Libraries\STM32F10x_StdPeriph_Driver\inc这样编译器才能找到#include "stm32f10x.h"。
第二步:写代码,让USART2真正工作起来
我们选择USART2,因为它使用的引脚 PA2(TX) 和 PA3(RX) 在蓝丸板上更容易引出(不像USART1可能占用SWD调试口)。
初始化代码详解(基于标准库)
#include "stm32f10x.h" void USART2_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; // Step 1: 使能时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); // USART2在APB1总线 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // GPIOA也在APB2 // Step 2: 配置PA2为复用推挽输出(TX) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用功能,推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // Step 3: 配置PA3为浮空输入(RX) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // UART接收脚通常设为浮空 GPIO_Init(GPIOA, &GPIO_InitStructure); // Step 4: 配置USART2参数 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_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 收发模式 USART_Init(USART2, &USART_InitStructure); // Step 5: 启动USART2 USART_Cmd(USART2, ENABLE); }📌 关键点说明:
- 时钟必须先开:否则后续配置无效;
- TX引脚必须设为
AF_PP:这样才能由USART硬件控制输出电平; - RX引脚设为
IN_FLOATING即可,内部已有上拉/下拉可根据需要调整; - 波特率设为115200是目前调试最常用的高速率,响应快且兼容性好。
第三步:重定向printf,让调试变得优雅
你知道吗?每次你写下printf("i=%d\n", i);,底层其实是在调用一个叫fputc()的函数来逐个发送字符。
我们可以“劫持”这个过程,让它把每个字符送到串口而不是电脑控制台。
实现fputc重定向
#include <stdio.h> // 重定义fputc,将printf输出导向USART2 int fputc(int ch, FILE *f) { // 等待发送缓冲区为空 while (!USART_GetFlagStatus(USART2, USART_FLAG_TXE)); // 发送一个字节 USART_SendData(USART2, (uint8_t)ch); return ch; // 返回已发送字符 }⚠️ 注意事项:
- 必须包含<stdio.h>;
- 必须在 Keil 工程中启用MicroLIB!
- 设置路径:Project → Options → Target → ✔ Use MicroLIB
MicroLIB 是一个轻量级C库,专为嵌入式设计,支持printf重定向。如果不勾选,printf可能链接失败或占用过多内存。
第四步:主函数验证,跑通第一个“Hello”
int main(void) { SystemInit(); // 初始化系统时钟(72MHz) USART2_Init(); // 初始化串口 printf("🎉 Keil串口调试成功!\r\n"); printf("系统时钟频率:%d Hz\r\n", SystemCoreClock); int counter = 0; while (1) { printf("计数器:%d\r\n", counter++); for (volatile int i = 0; i < 1000000; i++); // 延时约1秒 } }编译 → 下载 → 上电!
然后打开你的串口助手(比如XCOM),设置:
- 串口号:根据设备管理器识别(如 COM5)
- 波特率:115200
- 数据位:8
- 停止位:1
- 校验位:None
点击“打开串口”,如果一切顺利,你会看到屏幕上不断刷出:
🎉 Keil串口调试成功! 系统时钟频率:72000000 Hz 计数器:0 计数器:1 ...恭喜你,你已经拥有了自己的“开发者之眼”!
常见坑点与避坑指南
别高兴太早,下面这些问题90%的人都踩过:
| 现象 | 原因 | 解法 |
|---|---|---|
| 完全没输出 | ① TX/RXD接反 ② 没开MicroLIB ③ 时钟未初始化 | 查线序、检查选项、确认SystemInit() |
| 输出乱码 | 波特率不匹配或主频不准 | PC和MCU同设115200;确保SystemCoreClock正确 |
| 打印一次后卡死 | 未等待TXE标志 | 检查是否加了while循环检测TXE |
| printf不编译 | 未包含 或未用MicroLIB | 补头文件,勾选Use MicroLIB |
| 接收不到数据 | RX脚模式错误或外部干扰 | 设为浮空输入,加滤波电容 |
💡 经验分享:
如果你发现波特率总是偏差大,可能是晶振频率不对。STM32内部RC振荡器精度较差,建议后期使用外部8MHz晶振并正确配置PLL倍频至72MHz。
进阶思路:不只是“打印”,还能做什么?
当你熟练掌握基础串口调试后,可以尝试以下扩展:
1. 接收命令,实现交互式调试
if (USART_GetFlagStatus(USART2, USART_FLAG_RXNE)) { uint8_t ch = USART_ReceiveData(USART2); if (ch == 'r') { printf("收到重启指令!\r\n"); NVIC_SystemReset(); } }2. 使用中断方式收发
避免轮询占用CPU,提升效率:
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); NVIC_EnableIRQ(USART2_IRQn);3. 结合FreeRTOS做日志分级
#define DEBUG(fmt, ...) do{ printf("[DEBUG] " fmt "\r\n", ##__VA_ARGS__); }while(0) #define INFO(fmt, ...) do{ printf("[INFO ] " fmt "\r\n", ##__VA_ARGS__); }while(0)4. 构建简易AT指令集
用于控制Wi-Fi、蓝牙模块,甚至自己实现远程升级(ISP)。
写在最后:调试能力,才是工程师的核心竞争力
很多人觉得“会写算法”、“懂RTOS”才高级,但我想说:能把最基础的串口调试跑通的人,才是真正踏实的工程师。
因为调试不是一项功能,而是一种思维方式——
你得学会提出假设、验证现象、定位问题、迭代修正。这个过程比写出完美代码更重要。
Keil只是一个工具,串口只是一条通道,但它们共同教会我们的,是如何与沉默的硬件对话。
所以,下次当你面对一片漆黑的终端时,别慌。
插上线,打开串口助手,写一句printf("Start...\n");
哪怕只看到一个字符,也是通往真相的第一步。
👉 如果你在实践中遇到了其他问题,欢迎在评论区留言交流。我们一起把这条路走得更稳、更远。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考