给杰理 AC701N(BR28)刷机分两种情况。板子上的应用固件正常运行时,插一根 USB 数据线就能由电脑软件直接触发刷机;如果刷入了损坏的固件导致死机或启动崩溃,就得在芯片上电复位阶段靠外部工具发握手信号救砖。这两种方式只是进入下载模式的入口不同,进去之后走的是同一套 USB 下载通道。

1、整体烧录流程

电脑通过 USB 烧录杰理芯片的全过程分为三个阶段,先让芯片进入 ROM 下载模式,接着把临时加载程序传进芯片 RAM 运行,最后由这段 RAM 程序完成 Flash 擦写。

flowchart TD
    A[电脑发起烧录] --> B{杰理应用能否响应USB请求}
    B -->|能够响应| C[应用接收请求并切换到ROM下载模式]
    B -->|已经无法响应| D[外置工具使杰理复位并发送启动握手]
    D --> E[杰理ROM识别握手并进入下载模式]
    C --> F[电脑识别杰理下载设备]
    E --> F
    F --> G[电脑上传临时程序到杰理RAM并调用]
    G --> H[临时程序执行查询和Flash读写]
    H --> I[电脑发送启动命令]
    I --> J[杰理启动Flash中的固件]

整个过程中涉及三段职责完全不同的程序:

程序名称 存放位置 负责的工作 断电后的状态
ROM 启动程序 芯片出厂固化的掩膜存储区 上电最先执行,负责正常引导或提供基础下载入口 永久保留
RAM 临时加载程序(Loader) 存放在电脑端,刷机时上传到芯片 RAM 初始化存储驱动,向电脑提供 Flash 查询、读取、擦除与写入命令 断电即失,每次刷机重新上传
Flash 启动程序(Bootloader) 随固件烧录进 Flash(如 uboot.boot) 正常开机时引导应用固件,衔接 SDK 的启动与升级流程 保存在 Flash 中,随烧录更新

板子平时正常开机走的是“ROM 启动程序 → Flash 启动程序 → 应用固件”这条路;而电脑刷机走的是“ROM 下载入口 → RAM 临时加载程序 → Flash 读写操作”这条路,上游文档对 UBOOT1.00 下载阶段的说明详细解释了这套分工。

所以救砖的核心逻辑很简单,只要能让芯片重新跑一次出厂 ROM,并在开机瞬间拦住它进入下载模式,后续的 Flash 擦写全由电脑现场传进 RAM 的 Loader 接管,哪怕 Flash 里的原有程序全毁了也不影响重刷。

2、固件正常时的软触发切换

当应用固件正常运行时,电脑直接连到杰理芯片原生 USB 的 D+ 和 D- 引脚。固件初始化 USB 从机接口后,会监听控制端点(EP0)上的特定请求。

电脑端的 usb_upgrade.py 脚本通过 libusb 分两步触发模式切换:

控制传输请求参数 作用与预期响应
bmRequestType=0x83、GET_STATUS (0x00)、wValue=0xFC、wIndex=0xFE、wLength=8 探测固件是否开放了在线升级入口,支持时会返回 8 个 0xFF
保持相同的请求头参数,将读取长度 wLength 改为 0 正式通知固件重启并切入 ROM 下载模式

电脑端发起探测与切换的核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1. 发送 8 字节控制读取请求,探测运行态固件是否支持在线升级入口
response = (ctypes.c_ubyte * 8)()
result = usb.lib.libusb_control_transfer(
handle, 0x83, 0, 0xFC, 0xFE, response, 8, 3000
)
if result != 8 or bytes(response) != b"\xff" * 8:
raise RuntimeError("官方 USB 升级入口探测失败,未发送切换请求")

# 2. 发送 0 长度同类请求,触发固件调用 go_mask_usb_updata() 切入下载模式
result = usb.lib.libusb_control_transfer(
handle, 0x83, 0, 0xFC, 0xFE, None, 0, 3000
)
# 芯片切换瞬间会直接断开 USB,因此允许返回成功、IO 错误、设备断开或超时
if result not in (0, -1, -4, -7):
raise RuntimeError(f"USB 切换请求失败:{result}")

杰理固件里的 user_setup.c 在 setup_other() 回调中收到第二个零长度请求后,会调用 msd_upgrade.c 中的 go_mask_usb_updata()。在 BR28 平台上,该函数会关闭本地中断和 RAM 保护,写入 UPGRADE_USB_SOFT_KEY 软启动状态标记,最后操作 SDK 的电源控制寄存器触发复位并切入 ROM 下载模式。

