没有开发板,也能跑通Cortex-M的AI库~
用QEMU搭一套模拟环境,把TSS导出的异常检测模型真正跑起来~
先来介绍小编这次的情况:
手里只有TSS(Time Series Studio,是NXP推出的针对时间序列数据进行模型自动化训练的一个专用工具,可以根据用户传入的数据集,自动搜索数据处理方法以及适配的模型)导出的一个算法库压缩包,里面是一个预编译好的静态库libtss.a、一份头文件TimeSeries.h,外加一个metadata.json;另外还有一份采集好的数据okfan.csv。
任务很直接:验证这个模型能不能正常推理。
问题也很直接——手边没有对应的开发板。库是给Cortex-M33编的,可我们不想因为“等一块板子”就把事情卡在这里。那能不能干脆不用硬件,先在PC上把它跑通?
答案当然是可以的。我们用QEMU把一颗Cortex-M33“虚拟”出来,用半主机(semihosting)让固件直接读到PC上的csv,再把每一帧的推理结果打回PC终端。下面就一步步把这套环境搭出来。
整理思路:把板子换成QEMU
在动手之前,先把整条链路理清楚。我们要做的事,本质上就是把“真实开发板”这一环,换成一颗跑在QEMU里的虚拟Cortex-M33,其它环节尽量保持不变。

