Keil串口通信调试:新手必备的实战入门篇
2026/4/28 14:49:05 网站建设 项目流程

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 MDK5.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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询