电脑随后等待杰理以 UBOOT1.00 下载设备的身份重新枚举上线,并通过 /sys/bus/usb/devices 与 /sys/class/scsi_generic 比对切换前后的 USB 物理端口路径(例如 1-2.3),确保接下来刷写的是同一块开发板。

这套软切换完全依赖应用固件活着且能正常响应 USB 中断。如果刷进去的固件已经卡死,或者一开机就崩溃重启,电脑发控制请求就石沉大海了,这时候必须改用下面的开机硬件握手入口。

3、固件损坏时的开机硬件握手

杰理官方的强制升级工具(烧录盒)能够在芯片上电复位的瞬间向 USB 引脚发送一段特殊的握手信号。芯片重跑出厂 ROM 代码时一旦识别到这段握手波形,就会放弃加载 Flash 里的坏固件,直接停留在 UBOOT1.00 下载模式,官方《升级与下载说明》中就有针对 BR28 下载设备的演示。

要完成这次抢救,硬件上必须配合做好两件事:先让芯片真正复位重启以重新打开开机检测窗口,再赶在窗口期内把 USB_KEY 握手波形发进去。

3.1 USB_KEY 握手信号

根据开源社区的逆向记录,USB_KEY 握手信号的电气特征如下:

信号参数 逆向记录典型值
固定识别码 0x16EF(二进制 0001 0110 1110 1111)
发送位序 高位先发(MSB First)
物理传输形式 利用 D+ 和 D- 两根线,一根提供时钟,另一根传输数据
典型时钟频率 约 50 kHz,发完 16 位数据耗时约 320 μs
采样边沿与芯片响应 时钟上升沿采样数据;芯片识别成功后会将两根线同时拉低约 1~2 ms

关于时钟线与数据线的具体分配,同一逆向作者的 USB_KEY 专题页将 D+ 标为时钟、D- 标为数据,而在另一篇如何进入 UBOOT 的文档中文字表述恰好相反。动手自制恢复工具时,建议在目标芯片上实测确认两根线的分工、开机握手窗口时间以及拉低响应波形。

这里有一个很多新手容易踩的坑,USB_KEY 是一段双线独立翻转的原始 GPIO 电平时序,不能用普通电脑的 USB 接口直接发送。电脑侧的 libusb 传输接口只能发起标准的控制、批量或中断传输,如果你把 0x16EF 当作普通 USB 数据包发出去,主机控制器硬件会自动给它套上 SYNC 同步头、PID 包标识、CRC 校验并做 NRZI 差分编码,到达引脚上的波形早就面目全非了。因此生成 USB_KEY 必须靠外部单片机的 GPIO 直接操作两根信号线。

3.2 让芯片真正复位

最稳妥的复位办法是用外部控制器操纵负载开关或 MOS 管电路,直接切断杰理芯片的电源,等板载滤波电容彻底放完电后再重新供电并发起握手。在这期间,外部恢复控制器自身必须保持独立供电。

**对于接了锂电池的设备,必须同时核对电池和 USB 两条供电路径;如果电池一直在给杰理芯片供电,光靠断开 USB 5V 根本无法让芯片断电复位。**此外还要留意板子上其他带电芯片是否会通过 GPIO 信号线向杰理反向漏电。

官方强制升级工具的操作说明提到,在默认拨码下按下升级键会切断电源输出至少 250 ms 后再发起握手;自制恢复电路的具体断电等待时间则要看目标板电容的实际放电速度。

除了断电,另一种办法是触发芯片的长按复位引脚。例如 AC701N 工程在 board_701n_demo_cfg.h 和 cpu/br28/setup.c 的 setup_arch() 中默认将 PB1 配置为长按 8 秒低电平触发硬件复位。这种方式适合固件已经跑完早期硬件初始化、后来在业务循环里卡死的场景;如果刷入的坏固件在执行到 setup_arch() 配置复位引脚之前就崩溃了,拉低 PB1 同样无效,还是得靠断电或纯硬件复位路径。

4、RAM 加载程序接管 Flash

无论是通过软件请求切换,还是靠硬件 USB_KEY 握手拦停,芯片最终都会进入 UBOOT1.00 模式并枚举为一个 USB 大容量存储(MSC)设备。此时芯片里的出厂 ROM 只具备最基础的写内存和函数跳转能力,本身没有全套 Flash 擦写驱动,电脑必须先把 br28loader.uboot 上传到芯片 RAM 里跑起来。

