ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

嵌入式六实验打通裸机开发:GPIO、串口、PWM、Flash、SPI与看门狗

嵌入式六实验打通裸机开发:GPIO、串口、PWM、Flash、SPI与看门狗

简介:这是一份面向高校嵌入式课程学习者与实验课备教师的学习资料,基于苏州大学金葫芦平台与AHL-STM32L431开发套件编写,聚焦六个验证性实验,帮助读者在没有硬件思路、不熟悉开发流程时,快速对照完成从环境搭建到外设驱动的完整训练。内容覆盖AHL-GEC-IDE集成开发环境的安装与工程导入、GPIO输入输出与 printf 调试、串口接收中断与多字节组帧、PWM 频率与占空比配置、Flash 读写与擦除、SPI 主从通信,以及独立看门狗的定时复位机制,每个实验均含实验目的、准备、过程记录与总结,并配有操作截图,便于对照复现与撰写报告。资源为单个 docx 文档,压缩包约10.04MB,篇幅完整、目录按实验一至实验六顺序编排,可当作实验手册或复习提纲使用。目前已有400人学习下载,适合嵌入式入门阶段需要系统梳理软硬件交互流程、积累实践经验的学生参考。

1. 六个验证性实验能教会什么:从点亮一盏灯到看门狗复位

很多人拿到一份嵌入式系统实验报告,第一反应是“又是一摞截图”。可真拆进去会发现,值钱的是截图背后那条链:改一行 GPIO 编程代码、编译出 hex、通过串口把机器码灌进目标板、看着 LED 从灭到亮。六个实验——GPIO 输入输出、串口通信与接收中断、PWM 构件、Flash 在线编程、SPI 构件、独立看门狗——连起来正好是一条裸机开发主线:先会控制引脚,再会收数据,再会输出模拟量,再会把数据存下来,再会跟外部器件对话,最后学会让系统在跑飞时自己复位。适合正在做课设、想把验证性实验改造成能写进简历的小项目、或者第一次接触构件化编程的人。

2. 工程骨架拆解:导入工程、编译、串口下载到 hex 落地的完整链路

这六个实验看起来各干各的,实际上是同一套工程模板换不同构件。把骨架吃透,后面五个实验基本是复制粘贴加改参数。这一章先把目录怎么摆、编译产物在哪、main.c和isr.c怎么分工讲清楚,后面写代码才不会到处找文件。

2.1 六个实验对应六套独立工程,不要合并目录

最省事的做法反而是最坑的:把所有实验的模板工程都导入同一个工作目录,结果每个工程编译完都在自己的Debug下产出同名.hex,下载时点错文件,现象和代码对不上,能查一下午。我一般按实验编号一实验一目录,每套工程自带src、inc和产物目录,模板工程复制过来后立刻改名。

embsys-lab/ ├── 01-gpio/ │ ├── src/ │ │ ├── main.c # 业务逻辑,只调构件接口 │ │ ├── isr.c # 中断服务例程 │ │ └── startup.S # 启动文件,向量表与复位入口 │ ├── inc/ │ │ └── gpio_comp.h # 构件对外声明 │ └── Debug/ # 编译产物,.hex/.elf 落在这里 ├── 02-uart-irq/ ├── 03-pwm/ ├── 04-flash/ ├── 05-spi/ └── 06-iwdg/

逻辑说明:src放可改代码,inc放构件头文件,Debug是编译输出目录,不要手改里面任何文件,它每次编译都会被覆盖。参数说明:编号前缀01~06是为了让资源管理器里的排序和实验顺序一致,答辩或写报告时翻起来快。

分目录还有一个实际好处:Debug里的.hex文件名默认跟工程名走,目录分开之后,即便文件名撞了也不会互相覆盖,下载前扫一眼路径就知道对不对。

2.2 从导入工程到机器码落地:四步链路和每步的失败点

流程本身不复杂:导入工程 → 编译工程 → 连接目标板 → 选择Debug下的.hex下载。问题在于这四步的报错信息都很“闷”,得知道每步在验什么。

