Skip to content

LV025-reboot命令实例 ​

一、 命令的定义与注册 ​

1. 注册位置与时机 ​

reboot 是引入行命令模块之后最简的一条业务命令:处理函数不含参数解析、不读写 Flash,只做"告别一句、复位芯片"。它挂在 Bootloader 的业务层 iap_service_init() 里,和 go 相邻:

c
// atk-f103/Drivers/BSP/Iap/iap_service.c
void iap_service_init(void)
{
    /* ... 串口与延时初始化、打印版本 ... */

    (void)usart1_cmd_register("go", cmd_go);         /* 跳到 APP 区执行 */
    (void)usart1_cmd_register("reboot", cmd_reboot); /* 复位芯片 */

    /* ... 之后才 iap_rx_init() 启动串口接收 ... */
}

注册点必须早于 usart1_rx_start()(由 iap_rx_init() 内部触发),理由在 LV020-命令系统设计 里说过:中断一开,字节随时可能到达,命令表此时必须是最终形态。

2. 处理函数原型 ​

处理函数符合模块约定的签名,参数是整行原文(命令名加可能存在的参数),本命令不需要参数,直接忽略:

c
// atk-f103/Drivers/BSP/Iap/iap_service.c
static void cmd_reboot(const char *line)
{
    (void)line;                                           /* 不带参数 */
    printf("IAP: reboot command received, restarting...\r\n");
    while ((USART1->SR & USART_SR_TC) == 0U)               /* 等最后一行日志发完 */
    {
    }
#if SVC_NET_EN
    iap_tcp_shutdown();
#endif
    HAL_NVIC_SystemReset();                                /* 等同按复位键 */
    while (1)                                              /* 复位未生效时的兜底 */
    {
    }
}

四段代码对应四件事,逐段看。

二、 处理流程 ​

1. 打印与等待发送完成 ​

printf 在本工程里最终走 fputc(),每个字符循环等 TXE 再写 DR。TXE 只表示"发送数据寄存器空了",可以接收下一个字节,不表示最后一个字节已经从移位寄存器发完。函数返回时,最后一个字符很可能还停在移位寄存器里,如果这时立刻复位,串口上就会看到半行日志。

USART_SR 的 TC(Transmission Complete)位才是"移位寄存器也空了"的判据,所以这里死等 TC。同样的等待在 iap_load_app() 跳转前也出现过一次,原因是同一条:跳转后 USART1 会被 APP 重新初始化。

2. 关闭网络通道 ​

有网络会话时先调 iap_tcp_shutdown(),注释写得很直接:

  • 软复位(SYSRESETREQ)只复位 MCU 内核与片内外设,不会复位外置的 DM9000;
  • 重启初期 GPIO 已经使能 EXTI,而 FSMC 还没初始化,若 DM9000 此时仍保持中断输出,残留中断会在这个窗口打进来,此时读 FSMC 总线就是一次 HardFault。

所以顺序是"先断网络、再复位"。这条经验同样适用于 cmd_go:跳转前也要先关网络通道。

3. 触发复位与兜底 ​

HAL_NVIC_SystemReset() 是 CMSIS 函数 NVIC_SystemReset() 的 HAL 包装,本质是往 SCB->AIRCR 写 VECTKEY | SYSRESETREQ,效果同按复位键:内核、外设寄存器、栈指针全部回到复位状态,程序从复位向量重新开始。

后面的 while (1) 是防呆:如果因为调试器挂住、或者异常屏蔽导致复位请求没生效,程序也不能继续往下跑——reboot 的语义是"最后一次动作",不能返回主循环。

三、 设计要点 ​

1. 为什么等 TC 而不是 TXE ​

对比一下两种等待的后果:

等待对象语义提前复位的现象
TXE发送数据寄存器空,可写下一字节末尾 1 ~ 2 字节丢失,日志被截断
TC移位寄存器也空,一帧真正发完不会截断,最多多等一个字节时间(约 87µs)

代价只是一个字符的发送时间,收益是"最后一行日志一定能看到"。对于负责"告别"的命令,这个取舍没有悬念。

2. 为什么软复位前必须先关网络 ​

DM9000 是挂在 FSMC 上的独立芯片,它的中断线接在 MCU 的 EXTI 上。软复位不会给外部芯片断电,也不会清掉它内部的中断状态寄存器(ISR 位需要写 1 才清)。于是出现这样的窗口:

(1)MCU 复位,MX_GPIO_Init() 先把 EXTI 配好并使能;

(2)MX_FSMC_Init() 还没执行,0x6C000000 段不可访问;

(3)DM9000 仍有包到达,拉低 INT,EXTI 触发,回调里读 DM9000 寄存器 → 访问未初始化的 FSMC 地址 → HardFault。

iap_tcp_shutdown() 会关掉监听与当前连接,并让 DM9000 不再产生接收中断,从而关掉这个窗口。

3. 为什么不用看门狗复位 ​

用"停止喂狗等 IWDG 咬"也能复位,但有几处不合适:

  • 时延固定 3 秒,命令响应看起来像卡死;
  • 这 3 秒里系统仍在跑主循环,可能继续打印、继续处理网络包,行为不确定;
  • 调试时若恰好把 IWDG 关了(或参数被改),命令就永远不生效;
  • deadloop 命令的语义才是"故意让狗咬",reboot 应该是一次确定的立即动作。

复位之后固件会重新走一遍完整初始化流程,看门狗按既定策略在初始化末尾重新配置并立刻开始喂狗,所以 reboot 本身不需要碰 IWDG。

四、 APP 侧的实现与对比 ​

1. 两个 App 的处理函数 ​

flash-app 与 sram-app 里各有一条同名命令,处理函数比 Bootloader 的还短——它们没有网络通道要关:

c
// sram-app/Core/Src/main.c
static void app_cmd_reboot(void)
{
    printf("APP: reboot command received, restarting...\r\n");
    while ((USART1->SR & USART_SR_TC) == 0U)              /* 等最后一行日志发完 */
    {
    }
    HAL_NVIC_SystemReset();                               /* 等同按复位键 */
    while (1)                                             /* 复位未生效时的兜底 */
    {
    }
}

注册发生在 main() 的 USER CODE 2 区,位置同样在 usart1_rx_start() 之前:

c
// sram-app/Core/Src/main.c
(void)usart1_cmd_register("reboot", app_cmd_reboot);
usart1_rx_start();                      /* 启动串口中断接收(行缓冲模式) */
printf("APP: ready, send a line to echo, commands: 'reboot' restart chip\r\n");

一个容易忽略的细节:APP 里 reboot 之后芯片从 0x08000000 重新启动,跑到的是 Bootloader 而不是 APP。这正是 IAP 的正常回路——复位后先由 Bootloader 决定跳不跳 APP,用户想再进 APP 得再发一次 go。

2. 与 go 命令的区别 ​

go 与 reboot 都写在同一张命令表里,行为却完全相反:

对比项go [flash|sram]reboot
本质函数指针跳转,不复位SYSRESETREQ 软复位
外设状态保持(只停 SysTick 与 USART1 中断)全部回到复位值
栈与向量表手工换成 APP 的 MSP / VTOR由硬件从向量表重新加载
回到 Bootloader不会(除非 APP 自己复位)会,复位后即是 Bootloader
典型用途装完镜像立刻运行重新进入升级流程、清干净现场

两者在实现上的共同点是"跳转/复位之前都要把日志发完、把网络关干净"——凡是把控制权交出去的动作,收尾工作都一样。


本文档由 markdowncli 技能辅助生成