让 AI 自己改代码、烧录、看日志杰理 JL703N SDK 自动化调试实践平台杰理 JL703N 系列BR27SDKle_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.batisd_download 下载│ │ │ ◄── 日志服务TCP 127.0.0.1:5555 │ │ ▼ ▲ │ │ summary.json log_gui.py独占 COM 口 │ └───────┬───────────────────────┬──────────────────┘ USB │ │ USB 转串口CH340 ┌───────┴───────────────────────┴──────────────────┐ │ JL703NUSBMIDI CDC 复合设备 调试 UART TX │ └──────────────────────────────────────────────────┘硬件只需要两根线设备 USB → PC固件运行时枚举成 USB 复合设备其中的 CDC 虚拟串口用来发命令进入升级模式后同一根线用来下载。调试串口 → USB 转串口模块 → PC看日志。2. 自动进入下载模式2.1 原理go_mask_usb_updata()SDK 里有一个现成的函数apps/common/device/usb/device/msd_upgrade.cAT(.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);}调用它设备就会复位进入芯片 ROMmask里的 USB 升级模式等待 PC 下载。问题在于PC 怎么触发固件调用它SDK 里有 3 个调用点触发方式条件Windows 下是否需要装驱动U 盘MSD私有 SCSI 命令0xFB/0xFD/0xFC打开 PC 模式TCFG_PC_ENABLE不需要但要开 U 盘功能EP0 控制请求GET_STATUSrecipientOtherwValue0xFCwIndex0xFEwLength0见user_setup.c任何 USB 设备模式下都有需要设备绑的是系统类驱动libusb 拿不到要用 Zadig 装 WinUSB 或 UsbDk配置工具协议命令ENTER_UPGRADE_MODE (0x26)见new_cfg_tool.c打开配置工具走 USB CDC不需要用系统自带的usbser驱动本文用第 3 种免驱普通串口就能发。2.2 固件配置只需要改两处① 打开配置工具并让它走 USBapps/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.cvoidusb_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 高字节按类位图做了偏移MIDICDC 时是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 A5CRC162小端计算范围从 LEN 开始到帧尾LEN2小端 2 DATA 长度ID1PC → 设备填0x21可视化配置工具通道设备回复是0x00SQ1包序号随便填回复里原样带回DATAN前 4 字节是小端命令号CRC16 是 CRC-16/XMODEM多项式0x1021初值0不反转不异或。固件里的CRC16()在库里看不到源码我是用查询命令逐个试出来的只有 XMODEM 能收到设备回复。自测方法123456789的结果应该是0x31C3。常用命令命令命令号完整帧SQ0x10可直接用串口助手按 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.htypedefstruct{u16 protocolVer;u8 progCrc[32];u8 sdkName[32];// 如 ac703n_V106-20250427-$1ef092b8u8 pid[16];// 如 JL703Nu8 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 pyserialimportstruct,timeimportserial,serial.tools.list_ports JL_VID0x3654defcrc16_xmodem(data:bytes)-int:crc0forbindata:crc^b8for_inrange(8):crc((crc1)^0x1021)ifcrc0x8000else(crc1)crc0xFFFFreturncrcassertcrc16_xmodem(b123456789)0x31C3defbuild_frame(cmd:int,sq:int0x10,payload:bytesb)-bytes:datastruct.pack(I,cmd)payload bodystruct.pack(H,2len(data))bytes([0x21,sq])data# LEN ID SQ DATAreturnb\x5A\xAA\xA5struct.pack(H,crc16_xmodem(body))bodydeffind_jl_port():forpinserial.tools.list_ports.comports():ifp.vidJL_VID:returnp.devicereturnNonedefquery_info(port):返回设备 SDK 名用来确认链路正常withserial.Serial(port,115200,timeout1)ass:# CDC 口波特率无意义s.write(build_frame(0x400))rxs.read(9102)iflen(rx)9orrx[:3]!b\x5A\xAA\xA5:returnNoneassertstruct.unpack_from(H,rx,3)[0]crc16_xmodem(rx[5:])returnrx[934:966].split(b\0)[0].decode()defenter_upgrade(port):try:withserial.Serial(port,115200,timeout0.05,write_timeout1)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__:portfind_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增量编译一般 410 秒。几个要点Makefile 的 post-build 会自动调cpu\br27\tools\download.bat也就是编译完就尝试下载。设备不在下载模式时isd_download会在 1 秒内报Device Offline退出不会卡住。所以先编译、编译成功后再让设备进升级模式编译失败时设备还在正常运行不会卡在 ROM 模式里。判断编译是否成功看make的返回码并在输出里搜error:。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):psubprocess.run([cmd,/c,os.path.join(tools_dir,download.bat)],cwdtools_dir,envenv,capture_outputTrue,timeout300)outp.stdout.decode(gbk,replace)ifDevice Offlineinout:# ROM 刚复位还没枚举好等一下重试time.sleep(1.5)continuereturnDownload completedinout,outreturnFalse,outisd_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 ......踩过的两个坑isd_download.exe 不是内部或外部命令AI 助手的 shell 环境可能设置了NoDefaultCurrentDirectoryInExePath导致 cmd 不在当前目录查找 exe。启动子进程时把这个环境变量删掉。注意 Windows 上os.environ的键是大写要不区分大小写地匹配。不要用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.batisd_download.exe -dev br27 -boot 0x120000 -reboot 1000在download.c的下载命令末尾加上-reboot 500isd_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 ......烧完设备自动运行新固件。判断设备是否已经重启等 VID0x3654 的 CDC 口重新出现。6. 抓日志6.1 串口只能被一个程序打开调试串口我这里是 CH3402 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)srvsocket.socket();srv.bind((127.0.0.1,5555));srv.listen(8)threading.Thread(targetaccept_loop,args(srv,),daemonTrue).start()serserial.Serial(COM3,2000000,timeout0.1)pendingbwhileTrue:pendingser.read(4096)*lines,pendingre.split(rb\r?\n,pending)forrawinlines:linerawb\nprint(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-filesno -- *.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/.hSDK 仓库里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-dirty6.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:1487AI 改一次参数、跑一轮读metric_lines就能知道效果再决定下一步怎么改。7. 串起来总控脚本defrun(buildTrue,flashTrue,log_seconds8):summary{steps:{}}loggerLogReader().start()# 优先连日志服务连不上再开串口ifbuild:t0datetime.now()summary[steps][build]make_build()# 返回 ok/errors/secondsifnotsummary[steps][build][ok]:returnsummary# 编译失败就不动设备marklogger.mark()ifflash:portfind_jl_port()enter_upgrade(port)ok,outdownload(TOOLS_DIR)bootedwait_for_port(JL_VID,timeout30)# 新固件起来后 CDC 口会重新出现summary[steps][flash]{download_ok:ok,booted:booted}time.sleep(log_seconds)summary[steps][log]analyze_log(logger.since(mark),build_startt0)summary[ok]all_steps_ok(summary)json.dump(summary,open(out_dir/summary.json,w),ensure_asciiFalse,indent2)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. 注意事项配置工具协议没有任何鉴权插上 USB 的任何人都能让设备进升级模式还能在线读写 flash。开发阶段很方便量产固件是否保留TCFG_CFG_TOOL_ENABLE要评估。不要在设备写 flash 时发升级命令设备会立即复位可能打断正在进行的擦写。进入升级模式后如果不想烧录重新上电或者运行一次reboot.bat。日志服务开着时其他串口工具不能再打开同一个 COM 口。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_MODEapps/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在这里加-rebootcpu/br27/tools/reboot.bat单独复位设备