导入这一步验的是工程描述文件能不能被 IDE 认出来,路径里带中文或者工程是从别人机器上拷过来的,很容易导入失败或导入成空工程。编译这一步验的是工具链和头文件路径,报“找不到 xxx.h”基本都是inc路径没加进去。连接这一步其实是让上位机通过串口跟板上固件握手,握手成功会回显“串口号 + BIOS 版本号”,回显不出来就说明串口被占用、线没插对或者板子没上电。下载这一步只是把 hex 按协议分包灌进去并校验。

# 编译完之后别急着下载,先看产物在不在、时间戳是不是刚刚 ls -l Debug/*.hex # -rw-r--r-- 1 user user 20480 Mar 14 10:22 Debug/lab01.hex

逻辑说明:ls -l打出文件大小和修改时间,20KB 左右是这类教学工程的正常量级;如果时间还是昨天的,说明编译没真正跑成功,或者你看的是另一个工程的Debug。这一步三十秒,能省掉“下载成功但现象没变”的大坑。

还有个小习惯值得养成:每次改完代码先编译,编译完先看有没有 warning。构件化工程里很多“编译过了但不工作”的问题,源头就是类型不匹配被隐式转换掉了,比如把uint8_t的引脚号写成int再传给按位操作的宏。

2.3 main.c 与 isr.c 的分工:构件化编程的真实接口长什么样

这套平台的核心思路是“构件化”:芯片寄存器操作被封装成一个个构件,学生写的是gpio_set()这种接口,不是GPIOA->ODR |= (1 << 5)。好处是新手半天就能点亮灯,坏处是封装挡住了细节,一旦出问题就变成黑匣子。所以我建议第一遍照着构件接口写,第二遍一定要翻头文件看看它到底做了什么。

/* main.c —— 只负责“想做什么”,不碰寄存器 */ #include "gpio_comp.h" #include "systick_comp.h" #define LED_PORT PORTC #define LED_PIN 13 int main(void) { gpio_init(LED_PORT, LED_PIN, GPIO_OUT_PP); /* 推挽输出 */ while (1) { gpio_set(LED_PORT, LED_PIN, 1); /* 高电平点亮 */ delay_ms(500); gpio_set(LED_PORT, LED_PIN, 0); delay_ms(500); } }

逻辑说明:gpio_init一次配好引脚方向和输出类型,gpio_set只改电平,delay_ms通常基于 SysTick 或定时器。参数说明:GPIO_OUT_PP是推挽输出,能主动拉高拉低,驱动 LED 够用;如果要接继电器这类需要开漏的负载,换成GPIO_OUT_OD并在外部加上拉电阻。LED_PIN一定对照原理图确认,板子上 LED 接的端口每届板子可能不一样,抄别人的引脚号是高频翻车点。

中断那部分单独放在isr.c,因为中断入口函数名是启动文件向量表里写死的,名字错一个字母就永远进不去,而且编译器不会报错。

/* isr.c —— 只负责“被打断时做什么” */ #include "uart_comp.h" volatile uint8_t g_rx_byte; volatile uint8_t g_rx_flag; void USART1_IRQHandler(void) /* 函数名必须与向量表一致 */ { if (uart_rx_ready(UART1)) { g_rx_byte = uart_read(UART1); /* 读数据寄存器,硬件顺带清标志 */ g_rx_flag = 1; } }

逻辑说明:读数据寄存器这一步同时完成了“取数据”和“清接收标志”,如果不读,中断标志一直挂着,退出中断后会立刻再进来一次,程序看起来像卡死。参数说明:volatile不能省,g_rx_flag在中断里写、在主循环里读,不加volatile编译器可能把它优化进寄存器,主循环永远等不到变化。

startup.S里做的是复位后初始化栈指针、拷贝数据段、跳main,实验一要求“了解汇编与 C 如何互相调用”,指的就是这条路径:汇编里bl main进 C,C 里通过函数指针或向量表回到汇编写的入口。改启动文件风险很高,验证性实验阶段不建议动,看懂就行。