在 Linux 主机上,jieli_uboot.py 通过打开对应的 /dev/sg* 字符设备,利用 SG_IO 接口发送 16 字节的杰理私有 SCSI 命令(有效参数不足 16 字节时在末尾补 0xFF)。这里只是借用了标准 SCSI 的通道外壳,实际交互全按杰理自定义协议走:

  1. 电脑发送标准 INQUIRY 命令(12 00 00 00 24 00)读取 36 字节设备信息,核对返回的产品标识是否为 UBOOT1.00。
  2. 从芯片 RAM 地址 0x120000 开始,按每块 512 字节循环发送 0xFB06 命令上传 Loader 二进制数据,每次请求都在命令块里附带写入地址、块长度和 CCITT CRC16 校验值。
  3. 上传完毕后发送 0xFB08 命令,让芯片跳转到 0x120000 执行 Loader,并传入目标存储介质等初始化参数。
  4. 根据上游《USB loader protocol v2》对内存跳转的分析,ROM 调用 Loader 入口函数时会把底层的 USB 收发与命令回调指针传给它,Loader 随即注册自己的命令处理表,无缝接管后续的 USB 通信。
  5. 电脑发送 0xFC0A 和 0xFC0B 查询在线存储设备类型与 Flash JEDEC ID,确认是预期容量的 SPI0 NOR Flash 后,执行分块擦除、写入或读取。
  6. 全部操作完成后,电脑发送 0xFC0C 命令通知芯片重启并引导新固件。

主机端用到的核心私有 SCSI 命令汇总如下:

阶段操作 命令码 执行效果
写入芯片 RAM 0xFB06 将单块 512 字节数据写入指定的芯片 RAM 地址并校验 CRC16
调用 RAM 程序 0xFB08 跳转到指定的 RAM 入口地址执行 Loader,并传入两字节参数
查询在线存储 0xFC0A、0xFC0B 返回存储介质类型(如 0x03 为 SPI0 NOR)、容量指数与 3 字节 Flash ID
读取 Flash 0xFD05 按指定起始地址和长度回读 Flash 数据
擦除与写入 Flash 0xFB00、0xFB01、0xFB04 分别执行 64KB 块擦除、4KB 扇区擦除,以及带 CRC16 的分块写入
启动应用固件 0xFC0C 结束下载会话,触发芯片复位并从 Flash 启动应用

jieli_uboot.py 中构造私有命令、处理 MengLi 加扰以及上传运行 Loader 的核心实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def mengli_transform(data: bytes) -> bytes:
"""应用 BR28 ROM 使用的对称 MengLi 加扰/解扰算法"""
output = bytearray(data)
magic = "孟黎我爱你,玉林".encode("gb2312")
crc = binascii.crc_hqx((0xFFFF).to_bytes(2, "little"), 0xFFFF)
for index in range(len(output)):
offset = index % len(magic)
crc = binascii.crc_hqx(magic[offset : offset + 1], crc)
output[index] ^= crc & 0xFF
return bytes(output)

def _cdb(command: int, arguments: bytes = b"") -> bytes:
"""构造 16 字节杰理私有 SCSI 命令描述块,不足部分填 0xFF"""
value = command.to_bytes(2, "big") + arguments
return value + b"\xff" * (16 - len(value))

def install_loader(self, loader: bytes, address: int, argument: int,
transform: str, block_size: int) -> None:
"""将 RAM Loader 按 512 字节分块写入芯片内存并跳转执行"""
for offset in range(0, len(loader), block_size):
block = loader[offset : offset + block_size]
if transform == "mengli":
# ROM 在接收每个 FB06 数据块时都会独立重置 MengLi 解扰状态
block = mengli_transform(block)
crc = binascii.crc_hqx(block, 0)
arguments = (
(address + offset).to_bytes(4, "big")
+ len(block).to_bytes(2, "big")
+ b"\x00"
+ crc.to_bytes(2, "little")
)
self.execute(self._cdb(0xFB06, arguments), data_out=block)
# 全部块写完后发送 FB08 跳转到 RAM 入口地址执行
self._response(0xFB08, address.to_bytes(4, "big") + argument.to_bytes(2, "big"))

5、Loader 来源与 SDK 容器解析

5.1 预编译 Loader 与 MengLi 加扰

上面脚本实际上传的 br28loader.uboot 文件大小为 23,552 字节(SHA-256 为 ab1b77eb73385aaa78ab769253b2af7ef63d0ade46f391bd7424cc598a40267c),直接取自开源项目 kagaimiq/jl-uboot-tool 中的预编译二进制文件。

