Skip to content

LV055-看门狗问题与解决方案 ​

一、 两个现象 ​

1. 现象 A:拔掉网线就启动不起来 ​

拔掉网线给板子上电,串口日志打到一半就断,然后从头再来,如此反复。插上网线则一切正常。

同一份代码,插线能跑、拔线不能跑,说明问题不在业务逻辑,而在"某件依赖外部链路的事"把启动流程拖长了。

2. 现象 B:跳转之后又回到 Bootloader ​

flash-app 或 sram-app 已经落盘成功,发 go flash / go sram 之后,日志出现跳转提示,APP 的 APP: ready... 有时刚打出来、有时还没打出来,板子就重新启动,又回到 Bootloader 的命令行。整个过程大约 3 秒一个循环。

这一条与网线无关,插不插线都会复现;能落盘说明下载链路没问题,问题出在"跳过去之后"。

3. 怎么区分两者 ​

两个现象都属于"被看门狗复位",但发生的阶段不同,靠日志就能分开:

判据现象 A现象 B
复位发生的位置Bootloader 初始化中段(PHY 等待附近)跳转之后,APP 刚起步
是否见过 jump to APP没有有
是否见过 APP: ready...没有有时有,有时没有
是否与网线有关有关(拔线必现)无关
周期10 秒左右(撞上 10 秒等待)约 3 秒

两者的共性是同一条硬件事实:看门狗一旦开始计时,除复位之外没有任何办法让它停下。区别只是"谁在计时窗口内没喂上狗"。

二、 根因 A:初始化阶段就被咬 ​

1. 3 秒窗口从哪来 ​

看门狗参数是 LSI 40kHz、32 分频、重装值 3750:

[ t_{out} = \frac{32 \times 3750}{40000} = 3.0\ \text{s} ]

LSI 本身精度很差(±40% 量级),实际超时可能在 2 ~ 4 秒之间波动,但它不是本次问题的变量——变量是"启动花了多久"。

2. 初始化里的 10 秒 ​

CubeMX 默认把 MX_IWDG_Init() 放在所有外设初始化之前,函数体里直接 HAL_IWDG_Init(),看门狗当场开始倒计时。

而初始化链的末端是 lwip_comm_init() → DM9000_Init()。DM9000 上电后要与对端做 PHY 自协商,驱动里的等待逻辑是"最多轮询 100 次、每次 100ms":

  • 插着网线:几十毫秒就协商完成,总耗时远小于 3 秒;
  • 没插网线:协商永远不成功,老老实实等满 10 秒。

3 秒的窗口撞上 10 秒的阻塞,必然被复位。

看门狗使能时机对比

3. 为什么是无限循环 ​

看门狗复位不是"停在那里",而是重新走一遍启动流程:复位 → 初始化 → 又在同一处等 10 秒 → 再被咬。只要外部条件不变(网线仍然没插),循环就永远出不去。

这也解释了为什么日志"每次停在差不多的地方":每次都在 PHY 等待的同一段代码里被复位。

三、 根因 B:跳转之后无人喂狗 ​

1. 原理:硬件上就没有"停止"通路 ​

先把 IWDG 这条链路摊开看:时钟、分频、计数器、复位输出,中间没有任何一个可以"关掉它"的开关。

IWDG 的硬件结构

第一层:时钟是独立的。 IWDG 由 LSI(内部低速 RC 振荡器,约 40kHz)直接驱动,不经过 SystemInit() 里配置的 PLL、AHB/APB 分频链。这意味着主时钟配错、系统进低功耗、程序跑飞把时钟改了,它都照常倒计时——这正是它作为"最后一道保险"的价值。

第二层:软件能碰的只有一个寄存器。 整个模块对外只暴露 IWDG_PR、IWDG_RLR、IWDG_SR 与 IWDG_KR,而前三个在启动后都受写保护,唯一的控制入口是键寄存器:

写入值作用
0xCCCC启动看门狗计数
0xAAAA喂狗,计数器重装为 RLR
0x5555解除 PR/RLR 的写保护

