找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

5979

积分

0

好友

758

主题
发表于 5 天前 | 查看: 10| 回复: 0

没有开发板,也能跑通 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,其它环节尽量保持不变。

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 配好目标参数直接编:

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 的硬盘。

实现上其实很轻。触发方式是一条断点指令,寄存器里放操作码和参数:

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 最开头,先把 CPACRCP10/CP11 打开:

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 括起来,符号才能干净地解析:

target_link_libraries(tss_app   PRIVATE
  -Wl,--start-group  libtss.a  m  c  -Wl,--end-group)

驱动本身反而是最简单的一环。TSS 的调用套路很固定:拿到任务句柄,读出模型属性(每帧多大、判定阈值多少),初始化,然后进循环——读一帧、推理一帧、打印一帧。

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.jsonbenchmark 的混淆矩阵第一行 [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 换成自己的数据跑一遍。遇到卡住的地方,多半逃不出上面那张坑表;要是碰到新的坑,也欢迎到云栈社区一起交流。




上一篇:GUI Guider+Zephyr嵌入式实战:从UI设计到开发板运行全流程
下一篇:从 Figma 到 GUI Guider:嵌入式 GUI 设计稿转 LVGL 工程实践
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-9-11 23:51 , Processed in 0.390784 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表