这里提到的“加扰”和“解扰”,其实就是用固定规则把原始数据打乱成乱码、再用同一规则还原回来的简易对称加密,用来防止别人直接反汇编看穿里面的明文机器码。从上一节的 mengli_transform() 代码可以看到,杰理拿中文字符串 "孟黎我爱你,玉林"(GB2312 编码)当作种子,每处理一个字节就滚一次 CRC16,取算出来的低 8 位与原始数据做异或(^)。因为同一个字节对同一个数连续异或两次就会变回原样,所以加扰和解扰跑的是完全同一套函数。

BR28 的出厂 ROM 在收到每个 0xFB06 数据块时,都会自动跑一遍这个函数把乱码还原成明文机器码再写入 RAM。由于上游仓库提供的这份 br28loader.uboot 在发布前已经提前做过了一次异或打乱(见上游 BR28 配置与上传逻辑),因此主机配置文件中设置 loader_transform="none" 代表直接原样发送这份乱码文件,芯片收到后异或一次正好还原成可执行的明文程序。如果电脑上传前多此一举再调一次该函数,反而会把它还原成明文发过去,经芯片 ROM 再异或一次又变成乱码,程序直接跑飞。

5.2 原厂 SDK 的 JLFV 容器结构

在杰理官方 SDK 的 cpu/br28/tools/download.sh 构建脚本中,会将 br28_loader_ftlv1.bin 复制为 br28loader.bin 供官方烧录工具调用。但如果你直接拿 SDK 里的这个 .bin 文件喂给上面的 Python 脚本上传,芯片一定会跑飞,因为原厂 SDK 的 Loader 文件外层套了一层 JLFV 容器。

对 SDK 里的 br28_loader_ftlv1.bin 和 br28_loader_ftlv2.bin 进行离线拆解,可以看到其内部容器布局如下:

容器区域 字节偏移范围 存放内容与用途
容器总头 0x00~0x1f 固定 32 字节,经 ENC 解扰后可见 JLFV 魔数、文件总长度、目录记录数及头部 CRC16
两条目录记录 0x20~0x5f 每条 32 字节,分别描述后续各段的起始偏移、长度和段 CRC16
version 段 0x60~0xbf 记录 Loader 与底层驱动的版本字符串,仅用于主机核对,不上传到芯片
data 段 从 0xc0 到末尾 真正需要上传到芯片 RAM 的 MengLi 加扰程序数据

两份原厂 SDK 文件的具体尺寸对比如下:

SDK 文件名 容器文件总长度 提取出的 data 段长度
br28_loader_ftlv1.bin 33,984 字节 33,792 字节(0x8400)
br28_loader_ftlv2.bin 27,328 字节 27,136 字节(0x6a00)

这套容器的加扰与校验规则相当讲究,总头和每条目录记录都各自从初始状态独立做了一次 ENC 解扰,不能把整个文件头当成一段连续数据去解;而 data 段内部则保持其原有的传输加扰状态。校验方面全部采用初值为 0 的 CCITT CRC16 算法,其中总头 CRC 覆盖解扰后头部的 [6, 32) 字节,目录表 CRC 直接覆盖未解扰的原始目录表区域,各个内容段则各自拥有独立的 CRC16。

反汇编官方的 Linux 烧录工具也能确认这一过程,官方工具会先解析 JLFV 容器并校验各段 CRC,随后只提取出里面的 data 段,按每块 512 字节通过 0xFB06 上传到 -boot 0x120000 指定的地址,最后带上 -div8 等时钟分频与存储选择参数调用 0xFB08 启动。由于目前的 Python 刷机脚本只负责裸传二进制载荷,若想换用原厂 SDK 里的 Loader,必须先按目录表解析校验并将 data 段单独剥离出来。

6、下载模式下的探测、烧录与回读

芯片跑起 RAM Loader 后,主机侧的 flash.py 和 verify_flash.py 提供了三种不同粒度的操作入口:

脚本入口与参数 电脑与杰理芯片执行的操作 结束后的设备状态
flash.py --probe 上传并运行 RAM Loader,读取在线存储类型、Flash ID 并核对容量,全程不擦写 Flash 停留在当前下载模式,不发送启动应用命令
verify_flash.py 按每块 256 字节回读镜像覆盖范围的 Flash 数据,在电脑端逐字节比对并校验 SHA-256 比对通过后发送 0xFC0C 启动应用固件
flash.py 擦除目标区域并按每块 256 字节写入 jl_isd.bin,按需清理旧 OTA 代码区 写入完成后发送 0xFC0C 启动应用固件