三个命令字里没有"停"。写 0x5555 只是让你能改分频与重装值,并不停止计数;写其它任何值都不产生动作(键寄存器只认这三个值,非法值被硬件忽略)。

第三层:只有两条路能结束倒计时。 一条是计数器自然减到 0——产生复位;另一条是芯片断电。系统复位会把 IWDG_KR 清回默认值,启动位随之消失,计数不再继续,固件从复位向量重新开始。

所以"关掉看门狗"这件事在 F103 上是不存在的操作,不是驱动没提供、也不是 CubeMX 没生成,而是硅片上没有这条通路。

2. 为什么偏偏要设计成关不掉 ​

因为看门狗的职责是"在软件失控时把系统拉回来"。如果留一个软件可写的停止位,那么跑飞的程序(或者一段被写坏的代码)同样能写这个位,把自己身上的保险关掉——保护就变成了自欺。

于是硬件的取舍是明确的:

  • 允许你调整超时(改 PR/RLR,但要先解锁);
  • 允许你延后复位(喂狗重装计数器);
  • 不允许你取消监控(没有停止位,也无法用调试口永久关闭)。

把这三条映射到 IAP 场景,就是必须接受的前提:Bootloader 一旦使能过看门狗,"跳到 APP 之前先把它关掉"这条路从原理上就走不通。

3. 于是只剩"喂"和"接管"两条路 ​

跳过不去、关又关不掉,Bootloader 在交权这件事上能做的动作只有两个:

做法效果是否可行
跳转前喂一次狗把窗口重置为完整 3 秒只能买 3 秒,APP 不接管照样复位
跳转前拉长 RLR把窗口改成 10 秒、30 秒保护被削弱,且 APP 迟早要面对同一问题
跳转前关闭看门狗彻底没有窗口硬件不支持
让 APP 重新装载并接管窗口刷新,责任转移,保护强度不变唯一的正解

这也解释了为什么本工程 iap_load_app() 里根本没有"关闭 IWDG"的代码,只留了一句注释:"喂狗责任转给 APP"。它不是遗漏,而是在硬件约束下唯一能做的事。

顺带说明两种"看起来能关"的例外,都不是产品可用的手段:

  • 调试器把内核 halt 时可以靠 DBGMCU_CR 的 IWDG_STOP 位冻结计数,但这只在调试会话里成立,脱机运行无效;
  • 选项字节可以让 IWDG 由硬件自动启动,那种情况下更彻底——连"启动时机"都不归软件管。

4. 跳转不是复位,计时继续 ​

把"Bootloader 使能了看门狗"和"跳转不等于复位"这两件事实叠起来,就得到现象 B 的完整机理:

(1)Bootloader 在初始化末尾 iwdg_start(),计数器开始 3 秒倒计时,主循环每 500ms 喂一次,所以停在命令行多久都没事;

(2)收到 go,iap_load_app() 停掉 SysTick 与串口中断、关网络,然后跳转——喂狗随之停止,但计数器还在跑;

(3)APP 从 Reset_Handler 开始重新初始化,这段时间里没人喂狗;

(4)如果 APP 没有接管、或接管得太晚,计数器减到 0,硬件复位,于是又回到 Bootloader。

看门狗在跳转前后的移交

现象 B 之所以容易误判,是因为它的表现很像"APP 一启动就崩":日志断在 APP 刚打印的地方,而复位原因又没有任何提示。区别在于崩通常是立即的、位置随机的,而这条是稳定约 3 秒的周期。

5. 接管晚于窗口的代价 ​

于是多出一条硬约束:APP 从 Reset_Handler 到调用 iwdg_start() 的耗时,必须小于 Bootloader 留给它的剩余窗口(最坏 3 秒,LSI 偏快时只有 2 秒出头)。

本工程的两个 APP 满足这条约束:main() 里在 iwdg_start() 之前只有 HAL_Init()、时钟、GPIO、USART 初始化与一行 ready 打印,实测几十毫秒量级。