3. GPIO 与串口中断:两个最容易被低估的验证实验

六个实验里,前两个看着最简单,实际最容易留下知识空洞。点灯谁都会,但“为什么按键读出来一直是 0”“为什么中断只进一次”这类问题,恰恰是后面做课设时最常卡住的地方。这一章把 GPIO 输入输出的语义和中断组帧讲透,代码可以直接抄进自己的工程。

3.1 GPIO 输出构件:点灯、蜂鸣器、继电器是同一个动作

输出引脚的本质就一句话:让引脚呈现高电平或低电平。LED、蜂鸣器、继电器只是外部驱动电路不同,软件侧完全一样。实验一里从 LED 换到蜂鸣器再换到继电器,练的就是这个抽象能力。

/* 把“设备动作”封一层,主循环里读起来就是业务语言 */ static void led_on(void) { gpio_set(LED_PORT, LED_PIN, LED_ACTIVE_LEVEL); } static void led_off(void) { gpio_set(LED_PORT, LED_PIN, !LED_ACTIVE_LEVEL); } int main(void) { gpio_init(LED_PORT, LED_PIN, GPIO_OUT_PP); gpio_init(BEEP_PORT, BEEP_PIN, GPIO_OUT_PP); while (1) { led_on(); beep_on(); /* 蜂鸣器多数是有源,给电平就响 */ delay_ms(200); led_off(); beep_off(); delay_ms(800); } }

逻辑说明:LED_ACTIVE_LEVEL这个宏是关键,有的板子 LED 阳极接电源、引脚拉低才亮,写死1就会出现“程序跑起来了但灯是反的”。参数说明:无源蜂鸣器不能靠给电平驱动,得用 PWM 输出方波,那属于实验三的内容;有源蜂鸣器才是给电平就响。判断方法很简单,给一个固定电平听有没有声,没声基本就是无源的。

驱动能力也要留意。GPIO 单脚一般只能出几毫安到十几毫安,直接驱动继电器线圈会拉垮电平甚至伤引脚,正规做法是加三极管或专用驱动芯片。实验板上通常已经做好了这部分电路,但你自己画板子时这块必须补上。

3.2 GPIO 输入:按键读到的到底是不是 0

输入实验比输出更容易翻车,因为“读不到”有三种完全不同的原因:引脚配错、上下拉没设、机械抖动。

#define KEY_PORT PORTA #define KEY_PIN 0 void key_init(void) { /* 上拉输入:按键另一端接地,按下读到 0,松开读到 1 */ gpio_init(KEY_PORT, KEY_PIN, GPIO_IN_PULLUP); } uint8_t key_pressed(void) { static uint8_t last = 1; uint8_t now = gpio_get(KEY_PORT, KEY_PIN); if (last == 1 && now == 0) { /* 下降沿:按下瞬间 */ delay_ms(20); /* 软件消抖,等抖动过去 */ if (gpio_get(KEY_PORT, KEY_PIN) == 0) { last = now; return 1; } } last = now; return 0; }

逻辑说明:gpio_get返回引脚当前电平,上拉输入在悬空时是 1,按下接地变 0,所以判断的是下降沿。delay_ms(20)是软件消抖,机械按键触点在闭合瞬间会抖十几毫秒,不做处理一次按下会被识别成好几次。参数说明:如果硬件上已经有下拉电阻且按键接电源,就要改成GPIO_IN_PULLDOWN并反转判断逻辑,这个一定要看原理图,不要凭感觉。

消抖还有中断版做法:把引脚配成双边沿触发,在中断里启动一个 20ms 定时器,定时到了再采样一次。按键多、主循环又忙的时候这种更稳,但验证性实验阶段用轮询足够,先把逻辑跑通。

3.3 串口接收中断与多字节组帧

实验二的核心不是“会发一个字节”,而是“收到一串字节怎么拼成一帧”。串口是字节流,没有边界,硬件只保证你按顺序收到一个个字节,至于哪几个字节属于同一条消息,得靠协议自己定。

