From 1308c98769da6bd183a0be8ce043e609a3868e55 Mon Sep 17 00:00:00 2001 From: QIU SHENGMING Date: Thu, 1 Oct 2026 15:06:20 +0800 Subject: [PATCH] Document where Moto/DFU entry is, and let a boot key hold PTT plus a key The bootloader's DFU handler is reachable only when SRAM[0x20000020] is 3, which only a program that then resets can write. The application's 0x05DD takes that path only with ENABLE_OVERLAY; this build resets straight back into the application instead. Ruled out by measurement: PTT alone, PTT+SIDE1/SIDE2, MENU, a host byte in the boot window including 0x0530, and 0x05DD. boot-key now reads a + separated list, because the firmware's own BOOT_GetMode() needs PTT and a matrix key together for every special boot mode. Tested against all four documented combinations. --- AGENTS.md | 31 +++++++++++++++++++++++++++++++ AGENTS.zh-CN.md | 28 ++++++++++++++++++++++++++++ README.md | 30 ++++++++++++++++++++++++++++++ README.zh-CN.md | 27 +++++++++++++++++++++++++++ qemu/py32f071.c | 30 +++++++++++++++++++++++++----- 5 files changed, 141 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c0292e8..3a0e9c4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -430,6 +430,37 @@ Two lessons, both general: The right move was to ask what changed between those two runs, not to distrust the earlier note. +### Moto/DFU: the entry is a build flag, not a key + +The factory bootloader in the first 10 KB does contain a Moto DFU handler at 38400 baud, and +the emulator runs the bootloader correctly. It is nevertheless unreachable from outside, and +the reason is in the bootloader's own code: + + 0x13f2 ldrb r0, [r4, #0] ; r4 = 0x20000020, a byte in SRAM + 0x13f4 cmp r0, #1 + 0x13f6 beq ... + 0x13f8 cmp r0, #2 + 0x13fa beq ... + 0x13fc cmp r0, #3 + 0x13fe bne ... ; anything else keeps waiting + 0x140e bl 0x06f0 ; only mode 3 gets here: the DFU handler + +SRAM survives a soft reset and a power cycle does not, so that byte can only be set by a +program that then resets. In the application that is `overlay_FLASH_RebootToBootloader()`, +reached from the serial command `0x05DD` **only when the build defines `ENABLE_OVERLAY`**; +without it the same command is a plain `NVIC_SystemReset()`. Confirmed by sending `0x05DD` to +a running radio: no `0x0518` follows, and the PC never leaves the application. + +Four ways in were ruled out by measurement, not by reading: PTT alone (the firmware's own +`BOOT_GetMode()` needs a second key), PTT+SIDE1/SIDE2 and MENU (the application's special +modes), a host byte inside the boot window including the `0x0530` handshake, and `0x05DD`. +Run alone with no valid application the bootloader does not enter DFU either: it stops in one +of the six self-branches at `0x080000dc`, which are hang slots, not a wait for input. + +The general lesson: when a firmware's mode is chosen from a byte in RAM, the trigger is not an +input pin -- it is whatever wrote that byte before resetting. Find the writer in the source +(`0x05DD` here) and the `#ifdef` around it, and you have the whole condition. + ## The keypad: two real bugs, both fixed The old note here said "keys reach the firmware but the UI does not react" and diff --git a/AGENTS.zh-CN.md b/AGENTS.zh-CN.md index 314961c..0593373 100644 --- a/AGENTS.zh-CN.md +++ b/AGENTS.zh-CN.md @@ -348,6 +348,34 @@ NUL,`startswith("LCDW")` 永远匹配不上。把同一个文件按 UTF-16 解 - **"以前是好的"就是一条二分指令。** 引导的 Moto 洪水在 FLASH 控制器建模**之前**被观察到, 之后就不再复现。正确的动作是问"这两次之间改了什么",而不是怀疑早先的记录。 +### MOTO/DFU:入口是一个编译开关,不是一个按键 + +前 10 KB 里的出厂引导**确实**含有 38400 波特的 Moto DFU 处理器,模拟器也把引导跑对了。 +但它从外部**进不去**,原因就在引导自己的代码里: + + 0x13f2 ldrb r0, [r4, #0] ; r4 = 0x20000020,SRAM 里的一个字节 + 0x13f4 cmp r0, #1 + 0x13f6 beq ... + 0x13f8 cmp r0, #2 + 0x13fa beq ... + 0x13fc cmp r0, #3 + 0x13fe bne ... ; 其它值继续等 + 0x140e bl 0x06f0 ; 只有 mode 3 走到这里:DFU 处理器 + +SRAM 能挺过软复位、挺不过断电,所以这个字节只能由「写完就复位」的程序设置。在应用里那是 +`overlay_FLASH_RebootToBootloader()`,由串口命令 `0x05DD` 触发——**但只在构建定义了 +`ENABLE_OVERLAY` 时**;没有它,同一条命令就是普通的 `NVIC_SystemReset()`。实测:向运行中的 +电台发 `0x05DD`,之后**没有** `0x0518`,PC 也从未离开应用区。 + +四种入口都**用测量**而不是阅读排除了:单独按 PTT(固件自己的 `BOOT_GetMode()` 需要第二个键)、 +PTT+SIDE1/SIDE2 与 MENU(那是应用的几个特殊模式)、开机窗口内主机发字节(含 `0x0530` 握手)、 +以及 `0x05DD`。只跑引导、没有有效应用时它同样不进 DFU:它停在 `0x080000dc` 那六个自跳转之一, +那些是**挂起槽**,不是在等输入。 + +普适的教训:当一个固件的模式由 **RAM 里的一个字节**决定时,触发它的一般不是某个输入引脚,而是 +复位前写这个字节的那个程序。去源码里找到写入者(这里是 `0x05DD`)和它外面的 `#ifdef`,整个条件 +就到手了。 + ## 键盘:两个真 bug,都已修复 这里原来的笔记写的是"按键到达了固件但界面不反应",并且归咎于机器模型。结果发现有**两个 diff --git a/README.md b/README.md index f807562..056da2f 100644 --- a/README.md +++ b/README.md @@ -381,6 +381,36 @@ slot and resets. Both halves are reachable from the page. - `tools/uvk5_slots.py` does the same offline: write a slot into a flash image, and print what each slot holds. +### Moto/DFU flashing, and the flag this build does not set + +The factory bootloader is on the machine and it does speak the flashing protocol: a real +`0x0518` / `0x0530` / `0x0519` exchange at 38400 baud, in the 10 KB before the application. +What it will not do is enter that mode from the outside. Measured, not assumed: + +| an attempt at entering DFU | what actually happened | +| --- | --- | +| PTT held from reset | an ordinary boot. The firmware's own `BOOT_GetMode()` returns `BOOT_MODE_NORMAL` without a second key | +| PTT+SIDE1, PTT+SIDE2, MENU | the application's special modes (F_LOCK, AIRCOPY, MULTIBOOT), never the bootloader | +| a host byte during the boot window, `0x0530` included | ignored; the PC never leaves the application region | +| the firmware's own `0x05DD` reset command | a plain reset, straight back into the application | + +The bootloader's decision is one byte: `ldrb r0,[r4]` with `r4 = 0x20000020`, compared against +1, 2 and 3, where only **3** reaches the DFU handler. That byte is in SRAM, so it survives a +*soft* reset and nothing else: the program already running has to write it and reset. In the +firmware that is `overlay_FLASH_RebootToBootloader()`, and the `0x05DD` path takes it only when +the build defines `ENABLE_OVERLAY`: + + case 0x05DD: // reset + #if defined(ENABLE_OVERLAY) + overlay_FLASH_RebootToBootloader(); + #else + NVIC_SystemReset(); <-- what this build does + #endif + +**So MOTO flashing is not waiting on the emulator.** The bootloader runs, its DFU handler is +present, and the entry condition is known and reproducible; this build is simply not compiled +with the one flag that reaches it. The multi-system release has the same property for the same +reason -- compare the note the page prints for a build with no boot menu. The layout is the firmware's, from `App/driver/mb_flash.h`: slot 0 at `0x020000` backs up the internal image, slots 1..4 follow at `0x040000` in 128 KiB steps, the image starts one 4 KiB sector into the slot, and the 64-byte header carries magic `FMB1`, the image size and diff --git a/README.zh-CN.md b/README.zh-CN.md index ff1b0df..5e5b8e4 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -339,6 +339,33 @@ v6.0.0 版把开机菜单和四个固件槽放在外部 flash 里:开机按住 决定(默认 8 秒:开机路径可能花 20 秒把当前固件"采纳"进槽 0,之后才会去采样键盘)。 - `tools/uvk5_slots.py` 做同样的事但离线:把槽写进 flash 镜像,并打印每个槽的内容。 +### MOTO/DFU 刷机,以及这份构建没有打开的开关 + +出厂引导就在机器上,而且它**确实会说刷机协议**:真正的 `0x0518` / `0x0530` / `0x0519` 交互, +38400 波特,在应用区之前的 10 KB 里。它不肯做的,是**从外部**进入那个模式。下面是量出来的,不是猜的: + +| 尝试进入 DFU 的方式 | 实际发生了什么 | +| --- | --- | +| 复位起按住 PTT | 普通启动。固件自己的 `BOOT_GetMode()` 在没有第二个键时返回 `BOOT_MODE_NORMAL` | +| PTT+SIDE1、PTT+SIDE2、MENU | 进了应用的几个特殊模式(F_LOCK、AIRCOPY、MULTIBOOT),从不是引导 | +| 开机窗口内主机发字节(含 `0x0530`) | 被忽略;PC 从未离开应用区 | +| 固件自带的 `0x05DD`(reset)命令 | 普通复位,直接回到应用 | + +引导的判据只有一个字节:`ldrb r0,[r4]`,`r4 = 0x20000020`,与 1、2、3 比较,只有 **3** 才 +会进入 DFU 处理器。这个字节在 SRAM 里,所以它只能挺过**软**复位:必须是**正在运行的程序**写它 +然后复位。在固件里那就是 `overlay_FLASH_RebootToBootloader()`,而 `0x05DD` 只有在构建定义了 +`ENABLE_OVERLAY` 时才走那条路: + + case 0x05DD: // reset + #if defined(ENABLE_OVERLAY) + overlay_FLASH_RebootToBootloader(); + #else + NVIC_SystemReset(); <-- 这份构建走的是这里 + #endif + +**所以 MOTO 刷机不是卡在模拟器上。** 引导能跑、DFU 处理器存在、进入条件已知且可复现;这份固件 +构建只是没有打开唯一能走到那里的那个开关。多系统版出于同样的原因有同样的性质——可以参考网页在 +没有开机菜单的构建上打印的那句提示。 布局来自固件源码 `App/driver/mb_flash.h`:槽 0 在 `0x020000`,是内部镜像的备份;槽 1..4 从 `0x040000` 起、每 128 KiB 一个;镜像从槽内偏移 4 KiB 开始;64 字节头部含魔数 `FMB1`、镜像大小和 CRC-32。固件自带的 `0x0720`..`0x0727` 串口命令也按同样方式写槽, diff --git a/qemu/py32f071.c b/qemu/py32f071.c index 104359d..e6c36fe 100644 --- a/qemu/py32f071.c +++ b/qemu/py32f071.c @@ -3595,12 +3595,32 @@ static void uvk5_arm_boot_key(UVK5MachineState *s) if (key == NULL || *key == '\0') { return; } - s->boot_key_ptt = (g_ascii_strcasecmp(key, "PTT") == 0); - if (s->boot_key_ptt) { - object_property_set_bool(OBJECT(&s->keypad), "ptt", true, &err); - } else { - object_property_set_str(OBJECT(&s->keypad), "press", key, &err); + /* + * "+" holds more than one: the firmware's own BOOT_GetMode() reads PTT and a + * matrix key, and returns F_LOCK for PTT+SIDE1, AIRCOPY for PTT+SIDE2 and + * RESCUE_OPS for PTT plus the key named by the SET_KEY setting. A boot key that + * can only be one of those cannot reproduce any of the special boot modes, which + * is what made the bootloader look unreachable from here. + */ + char **parts = g_strsplit(key, "+", -1); + for (char **part = parts; part != NULL && *part != NULL; part++) { + const char *one = g_strstrip(*part); + if (*one == '\0') { + continue; + } + if (g_ascii_strcasecmp(one, "PTT") == 0) { + s->boot_key_ptt = true; + object_property_set_bool(OBJECT(&s->keypad), "ptt", true, &err); + } else { + object_property_set_str(OBJECT(&s->keypad), "press", one, &err); + } + if (err != NULL) { + error_report("boot-key: %s", error_get_pretty(err)); + error_free(err); + err = NULL; + } } + g_strfreev(parts); if (err) { warn_report("boot-key %s: %s", key, error_get_pretty(err)); error_free(err);