简介:面向高校嵌入式系统课程的学习者与实验指导教师,该文档汇集基于苏州大学金葫芦平台和AHL-STM32L431开发套件的六个验证性实验报告,可辅助完成课程实验、复习GPIO输入输出、串口通信与中断、PWM信号生成、Flash读写擦除、SPI总线通信以及独立看门狗复位等基础内容。压缩包内共1个docx文件,约10.04MB,按实验一至实验六依次编排,每个实验均记录实验目的、软硬件准备、操作过程、具体步骤截图和实验总结,较完整地呈现从开发环境安装、工程导入编译、串口下载运行到现象观察与程序修改的流程。已有400人学习下载。读者可据此对照验证性实验的完成标准,理解构件化编程与硬件交互要点,也可参考其报告结构、步骤记录和总结写法,为课程实验复盘与报告撰写提供直接素材。
1. 六个验证性实验打包下来,先认清它能给你什么
拿到这份资源的第一眼,多数人会失望:六份实验报告,正文里一堆「此处截图」的空位,总结部分基本是空白,典型的「什么都有就缺一个实验体会」。但嵌入式系统这门课真正卡人的从来不是报告文字,而是从导入工程、编译、串口更新下载 .hex 到现象对得上这几步。这份资源跑在 STM32L431 教学开发套件加配套集成开发环境上,六个实验分别是 GPIO、串口通信与中断、PWM、Flash、SPI、独立看门狗,每个实验都留了完整的工程目录和操作顺序。它适合两类人:手上有同款板子、课设催着交的;以及只有一块 STM32 最小系统板、想照着把构件化编程这套思路搬过去的。别指望它教你 C 语言,它教的是「怎么让板子按你想的动」。
2. GPIO 与第一个工程:把「导入-编译-串口更新」这条链路跑顺
第一个实验看起来最没技术含量,实际上它决定了后面五个实验能不能做下去。点灯本身不难,难的是配套 IDE 的工程结构、Debug 目录里产物的位置、串口更新窗口里那一串「串口号 + BIOS 版本号」到底代表什么。这一章把这条链路拆开,每一步都说清楚它在干什么,后面五个实验就是换个工程重复这套动作。
2.1 GPIO 的输入输出模型与点灯极性
STM32L431 的每个引脚都能配成输入、输出、复用、模拟四种模式,输出里又分推挽和开漏。推挽是上下两个管子轮流导通,能主动拉高也能主动拉低,驱动 LED 用它;开漏只能拉低,高电平靠外部上拉,做电平转换或 I2C 总线才用。输入这边要关注上拉、下拉、浮空三态:按键一端接地、另一端接引脚,引脚就得配上拉,否则松开时是浮空,读到的值随机跳。
点灯最容易翻车的地方是极性。教学板上的 LED 常见两种接法:引脚经限流电阻接 LED 阳极、阴极接地,写高电平点亮;或者 LED 阳极接 VCC、阴极接引脚,写低电平点亮。资源里的样例把灯点亮那一步能跑通,但换一块板子照抄代码,很可能变成「写 1 灭、写 0 亮」。我一般第一次上板不写循环,先把引脚电平写死成 1,看一眼亮不亮,再写死成 0 看一次,两次就能确定极性,比翻原理图快。
/* gpio_blink.c —— 构件化 GPIO 输出,函数名以你手上构件头文件为准 */ #include "gpio.h" #include "mcu.h" /* 构件库对时钟、延时的封装 */ #define LED_PORT GPIOB #define LED_PIN GPIO_PIN_0 int main(void) { /* 构件库通常把端口时钟使能藏在 gpio_init 里; 裸机写法必须先 RCC 使能时钟再配寄存器,顺序反了配置无效 */ gpio_init(LED_PORT, LED_PIN, GPIO_MODE_OUT_PP); /* 先给一个确定的初始电平,避免上电瞬间闪一下被当成"程序跑飞" */ gpio_write(LED_PORT, LED_PIN, 1); for (;;) { gpio_toggle(LED_PORT, LED_PIN); delay_ms(500); /* 500ms 翻转,肉眼可见且不刺眼 */ } }上面这段里,GPIO_MODE_OUT_PP是推挽输出模式,delay_ms的实参决定翻转节奏,500 太慢可以改 200,但别低于 50,人眼分辨不出还容易误判成常亮。gpio_toggle如果构件库里没有,用「读当前值取反再写回」两行替代即可。真正要留意的是时钟:裸机下忘记开 GPIOB 时钟,寄存器写进去读出来全是 0,现象就是「编译没错、下载成功、灯不亮」,很多人会在这里怀疑人生半小时。
2.2 从模板工程到 .hex:五步流程里每一步的实际含义
资源里记录的操作顺序是「建工作文件夹 → 复制模板工程并重命名 → 导入工程 → 编译 → 串口更新下载」,这五步每一步都有它的道理,不是走形式。建文件夹是为了让六个实验互不污染,模板工程复制一份改个名,是为了保留一份干净的原始版本——我见过太多人直接在模板上改,改崩了连回退的地方都没有,这类后悔药只能提前吃。
编译这一步要看输出窗口,不是看有没有弹窗报错。编译成功会在工程目录下生成Debug文件夹,里面躺着.hex文件,这才是要下载的东西。串口更新窗口里的「连接 GEC」是在枚举串口,找到目标板后回显「串口号 + BIOS 版本号」,版本号对不上说明板子里的引导程序和你手上的 IDE 不匹配,后面的下载大概率会失败。
# 下载前先确认 .hex 是刚编译出来的,别把上个工程的文件刷进去 find . -name "*.hex" -newermt "-10 minutes" -ls # 顺手看一眼文件大小,几百字节的 hex 基本是空工程或编译没通过 ls -lh Debug/*.hex-newermt "-10 minutes"是筛出十分钟内修改过的文件,时间窗按你编译耗时调。文件大小这一步很关键:正常点灯工程编出来是几 KB 到十几 KB,如果只有几百字节,多半是链接阶段出了问题或者你打开的是个空模板。选择文件时认准工程目录下Debug里那个,别用桌面上随手拷的旧文件——这是最高频的「下载成功但现象不对」来源。
2.3 printf 调试的三个前提与 C/汇编互调
第一个实验要求掌握 printf 输出调试,这件事在嵌入式里不是#include <stdio.h>就完事。printf 最终要落到某个物理通道上,通常是串口,所以三个前提缺一不可:串口已经初始化并能发送;fputc被重定向到你的发送函数;工程没有启用半主机模式。
半主机是 ARM 调试环境的一个机制,没有重定向时 printf 会通过调试器把字符送到主机端显示。仿真器连着的时候看着一切正常,一脱机运行,程序就停在 printf 里面出不来,现象是「灯不闪了、程序像死机」。这个坑我踩过一次,后来养成习惯:凡是工程里用了 printf,先在脱机状态下跑一遍确认不卡。
/* 把 printf 重定向到串口 0,函数名按构件库实际接口替换 */ #include <stdio.h> int fputc(int ch, FILE *f) { uart_send_byte(UART0, (uint8_t)ch); /* 发一个字节 */ while (!uart_tx_done(UART0)) { } /* 等发送完成,波特率高时可保留 */ return ch; }uart_send_byte换成你构件库里的发送函数,uart_tx_done是发送完成标志查询,去掉这行在低波特率下会丢字符。如果编译报重复定义,说明构件库自己已经重定向过,别重复写。另外要确认工程开了 MicroLIB 或对应的精简库选项,否则半主机那段启动代码还会被链进来。
汇编与 C 互调那个样例(资源里叫 asm 版本)值得单独看一眼。它演示的是 C 函数里调用一段汇编、汇编里再回调 C 函数,核心是寄存器约定:参数走 r0-r3,返回值走 r0,调用前后要保住 callee-saved 寄存器。看不懂汇编没关系,但要理解一件事——中断服务例程的入口名必须和启动文件里的向量表名字严格一致,多一个下划线少一个字母,中断就永远进不去,编译器不会报错。
2.4 六个实验的目录规划建议
资源里强调「分门别类,各有归处」,这话听着像辅导员口吻,实际很有用。我的做法是每个实验一个顶层文件夹,里面固定三个子目录:src放工程源码,hex放每次编译出来的产物并按日期重命名,doc放截图和当次的参数记录。六个实验做完,hex目录里就是一条完整的时间线,哪次改动导致了现象变化,回查特别快。
提示:
Debug目录是编译产物目录,别把它当成源码目录往里加文件,清理工程时它会被整个删掉。
把这一步做完,后面五个实验的流程就是复制粘贴:导入、编译、连接、选 hex、更新、看现象。差异只在代码逻辑和观察点上。
3. 串口中断与组帧:把「收到一字节减 3」跑通
第二个实验是六个里信息密度最高的。它同时涉及中断机制、串口时序、组帧策略和上位机编程,资源里给的验证动作是「MCU 收到 1 字节后减 3 再发回去」,看着简单,但它把「接收中断触发 → 取数据 → 处理 → 回发」这条闭环完整串了起来。跑通这个,后面的 SPI、Flash 都只是换外设。
3.1 中断服务例程与主循环的分工边界
中断的本质是硬件打断当前执行流,跳到向量表指定地址去执行。STM32L431 的串口接收中断在收到一个字节后触发,进入USARTx_IRQHandler。这里有条铁律:中断服务例程里只做最短的事——读数据、存起来、清标志,然后立刻返回。在中断里做延时、等发送完成、跑浮点运算,轻则丢字节,重则整个系统像卡死。
主循环和中断之间共享变量,必须加volatile。不加的话编译器看主循环里那个变量「没人改」,直接优化成只读一次,现象就是「中断明明进了,主循环拿到的还是旧值」。这个坑在 Keil 的优化等级调高之后尤其容易出现,调试版正常、发布版失效,属于典型的玄学问题。
/* 环形缓冲 + 中断收字节,主循环取走处理 */ #define RX_BUF_SIZE 64 static volatile uint8_t rx_buf[RX_BUF_SIZE]; static volatile uint16_t rx_head = 0; /* 中断写 */ static volatile uint16_t rx_tail = 0; /* 主循环读 */ void USART0_IRQHandler(void) /* 名字必须与启动文件向量表一致 */ { if (uart_rx_ready(UART0)) { /* 构件库的接收标志判断 */ uint8_t ch = uart_read_byte(UART0); uint16_t next = (rx_head + 1) % RX_BUF_SIZE; if (next != rx_tail) { /* 留一个空位,用来区分满和空 */ rx_buf[rx_head] = ch; rx_head = next; } /* 缓冲满了就丢弃,绝不在中断里等主循环腾位置 */ } } int main(void) { uart_init(UART0, 115200); uart_enable_rx_irq(UART0); /* 使能接收中断,漏这行中断永远不进 */ for (;;) { while (rx_tail != rx_head) { uint8_t ch = rx_buf[rx_tail]; rx_tail = (rx_tail + 1) % RX_BUF_SIZE; uart_send_byte(UART0, (uint8_t)(ch - 3)); /* 实验要求的减 3 */ } } }RX_BUF_SIZE取 64 够用,改成 2 的幂(32、64、128)能把取模运算优化成位与,中断里省几个周期。ch - 3是对 ASCII 码做减法:输入字符'7'是 0x37,减 3 得 0x34,也就是字符'4'。所以串口工具里看到回显是4,不是数字 4 的运算结果,也不是 0x04——第一次做这个实验的人十个里有八个会在这里愣一下,以为是编码问题。
3.2 多字节组帧的四种策略与选型
单字节收发只能验证链路通不通,真实场景永远是多字节一帧。帧怎么切,是串口编程的核心问题。资源里提到「接收多字节组成一帧」,没展开讲策略,这里补上四种常见做法和各自代价。
| 策略 | 判帧条件 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 定长帧 | 收满 N 字节即一帧 | 实现最简单,不用扫描 | 长度变化就得改协议 | 命令字固定长度的控制协议 |
| 帧头帧尾 | 检测到帧头 0xAA、帧尾 0x55 | 长度可变,容错好 | 数据里出现同样字节要转义 | 传感器上报、调试口 |
| 长度字段 | 帧头后一字节表示后续长度 | 无需扫描,效率高 | 长度字节本身被干扰就整帧崩 | 二进制协议、大数据块 |
| 超时分帧 | 两字节间隔超过 N 个字符时间 | 不需任何协议约定 | 波特率低时延迟明显 | 上位机手工输入、AT 指令 |
教学实验用定长帧或超时分帧最省事。真做产品我倾向帧头帧尾加转义,代价是状态机要写对,但抗干扰能力强。选型时先问一句:这帧数据是给人看的还是给机器解析的?给人看的用超时分帧,回车换行切一行;给机器解析的用长度字段加校验和。
组帧的状态机用 switch 写最清晰:WAIT_HEAD、RECV_LEN、RECV_DATA、CHECK四个状态,每来一个字节推进一步,超时就复位到WAIT_HEAD。这套写法在中断里跑只需要几行,比「收完再从头扫描」稳得多。
3.3 上位机侧的最小串口程序
实验要求掌握上位机的串口编程,资源里给的示例是 C#。C# 的SerialPort组件把底层都封好了,但有几个坑必须提前知道:DataReceived事件在后台线程触发,直接在里面更新界面控件会抛跨线程异常;ReadLine必须有换行符才返回,二进制数据用它会一直阻塞;BytesToRead拿到的是当前缓冲里的字节数,不代表一整帧。
// 最小串口收发:发一个字符,把回包按十六进制打出来 using System; using System.IO.Ports; class Program { static void Main() { var port = new SerialPort("COM3", 115200, Parity.None, 8, StopBits.One); port.ReadTimeout = 1000; port.DataReceived += (s, e) => { int n = port.BytesToRead; var buf = new byte[n]; port.Read(buf, 0, n); // 用十六进制打印,能同时看清可打印字符和不可打印字节 Console.WriteLine(BitConverter.ToString(buf)); }; port.Open(); port.Write("7"); // 发 ASCII '7',MCU 应回 '4'(0x34) Console.ReadLine(); port.Close(); } }COM3换成设备管理器里的实际串口,115200必须和 MCU 侧一致。BitConverter.ToString输出形如34的十六进制串,比直接转字符串靠谱——回包里混进 0x00 或 0xFF 时,字符串方式会显示成空白,看着像没收到。如果要发十六进制而不是 ASCII,改port.Write(new byte[]{0x37}, 0, 1)。做组帧测试时,我习惯在发送区加个「循环发 100 次不同内容」的按钮,一秒钟就能压出缓冲区溢出的问题。
3.4 波特率误差与乱码的定量判断
乱码先查波特率,这是常识,但「查」到什么程度算查清?STM32L431 的串口波特率由外设时钟分频得到,分频系数是整数加小数,实际波特率和标称值之间有误差。误差控制在 2% 以内基本可靠,超过 3% 就开始间歇性出错,表现是「短帧正常、长帧末尾几个字节乱掉」。
算一遍心里有数:假设外设时钟 80MHz,目标 115200,分频值约 694.4,取整后实际波特率约 115108,误差 0.08%,很稳。换成 921600 这类高波特率,分频值只剩几十,取整带来的误差就明显了。所以教学实验一律用 115200 是有道理的,不是随便定的。真遇到高波特率乱码,第一反应应该是换低波特率验证,而不是去改代码逻辑。
另一个常被忽略的点是共地。USB 转 TTL 模块和开发板只接 TX、RX 不接地,电平参考点漂移,短距离能用,稍长就出错。这类问题不玄学,接上地线就好。
4. PWM、Flash、SPI、看门狗:四个构件实验的参数与现象对照
后四个实验共享同一套操作流程,差别全在参数和现象上。这一章按「参数怎么算 → 现象应该是什么 → 对不上先看哪」的顺序,把四个实验的关键点摊开。构件化编程的好处是接口统一,坏处是出了问题不知道底下发生了什么,所以每个实验我都给一层「底下是什么」的解释。
4.1 PWM:频率、占空比、分辨率的三者互斥
PWM 的本质是一个定时器在数数,数到比较值翻转电平,数到周期值归零。三个参数互相牵制:频率由时钟和预分频、周期共同决定,占空比由比较值决定,而占空比能取多少档取决于周期值有多大。公式是:
f_pwm = f_timer_clk / ((PSC + 1) * (ARR + 1)) duty = (CCR + 1) / (ARR + 1) resolution = log2(ARR + 1) // 占空比可调的档位数的二进制位数PSC是预分频,ARR是自动重装值(周期),CCR是比较值。想要 1kHz、1000 档调节,取 PSC=79、ARR=999,定时器时钟 80MHz 时分频后 1MHz,数 1000 个数正好 1kHz,占空比每档 0.1%。
| 目标频率 | 定时器时钟 | PSC | ARR | 分辨率 | 典型用途 |
|---|---|---|---|---|---|
| 1 kHz | 80 MHz | 79 | 999 | 约 10 位 | LED 调光、蜂鸣器音调 |
| 20 kHz | 80 MHz | 39 | 99 | 约 6.6 位 | 有刷电机调速(超出听觉范围) |
| 50 Hz | 80 MHz | 79 | 19999 | 约 14 位 | 舵机角度控制 |
| 100 kHz | 80 MHz | 0 | 799 | 约 9.6 位 | 电源类应用 |
/* PWM 初始化 + 呼吸灯,函数名按构件库实际接口替换 */ #define PWM_ARR 999 #define PWM_PSC 79 void pwm_setup(void) { pwm_init(PWM_TIM, PWM_CH1, PWM_PSC, PWM_ARR); /* 1 kHz,10 位分辨率 */ pwm_set_duty(PWM_TIM, PWM_CH1, 0); /* 从全暗开始 */ pwm_start(PWM_TIM); } /* 主循环里做呼吸:duty 从 0 到 ARR 再回 0,步进决定速度 */ void breathing(void) { for (int i = 0; i <= PWM_ARR; i += 10) { pwm_set_duty(PWM_TIM, PWM_CH1, i); delay_ms(10); } for (int i = PWM_ARR; i >= 0; i -= 10) { pwm_set_duty(PWM_TIM, PWM_CH1, i); delay_ms(10); } }步进取 10 意味着 100 档,循环一次约 1 秒,看起来是平滑的呼吸。改成 1 会到 1000 档,但人眼分辨不出来,反而拖慢循环。观察现象时要注意:如果灯是「一闪一闪」而不是「渐亮渐暗」,说明频率太低(低于 100Hz 人眼能感到闪烁),把 ARR 调小、频率提到 1kHz 以上。资源里实验三的动作是「修改函数里的一个参数再观察」,改CCR最直观,改ARR会同时改变频率和亮度上限,改之前想清楚要验证哪个。
4.2 Flash:擦写单位、寿命与掉电风险
Flash 和 EEPROM 最大的区别是「写之前必须先擦,擦的单位远大于写的单位」。STM32L431 的 Flash 按页组织,一页 2KB,擦除操作一次至少一页,而写入可以按 8 字节(双字)来。这意味着改一个字节,实际流程是:把整页读到 RAM、改掉目标字节、擦除整页、把 RAM 里的内容写回去。中间要是掉电,整页数据全丢。
擦写寿命按数据手册是 1 万次量级。做实验无所谓,真拿它存配置,一天写 100 次,一年三万多,早就超了。所以常见做法是加一层磨损均衡或者把写操作合并,别每次改参数都存一次。
/* Flash 读改写的最小骨架,地址与页大小按芯片手册核对 */ #define FLASH_PAGE_ADDR 0x0801F800UL /* 最后一页起始地址,按实际容量算 */ #define FLASH_PAGE_SIZE 2048U int flash_save(const uint8_t *data, uint16_t len) { uint8_t page_buf[FLASH_PAGE_SIZE]; if (len > FLASH_PAGE_SIZE) return -1; /* 1. 整页读出来 */ memcpy(page_buf, (const void *)FLASH_PAGE_ADDR, FLASH_PAGE_SIZE); /* 2. 改要改的部分 */ memcpy(page_buf, data, len); /* 3. 擦除整页 */ if (flash_erase_page(FLASH_PAGE_ADDR) != 0) return -2; /* 4. 写回 */ if (flash_write(FLASH_PAGE_ADDR, page_buf, FLASH_PAGE_SIZE) != 0) return -3; return 0; }FLASH_PAGE_ADDR千万别照抄,要用「芯片容量 - 页大小」算出来,或者查手册里的页表。写错地址可能擦掉程序区,现象是「下载后一运行就死」,重新下载又能跑,然后一存数据就死,循环往复。这个坑很隐蔽,因为擦写函数本身返回值是成功的。
写回后一定要读出来比对,这是唯一能确认「写进去了」的手段。资源里实验四的观察点就是读出的值和写入值是否一致,比对时把地址和数据都打出来,比只看一个「成功」字样有用得多。
4.3 SPI:四种模式与时钟极性
SPI 是四根线:SCK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。配置上最容易出错的是模式,由 CPOL 和 CPHA 两个位决定四种组合,本质是「空闲时时钟是高还是低」和「在第一个边沿采还是第二个边沿采」。
| 模式 | CPOL | CPHA | 空闲电平 | 采样边沿 | 常见器件 |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 低 | 上升沿 | 多数 Flash、部分传感器 |
| 1 | 0 | 1 | 低 | 下降沿 | 部分 ADC |
| 2 | 1 | 0 | 高 | 下降沿 | 较少见 |
| 3 | 1 | 1 | 高 | 上升沿 | 部分显示屏 |
模式不匹配的典型现象是:读回来的全是 0x00 或 0xFF,或者数据整体错位一位。因为采样点落在跳变沿上,采到的就是不确定值。判断方法很直接——把四种模式挨个试一遍,能读出正确 ID 的那个就是对的。器件手册里一定写着模式,只是有的写成「数据在时钟上升沿有效」,这时候要自己翻译成 CPOL/CPHA。
/* SPI 主机读一个字节:先发再收,SPI 是全双工的 */ uint8_t spi_read_byte(uint8_t dummy) { uint8_t rx; spi_cs_low(SPI1, SLAVE_CS_PIN); /* 片选拉低,选中从机 */ rx = spi_transfer(SPI1, dummy); /* 发一个字节同时收一个字节 */ spi_cs_high(SPI1, SLAVE_CS_PIN); /* 片选拉高,结束本次事务 */ return rx; }dummy是「为了产生时钟而发的无效数据」,主机不发就收不到。连续读多个字节时,片选要在整个事务期间保持低电平,读完再拉高——每字节拉一次片选,有些器件会把这当成两次独立命令,返回全错。这条在实验里不一定暴露,因为样例可能只读一个字节,等你改成连续读寄存器就翻车了。
从机这边,构件库一般只做主站,实验也以主机读写为主。如果要做主从对接,记住 SPI 没有应答机制,主站永远不知道从机在不在,只能靠读 ID 或者回环测试确认。
4.4 独立看门狗:超时计算与喂狗位置
看门狗是一个独立计数器,启动后自己往下数,数到 0 就复位芯片。程序正常跑的时候要定期「喂狗」把计数器重置,程序卡死就没人喂,超时复位,系统自愈。它的价值在于「程序跑飞了也能自己站起来」,代价是喂狗间隔必须小于超时时间。
STM32L431 的独立看门狗由内部低速时钟驱动,约 32kHz,而且这个时钟精度很差(手册给的偏差能到几十个百分点),所以超时时间不能算得太紧。分频系数按 4 的倍数递增,超时公式大致是:
t_timeout ≈ (4 * 2^PR) * (RL + 1) / 32000PR是预分频寄存器值(0~7),RL是重装值(0~4095)。要 1 秒超时,取 PR=4、RL=1999,算下来约 1 秒。
| 目标超时 | PR | RL | 实际约值 | 喂狗间隔建议 |
|---|---|---|---|---|
| 100 ms | 3 | 199 | 100 ms | 50 ms 以内 |
| 500 ms | 4 | 999 | 500 ms | 250 ms 以内 |
| 1 s | 4 | 1999 | 1 s | 500 ms 以内 |
| 4 s | 6 | 1999 | 4 s | 2 s 以内 |
/* 看门狗初始化与喂狗 */ void wdg_setup(void) { iwdg_init(IWDG_PR_4, 1999); /* 约 1 秒超时 */ iwdg_start(); } int main(void) { board_init(); wdg_setup(); for (;;) { task_sensor_poll(); /* 各任务正常执行 */ task_uart_handle(); iwdg_feed(); /* 喂狗放在主循环末尾,所有任务都跑完才喂 */ delay_ms(200); /* 200ms < 500ms 建议间隔,留足余量 */ } }最关键的一条:喂狗绝不能放在中断里。中断可能还在正常触发,而主循环已经卡死,中断喂狗等于把看门狗废掉了。放在主循环末尾,意味着「所有任务都活着」才喂,这才是它应有的语义。
验证它真的生效,别只看程序能跑。故意在主循环里插一个死循环,看板子是不是在 1 秒左右复位,复位后现象是从头开始跑(比如 LED 重新从灭到亮)。这一步不做,你永远不知道看门狗是配对了还是配错了却恰好没触发。
5. 六个实验的避坑清单:连不上、下不进、现象对不上怎么查
六个实验做下来,报错的地方高度集中在几个位置,跟具体实验关系不大。这一章按「现象 → 原因 → 解决」记录五条最常见的,最后给一套固定的自查顺序。这套顺序的价值在于:出问题的时候不用猜,从头走一遍,五分钟内定位到是哪一环。
5.1 五条踩坑记录
现象一:点「连接」提示找不到目标,或者串口列表是空的。原因通常有三个:线是充电线,只有电源针没有数据针;USB 转串口的驱动没装,设备管理器里带黄色感叹号;串口被别的程序占着——最常见的就是上一个串口工具没关。解决顺序是:换一根确定能传数据的线(手机数据线一般可以),设备管理器里确认端口号出现且无感叹号,把所有串口工具和 IDE 的串口窗口全部关掉再重连。第三步最容易被忽略,串口是独占资源,一个进程打开着,另一个就是打不开,报的错还很含糊。
现象二:编译零错误、下载提示成功,板子完全没反应。八成是下载了错的 .hex。工程目录复制来复制去,Debug文件夹不止一个,串口更新窗口里选文件时很容易点进上个工程的目录。文件名可能都叫Template.hex,根本看不出来。解决:下载前看一眼文件修改时间和大小,或者干脆先「清理工程」再重新编译,让Debug目录里只剩一个最新的 hex。这个坑我吃过不止一次,后来养成习惯——下载前必看时间戳。
现象三:串口能收到数据,但全是乱码或者固定几个字节。先查波特率,两边必须完全一致,115200 对 115200,别一边 115200 一边 9600。波特率对了还乱,查共地——USB 转串口模块的地必须和开发板的地接上。再往下查数据位、停止位、校验位,8-N-1 是默认,但有些工具默认带校验。最后才怀疑代码。乱码这件事有个快速判断法:如果每次乱的内容都一样,是配置问题;如果每次乱得不一样,是时序或干扰问题。
现象四:中断写了,编译通过,就是进不去。三个检查点,按顺序过:中断使能位有没有置上(外设级和 NVIC 级都要);中断服务函数名和启动文件里的向量表名是否完全一致,多一个下划线都不行;共享变量有没有加volatile,没加的话优化等级一高就被优化掉。还有一种情况是中断进了但立刻又退出——标志位没清,导致反复进中断,主循环饿死。表现是程序像卡住,实际 CPU 一直在跑中断。
现象五:加了看门狗之后,板子反复复位,程序跑不到几秒就重启。原因几乎一定是喂狗间隔大于超时时间,或者初始化之后第一件事耗时太长还没喂就超时了。独立看门狗的时钟精度差,理论 1 秒超时实际可能只有 700ms。解决:把超时时间放大一倍(RL 翻倍或者降低喂狗频率),确认稳定后再慢慢收紧。另外检查喂狗位置,放在某个可能被跳过的分支里,等于没喂。
5.2 一套固定的自查顺序
出问题的时候最怕乱试,改一处编译一次,五分钟后自己都忘了改了什么。我的做法是固定从下往上查,每一层确认通过再上移。
| 顺序 | 检查项 | 通过标准 | 不通过的处理 |
|---|---|---|---|
| 1 | 供电与连接 | 板子电源灯亮,设备管理器出现串口 | 换线、换口、装驱动 |
| 2 | IDE 识别目标 | 连接后回显串口号和版本号 | 关掉占用串口的程序,换 USB 口 |
| 3 | 编译产物 | Debug下有刚生成的 .hex,大小合理 | 清理工程重编,看编译窗口警告 |
| 4 | 下载动作 | 提示更新成功,进度条走完 | 检查文件是否选对,重新连接 |
| 5 | 最小现象 | 点灯能亮、串口能回一个字节 | 回到实验一的最简工程验证链路 |
| 6 | 目标现象 | 与代码逻辑一一对应 | 加打印,逐段确认执行到哪 |
第 5 步是分水岭:最小现象能出来,说明链路是通的,问题在代码逻辑;出不来,问题在工具链或硬件,别急着改代码。很多人跳过这一步直接怀疑代码,结果在一个本来就通不了的环境里改了半天。
注意:改代码时一次只改一个地方,改完立刻记录改了什么、现象怎么变。同时改三处,最后不知道是哪处起的作用,等于白改。
6. 把验证性实验改成能讲清楚的东西:三个改法和一个验证习惯
验证性实验的定位是「证明这条链路能跑通」,跑通了它就没有更多信息了。想让它对你个人有价值,得往下推一步,把「跑通」变成「可控」。资源里六个实验都是验证性的,我一般会挑三个点上手改,改完对这套板子的理解就不一样了。
| 改造点 | 原实验动作 | 改法 | 能证明什么 |
|---|---|---|---|
| 串口协议 | 收一字节减 3 回发 | 改成帧头 0xAA + 长度 + 数据 + 校验和 | 组帧状态机和校验逻辑能独立工作 |
| PWM 控制 | 改一个参数看现象 | 用串口收到的字节实时改占空比 | 外设配置与通信链路能协同 |
| 看门狗 | 配置后观察不复位 | 主循环插死循环,验证定时复位 | 看门狗真的在守护,不是配了个摆设 |
第一个改造最值得做。把减 3 的裸回发换成带校验的帧,你就必须处理「半帧到达」「校验失败丢弃」「缓冲区不够」这几种情况,而这些正是实际项目里天天遇到、课堂上永远不考的。改造时先定协议:帧头 1 字节、长度 1 字节、数据 N 字节、校验 1 字节(异或或累加和),接受端写一个四状态的状态机。跑通之后用上位机故意发半帧、发错校验,看板子是不是安静地丢弃而不是死机。
第三个改造只要三行代码,收益却最大:
/* 临时插入的死循环,用来验证看门狗确实会复位 */ for (;;) { /* 故意不喂狗,正常情况下约 1 秒后芯片复位 */ }下载后看现象:LED 停止闪烁,约 1 秒后突然恢复从头开始,说明复位生效。如果一直卡着不复位,要么看门狗没启动,要么喂狗动作藏在某个还在运行的中断里。这一步花五分钟,能省掉将来在产品里「看门狗形同虚设」的大麻烦。
还有一个习惯我得单独说:改参数的时候,一次只改一个,并且把改之前的值、改之后的值、观察到的现象记在同一行里。六个实验做完,这份记录比实验报告本身有用得多——报告是给别人看的,记录是给自己查的。从那以后我每次上板前都强制走一遍「供电、连接、产物、最小现象」这四步,宁可多花两分钟,也不在一片混乱里瞎猜。希望帮到你。
本文还有配套的精品资源,点击获取