#define FRAME_HEAD 0xAA #define FRAME_MAX 32 static uint8_t buf[FRAME_MAX]; static uint8_t idx = 0; /* 在中断里只做搬运,解析放主循环 */ void uart_frame_feed(uint8_t b) { if (idx == 0 && b != FRAME_HEAD) return; /* 未同步,丢弃直到帧头 */ buf[idx++] = b; if (idx >= 3 && idx == (uint8_t)(buf[1] + 3)) { uint8_t sum = 0; for (uint8_t i = 0; i < idx - 1; i++) sum += buf[i]; if (sum == buf[idx - 1]) { g_frame_ready = 1; /* 只置标志,不在这里解析 */ } idx = 0; /* 无论成败都复位 */ } if (idx >= FRAME_MAX) idx = 0; /* 防缓冲区溢出 */ }

逻辑说明:帧格式约定为帧头(1) + 长度(1) + 数据(n) + 校验和(1),总长 =n + 3。第一个分支负责重同步,一帧中途丢了字节就靠“等下一个帧头”自愈。校验和用累加和,简单够用,要抗错可以换 CRC16。

参数说明:FRAME_MAX必须大于最长帧,否则长帧会被截断;idx在中断和主循环之间共享,实际工程里要加临界区保护,简单做法是进临界区前关中断、出来再开。中断里不做解析是血泪经验,解析逻辑一复杂,中断服务时间变长,下一个字节进来时数据寄存器已被覆盖,直接丢字节。

主循环那边就是典型的“标志位驱动”:

int main(void) { uart_init(UART1, 115200); uart_irq_enable(UART1); while (1) { if (g_frame_ready) { g_frame_ready = 0; handle_frame(buf + 2, buf[1]); /* 业务处理 */ } do_other_work(); } }

参数说明:115200必须和上位机完全一致,差一点就是满屏乱码。uart_irq_enable要在uart_init之后调,顺序反了会先进中断后初始化,行为不可预测。

3.4 PC 端 C# 串口上位机联调

实验二还带了 PC 端串口编程,这是很多人第一次写“能和硬件对话的界面”。用 C# 的SerialPort类就能做,重点在跨线程。