回读比对是在电脑端内存中完成的,芯片上的 RAM Loader 只管响应 0xFD05 把指定地址的 Flash 原始字节读上来。

在执行 flash.py 正常烧录时,脚本会优先对满足 64KB 对齐的区间使用 0xFB00 块擦除,剩余尾部按 4KB 扇区使用 0xFB01 擦除,然后再分块写入 jl_isd.bin。对于开启了双备份 OTA 的工程,如果只更新基址固件而保留第二备份区的旧固件,芯片重启后仍可能跳去运行旧版本。为此脚本提供了 --activate-base-bank 选项,开启后会先读取打包日志核对 CODE_BOUNDARY_LINE 与 VM_START_ADDR 边界,确认无误后将旧 OTA 代码区一并擦空:

1
2
3
4
5
6
7
8
9
10
11
12
# 1. 擦除基址镜像覆盖范围(优先 64KB 块擦除,尾部 4KB 扇区擦除)
erase_image_range(device, len(image))

# 2. 按 256 字节分块写入 jl_isd.bin 镜像
write_image(device, image, io_block_size)

# 3. 若启用了双备份基址激活策略,擦除旧 OTA 代码区防止启动时跳回老版本
if secondary_bank is not None:
erase_secondary_bank(device, *secondary_bank, io_block_size, verify=False)

# 4. 发送 FC0C 命令重启芯片并引导新固件
device.run_app()

擦除和烧录的地址范围必须严格按当前工程的镜像布局设定,绝对不能越界擦除存放用户配置、蓝牙配对记录和出厂校准参数的 VM 区与工厂数据区。

7、自制电脑可控的恢复下载器

如果不想每次遇到坏固件都去手动插拔电源或按官方烧录盒按键,可以自己用一块带 USB 的小单片机做一个受电脑脚本控制的恢复下载器。这个小工具只需要把三件事接到电脑命令接口上,分别是切断与恢复目标板供电、用 GPIO 发送 USB_KEY 握手波形,以及通过模拟开关切换 USB 数据线走向。

flowchart LR
    A[电脑脚本] -->|发送操作命令| B[独立供电的恢复控制器]
    B -->|控制目标供电| C[负载开关或MOS电源电路]
    C --> D[杰理目标板]
    B -->|生成启动握手| E[USB数据线切换电路]
    A -->|握手成功后传输固件| E
    E -->|按阶段连接电脑或控制器| D

整套全自动恢复流程非常清晰,电脑脚本向恢复控制器发送救砖指令后,控制器先关断 MOS 管切断杰理目标板供电,延时等待电容放电完毕后重新合上供电,同时将模拟开关切到控制器一侧,用 GPIO 在 D+/D- 上打出 0x16EF 握手信号;检测到杰理拉低总线响应后,控制器立刻将自身 GPIO 设为高阻输入状态,并把模拟开关拨回电脑主机一侧,通知电脑脚本直接接管刚刚枚举出来的 UBOOT1.00 设备完成后续刷机。

目前开源社区里有两份现成的项目非常适合拿来改造:

开源参考项目 现成可复用的部分 改造成全自动工具还需补齐的部分
ESP32-S3 BR25 USB_KEY 工具 包含完整的接线图与 Arduino 固件,已实现 POWER_CTL 供电控制和 0x16EF 握手发送 原代码为上电自动单次执行,需补充电脑串口命令接收逻辑、USB 模拟开关切换控制及 BR28 时序核对
RP2040 Pico 下载器设计 给出了完整的 USB 信号切换电路、握手检测状态机及配套 C 源码 原设计将目标板 VBUS 直通、靠手动按键触发重启,需补上目标板供电开关电路与电脑命令接口

这两个方案刚好互补,前者写好了断电与握手动作,后者画好了把 D+/D- 交还给电脑的模拟开关切换电路,把两边合在一起就能做出一块完整的全自动救砖小板。

杰理官方的第四代强制升级工具其实也预留了类似的入口,官方《工具拨码开关说明》里提到将第 7 位拨码拨上去即可开启电脑通信模式,收到电脑发来的升级指令后会自动执行相当于按下升级键的动作。不过官方并没有公开这套上位机触发指令的具体字节格式,而且也没法由脚本自由调节断电放电时长,想要彻底融入 Linux 自动化编译刷机流水线,还是自己用单片机搭一块可控下载器最省心。

站内搜索

没有找到内容!