☰
杰理 JL703N SDK 实现 AI 自动化调试:自动进入下载模式、下载、重启与日志抓取
2026/10/15 19:15:00 网站建设 项目流程

让 AI 自己改代码、烧录、看日志:杰理 JL703N SDK 自动化调试实践

平台:杰理 JL703N 系列(BR27,SDKle_audio/ AC703N),其他 BR2x/BR5x 芯片思路相同
环境:Windows 10/11、Python 3、JLStudio 工具链(C:\JL\pi32)
适合:想让 AI 编程助手(Claude Code、Cursor 等)自己跑「改代码 → 编译 → 烧录 → 看日志 → 判断结果」闭环的嵌入式开发者


0. 先看效果

搭好之后,AI 改完代码只需要跑一条命令:

python run_test.py --build --flash --log 8 --tag fix_xxx

不到 1 分钟,它会自动完成下面这些步骤:

>> 编译 ... OK (4.1s) >> 进升级模式 + 下载 ... OK ← 不用按键、不用拔线 >> 抓日志 8s ... { "ok": true, "steps": { "build": {"ok": true, "seconds": 4.1, "errors": []}, "flash": {"ok": true, "steps": {"enter_upgrade": {...}, "download": {"rebooted": true}, "boot": {...}}}, "log": { "boot_seen": true, "build_info": {"compile_time": "Oct 14 2026 15:16:04", "is_current_build": true}, "unexpected_resets": 0, "new_error_lines": [], "metric_lines": ["[METRIC] dac_lvl pre min:8 max:11 ..."] } } }

AI 读这份summary.json就能判断:编译过没过、固件是不是真的更新了、开机后有没有死机复位、有没有新出现的错误日志,以及自己加的调试指标是多少。

整个闭环拆成 4 个问题:

问题方案
怎么不按键让设备进下载模式?通过 USB CDC 虚拟串口发配置工具协议命令,固件调用go_mask_usb_updata()复位进 ROM 升级模式
怎么在命令行编译、下载?make编译,然后调 SDK 自带的download.bat(内部是isd_download.exe)
下载完怎么自动重启?isd_download.exe加-reboot 500
怎么让 AI看日志、判断对错?脚本抓调试串口日志,固件按约定格式打印版本和指标,脚本汇总成 JSON

下面逐个讲,每一步都给出可以直接用的代码。


1. 整体架构

┌──────────────── PC ─────────────────────────────┐ │ AI 助手 │ │ │ 调用 │ │ run_test.py ──► make(编译) │ │ │ ──► CDC 串口:发「进入升级模式」命令 │ │ │ ──► download.bat(isd_download 下载)│ │ │ ◄── 日志服务(TCP 127.0.0.1:5555) │ │ ▼ ▲ │ │ summary.json log_gui.py(独占 COM 口) │ └───────┬───────────────────────┬──────────────────┘ USB │ │ USB 转串口(CH340) ┌───────┴───────────────────────┴──────────────────┐ │ JL703N:USB(MIDI + CDC 复合设备) 调试 UART TX │ └──────────────────────────────────────────────────┘

硬件只需要两根线:

  1. 设备 USB → PC:固件运行时枚举成 USB 复合设备,其中的 CDC 虚拟串口用来发命令;进入升级模式后,同一根线用来下载。
  2. 调试串口 → USB 转串口模块 → PC:看日志。

2. 自动进入下载模式

2.1 原理:go_mask_usb_updata()

SDK 里有一个现成的函数(apps/common/device/usb/device/msd_upgrade.c):