反过来,如果 APP 的初始化链里也有慢速外设(比如它自己也要等 PHY 自协商),就必须重新设计:把 iwdg_start() 提前到初始化早期,并在每个长等待处显式喂狗——这正是我们在 Bootloader 里极力避免的形状,APP 侧要付的代价。

四、 解决方案 ​

两个现象对应两个改法,缺一个都不完整:A 的改法让 Bootloader 初始化不被咬,B 的改法让跳转之后有人喂狗。

1. 针对 A:配置与使能分离 ​

思路是初始化期间不让看门狗计时,初始化完成后再让它开始计时。落到代码上是把 CubeMX 生成的 MX_IWDG_Init() 改成空操作,另加一个 iwdg_start() 承担真正的使能。

空操作写在 USER CODE 区里,return 之后的生成代码不可达,ARMCC 会报 #111-D(unreachable code),用 #pragma diag_suppress 111 抑制到函数尾再恢复:

c
// atk-f103/Core/Src/iwdg.c
void MX_IWDG_Init(void)
{
  /* USER CODE BEGIN IWDG_Init 0 */
#pragma diag_suppress 111
  return;
  /* USER CODE END IWDG_Init 0 */

  hiwdg.Instance = IWDG;
  hiwdg.Init.Prescaler = IWDG_PRESCALER_32;
  hiwdg.Init.Reload = 3750;
  if (HAL_IWDG_Init(&hiwdg) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN IWDG_Init 2 */
#pragma diag_default 111
  /* USER CODE END IWDG_Init 2 */
}

真正的使能放在文件末尾的 USER CODE BEGIN 1 区,参数与生成区保持一致:

c
// atk-f103/Core/Src/iwdg.c
void iwdg_start(void)
{
  hiwdg.Instance = IWDG;
  hiwdg.Init.Prescaler = IWDG_PRESCALER_32;  /* 32 分频 */
  hiwdg.Init.Reload = 3750;                  /* 3s = 32 x 3750 / LSI 40kHz */
  if (HAL_IWDG_Init(&hiwdg) != HAL_OK)
  {
    Error_Handler();
  }
}

这样做的附带好处是:CubeMX 重新生成代码时,MX_IWDG_Init() 的空操作和 iwdg_start() 都会保留(都在 USER CODE 区),不需要每次手工改回来。

2. 针对 B:跳转后由 APP 接管 ​

Bootloader 侧不需要(也没法)做任何"关闭"动作,只需要把责任交出去;真正的动作在 APP 里:用同一组参数重新初始化一次。HAL_IWDG_Init() 内部会往 KR 写 0x5555 解锁、写入 PR 与 RLR、写 0xAAAA 重装计数、再写 0xCCCC 启动,计数器由此回到满值。

c
// sram-app/Core/Src/main.c(flash-app 完全一致)
SCB->VTOR = APP_BASE;                 /* 向量表重定位, 早于 HAL_Init */
HAL_Init();
/* ... 时钟、GPIO、USART1 初始化 ... */
(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");

/* 与 Bootloader 一致: 所有初始化完成后再使能看门狗, 之后由主循环周期喂狗 */
iwdg_start();

iap_load_app() 里没有"关闭看门狗"的代码,注释写明"喂狗责任转给 APP",就是怕后人误以为漏了一步。

参数必须四处一致,任何一处不同都会让现象 B 以另一种形式复现:

情况现象
APP 没接管、也没喂狗跳转后约 3 秒复位,日志停在 APP 初始化中途
APP 接管但 RLR 更大(如 6250 → 5 秒)保护变弱,真死机时要等更久才复位
APP 接管但 RLR 更小(如 1250 → 1 秒)正常业务里偶发复位,且很难归因到看门狗
APP 接管但分频不同同上,实际超时与预期不符

3. 三个工程的调用点与参数 ​

使能点统一定在"所有初始化完成、状态已经报出去"之后,三个工程完全一致:

工程调用位置喂狗位置周期
atk-f103(Bootloader)iap_service_init() 末尾(串口 IAP、外部 SRAM 自检、lwIP 与 TCP 全部就绪后)主循环,每 500ms 一批500ms
flash-appAPP: ready... 打印之后主循环,30 × 100ms 内每圈100ms
sram-app同上同上100ms

四个参数保持一致:LSI 40kHz、32 分频、Reload 3750,即 3 秒。裕量分配也很清楚——Bootloader 500ms 喂一次约 6 倍裕量,APP 100ms 喂一次约 30 倍裕量。

4. 责任链一览 ​

阶段看门狗状态谁负责喂漏喂的后果
Bootloader 初始化未启动无需无(现象 A 已消除)
Bootloader 命令行等待运行中(3 s)Bootloader 主循环,500 ms 一次3 秒后复位,回到 Bootloader
go 跳转窗口运行中,剩余时间递减无人(客观空档)窗口超时 → 复位(现象 B)
APP 初始化运行中无人,直到 iwdg_start()同上
APP 主循环运行中,已重新装载APP 主循环,100 ms 一次3 秒后复位,回到 Bootloader
reboot复位时停止——

五、 为什么不选"喂得更勤" ​

第一反应往往是"在初始化里也喂狗"。这条路走不通,原因是阻塞点的形态:

  • PHY 自协商是单次长阻塞,那 10 秒里没有任何可以插进去喂狗的位置;
  • 除非改驱动,把等待拆成"每 100ms 回调一次喂狗",但这会把看门狗逻辑渗进外设驱动,且以后再加一个慢速外设又要重来一次;
  • 更根本的问题是:在初始化完成前就让看门狗计时,本质是在赌"初始化一定够快"。插线时赌赢,拔线时赌输,故障是否出现取决于现场条件——这是最难排查的一类问题。

把使能点后移,则完全不依赖时序假设:初始化多久都不会被咬,只看主循环是否还在跑。

至于现象 B,也不能靠"Bootloader 跳转前多喂几次"来解决:喂狗只能把窗口往后推 3 秒,APP 不接管的话,3 秒后照样复位。这个问题的正解只有一个——让 APP 接管。

另外两条配套约定:

  • 三个工程只改 USER CODE 区的代码,CubeMX 重新生成不丢;
  • 参数改动时四处同步:Bootloader 的 iwdg_start()、两个 APP 的 iwdg_start()、以及 CubeMX 工程里的 IWDG 配置。

六、 验证与回归 ​

场景期望结果
拔网线上电日志完整打完,停在 go/reboot 命令行,不重启(现象 A 消失)
插网线上电与拔线一致,PHY 协商提前完成
发 app.img 途中拔线3 秒后报超时并回到扫描态,不重启
go flash / go sram 跳转APP 起来后持续运行,不在跳转窗口内复位(现象 B 消失)
APP 运行中阻塞超过 3 秒应当复位(说明看门狗确实在工作)
APP 正常运行不复位
reboot 之后回到 Bootloader 命令行,可再次 go,不出现"刚复位又被咬"
临时把 APP 的 iwdg_start() 注释掉跳转后约 3 秒复位——用于确认接管确实在起作用

中间四行是回归的关键:既要证明"该复位时会复位",也要证明"不该复位时不会复位",尤其是跳转窗口这一格——它同时覆盖了"Bootloader 使能时机"和"APP 接管"两件事。只验证前者容易把超时改得过长,只验证后者容易把看门狗变成摆设。

最后一行的破坏性验证建议只做一次、做完立刻恢复:它是确认"APP 接管生效"最直接的手段,若注释掉之后反而不复位,说明 APP 根本没在喂狗,问题在别处。

排查时可用的两个观测点:usart1_rx_error_count() 是否增长(初始化被阻塞的间接证据)、以及复位后日志是否总停在同一位置(看门狗被咬的典型特征)。


本文档由 markdowncli 技能辅助生成