图:整条链路——PC出数据,QEMU里的裸机固件做推理,结果再打回PC终端
这里有两个角色。一个是跑在QEMU里的裸机固件tss_app.elf,它负责调用libtss.a做推理;另一个是宿主机(也就是我们的PC),数据文件放在这里,最后的打印也回到这里。两者之间靠半主机这条“暗线”通信——待会儿会专门讲它。
先从库里“问”出编译参数
很多人上来就想着写代码、链接库,结果编出来的固件一跑就跑飞。原因往往是同一个:编译固件用的ABI和这个预编译库对不上。
库是别人编好的二进制,它用的是哪颗核、有没有FPU、浮点用什么调用约定、枚举多大——这些都已经“焊死”在libtss.a里了。我们的固件必须完全对齐,否则在库的边界上,结构体和枚举的排布会悄悄错位,轻则结果是垃圾,重则直接崩。
好在这些信息不用猜,库文件自己就带着。用readelf把它的编译属性读出来即可:
| 读库的编译属性 $ arm-none-eabi-readelf -A libtss.a Tag_CPU_arch: v8-M.mainline Tag_CPU_name: "8-M.MAIN" Tag_FP_arch: FPv5/FP-D16 for ARMv8 Tag_ABI_VFP_args: VFP registers Tag_ABI_enum_size: small |
这几行信息量很大。翻译过来就是:这是一颗Cortex-M33(v8-M 主线),带FPv5-SP-D16单精度FPU,浮点参数走VFP寄存器(也就是-mfloat-abi=hard),枚举是小尺寸(对应-fshort-enums)。
再顺手看一眼库还依赖谁:
| 库对外部符号的依赖 $ arm-none-eabi-nm libtss.a | grep ' U ' U expf U logf U sqrtf U fabsf U floorf U memset ... |
清一色是expf/logf/sqrtf这类数学函数——这说明链接时必须把libm带上,否则一堆符号找不到。到这一步,编译和链接要用的参数就全定下来了:
| 锁定的目标参数(全部来自库本身,不是拍脑袋) -mcpu=cortex-m33 -mfpu=fpv5-sp-d16 -mfloat-abi=hard -fshort-enums;QEMU 机器选 mps2-an505(内置 Cortex-M33),链接时补上 libm。 |
先跑通:搭一个“不含库”的最小固件
拿到参数,别急着把库塞进去。裸机环境本身就有不少坑:核选错、FPU没开、半主机读文件路径不对……这些问题要是和库混在一起,排查起来会非常痛苦。
所以我们先做一个不依赖libtss.a的“冒烟测试”固件,只验证三件事:终端能不能打印、浮点运算对不对、能不能从PC上读到文件。用CMake配好目标参数直接编:
| 编译并在QEMU里跑冒烟测试 cmake -S . -B build -G Ninja -DTSS_SMOKE_TEST=ON -DTSS_MCPU=cortex-m33 -DTSS_MFPU=fpv5-sp-d16 -DTSS_MFLOAT_ABI=hard cmake --build build qemu-system-arm -machine mps2-an505 -cpu cortex-m33 -kernel build/tss_app.elf -semihosting-config enable=on,target=native -nographic -serial none -monitor none |
跑完终端里是这样的:
| 冒烟测试通过 ==== bring-up smoke test ==== [1] console output via semihosting: OK [2] float math (3.5*2 1) = 8 (expect 8): OK [3] reading host file: tss_lib/okfan.csv first bytes: 134 123 134 125 135 126 ... file read: OK ==== smoke test PASSED ==== |
看到第[2]行输出8特别关键——它说明FPU已经正确开启。如果浮点没配好,程序往往会卡死在第一条浮点指令上,根本走不到这一行。既然三项全绿,说明工具链、核、半主机这一整套底座都是可靠的,接下来再上库就踏实多了。
半主机-让固件“隔空”读到PC上的csv
这里得专门说说半主机,因为它是整套方案能成立的关键。
裸机固件本身没有文件系统,可我们的数据okfan.csv明明在PC上。半主机的作用,就是让固件通过一条特殊指令把I/O请求“托管”给QEMU,由QEMU在宿主机上帮它开文件、读字节。对固件来说,就像凭空多了一双手,能直接摸到PC的硬盘。
实现上其实很轻。触发方式是一条断点指令,寄存器里放操作码和参数:
| 半主机调用的核心:一条 bkpt 0xAB static inline uint32_t semihosting_call(uint32_t op, void *arg) { register uint32_t r0 __asm__("r0") = op; register void *r1 __asm__("r1") = arg; __asm__ volatile("bkpt 0xAB" : " r"(r0) : "r"(r1) : "memory"); return r0; /* SYS_OPEN=0x01 读文件, SYS_READ=0x06 ... */ } |
QEMU命令行里的-semihosting-config enable=on就是在告诉它“这条断点你别当异常,帮我把I/O接过去”。有了它,固件里一个几十行的流式读取器就能把csv 一帧一帧地喂进来——每行1000个浮点数,正好是模型要的一帧(data_len 500×data_dim 2)。
| 一个容易踩的小坑:路径是相对QEMU的工作目录 半主机开文件时,路径是相对于启动 QEMU时所在的目录来解析的,不是相对固件。所以我们统一先切到工程根目录再启动QEMU,代码里就能安心写成"tss_lib/okfan.csv"。 |
开FPU、链接库,写推理驱动
底座稳了,现在把库接进来。有两件事必须做对,缺一个都白干。
第一件,上电先开FPU。既然我们用-mfloat-abi=hard,程序里的浮点会直接编成硬件浮点指令;但Cortex-M33复位后FPU默认是关的,第一条浮点指令就会触发HardFault。所以在Reset_Handler最开头,先把CPACR里CP10/CP11打开:
| startup.c:复位后第一件事就是开 FPU void Reset_Handler(void) { /* 打开 CP10 & CP11,让 FPU 可用,必须在任何浮点代码之前 */ volatile uint32_t *cpacr = (volatile uint32_t *)0xE000ED88u; *cpacr |= (0xFu << 20); __asm__ volatile("dsb"); __asm__ volatile("isb"); /* ... 再拷 .data、清 .bss,然后进 main ... */ } |
第二件,把libtss.a和libm放进同一个链接组。它们之间有相互引用,用--start-group/--end-group括起来,符号才能干净地解析:
| CMake:库要放进链接组 target_link_libraries(tss_app PRIVATE -Wl,--start-grouplibtss.a mc-Wl,--end-group) |
驱动本身反而是最简单的一环。TSS的调用套路很固定:拿到任务句柄,读出模型属性(每帧多大、判定阈值多少),初始化,然后进循环——读一帧、推理一帧、打印一帧。
| main.c:异常检测的推理主循环 p_ops = tss_get_task_ops(); /* 拿到任务句柄 */ attr= p_ops->algo_attribute(); /* data_len/dim, threshold=0.9 */ p_ops->ad_ops.init(); while (csv_read_frame(&rdr, frame, 1000) == 1000) { p_ops->ad_ops.predict(frame, &prob); /* prob < 0.9 判为 Anomaly,否则 Normal */ } |
这里判定逻辑很朴素:predict()给出这一帧“正常”的概率,低于模型推荐阈值0.9就判为异常。阈值不是我们定的,是从metadata.json里读出来的。
跑起来:251帧的结果对不对?
到这一步,正式的推理固件就可以编译、下到QEMU里跑了。数据okfan.csv一共251行,也就是251帧。终端会逐帧打印概率和判定:
| 真机(模拟器)运行结果 ==== TSS Anomaly Detection on Cortex-M33 (QEMU) ==== frame size : 1000 floats threshold : 0.9000 frame |probability | verdict ------ -------------- --------- 0 | 0.932860| Normal 1 | 0.940757| Normal 9 | 0.861199| ANOMALY ... ==== Summary ==== frames processed : 251 Normal : 201 Anomaly : 50 |
重点看最后的汇总:251帧里判出201帧Normal、50帧Anomaly。这个数字不是随便看看就过——它正好和metadata.json里benchmark的混淆矩阵第一行[201, 50]完全对上。
这说明什么?说明我们这套QEMU环境跑出来的结果,和模型出厂时在标准流程里跑出来的结果是一致的。库没接错、参数没配错、推理逻辑也没写错——整条链路是可信的。
| 为什么这条对照很重要 模拟器最怕“跑是跑了,但结果是错的还不自知”。有了metadata里的基准数字做锚点,我们才敢说这次验证是真的通过,而不是碰巧打印出了一串看着像样的数。 |
顺手记下几个最容易踩的坑
整套流程走下来,真正卡人的其实就那么几处。列在这里,方便大家对照排查:
| 症状 | 十有八九是这个原因 |
| 跑到第一次打印浮点前就卡死 / HardFault | FPU 没开:Reset_Handler 里漏了写 CPACR。 |
| 结果是一堆乱码或数值明显不对 | ABI 不匹配:-fshort-enums、-mfloat-abi 和库对不上。 |
| 提示打不开 okfan.csv | 路径是相对QEMU工作目录的,启动前先切到工程根目录。 |
写在最后
回到开头那个问题:没有开发板,能不能验证一个Cortex-M的AI库?现在答案很清楚——能,而且验证结果可以和基准对得上。
这套QEMU 半主机的思路,最舒服的地方在于:数据在PC上、结果打回PC上,改一改数据文件就能反复跑,完全不用等硬件、不用接线。当然它替代不了最终的真机测试——时序、外设、功耗这些还得回到板子上看——但在“先把库和模型验证通”这个阶段,它足够快也足够可信。
大家如果手里也有TSS导出的库,不妨把okfan.csv换成自己的数据跑一遍。遇到卡住的地方,多半逃不出上面那张坑表;要是碰到新的坑,也欢迎一起交流。
关注
61文章
1434浏览量
200612免责声明:本文为转载,非本网原创内容,不代表本网观点。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
如有疑问请发送邮件至:bangqikeconnect@gmail.com