AT(.volatile_ram_code)voidgo_mask_usb_updata(){usb_g_hold(0);// 保持 USB 连接local_irq_disable();ram_protect_close();nvram_set_boot_state(UPGRADE_USB_SOFT_KEY);// 写启动标志:USB 软件触发升级cpu_reset();// 复位后 ROM 读到标志,进入 USB 升级模式while(1);}

调用它,设备就会复位进入芯片 ROM(mask)里的 USB 升级模式,等待 PC 下载。问题在于:PC 怎么触发固件调用它?

SDK 里有 3 个调用点:

触发方式条件Windows 下是否需要装驱动
U 盘(MSD)私有 SCSI 命令0xFB/0xFD/0xFC打开 PC 模式(TCFG_PC_ENABLE)不需要,但要开 U 盘功能
EP0 控制请求GET_STATUS(recipient=Other,wValue=0xFC,wIndex=0xFE,wLength=0),见user_setup.c任何 USB 设备模式下都有需要:设备绑的是系统类驱动,libusb 拿不到,要用 Zadig 装 WinUSB 或 UsbDk
配置工具协议命令ENTER_UPGRADE_MODE (0x26),见new_cfg_tool.c打开配置工具,走 USB CDC不需要,用系统自带的usbser驱动

本文用第 3 种:免驱,普通串口就能发。

2.2 固件配置

只需要改两处:

① 打开配置工具,并让它走 USB(apps/soundbox/board/br27/sdk_config.h,可以在 JLStudio 的「板级配置」里改):

#defineTCFG_CFG_TOOL_ENABLE1// FW编辑、在线调音#defineTCFG_COMM_TYPETCFG_USB_COMM// 通信方式:USB

打开后,app_config.h会自动打开TCFG_USB_CDC_BACKGROUND_RUN,插上 USB 时固件会在后台枚举 USB 设备。

② 确保后台枚举时带上 CDC 类(apps/common/device/usb/device/task_pc.c):

voidusb_cdc_background_run(constusb_dev usbfd){usb_device_mode(usbfd,MIDI_CLASS|CDC_CLASS);// 至少要有 CDC_CLASScdc_set_wakeup_handler(usb_cdc_wakeup);}

我的工程原来只开了MIDI_CLASS,所以要补上CDC_CLASS。CDC 自带 IAD 描述符,SDK 会把设备类设为0xEF/0x02/0x01,复合设备在 Windows 和 macOS 上都能正常识别。

编译烧录后插上 USB,设备管理器的「端口」下会出现「USB 串行设备 (COMxx)」:

  • VID =0x3654(杰理)
  • PID 会随 USB 类组合变化:SDK 在user_setup.c里把 PID 高字节按类位图做了偏移,MIDI+CDC 时是0x4D55,只有 MIDI 时是0x4855。所以用 VID 找设备,不要写死 PID。

2.3 协议格式

配置工具协议的帧格式(从new_cfg_tool.c的online_cfg_tool_data_deal和all_assemble_package_send_to_pc反推):

| 5A AA A5 | CRC_L CRC_H | LEN_L LEN_H | ID | SQ | DATA ... | 帧头 CRC16 长度 通道 序号 命令号(4字节小端) + 参数
字段字节数说明
帧头3固定5A AA A5
CRC162小端,计算范围从 LEN 开始到帧尾
LEN2小端,= 2 + DATA 长度
ID1PC → 设备填0x21(可视化配置工具通道);设备回复是0x00
SQ1包序号,随便填,回复里原样带回
DATAN前 4 字节是小端命令号

CRC16 是 CRC-16/XMODEM(多项式0x1021,初值0,不反转,不异或)。固件里的CRC16()在库里看不到源码,我是用查询命令逐个试出来的:只有 XMODEM 能收到设备回复。自测方法:"123456789"的结果应该是0x31C3。

常用命令:

命令命令号完整帧(SQ=0x10,可直接用串口助手按 HEX 发送)
查询设备信息0x000004005A AA A5 F9 28 06 00 21 10 00 04 00 00
进入升级模式0x000000265A AA A5 EE E4 06 00 21 10 26 00 00 00
设备重启(普通复位)0x000005055A AA A5 8C A3 06 00 21 10 05 05 00 00

查询命令会回复 102 字节的S_QUERY_BASIC_INFO(定义在cfg_tool.h):

typedefstruct{u16 protocolVer;u8 progCrc[32];u8 sdkName[32];// 如 "ac703n_V106-@20250427-$1ef092b8"u8 pid[16];// 如 "JL703N"u8 vid[16];u16 max_buffer_size;u8 support_node_merge;u8 comm_type;// 3 = USB}S_QUERY_BASIC_INFO;

能收到这个回复,就说明链路和 CRC 都没问题。

2.4 最小可用的 Python 实现

依赖:pip install pyserial

importstruct,timeimportserial,serial.tools.list_ports JL_VID=0x3654defcrc16_xmodem(data:bytes)->int:crc=0forbindata:crc^=b<<8for_inrange(8):crc=((crc<<1)^0x1021)ifcrc&0x8000else(crc<<1)crc&=0xFFFFreturncrcassertcrc16_xmodem(b"123456789")==0x31C3defbuild_frame(cmd:int,sq:int=0x10,payload:bytes=b"")->bytes:data=struct.pack("<I",cmd)+payload body=struct.pack("<H",2+len(data))+bytes([0x21,sq])+data# LEN ID SQ DATAreturnb"\x5A\xAA\xA5"+struct.pack("<H",crc16_xmodem(body))+bodydeffind_jl_port():forpinserial.tools.list_ports.comports():ifp.vid==JL_VID:returnp.devicereturnNonedefquery_info(port):"""返回设备 SDK 名,用来确认链路正常"""withserial.Serial(port,115200,timeout=1)ass:# CDC 口波特率无意义s.write(build_frame(0x400))rx=s.read(9+102)iflen(rx)<9orrx[:3]!=b"\x5A\xAA\xA5":returnNoneassertstruct.unpack_from("<H",rx,3)[0]==crc16_xmodem(rx[5:])returnrx[9+34:9+66].split(b"\0")[0].decode()defenter_upgrade(port):try:withserial.Serial(port,115200,timeout=0.05,write_timeout=1)ass:s.write(build_frame(0x26))exceptserial.SerialException:pass# 设备收到命令立刻复位,写的时候报错是正常的for_inrange(50):# CDC 口消失说明设备已复位iffind_jl_port()!=port:returnTruetime.sleep(0.1)returnFalse# 见 6.4:不一定是失败if__name__=="__main__":port=find_jl_port()print("port:",port,"sdk:",query_info(port))print("enter upgrade:",enter_upgrade(port))

如果写串口时报Write timeout,说明 CDC 口存在但固件不读数据,几乎都是板子上烧的还是没开TCFG_CFG_TOOL_ENABLE的旧固件。


3. 命令行编译

JLStudio 底层就是make。在 SDK 目录下直接执行:

set PATH=%CD%\tools\utils;C:\JL\utils;%PATH% make -j8

增量编译一般 4~10 秒。几个要点:

  1. Makefile 的 post-build 会自动调cpu\br27\tools\download.bat,也就是编译完就尝试下载。设备不在下载模式时,isd_download会在 1 秒内报Device Offline退出,不会卡住。所以先编译、编译成功后再让设备进升级模式:编译失败时设备还在正常运行,不会卡在 ROM 模式里。
  2. 判断编译是否成功:看make的返回码,并在输出里搜error:。
  3. download.bat是由download.c用预处理器生成的(见 Makefile 里的-E -P cpu/br27/tools/download.c -o $(POST_SCRIPT)),要改下载命令就改download.c,直接改 bat 会在下次编译时被覆盖。isd_config.ini同理,由isd_config_rule.c生成。

4. 下载

设备进入升级模式后,单独执行一次下载脚本:

importos,subprocessdefdownload(tools_dir):# tools_dir = SDK/cpu/br27/toolsenv={k:vfork,vinos.environ.items()ifk.upper()!="NODEFAULTCURRENTDIRECTORYINEXEPATH"}# 坑,见下文forattemptinrange(3):p=subprocess.run(["cmd","/c",os.path.join(tools_dir,"download.bat")],cwd=tools_dir,env=env,capture_output=True,timeout=300)out=p.stdout.decode("gbk","replace")if"Device Offline"inout:# ROM 刚复位还没枚举好,等一下重试time.sleep(1.5)continuereturn"Download completed"inout,outreturnFalse,out

isd_download成功时的输出(节选):

Device online Flash ID : CD7015 Start downloading data to flash ...... Write block: 7 6 5 4 3 2 1. 0. Download completed. Erase app2 core data ....[ SUCCESS ] rebooting device ......

踩过的两个坑:

  1. 'isd_download.exe' 不是内部或外部命令:AI 助手的 shell 环境可能设置了NoDefaultCurrentDirectoryInExePath,导致 cmd 不在当前目录查找 exe。启动子进程时把这个环境变量删掉。注意 Windows 上os.environ的键是大写,要不区分大小写地匹配。
  2. 不要用fail关键字判断失败:下载输出里有一张 OTA 空间检查表,其中会出现FAIL: testbox BLE upgrade ... require minial space size,这只是提示信息。要用正向标志Download completed判断成功。

5. 下载后自动重启

进入 ROM 下载模式后,运行的是芯片 ROM 和下载工具下发的 loader,固件代码完全不参与。所以下载完要不要重启,固件里没有开关可配,只由isd_download.exe的-reboot <毫秒>参数决定。

SDK 里本来就带了一个单独复位的cpu/br27/tools/reboot.bat:

isd_download.exe -dev br27 -boot 0x120000 -reboot 1000

在download.c的下载命令末尾加上-reboot 500:

isd_download.exe -tonorflash -dev br27 -boot 0x120000 -div8 -wait 300 -uboot uboot.boot -app app.bin ... %KEY_FILE% %FORMAT% -reboot 500

实测在 BR27 上-reboot可以和-tonorflash写在同一条命令里,输出末尾会多出rebooting device ......,烧完设备自动运行新固件。

判断设备是否已经重启:等 VID=0x3654 的 CDC 口重新出现。


6. 抓日志

6.1 串口只能被一个程序打开

调试串口(我这里是 CH340,2 Mbps,对应TCFG_DEBUG_UART_BAUDRATE)同一时间只能被一个程序打开。可是人要看日志,AI 也要读日志。

我的做法是写一个日志服务:它独占串口,一边在 GUI 窗口里实时显示(错误行标红、复位行标蓝),一边通过本机 TCP 把每一行转发出去。AI 的脚本连 TCP 读日志;日志服务没开时,再直接打开串口。核心代码很短:

importre,socket,threading,serial clients,lock=[],threading.Lock()defaccept_loop(srv):whileTrue:c,_=srv.accept()withlock:clients.append(c)srv=socket.socket();srv.bind(("127.0.0.1",5555));srv.listen(8)threading.Thread(target=accept_loop,args=(srv,),daemon=True).start()ser=serial.Serial("COM3",2000000,timeout=0.1)pending=b""whileTrue:pending+=ser.read(4096)*lines,pending=re.split(rb"\r?\n",pending)forrawinlines:line=raw+b"\n"print(line.decode("utf-8","replace"),end="")# 人看withlock:# 转发给 AI 脚本forcinclients[:]:try:c.sendall(line)exceptOSError:clients.remove(c)

想用自己习惯的串口工具看也可以:PuTTY 选 Raw 模式,连127.0.0.1:5555。

6.2 让 AI 确认"固件真的更新了"

AI 最容易犯的错误是:烧录其实失败了,它却在旧固件的日志里找结论。解决办法是开机打印编译时间,脚本再拿它和本轮编译的开始时间比对。

User/build_info.c:

#ifndefBUILD_GIT_HASH#defineBUILD_GIT_HASHunknown#endif#defineSTR_(x)#x#defineSTR(x)STR_(x)voidBuildInfo_Print(void){printf("[BUILD] compile time: %s %s, git: %s%s\n",__DATE__,__TIME__,STR(BUILD_GIT_HASH),BUILD_GIT_DIRTY?"-dirty":"");}

只放__DATE__ __TIME__还不够:增量编译时这个文件不会被重编,时间就停在旧值。在 Makefile 里让它每次都重编:

BUILD_GIT_HASH := $(or $(word 1,$(shell git rev-parse --short HEAD)),unknown) BUILD_GIT_DIRTY := $(if $(strip $(shell git status --porcelain --untracked-files=no -- *.c *.h)),1,0) $(BUILD_DIR)/User/build_info.c.o: FORCE $(BUILD_DIR)/User/build_info.c.o: DEFINES += -DBUILD_GIT_HASH=$(BUILD_GIT_HASH) -DBUILD_GIT_DIRTY=$(BUILD_GIT_DIRTY) .PHONY: FORCE FORCE:

判断 dirty 时只看.c/.h:SDK 仓库里sdk.map、jl_isd.fw这类编译产物也被跟踪,每次编译都会变,如果算进去 dirty 永远是 1。

开机早期调用BuildInfo_Print()(比如在app_main.c调用的用户初始化函数里),日志里就会出现:

[00:00:00.126][BUILD] compile time: Oct 14 2026 15:16:04, git: 4403254-dirty

6.3 让日志变成"AI 能判断的结论"

原始日志有几百行,AI 直接读既费 token 又容易误判。我在脚本里做了这几件事,最后汇总成summary.json:

检查项做法
定位"烧录后的开机"设备日志带运行时间戳[hh:mm:ss.mmm]。以时间戳倒退的那一行作为重启点。不能只看"时间戳很小":连续烧录时,旧固件可能才运行了几秒
意外复位(死机、看门狗、断言)重启点之后时间戳又倒退一次,就算一次意外复位,必须为 0
固件版本找[BUILD]行,编译时间早于本轮编译开始时间就判失败
新错误匹配 `error
自定义指标约定:固件里给 AI 看的指标行都带[METRIC]前缀,脚本原样收集
驱动的单字符标记比如 DAC 欠载时驱动会putchar('U'),这类标记单独成行,按^U+$统计

[METRIC]这个约定是整套流程里最有用的部分。比如调音频延时时,我让固件每秒打印一次 DAC 缓冲水位:

[METRIC] dac_lvl pre min:8 max:11 avg:10 ... isr_gap_max_us:1487

AI 改一次参数、跑一轮,读metric_lines就能知道效果,再决定下一步怎么改。


7. 串起来:总控脚本

defrun(build=True,flash=True,log_seconds=8):summary={"steps":{}}logger=LogReader().start()# 优先连日志服务,连不上再开串口ifbuild:t0=datetime.now()summary["steps"]["build"]=make_build()# 返回 ok/errors/secondsifnotsummary["steps"]["build"]["ok"]:returnsummary# 编译失败就不动设备mark=logger.mark()ifflash:port=find_jl_port()enter_upgrade(port)ok,out=download(TOOLS_DIR)booted=wait_for_port(JL_VID,timeout=30)# 新固件起来后 CDC 口会重新出现summary["steps"]["flash"]={"download_ok":ok,"booted":booted}time.sleep(log_seconds)summary["steps"]["log"]=analyze_log(logger.since(mark),build_start=t0)summary["ok"]=all_steps_ok(summary)json.dump(summary,open(out_dir/"summary.json","w"),ensure_ascii=False,indent=2)returnsummary

每轮的完整输出(build.log、download.log、device.log、summary.json)都存到results/<时间>_<tag>/,方便事后回看。


8. 教会 AI 用这套工具

在工程根目录的CLAUDE.md(或你用的 AI 工具对应的规则文件)里写清楚:

## 硬件闭环测试 改完固件代码后,必须用实测结果说话,不能只凭"编译通过"下结论: - 只确认能编译: python tools/ai_test/run_test.py --build - 改了初始化/外设: python tools/ai_test/run_test.py --build --flash --log 15 - 改了音频通路: python tools/ai_test/run_test.py --build --flash --log 8 --latency 读 results/<最新目录>/summary.json: - ok 为 false 时先看是哪一步失败 - build_info.is_current_build 必须为 true,否则说明烧录没生效,日志不可信 - unexpected_resets 必须为 0 - 只关注 new_error_lines;已知开机日志在 log_baseline.txt - 需要观察的量,在固件里用 [METRIC] 前缀打印

我实际用下来,AI 能自己完成这样的循环:加[METRIC]打印 → 跑测试 → 读指标 → 定位问题 → 改代码 → 再跑测试。我只需要在它碰到工具配置(比如 JLStudio 里的节点参数)时动手改一下。


9. 可选:声卡环回测音频延时

如果调的是音频产品,可以再加一个客观指标。用一块多通道声卡:

声卡 OUT L ──► 设备输入;设备输出 ──► 声卡 IN L (被测路径) 声卡 OUT R ───────────────────────► 声卡 IN R (直连参考)

两个声道同时播放同一段 1 kHz 短脉冲,同时录音。IN L 相对 IN R 的时间差就是设备延时,声卡自身的延时和播放、录音不同步的影响都会被抵消。

计算时有一个容易错的地方:1 kHz 正弦每 1 ms 重复一次,直接对波形做互相关,很容易对错一个周期。正确做法是先对包络做互相关得到粗略延时,再在 ±0.6 ms 范围内对波形做互相关、用抛物线插值得到精确值,精度可以到 µs 级。


10. 注意事项

  1. 配置工具协议没有任何鉴权:插上 USB 的任何人都能让设备进升级模式,还能在线读写 flash。开发阶段很方便,量产固件是否保留TCFG_CFG_TOOL_ENABLE要评估。
  2. 不要在设备写 flash 时发升级命令:设备会立即复位,可能打断正在进行的擦写。
  3. 进入升级模式后如果不想烧录:重新上电,或者运行一次reboot.bat。
  4. 日志服务开着时,其他串口工具不能再打开同一个 COM 口。
  5. Windows 上用 Python 改 SDK 文件时要注意换行符:文本模式写入会把 LF 变成 CRLF,导致 git 显示整个文件都改了。改文件时用二进制模式读写。

附:用到的 SDK 文件一览

文件作用
apps/common/device/usb/device/msd_upgrade.cgo_mask_usb_updata()进入 ROM 升级模式
apps/common/config/new_cfg_tool.c配置工具协议解析,ONLINE_SUB_OP_ENTER_UPGRADE_MODE
apps/common/config/include/cfg_tool.h命令号、回复结构体定义
apps/common/device/usb/device/user_setup.cEP0 私有升级请求、PID 偏移规则
apps/common/device/usb/device/task_pc.cusb_cdc_background_run(),USB 后台枚举的类组合
apps/soundbox/board/br27/sdk_config.hTCFG_CFG_TOOL_ENABLE、TCFG_COMM_TYPE、调试串口波特率
cpu/br27/tools/download.c生成download.bat,在这里加-reboot
cpu/br27/tools/reboot.bat单独复位设备

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询