从串口调试到按键中断:嵌入式开发入门的两大基石

发布时间:2026/10/20 9:25:58
从串口调试到按键中断:嵌入式开发入门的两大基石 从零开始的嵌入式之旅 Day 6 Day 7前五天我基本都是在跟一块板子死磕点灯、GPIO翻转、看时钟树、用手册翻寄存器。说实话做到Day 5结束的时候我最大的困惑不是寄存器记不住而是——我怎么知道我的MCU到底在干什么点灯亮了说明输出没问题但程序内部跑到哪一行、变量是不是符合预期、外设状态对不对全靠猜。所以在Day 6和Day 7我做了两件让学习曲线陡变的事情把串口调试通道打通以及把按键中断这门基本功中的基本功彻底搞明白。这篇文章就把这两天的完整过程和踩坑记录写下来给同样从零开始学嵌入式的人做个参照。你会遇到什么、该注意什么、为什么很多教程里一句带过的东西才是真正卡人的点这里都讲清楚。1. Day 6 主线打通串口让MCU开口说话1.1 为什么第一个认真学的外设必须是串口很多入门教程会把串口放在LED之后、I2C之前来讲但我自己体会下来串口根本不是一个外设这么简单它是我这一整天所有调试手段的基础。有了串口代码里随便print几行变量值、寄存器状态比对着调试器一步步断点快太多。尤其嵌入式开发里现象和原因隔得很远的问题比如某个模块不工作你最先要确认的往往是程序到底有没有走到这里。串口就是最便宜、最直观的那根探针。而且串口的应用场景覆盖极广设备日志输出、AT指令交互、与PC端上位机通信、板间通信几乎每个嵌入式项目都会用。花一整天把它吃透后面全都是回报。1.2 硬件接线三根线搞定串口通信在MCU和PC之间需要一根USB转TTL工具这是最常见的调试配件。接线只有三根TXD、RXD、GND。MCU的TX - 工具的RXMCU的RX - 工具的TXMCU的GND - 工具的GND这里有个新手最容易犯的认知误区这两根数据线是交叉接的不是同名直连。原因很简单A设备的发送脚必须接到B设备的接收脚数据才能流过去。如果同名接同名那就变成了互相往自己的接收脚送数据永远收不到。交叉这件事我第一次接的时候反复确认了三遍才动手因为接错不会烧东西但会让你怀疑人生——程序明明没问题终端就是一丝反应都没有。1.3 串口参数怎么设8-N-1和波特率从哪里来一串115200 8-N-1到底是什么意思115200波特率每秒传输的二进制位数量8数据位一个字节8位有效数据N无校验位11位停止位刚开始我照着115200 8-N-1配置以为这就完事了。真正深入了解之后才发现最关键的其实是波特率是怎么算出来的。MCU内部有一个波特率寄存器USARTDIV它把外设时钟分频得到每个bit的时间。公式是波特率 外设时钟 /16 × USARTDIV比如我的板子APB2外设时钟是72MHz目标波特率115200那USARTDIV算出来是39.06左右取整到带小数位的寄存器值后实际波特率和理论值会有零点几个百分比的偏差。这个偏差在短帧传输下毫无影响但如果你以后做长帧或对时序敏感的应用就必须回头认真算这个分频。我建议至少在理论层面走一遍这个计算过程不要只是点个自动配置就完事。嵌入式开发里知道配置工具帮你做了什么和只会点按钮的差距会在你遇到诡异bug时直接决定能不能解决。1.4 回环测试验证链路的最快方法参数配好之后别急着烧复杂的发送代码。先做一个回环测试uint8_t rx_buf; while (1) { if (HAL_UART_Receive(huart, rx_buf, 1, 100) HAL_OK) { HAL_UART_Transmit(huart, rx_buf, 1, 100); } }这段代码的意思很简单从串口接收一个字节再把同一个字节发回去。实际测试时用一根杜邦线把MCU板上的TX和RX直接短接然后在PC的串口终端里手动发送任意字符。如果终端里能回显出你刚敲进去的那个字符就说明MCU的串口模块本身收发正常如果没反应问题一定出在接线、参数、或者代码配置上跟电脑上的软件关系不大。回环测试是我整个Day 6里做得最值的一件事。它把所有变量拆出来了我不用考虑USB转TTL工具的收发方向不需要猜电脑串口软件有没有配错只需要验证MCU自己能不能在一个环里搞定收发。能搞定硬件链路就是通的。2. Day 6 踩坑实录从没反应到乱码的完整排查链路2.1 现象一串口终端完全没反应像在跟空气说话回环测试都没通过的时候我的排查顺序是检查GND共地。USB转TTL工具和MCU板子要插同一个GND这是最常见也最容易被忽略的问题。很多独立供电的模块如果不共地电平参考点都不一样数据信号根本没法被正确解析。检查TX/RX有没有接反。这一步建议灯板上的丝印和工具上的丝印逐字确认。我当时一度以为板子丝印标反了后来发现是我自己把工具侧的TXD误认为也要接板子的TXD。检查代码里串口初始化有没有真的执行。比如初始化函数被放在某个条件判断里或者时钟开关没使能外设根本没上电。一个很实用的技巧是用万用表量TXD引脚的静态电平。空闲状态下UART引脚应该维持在3.3V或5V的高电平。如果量出来是0V或者跳变基本可以确定配置有问题。这一步排查我花掉了整个上午。当时最大的教训是不要凭感觉跳过最基本的三条线检查直接怀疑代码。第一次做的时候我检查代码至少花了四十分钟最后发现只是工具上一根杜邦线松了。2.2 现象二有数据进来但全是乱码回环测试过了可是一接上电脑串口终端收到的内容就是一团乱码。这种情况十有八九是波特率对不上。嵌入式里波特率不匹配最常见的原因是外设时钟变了但分频值没跟着变。比如很多代码生成工具默认把APB1和APB2的分频配成某组值串口挂在哪个总线上就决定它的时钟源是多少。我之前只记了波特率寄存器里填的值却没注意到时钟树已经被初始化代码改过。后来每次初始化外设我都强迫自己先确认这个外设挂在哪条总线、当前总线时钟到底是多少再去看波特率分频。另外几个乱码原因也要排查连了错误的串口号比如工具枚举出了两个COM口其中一个是蓝牙或调试口串口助手的参数跟代码不一致比如代码里没有校验位助手却选了奇校验发送方和接收方的参考电平不匹配比如5V逻辑的信号接到3.3V的RX脚虽然多数时候能读但波形边沿质量差传输距离长了就会错2.3 现象三printf输出重定向到串口结果程序跑飞打通串口之后所有人都会干同一件事让printf输出到串口。这个操作的本质是重定向C标准库的printf函数到底层串口发送接口。我的做法是重写fputc函数把输出字符送到串口寄存器int fputc(int ch, FILE *f) { huart.Instance-TDR (uint8_t)ch; while ((huart.Instance-ISR USART_ISR_TXE) 0); return ch; }这里踩过一个特别典型的坑默认的C标准库会依赖一个叫半主机模式的机制来实现printf而半主机模式要求调试器在线连接。裸机开发时调试器没连着程序一执行到printf相关代码就卡死看起来就像是程序跑飞了。解决方式是在工程配置里关闭半主机模式同时确保自己重写的底层发送函数足够轻量、不依赖任何操作系统调用。这一步做完printf输出才真正稳定下来。提示如果程序里同时存在调试器断点调试和串口printf尽量让printf只在调试阶段使用。正式版固件里保留大量轮询式串口输出会拖慢主循环时这一点到了做实时控制的阶段你就会深有体会。2.4 Day 6 的收尾给调试建立可视化意识Day 6结束时我做了个小总结真正让这次串口调试变得容易的不是多高的代码水平而是把我看不见的东西变成能看见的东西。串口就是一个起点。后来又延伸出两个小工具体验非常值得推荐逻辑分析仪。国产的十几块钱的8通道玩具级设备就够用抓串口波形、I2C时序、PWM占空比属于低价高回报。波特率自动识别功能。很多串口助手有自动识别波特率虽然不能作为正式手段但拿来排除波特率配置问题非常方便。嵌入式这行一半时间在写代码另一半时间在做刑侦。建立起先看到现象、再推断原因的工作习惯比多看十篇教程都管用。3. Day 7 主线从轮询到中断按键交互的进阶3.1 为什么Day 7一定要学中断继续灯和串口之后下一个顺理成章的目标是按键输入。很多人会想按键不就是读一个GPIO引脚电平吗循环里轮询一下不就行了确实轮询可以解决检测到按键按下这个表层需求。但一旦你开始思考真实场景轮询的缺点立刻暴露按下按键的时间可能只有几毫秒到几十毫秒主循环如果忙于执行其他耗时任务就会错过检测窗口多个按键同时需要响应时轮询逻辑会越来越复杂CPU在轮询期间啥也干不了纯浪费中断的意义在于让MCU在事件发生的瞬间自动打断当前流程跳到专门的处理函数里执行对应逻辑执行完再回到之前被打断的地方。这就好比你在写报告电话铃响了就放下笔去接接完再接着写而不是每秒钟都扭头看一眼手机有没有来电话。按键虽然只是个廉价外设但它背后连接的是一整套中断机制的核心理念。核心理念通了后面移植到定时器中断、串口接收中断、DMA中断就只剩细节问题。3.2 按键硬件怎么接上拉下拉和有效电平绝大多数开发板上的按键设计都很简单一端接GND另一端接某个GPIO引脚GPIO内部使能上拉电阻。这样按键默认状态是高电平按下时被拉低程序检测到引脚从高变低就认为有按键按下。这种平时高、按下低的连接方式叫低电平有效。为什么普遍用低电平有效因为主控的GPIO驱动灌电流的能力通常比输出电流能力更好按键直接接地实现起来最简单也不容易受干扰。另外按键一脚悬空的时候引脚电平是不确定的必须依靠内部上拉或下拉电阻把电平钳住。没有这个电阻你会看到引脚电平在高低之间随机跳变程序检测到一堆幽灵按键。3.3 用代码生成工具配置EXTI中断的完整流程Day 7的重头戏是配置外部中断。我用的芯片主流开发板上外部中断缩写是EXTI全称外部中断/事件控制器。配置步骤可以拆成这样在代码生成工具里把按键对应的GPIO引脚设置为输入模式使能内部上拉把该引脚映射到EXTI中断线并设置触发方式为下降沿触发高电平变成低电平时触发使能NVIC嵌套向量中断控制器中该中断线的全局中断通道在启动代码自动生成的HAL库中断回调函数里写自己的处理逻辑第2步的下降沿触发值得多说两句。什么情况下你会需要上升沿触发按键释放的瞬间要识别的话就选上升沿。但实际项目中大多数是按下动作触发一次处理所以下降沿就够了。而边沿触发和电平触发的区别是边沿是在跳变的瞬间进一次中断电平触发是只要保持低电平就一直进中断。按键处理用边沿触发避免重复响应。配置完成之后中断初始化部分会自动生成我只需要补上回调函数void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin KEY_Pin) { key_flag 1; } }这个回调函数会在按键引脚检测到下降沿时被调用。我在这里只做一件事置一个标志位。至于这个标志位后续怎么用那是主循环的事。3.4 中断处理函数里绝对不能做的事关于中断服务函数技术社区里流传着一句话中断里别做任何耗时的事。这句话我用Day 7的亲身经历验证了浓度。一开始我没忍住想在中断回调里直接翻转一个LED、做延时、甚至往串口打印调试信息。结果发现中断里做延时会让中断处理时间拉长其他中断无法及时响应中断里调用串口发送的轮询等待如果波特率低一次发送就要消耗几百微秒中断里执行复杂运算会让主循环卡顿整个系统观感就是卡卡的正确的做法是中断回调只标记事件、保存数据、设置标志位真正具体的业务逻辑放到主循环去处理。这本质上是生产者和消费者模型中断是生产者只负责产生事件主循环是消费者负责消化事件。4. Day 7 实测中断不触发、按键抖动、共享变量的三个坑4.1 坑一中断回调写好了但代码就是跳不进去第一次写EXTI中断烧完程序按了好几下按键回调函数里的标志位一点变化都没有。我用调试器断在回调里根本进不来。检查了一圈根因有两个第一个是NVIC对应的全局中断没使能。代码生成工具里有一个独立的开关如果只在GPIO层面配了EXTI线却忘了打开NVIC的中断通道事件信号根本进不了CPU。这个点很容易被忽略因为普通的GPIO读写根本不需要NVIC参与。第二个是触发方式选择错误。我一开始想当然地看着按键默认高电平选了上升沿触发逻辑上自洽但把按下之后变化的方向和引脚的绝对电平搞混了。从高到低的跳变是下降沿不是上升沿。这点看手册看了半天才反应过来。提示调用回调函数的时候最好在回调入口先判断GPIO_Pin是不是你关心的那根引脚。一颗芯片可能有多路EXTI共用一个回调函数不判断引脚来源就会发生按了A键结果B键的逻辑被触发这种错乱。4.2 坑二机械按键抖动带来的按一下等于按十下好不容易把中断跑通了我又发现一个新现象每次按下按键调试器里变量的值都跳了好几下。这就是经典的按键抖动问题。机械按键内部是金属片接触按下和释放的瞬间金属片并不是瞬间稳定的会有几毫秒到十几毫秒的反复弹跳。这个弹跳反映到电平波形上就是一段密集的高低横跳。如果每个下降沿都触发中断MCU自然会把一次按压识别成多次。解决抖动的思路有三种我按从低到高的层次记录一下硬件消抖在按键两端并联一个小电容吸收高频跳变。最省事但如果按键接的是长线电容选不好会影响边沿速度延时消抖检测到按下之后等10-20ms再确认电平。简单但前面说过中断里不能延时这个方案更适合放在主循环里做定时扫描消抖每隔5-10ms读一次引脚连续多次读到同一状态才确认按键状态变化。这个方案不阻塞主循环也是行业里最常见的做法我最终采用的是第三种思路把消抖和按键状态检测都挪到主循环里做。中断只负责产生按下事件的提醒主循环在收到提醒后用定时扫描确认按键状态是否真的稳定。4.3 坑三主循环读取按键标志位变量莫名奇妙不更新消抖处理完我以为这一天可以顺利收工了。结果遇到一个诡异的C语言诡异问题主循环里读key_flag这个全局变量有时候读出来的值不是模拟量仿佛中断里写进去的值被吞掉了。这个问题的根源是编译器的优化。在默认优化等级下编译器可能把这个变量的值先加载到CPU寄存器里反复使用而不重新从内存读取。对于普通变量这种行为没问题可一旦变量的值由中断改变编译器并不知情主循环读到的自然就是过期的缓存。解决办法是在变量声明时加上volatile关键字volatile uint8_t key_flag 0;volatile的含义是告诉编译器这个变量的值可能在程序流程之外被改变每次使用都要老老实实从内存里重新读。 这在所有中断与主循环共享数据的场景里都是必要的。但要把话也说清楚volatile能保证读取的即时性却不能保证操作的原子性。在Cortex-M这类32位MCU上读写一个8位或32位变量一般是单条指令天然原子。但如果你在中断里对变量做复合操作比如自增又判断就需要临时关中断或者用更好的同步方式。这一块到了Day 10往后学实时操作系统时还会再遇到。4.4 两个嵌入式新手最容易忽略的细节Day 7下午我把这两个细节写进了自己的学习笔记觉得它们比按键本身更值得记第一个是中断和主循环谁先跑的问题。上电之后NVIC和EXTI的初始化顺序必须保证GPIO时钟先开EXTI线先配好NVIC使能在最后。顺序搞反中断可能在初始化到一半的时候触发导致处理函数面对一堆半配置的外设。第二个是中断优先级分组的问题。现代MCU的中断控制器支持多级抢占优先级但如果配置不当低优先级的中断可能永远抢不了高优先级的中断或者反过来高优先级中断把低优先级中断卡死。当时我把按键中断的抢占优先级配置得和系统滴答定时器中断一样结果按键长时间按住时系统时钟的节拍被频繁打断。后来把不同中断的优先级区分开整个系统才稳定下来。5. 从Day 6到Day 7我对嵌入式学习的几个新认识5.1 学习主线要从按章节学改成按需求学这两天的经历让我彻底改变了一个观念嵌入式学习不是把手册从头翻到尾而是带着一个明确的目标把沿途需要的东西一个一个捞上来。串口是我为了看见程序内部状态才学的中断是我为了让按键响应不再阻塞才学的。每一个知识点都有清晰的需求背景学起来完全不觉得枯燥记起来也比死记硬背寄存器靠谱得多。如果你也在入门我的建议是给自己找一条像按键控制LED并用串口上报状态这样的完整小链路然后顺着链路把每个环节吃透。5.2 养成问题先怪自己的调试心态Day 6和Day 7集中踩了一整天的坑我的情绪经历了从烦躁到平静的转变。后来发现嵌入式系统里大多数问题不是你有多笨而是有某个环节被想当然地跳过了。现在我排查问题默认按这个顺序来硬件链路线接对了吗共地了吗时钟和初始化顺序外设真的被使能了吗代码逻辑条件分支真的会执行到我怀疑的那行吗工具链问题编译优化、半主机、调试器配置这些环境问题永远是最后背锅的这个排查顺序已经帮我在后面Day 8、Day 9的ADC和PWM调试里省了大量时间。5.3 给同样从零开始的人几点实操建议最后说几个我从这两天里沉淀下来、直接就能用的小建议希望能帮你少走弯路日志输出要克制。调试阶段printf随便写但正式跑起来前把所有调试输出关掉或统一走一个日志开关否则后面排查问题的时候日志本身就是噪音按键状态要多画时序图。实测抖动波形会让你直观理解硬件行为别只靠眼睛看代码逻辑来推断中断服务函数越短越好。哪怕只是设置标志位也要保证设置操作本身不会被打断每次只改一个变量。我在这两天最惨烈的几次debug都是因为同时调整了多处代码出问题时根本无法定位来源。把改动拆小每个改动测一轮反而更快这两天的内容量看起来不大但串口和中断这两块是几乎所有嵌入式项目的基石串口是调试的窗口中断是实时响应的底座。把这两样基础打牢后面无论是接传感器、跑屏幕、做电机控制都会顺得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询