var port = new SerialPort("COM3", 115200, Parity.None, 8, StopBits.One); port.DataReceived += (s, e) => { int n = port.BytesToRead; var buf = new byte[n]; port.Read(buf, 0, n); // DataReceived 在后台线程触发,直接改控件会抛跨线程异常 this.BeginInvoke(new Action(() => txtLog.AppendText(BitConverter.ToString(buf) + "\r\n"))); }; port.Open();

逻辑说明:DataReceived是事件回调,触发线程不是 UI 线程,所以更新界面必须BeginInvoke回主线程,这是新手最常遇到的异常之一。参数说明:COM3换成设备管理器里实际看到的端口号;115200/None/8/One四个参数必须和板子侧配置逐项对齐,任何一项不同都收不到正确数据。

调试时建议先发单字节回环:板子收到什么原样发回什么,确认链路通了,再上组帧协议。中间加一个“收到 1 字节就减 3 再发回”的改动,本质是在验证收发方向都对,这个思路很笨但很好用,一眼就能看出是收错了还是发错了。

4. PWM、Flash、SPI 与看门狗:构件层是怎么把外设抽象掉的

后四个实验开始接触真正的外设。构件化把配置寄存器包装成几个参数,好处是上手快,代价是参数背后的物理含义被藏起来了。这一章每个外设都给一段可运行代码,再补一句“参数改错了会怎样”,这两件事一起看才算真懂。

4.1 PWM 构件:频率和占空比到底该改哪个

PWM 就两个参数:频率决定周期多长,占空比决定高电平占多少。呼吸灯调占空比,电机调速调占空比,蜂鸣器发声调频率。实验三要求“修改函数里的一个参数并观察现象”,改的就是这两个之一。

int main(void) { /* 1kHz、50% 占空比:人眼看不出闪,示波器上是一个标准方波 */ pwm_init(PWM_CH1, 1000, 50); while (1) { for (uint8_t d = 0; d <= 100; d += 5) { /* 呼吸灯:亮 */ pwm_set_duty(PWM_CH1, d); delay_ms(50); } for (uint8_t d = 100; d > 0; d -= 5) { /* 呼吸灯:暗 */ pwm_set_duty(PWM_CH1, d); delay_ms(50); } } }

逻辑说明:pwm_init(通道, 频率Hz, 占空比%),构件内部会根据频率反算预分频和重装载值。pwm_set_duty只改比较值,不动周期,所以亮度变化是连续的。参数说明:频率太低(低于 50Hz)人眼会看到闪烁;驱动 LED 一般 1kHz 以上,驱动舵机则是 50Hz 固定、占空比 2.5%~12.5% 对应角度。

这里有个容易被忽略的约束:频率和占空比分辨率是抢资源的。定时器时钟固定,频率调得越高,一个周期内的计数台阶越少,占空比能取到的档位就越少。想要 0~100 平滑可调,频率就别往几十 kHz 上顶。反过来,如果发现改占空比时亮度是跳变的,多半就是这个原因,降频或者换更高时钟源的定时器。

还有一点,PWM 输出引脚是复用的,pwm_init里通常已经帮你把引脚切到复用功能,如果你之前用gpio_init把它配成了普通输出,两处配置会打架,表现为引脚一直是固定电平。改外设前先确认引脚当前归谁管。

4.2 Flash 在线编程:写之前必须先擦,擦之前必须看清楚地址

Flash 的特点是“只能把 1 写成 0”,要写回 1 必须整页擦除。所以顺序永远是:解锁 → 擦页 → 写 → 上锁,四步一个都不能少,顺序还不能乱。

#define APP_FLASH_ADDR 0x0801F800UL /* 靠后的页,避开程序区 */ #define FLASH_PAGE_SIZE 2048 uint8_t saved[16] = {0}; void save_params(const uint8_t *data, uint16_t len) { flash_unlock(); if (flash_erase_page(APP_FLASH_ADDR) != FLASH_OK) { /* 先擦整页 */ flash_lock(); return; } /* 写入长度按字对齐,非对齐部分补 0xFF */ if (flash_write(APP_FLASH_ADDR, data, len) != FLASH_OK) { flash_lock(); return; } flash_lock(); /* 一定要上锁 */ } void load_params(void) { memcpy(saved, (const void *)APP_FLASH_ADDR, sizeof(saved)); }

逻辑说明:擦除的最小单位是页(这里 2KB),写入可以按字或半字,读直接当内存地址访问。参数说明:APP_FLASH_ADDR必须落在程序区之后,页对齐;0x0801F800这类地址每个芯片不一样,以手册和链接脚本为准,写错就是把自己的程序擦了,表现是复位后板子彻底不启动,只能重新下载。

代码里两个flash_lock()的提前返回值得注意。擦除失败最常见的原因是目标页被写保护了,这时不解锁保护怎么擦都没用;还有一种情况是擦除过程中断电,页处于半擦状态,读到的是乱值。所以参数区最好加一个魔数校验,读出来魔数不对就当空处理,用默认值恢复,这比读到脏数据直接跑要安全得多。

4.3 SPI 主从通信:片选、时钟极性、相位三件事对齐

SPI 是四根线:SCK、MOSI、MISO、CS。它比串口快得多,但没有应答机制,配置不一致时不会报错,只会安静地收到 0x00 或 0xFF。实验五用 SPI 读外部器件 ID,是最有效的自检手段。

#define CMD_READ_ID 0x9F uint8_t spi_read_id(void) { uint8_t id[3]; spi_init(SPI1, SPI_MODE0, 1000000); /* CPOL=0, CPHA=0, 1MHz */ cs_low(CS_DEV); spi_transfer(SPI1, CMD_READ_ID); /* 发命令 */ for (uint8_t i = 0; i < 3; i++) { id[i] = spi_transfer(SPI1, 0xFF); /* 发哑元,换回数据 */ } cs_high(CS_DEV); return id[0]; /* 与手册里的厂商 ID 对比 */ }

逻辑说明:SPI 是全双工,每发一个字节必然同时收一个字节,所以想读数据就得发一个无意义的0xFF把时钟推起来。参数说明:SPI_MODE0表示空闲低电平、第一个边沿采样,模式 0~3 必须和外设手册一致,不一致的现象就是读出来全是 0xFF;1000000是 1MHz,先用低速调通再往上提,杜邦线飞线的情况下高速很容易出错。

片选一定要软件手动控制,并且包在整个事务外面。常见错误是每个字节都拉一次片选,外设会认为每次都是新命令,读出来自然不对。另外多从机场景下,没被选中的从机 MISO 应该是高阻态,如果它一直输出,总线就被抢了,读到的数据是乱的,这种情况要么加缓冲器,要么保证从机支持三态输出。

如果读 ID 一直不对,按这个顺序查:先用示波器或逻辑分析仪看 SCK 有没有波形;有波形就核对模式 0~3;模式对了再看片选时序;最后才怀疑外设本身。跳过前两步直接换器件,多半是白忙。

4.4 独立看门狗:喂狗节奏与复位后的现场

独立看门狗(IWDG)是一个靠内部低速时钟跑的自由计数器,主程序必须在它数到 0 之前“喂”一次,否则芯片复位。它防的是程序跑飞、死循环、卡在某处这类软件故障。

int main(void) { iwdg_init(2000); /* 约 2 秒超时 */ while (1) { sensor_task(); comm_task(); display_task(); iwdg_feed(); /* 放在主循环最外层,所有任务都跑到了才喂 */ } }

逻辑说明:iwdg_init(2000)里的 2000 是毫秒,构件内部换算成重装载值。喂狗位置很有讲究,放在主循环末尾表示“这一圈所有任务都执行完了”,只要某个任务卡死,喂狗就停,2 秒后复位。参数说明:内部低速时钟精度很差,标称 2 秒的实际超时可能偏差两三成,所以超时时间要留足余量,别把喂狗周期卡在临界值上。

最容易犯的错是在定时器中断里喂狗。这样即使主循环已经死掉,中断照样在跑,狗照样被喂,看门狗彻底失效,这是典型的“养了一条睡着的狗”。另一个坑是调试:打断点停住超过超时时间,芯片会复位,看起来像断点失效,其实是狗在干活。调试阶段可以先不初始化 IWDG,或者把超时调到很长,发布前再改回来。

复位之后怎么知道是看门狗干的?大部分芯片会把复位原因记录在寄存器里,上电先读一次并打印出来,这个习惯能省很多排查时间。如果没有记录,也可以在上电时点一个特殊状态的 LED,跟正常启动区分开。

5. 常见问题排查:六个实验里最容易翻车的几个点

实验做不出来,九成不是代码逻辑问题,而是链路某一段断了。下面这几条都是按“现象 → 原因 → 解决”整理的,遇到问题先对着排一遍,比漫无目的改代码快得多。

现象一:点“连接目标板”一直找不到设备。原因通常是串口被别的软件占着(串口助手、另一个 IDE 实例),或者 Type-C 线只供电不传数据。解决:先关掉所有可能占用串口的程序,换一根确认能传数据的线,再在设备管理器里看有没有新增串口;如果连串口都没出现,问题在驱动或线材,跟板子无关。

现象二:下载显示成功,但现象和代码完全对不上。原因基本是下载了旧的.hex。改完代码没重新编译,或者选文件时点进了别的实验的Debug目录。解决:下载前先看Debug里 hex 的时间戳,确认是刚才编译出来的;养成“改代码 → 编译 → 看时间戳 → 下载”的固定顺序。

现象三:串口收到的全是乱码或者问号。原因有三个层次:波特率不一致最常见;其次是系统时钟配置和预期不同,导致实际波特率偏离;再其次是 USB 转串口模块质量差,高波特率下误码。解决:先把两边都降到 9600 试通,再往上提;确认工程里的时钟配置没被别人改过;换一个转换模块对比。

现象四:中断只进一次,之后就没反应了。原因是中断标志没清或者没重新使能。有些外设的标志是“读寄存器自动清”,有些必须手动写 0,手册里写得很清楚。解决:进中断第一件事就是把标志清掉,然后再处理数据;如果是嵌套或优先级问题,检查中断优先级配置有没有把它压到最低导致一直挂起。

现象五:加了看门狗之后,板子反复重启。原因是喂狗周期超过了实际超时时间,而实际超时又因为内部时钟偏差比设定值短。解决:把超时时间放大一倍先跑通,再逐步收紧;喂狗语句放在主循环最外层,不要放在某个耗时不确定的任务前后;确认没有在中断里喂狗。

提示:每换一个实验,先把上一个实验的工程完整关掉再导入新的,IDE 同时打开多个工程时,编译和下载的目标很容易指错。

6. 二次开发:把验证性实验改成能讲清楚的小项目

六个验证性实验做完,最大的问题是“每块都碰过,但拼不起来”。想让它变成能写进简历、能在面试里讲三分钟的东西,得做一次串联改造。我的建议是挑一个方向,把至少三个实验的外设串成一条完整的数据链路,而不是每个实验各加一点功能。

下面这张表是我常给学生用的改造映射,按难度从低到高排:

改造方向涉及实验关键改动点能讲清楚的能力
按键调光台灯GPIO + PWM按键状态机驱动占空比,加长按连续调输入消抖、状态机、外设参数映射
串口参数配置器串口中断 + Flash上位机下发参数帧,校验后写 Flash,上电读回帧协议、掉电保存、魔数校验
传感器采集显示SPI + 串口定时读 SPI 器件,组帧上报 PC时序配置、周期任务、数据上报
带看门狗的自恢复节点全部任务卡死检测 + 看门狗复位 + 复位原因上报可靠性设计、故障定位

拿“串口参数配置器”举例,改动量其实不大,但把中断、组帧、Flash 三块连起来了。核心是在原有帧处理上加一个命令字:

/* 帧格式:帧头 + 长度 + 命令 + 数据 + 校验 */ void handle_frame(const uint8_t *payload, uint8_t len) { uint8_t cmd = payload[0]; switch (cmd) { case CMD_SET_PARAM: if (len != 5) { send_ack(ERR_LEN); return; } memcpy(g_param, payload + 1, 4); save_params(g_param, 4); /* 写 Flash,掉电不丢 */ send_ack(OK); break; case CMD_GET_PARAM: send_frame(CMD_GET_PARAM, g_param, 4); break; default: send_ack(ERR_CMD); /* 未知命令要回错误,别静默丢弃 */ break; } }

逻辑说明:命令字放 payload 第一位,长度校验先做,避免memcpy越界;未知命令必须回错误帧,否则上位机会一直等超时,排查时分不清是丢包还是命令不支持。参数说明:g_param是 4 字节参数区,上电时先从 Flash 读一次并用魔数校验,校验不过就用默认值覆盖,这一步能兜住“擦写中途掉电”的极端情况。

改造过程中验证方法比代码更重要。我一般分三层验:单外设层用固定输入看固定输出,比如发一个已知帧看返回对不对;链路层拔掉一端看另一端是否报错,能报错说明超时和错误处理生效;异常层人为制造故障,比如把喂狗语句注释掉看是否复位、把 Flash 地址改错看魔数校验是否拦住。三层都过了,这个改造才算真的能讲。

从那以后我每次改完参数,都会先把 hex 的时间戳对一遍,再下载,再看现象,三步缺一步就重来。这个习惯看着笨,但确实把“改了没生效”这类问题挡在了门外。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表