尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32点灯验证与C++工程化调试实战:让板子开口说话

STM32点灯验证与C++工程化调试实战:让板子开口说话 来聊个特别接地气的问题你编译下载STM32程序板子上的LED确实在闪可是你怎么确定这段闪灭逻辑是你写的代码跑出来的而不是芯片里残留的旧程序、或者是板子硬件自己在那儿“抽风”我刚接触嵌入式那会儿就吃过这个亏。当时用Keil写了个点灯程序下载后灯亮了我高兴得不行结果把板子断电重新上电灯还是那个闪法但我在代码里改延时时间再下载灯的反应完全没变化。折腾半天才发现程序压根没烧进去真正让灯闪的是我上一轮实验留下的代码。从此我养成了一个习惯凡是“看起来正常”的现象都必须有可观测的证据来证明不能让感觉替代码作证。这一篇就从这个点展开结合STM32开发里最常见的“灯在闪可板子呢”的困惑把项目验证、调试手段和C工程化开发串起来聊一遍。这篇东西适合谁看呢一个是刚开始用STM32做嵌入式C开发的初学者另一个是被“点灯简单、调试难”卡住的人。我会从为什么要较真“代码有没有真跑”讲起然后逐步展开开发环境搭建、C工程化点的灯驱动、串口日志输出、按键中断以及一个新手必踩的问题速查表。整个过程下来你会获得一套能直接复用的验证方法让板子开口告诉你它在做什么而不是你隔着屏幕猜它在做什么。1. 先聊清楚灯闪了为什么还不踏实很多教程教点灯是这样的配好GPIO、写个HAL_GPIO_WritePin循环翻转、下载、看灯闪。这套流程本身没错但它掩盖了一个核心问题——你只看到了现象没看到程序执行的证据。灯在闪只能说明IO口电平在变化而IO口电平变化的原因可能是你刚下载的程序也可能是上电瞬间的默认状态、烧录器残留的固件、甚至板载的其他外设在操作这个引脚。1.1 嵌入式开发里“可见性”到底指什么做MCU开发跟做上位机开发有个本质不同你在PC上print一个变量能直接看到结果在MCU上printf不重定向就等于什么都没写代码在跑但你完全看不见。这就是所谓的“可见性”问题——你写的代码和实际硬件执行之间隔着一层黑盒。点灯只是最基础的电平输出你没办法从中确认“这段代码是我写的逻辑在跑”。比如你写了个延时1秒翻转一次的代码灯偏偏0.2秒闪一次你说灯在闪可这跟你写的代码有关系吗所以我要说的第一件事是嵌入式开发入门不要急着炫技先把“验证链路”搭起来。你要能回答三个问题程序下载进去了吗芯片有没有跑起来跑的是不是我这段代码这三个问题任何一个没有证据支撑后面所有调试都会变成玄学。1.2 一次典型的“灯闪了但板子没跑”场景还原我给你还原一个真实场景。某次我在一个项目里需要操控LED做呼吸灯效果写好代码编译0 Error 0 Warning下载提示也成功板子上的灯也确实在呼吸。我很满意继续写下一段功能。写到一半发现不对呼吸灯的周期跟我代码里设置的PWM频率完全不是一回事。我去查芯片的默认寄存器状态发现这个引脚被某个外设的初始化代码动过导致哪怕主程序根本没执行我的LED控制逻辑灯也能以另一种方式呼吸。拆开来看这种问题常见原因有几类。第一烧录配置里没有勾选Reset and Run程序下载完芯片没自动复位板子还在跑旧程序。第二代码里GPIO初始化没生效引脚是悬空或默认状态灯自己乱闪。第三你用了调试下载器但Keil的Flash Download配置里算法选错了程序根本没写进Flash下载成功的提示是假的。第四也是最隐蔽的你在main函数之前就死循环了——Startup文件里中断向量表不对芯片一直在HardFault里打转。这些情况都有一个共同点灯在闪但你的代码没在跑。遇到这种问题正确的解决思路不是盯着灯看而是引入可观测的证据。最简单有效的手段就是串口日志。下一篇之前我先给你一个心理预期所有让人觉得“应该是好了吧”的判断都必须变成“我看到了日志输出确认是执行了我写的分支”。这样你才能把点灯从“玄学”变成“工程”。2. 开发环境搭建别让工具链成为第一个拦路虎说完了“为什么需要证据”现在聊怎么备齐工具。我在这个系列的第一篇、第二篇里已经详细写过环境搭建这里针对“C工程化”再做一次梳理。很多初学者最大的误区是拿到开发板就直接开Keil写代码遇到编译不通过就怀疑自己代码不行其实大部分问题出在工程配置上。2.1 用STM32CubeMX生成带C支持的工程我的建议是当前阶段不要手工新建裸工程。用STM32CubeMX生成初始化代码再把C支持打开能省掉大量繁琐的寄存器配置环节把精力留给业务逻辑。具体操作分这几步打开STM32CubeMX选择你的芯片型号比如STM32F103C8T6。选芯片时注意后缀C8T6是64KB Flash、20KB RAMC6T6是32KB Flash选错了后面编译会出奇怪问题。配置时钟树。外部晶振如果是8MHz在RCC里选Crystal/Ceramic Resonator然后把HCLK设为72MHz让系统时钟跑满。配置GPIO。把LED引脚设为GPIO_Output初始电平根据板子原理图决定——低电平点亮就设High初始化时先灭高电平点亮就设Low。配置USART。如果要用串口日志打开USART1模式选Asynchronous波特率设115200其他默认。在Project Manager里Toolchain选MDK-ARM重点来了在Project设置里把“Use C”或者“C Mode”的选项打开。不同版本CubeMX的位置略有差异但你只要找到Code Generator相关的选项里面会有支持C的勾选项。生成完代码用Keil打开工程你会看到.c和.h文件都是C语言写的。这没关系C支持是在编译层面打开的你新建.cpp文件写C代码main.c可以通过extern声明来调用C函数。这里有个细节CubeMX生成的主函数是main.c不是main.cpp。我在实际项目里一般把main.c里生成的初始化代码留着业务逻辑写在单独的.cpp文件中通过函数接口来衔接。2.2 Keil里必须做的三个关键设置生成工程后别急着写代码先在Keil里做三个设置。第一个在Options for Target - Output里勾选Create HEX File这样编译后能看到.hex文件用第三方烧录工具时才方便。第二个在Debug标签页里选对你的下载器我用的是ST-Link就选ST-Link Debugger然后在Settings里确认能识别到芯片ID。如果这里识别不到芯片后面点下载一定会失败。第三个在C/C或C/C (AC6)标签页里把C标准选到相应版本。ARM Compiler 6默认支持C11如果你想用更新的语法在Misc Controls里加一个--cpp11或--cpp14就行。还有一个细节值得单独说编译器的选择。现在Keil MDK自带ARM Compiler 5和ARM Compiler 6两套编译器新装的一般默认AC6。AC6对C的支持更现代但对旧代码的兼容性不如AC5。如果你在编译C代码时遇到莫名其妙的语法错误可以尝试在Options for Target - Target标签页里切换编译器版本。我个人的经验是新项目直接用AC6别留恋AC5标准支持跟不上。设置好之后先编译一下CubeMX生成的原始代码确认整个工程能在一个正常的基线上运转。这里顺便说一个经验每拿到一个新工程第一件事永远是不改任何代码、直接编译、直接下载验证“空跑”是否正常。这就跟你写网页先开个空白页确认服务器通了一样把变量降到最低后面出问题才查得准。3. 核心实操用C重写一个“会说话”的LED驱动工具链通了接下来是这一篇的正菜用C写点灯代码但不止于点灯还要让灯的状态能够被观测。我们设计一个小类体系把LED封装成一个对象再通过串口把每一次状态变化打印出来。这样灯一亮日志就告诉你“LED_ON此刻系统运行时间xxx”你就能确凿地说灯在闪板子确实在跑我的代码。3.1 设计LED类让硬件操作变成对象的方法调用先看一下这个类怎么设计。简单来说类封装了引脚号和端口的操作逻辑外部代码只需要调用on、off、toggle这些方法不用关心底层寄存器怎么操作。代码如下// led.h #pragma once #include stm32f1xx_hal.h namespace board { class Led { public: Led(GPIO_TypeDef* port, uint16_t pin, bool active_high true); void on(); void off(); void toggle(); bool currentState() const { return is_on_; } private: GPIO_TypeDef* port_; uint16_t pin_; bool active_high_; bool is_on_; }; } // namespace board// led.cpp #include led.h namespace board { Led::Led(GPIO_TypeDef* port, uint16_t pin, bool active_high) : port_(port), pin_(pin), active_high_(active_high), is_on_(false) { // 构造时不做任何硬件操作只保存参数 } void Led::on() { HAL_GPIO_WritePin(port_, pin_, active_high_ ? GPIO_PIN_SET : GPIO_PIN_RESET); is_on_ true; } void Led::off() { HAL_GPIO_WritePin(port_, pin_, active_high_ ? GPIO_PIN_RESET : GPIO_PIN_SET); is_on_ false; } void Led::toggle() { if (is_on_) { off(); } else { on(); } } } // namespace board使用的时候在main.c里声明一个外部函数入口或者在main.cpp里直接实例化这个对象。我自己的习惯是不要修改CubeMX生成的main.c太多而是新建一个app_main.cpp在里面写一个app_main()函数作为业务入口然后在main.c的while(1)里调用它。这样CubeMX升级重新生成代码时不会把业务逻辑冲掉。// app_main.cpp #include led.h #include uart_log.h static board::Led led(GPIOC, GPIO_PIN_13, false); // 假设板子上LED是低电平点亮 void app_main() { led.off(); UART_Log(System start, LED is OFF\n); while (1) { led.toggle(); UART_Log(LED toggled, current state: %s\n, led.currentState() ? ON : OFF); HAL_Delay(500); } }3.2 为什么不直接写HAL函数非要包一层类你可能会想一个点灯逻辑直接调用HAL_GPIO_WritePin不就行了搞什么类封装这个疑问很合理尤其对一个小项目来说封装确实是多了一层。但这里的关键不是“简单”而是“扩展性”。LED在真实项目里几乎不会单独存在它可能是状态指示灯、报警灯、PWM调光灯。如果你裸写HAL调用后续要加PWM呼吸效果就得把所有调用点翻出来改。有了类封装你只需要在Led类内部把on/off/toggle的实现从GPIO翻转改成PWM占空比调节外部业务代码一行都不用动。C封装带来的另一个好处是“资源边界清晰”。你实例化一个Led对象它占用的引脚就由这个对象独占管理别人想在同一个引脚上乱操作一眼就能从代码review中看出来。后面如果加入多个LED循环创建对象数组即可比复制粘贴HAL代码干净得多。这也是嵌入式C的一个核心思想用语言特性表达硬件资源的所有权关系。3.3 命名空间与代码组织小项目也要有大项目的习惯上面代码里我用了namespace board这个习惯很多人一开始不重视。裸机C语言项目里函数名很容易撞车比如HAL库有HAL_GPIO_WritePin你自己写的驱动也可能起个类似的名称一旦重名链接阶段就会报重复定义。C的命名空间从语言层面解决了这个问题。我的建议是每个模块都放进自己的命名空间例如board、driver、app。哪怕项目很小这个习惯养成后代码组织和阅读的体验会提升非常多。再补充一个关键点C和C混合编程时头文件必须加extern C保护。因为CubeMX生成的stm32f1xx_hal.h这些头文件是C语言写的你在.cpp文件里include它们时C编译器会按C的符号修饰规则去查找函数而底层的HAL函数是C符号链接时就会找不到定义。解决办法是在C语言头文件外面加extern C { #include stm32f1xx_hal.h }但实际项目中很多C语言库头文件自身就带了extern C的条件编译保护比如stm32f1xx_hal.h里就有#ifdef __cplusplus extern C {这样的宏。如果你include的头文件没有这个保护就必须自己加上。怎么快速判断编译一次如果报undefined reference或者cannot open source input file大概率就是符号修饰问题。4. 让板子开口说话串口日志与printf重定向点灯代码写完灯也确实在按代码逻辑闪但我说过这还不够。我们要的是“证据”。串口日志就是最廉价也最有效的证据来源。通过串口把程序运行的关键节点、变量值、状态变化打印到PC端串口助手里你就能看着日志一行一行地确认程序是真正在你预期的地方跑着的。这是把“感觉正常”变成“确认正常”的关键一步。4.1 最小串口日志系统不依赖printf也能输出很多人想到日志第一反应是printf重定向。这没错但printf重定向牵扯到微库、浮点支持等问题容易让新手卡住。我先给你一个不依赖printf的最小实现用HAL库直接发送字符串// uart_log.h #pragma once void UART_Log(const char* str); void UART_LogNum(uint32_t value);// uart_log.cpp #include uart_log.h #include stm32f1xx_hal.h extern UART_HandleTypeDef huart1; void UART_Log(const char* str) { HAL_UART_Transmit(huart1, (uint8_t*)str, strlen(str), 100); } void UART_LogNum(uint32_t value) { char buf[12]; int len 0; if (value 0) { buf[len] 0; } else { char tmp[12]; int i 0; while (value 0) { tmp[i] 0 (value % 10); value / 10; } while (i 0) { buf[len] tmp[--i]; } } buf[len] \n; buf[len] \0; HAL_UART_Transmit(huart1, (uint8_t*)buf, len, 100); }注意这个实现里用到了strlen和extern的huart1。strlen需要包含string.hhuart1是CubeMX在主函数里定义的全局变量所以在cpp文件里用extern声明一下就能访问。为什么不直接打印数字因为UART_Log函数只能发送字符串而数字必须先转成字符串。上面这个LogNum就是干这个的虽然简陋但对调试来说完全够用。4.2 完整printf重定向三步打通标准输出上面那个最小实现适合快速验证但一旦程序复杂你要打印的东西就不只是数字和固定字符串了这时候还是得用printf。在STM32上打通printf核心就是重写fputc函数。标准C库的printf最终会调用fputc来逐个输出字符你只需要让fputc把字符通过串口发出去就能实现printf到串口的映射。具体操作分三步。第一步在Keil的Options for Target - Target标签页里勾选Use MicroLIB。MicroLIB是ARM专门为MCU裁剪的精简C库体积小、依赖少没有它printf默认走半主机模式而半主机模式需要额外调试器支持MCU单独跑的时候会卡死在printf调用上。第二步在uart_log.cpp里加入fputc的重写#include stdio.h extern UART_HandleTypeDef huart1; int fputc(int ch, FILE* f) { HAL_UART_Transmit(huart1, (uint8_t*)ch, 1, 100); return ch; }第三步包含stdio.h然后就可以在任意.cpp文件里直接printf(LED state: %s, counter: %d\n, ON, count)了。输出会自动通过串口1发送到PC。这里有一个很多人忽略的细节printf默认不支持浮点除非你启用MicroLIB的浮点支持选项或者在编译选项里加上--printf浮点库。否则printf(%.2f, 3.14)打印出来是空的或者乱码。我在项目里一般不直接用printf打印浮点而是放大成整数打印比如电压值乘以1000后用%d打印既省空间又避免了这个坑。4.3 串口日志的实际效果用日志验证程序行为有了串口日志前面那个“灯在闪但板子呢”的问题就有了答案。你下载程序后打开串口助手如果能看到一行行的日志在按预期节奏输出那就说明程序烧录成功、芯片在运行、代码执行到了你写的分支。如果日志只输出了一次就停了说明程序可能在某个地方死循环或者卡死了。如果日志完全没输出那你就要回头查烧录配置、复位设置、串口参数。我还喜欢在调试时给关键函数加入口日志和出口日志。比如void app_main() { UART_Log([APP] main enter\n); led.off(); UART_Log([APP] LED initialized\n); while (1) { led.toggle(); printf([APP] LED %s\n, led.currentState() ? ON : OFF); HAL_Delay(500); } }这样一旦程序运行顺序和预期不符日志能立刻告诉我停在了哪个环节。你应该把日志当成嵌入式开发的“示波器”——不是看波形而是看程序执行的轨迹。它虽然不如JTAG单步调试那么精细但胜在可以长时间运行监视不打断程序的实时性。5. 从点灯到交互按键中断和状态机改造很多人在点灯这一关之后下一步就开始迷茫灯会闪了然后呢我建议下一步做“按键控制LED状态”这个小项目。它虽然简单但涉及中断、事件驱动、状态管理等嵌入式开发的核心思维。而且这个过程中串口日志的验证价值特别明显按键按下去中断触发日志立刻打出“[EXTI] Button pressed”你能亲眼看到中断被CPU响应的过程比只看灯亮灭可靠得多。5.1 用CubeMX配置外部中断初始化代码自动生成在STM32CubeMX里配置外部中断的步骤不多。先把按键引脚设为GPIO_EXTI模式芯片会自动为该引脚连接EXTI中断线。然后在NVIC设置里使能对应的EXTI中断通道。CubeMX会自动帮我们生成HAL_GPIO_EXTI_Callback的回调框架但函数体需要你自己实现。关于按键消抖这里有个工程上的权衡EXTI回调里不应该做延时消抖因为中断服务函数要尽量短。正确做法是在回调里只设置一个标志位主循环里检测到标志位后再做延时消抖和后续动作。把按键事件放进中断里再把消抖放到主循环这个设计模式在嵌入式里非常常见叫作“中断置标志、主循环处理”。原因不复杂中断里做HAL_Delay会阻塞其他中断响应造成系统“假死”而主循环里做消抖相当于把这个非实时性要求不高的任务交给了后台实时性不受影响。5.2 用枚举和switch实现简单的按键状态机状态机是嵌入式开发里绕不开的话题。就拿LED举例我们可以给它定义几种状态熄灭、慢闪、快闪、常亮。按键每按一次就切换到下一个状态。用C的枚举类型和switch语句这套逻辑写出来非常清晰enum class LedMode : uint8_t { Off 0, SlowBlink, FastBlink, AlwaysOn }; static LedMode currentMode LedMode::Off; static volatile uint8_t buttonPressedFlag 0; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin BUTTON_PIN) { buttonPressedFlag 1; // 只置标志位不在中断里做事情 } } void updateLedMode() { switch (currentMode) { case LedMode::Off: led.off(); break; case LedMode::SlowBlink: led.toggle(); HAL_Delay(500); break; case LedMode::FastBlink: led.toggle(); HAL_Delay(100); break; case LedMode::AlwaysOn: led.on(); break; } }主循环逻辑就是反复检查buttonPressedFlag一旦置位就清零、消抖、切换模式。这个设计的好处是模式切换只改变一个枚举变量而Led对象的行为完全由switch统一驱动后续如果要加新模式只需要加一个枚举值和一个case不会破坏已有逻辑。这里顺带说一下C11的enum class和传统enum的区别。传统enum的枚举值是全局可见的你定义了Off以后整个编译单元里就不能再定义另一个Off容易命名冲突。enum class则把枚举值放在类型作用域内使用LedMode::Off来引用清晰且安全。在嵌入式开发中状态、模式这类东西用enum class管理代码可读性和可维护性会高很多。5.3 业务逻辑分层为什么主循环不能写成一坨做按键加LED这个小项目时你很容易写出一坨满足需求的代码——一个while循环里判断按键、消抖、翻转LED、延时全塞一起。这个阶段能跑通但如果后面加入更多外设你的主循环就会变成一场灾难你分不清哪段逻辑负责什么也不知道为什么按键响应越来越慢。嵌入式开发的工程化很大程度就是学习如何把业务逻辑分层。我推荐这套简单的三层结构底层驱动层Led、Uart这些直接操作寄存器的类、业务逻辑层状态机、模式切换、应用层主循环、事件分发。层与层之间只通过接口通信底层不知道业务逻辑的存在业务层不知道UI的存在。写到这个程度这个按键点灯项目就不仅仅是一个作业了它已经是一个具备架构雏形的嵌入式小应用。后面扩展传感器、显示、通信等模块时你会感谢自己当初愿意在这个时候多花半小时整理代码结构。6. STM32开发常见问题与排查技巧实录前面几节基本把一个“带验证、带日志、带状态管理”的点灯工程讲透了。这最后一节我想专门整理一份问题排查速查表。这些坑我一个一个都踩过写出来帮大家少走弯路。嵌入式开发就是这样代码本身的逻辑往往不难难的是当现象和预期不符时你从哪里入手找到根本原因。6.1 编译阶段高频错误对照表先看编译阶段这是每个新手最先遇到的拦路虎。我整理了几个最高频的错误连同解决办法一起放在表格里方便你对照处理错误现象常见原因解决办法cannot open source input file xxx.h头文件路径没加进Include Paths在Options for Target - C/C - Include Paths里加入头文件所在目录undefined symbol HAL_UART_TransmitHAL库源文件没加入工程把stm32f1xx_hal_uart.c等源文件添加到工程里很多重复定义的错误在头文件里定义了变量或函数实现头文件只放声明定义放到.cpp文件里L6218E: Undefined symbolC代码调用C函数但没加extern C检查相关头文件是否需要extern C保护编译通过了但下载后板子没反应Start文件或芯片型号选错检查C工程配置里的芯片型号和Startup文件是否匹配使用printf后程序卡死没有勾选Use MicroLIB在Options for Target - Target里勾选Use MicroLIB这里最有迷惑性的是“编译通过但板子没反应”。它不算编译错误但比编译错误更恼人。我的排查顺序固定是先查烧录器是否识别到芯片再查Flash Download里的编程算法是否正确然后查供电最后查复位引脚状态。按照这个顺序大部分“下载成功但没反应”的问题都能解决。6.2 调试阶段让人崩溃的三个场景调试阶段的问题比编译阶段更隐蔽因为程序明明在跑行为却不符合预期。第一个典型场景是串口输出乱码。这个90%是波特率不匹配。CubeMX里生成的是115200你串口助手也选了115200还是乱码那查一下系统时钟是不是真的跑到了72MHz。如果外部晶振没起振系统会自动切换到内部HSI 8MHz这时你配置的115200实际输出会变成约12800PC端收到的自然是一堆乱码。解决方法是打印系统时钟频率或者用示波器测MCO引脚的时钟输出。第二个场景是点灯逻辑正常但时不时跳变一下。这种情况先怀疑按键消抖和中断优先级其次怀疑供电不稳LED亮度变化引起的电流波动干扰了复位电路。这种“偶发性故障”最考验耐心我的建议是举一反三用串口日志记录每次模式切换的时间和原因先把偶发问题变成可复现问题再逐步缩小范围。第三个场景是调试器连接不上芯片。常见原因包括芯片进入了低功耗模式、SWD引脚被复用成了普通IO、板子供电不足、连接线接触不良。我之前遇到过一次怎么都连不上ST-Link的情况最后发现是杜邦线松动。排除法在硬件调试里永远是最可靠的思路换线、换接口、换板子供电方式一次只动一个变量。6.3 我踩过的坑那些“想当然”付出的代价这几年的开发经验里我印象最深的一个教训是永远不要假设硬件按照你想的方式工作。比如有次我用C写了一个Led类初始化时把引脚配置成推挽输出。灯没亮我怀疑代码写错了调了半天最后发现是LED的限流电阻焊错了位置。代码层面的问题可以通过调试器、日志快速定位硬件层面的问题往往更隐蔽。另一个教训是日志不是越多越好。刚开始做串口日志时我恨不得每行代码都打印一遍结果日志刷屏有用的信息全被淹没了。后来我养成了一个习惯日志分级。启动信息、状态切换这类重要信息用printf打印循环里高频执行的部分只在特殊情况下才打印否则用计数器累积、定期输出一次汇总。调试输出要像调料一样适量而不是把菜全部泡在酱油里。6.4 从点灯到项目给嵌入式新手的四点扩展建议这一篇快结束时我梳理了四个建议适合你把点灯技能往真实项目扩展时使用。第一把CPU使用率、RAM使用率、堆栈最大占用这些资源数据纳入常规检查。Keil的编译器可以生成.map文件里面有详细的资源占用信息定期看一眼别等程序跑飞了才后悔。第二建立自己的代码模块库。Led、Uart、Button这些类封装好以后放到一个固定的仓库目录里下一个项目直接复用。第三养成看原理图的习惯。板子上的LED是低电平点亮还是高电平点亮取决于硬件设计不取决于你的喜好。第四多读官方代码和优秀开源代码。HAL库的源代码、正点原子和野火的例程都值得仔细读一遍学习别人的命名规范和代码组织方式。这三个字一直支撑我走到现在要较真。嵌入式开发里没有“大概”“好像”“可能”这些词。灯在闪就问自己凭什么当你能用日志、用调试器、用示波器一步步回答出这个问题时你才真正从“点灯玩家”进化成了“嵌入式开发者”。最后再分享一个实用小技巧。串口日志初始化时我习惯打印一行固定格式的启动信息包含固件版本、编译日期时间、主频参数。这样做有两个好处一是每次下载程序后看一眼串口输出就知道新固件是否真的跑起来了避免“下载了旧程序还在跑”的尴尬二是当你有多个版本固件时串口助手里的版本号能帮你确定当前烧进去的是哪一版。这一个小小的习惯能帮你避免非常多的困惑。不信你试试。
返回列表