4、第一个推理程序:从官方例程入手,下载并编译ImageNet分类例程,在开发板上跑通第一个模型
好,到了真正动手的时候了。
前面几章我们聊了CMSIS-NN的理论、量化、算子。说实话,光看代码和公式,总觉得隔着一层纱。我自己的经验是,只有把程序烧进板子,看到LED闪烁或者串口打印出分类结果,那种「原来如此」的感觉才会真正出现。
这一章,我们就来干这件事——下载官方例程,编译,烧录,跑通第一个模型。
4.1 官方例程在哪里?
ARM官方在GitHub上维护了一个仓库叫 CMSIS_5,里面包含了CMSIS-Core、CMSIS-DSP、CMSIS-NN的全部源码和例程。我们需要的ImageNet分类例程就在 CMSIS_5/Device/Examples/ 目录下。
我个人习惯直接克隆整个仓库,虽然大一点,但后续查文档、看源码都方便。
git clone https://github.com/ARM-software/CMSIS_5.git
cd CMSIS_5/Device/Examples/
进去之后你会看到多个文件夹,我们重点关注 ImageNet 这个。里面包含了针对不同开发板的移植版本。
| 文件夹 | 目标开发板 | MCU型号 |
|---|---|---|
| ARMCM3 | ARM Cortex-M3 模拟器 | ARMCM3 |
| ARMCM4_FP | ARM Cortex-M4F 模拟器 | ARMCM4 |
| ARMCM7_SP | ARM Cortex-M7 模拟器 | ARMCM7 |
| STM32F746 | STM32F746G-Discovery | STM32F746 |
| STM32F769 | STM32F769I-Discovery | STM32F769 |
如果你手头是STM32F746 Discovery板,直接选 STM32F746 目录。其他板子可以参照类似结构移植,这个我们后面会讲。
4.2 例程结构解析
打开 STM32F746 目录,你会看到这样的文件结构:
STM32F746/
├── Inc/
│ ├── arm_nnexamples_cifar10.h
│ ├── arm_nnexamples_imagenet.h
│ └── ...
├── Src/
│ ├── arm_nnexamples_imagenet.c
│ ├── arm_nnexamples_cifar10.c
│ └── ...
├── RTE/
│ └── ...
├── MDK-ARM/
│ └── project.uvprojx
└── README.md
核心文件就两个:
- arm_nnexamples_imagenet.c:主程序,包含模型加载、输入预处理、推理、输出解析
- arm_nnexamples_imagenet.h:头文件,定义网络结构参数、权重数组声明
嗯,这里要注意。官方例程的权重是直接以C数组形式硬编码在头文件里的。这意味着你不需要额外加载模型文件,编译时就全部打包进固件了。对于初学者来说,这其实是个好事——少了一个出错的环节。
4.3 编译环境准备
官方例程支持三种工具链:
- MDK-ARM (Keil):最省事,直接打开 .uvprojx 项目文件
- IAR EWARM:需要导入项目
- GCC + Makefile:适合Linux用户或喜欢命令行的朋友
我个人推荐初学者用Keil。为什么?因为官方例程就是基于Keil做的,你双击打开就能编译,几乎不用改任何配置。我在项目中遇到过太多因为工具链版本不一致导致的奇怪问题,Keil在这方面兼容性最好。
4.4 编译步骤(以Keil为例)
好,我们一步步来。
- 打开项目:双击
MDK-ARM/project.uvprojx - 选择目标设备:在Project窗口右键 -> Options for Target -> Device,选择你的MCU型号。STM32F746 Discovery板选
STM32F746NGHx - 配置调试器:在Debug选项卡选择
ST-Link Debugger,点击Settings确认SWD连接正常 - 编译:按F7或点击Build按钮
第一次编译会有点慢,因为要编译CMSIS-NN库的全部源文件。我当年第一次编译等了将近两分钟,还以为是电脑卡死了。别急,喝口水等一等。
编译成功的话,你会看到类似这样的输出:
Build Output:
compiling arm_nnexamples_imagenet.c...
compiling arm_convolve_s8.c...
...
linking...
Program Size: Code=286432 RO-data=1245632 RW-data=4096 ZI-data=16384
".\Objects\imageNet.axf" - 0 Error(s), 0 Warning(s)
看到 0 Error(s) 就说明编译通过了。
#error "CMSIS-NN requires ARMCC or GCC",说明你的编译器版本不对。Keil默认用ARMCC,如果你装了AC6(ARM Compiler 6),需要在Options for Target -> Target中把编译器切回ARMCC。
4.5 烧录与运行
编译通过后,烧录就简单了。
- 用USB线连接开发板和电脑
- 在Keil中按F8或点击Download按钮
- 等待烧录完成,然后按开发板上的Reset键
程序运行后,串口会输出推理结果。你需要打开一个串口工具(比如PuTTY、SecureCRT,或者Keil自带的UART #1窗口),波特率设置为 115200。
你会看到类似这样的输出:
ImageNet classification example
Input image: cat.jpg
Top 5 predictions:
1: tabby cat (0.852)
2: tiger cat (0.091)
3: Egyptian cat (0.032)
4: Persian cat (0.011)
5: Siamese cat (0.005)
看到这个输出,恭喜你,第一个模型跑通了!
说实话,我第一次看到串口打印出分类结果时,还挺激动的。一个在PC上训练好的模型,经过量化、转换,最终在单片机里跑了起来,而且只用了不到一秒就完成了推理。这种感觉,你试试就知道了。
4.6 例程代码核心流程
我们来看看这个例程到底做了什么。打开 arm_nnexamples_imagenet.c,核心逻辑其实很短:
int main(void)
{
// 1. 初始化硬件(时钟、GPIO、UART)
SYSTEM_Init();
// 2. 加载输入图像(这里是从数组加载)
// 实际应用中可以从摄像头或Flash读取
q7_t *input_data = get_image_data();
// 3. 运行推理
arm_nnexamples_imagenet_run(input_data, output_data);
// 4. 解析输出,打印Top-5结果
print_top5(output_data);
while(1);
}
真正的推理函数 arm_nnexamples_imagenet_run 内部调用了CMSIS-NN的API:
void arm_nnexamples_imagenet_run(const q7_t *input, q7_t *output)
{
// 卷积层1
arm_convolve_HWC_q7_RGB(input, conv1_w, conv1_b, conv1_out,
32, 224, 224, 3, 3, 2, 2, 1, 1);
arm_relu_q7(conv1_out, 32 * 112 * 112);
arm_maxpool_q7_HWC(conv1_out, 32, 112, 112, 0, 2, 2, 2, pool1_out);
// ... 后续层类似
// 全连接层
arm_fully_connected_q7_opt(pool5_out, fc6_w, fc6_b, fc6_out,
512, 4096, 1);
arm_relu_q7(fc6_out, 4096);
// 输出层
arm_softmax_q7(fc8_out, 1000, output);
}
你看,每一层就是一个函数调用。参数分别是输入、权重、偏置、输出、维度信息。你不需要关心底层是怎么做乘加、怎么处理溢出的,CMSIS-NN全帮你搞定了。
4.7 常见问题与避坑
我在带学生做这个实验时,遇到过几个高频问题,列出来给你参考:
原因:多半是串口引脚配置不对。STM32F746 Discovery板的USB虚拟串口对应的是USART1,引脚是PA9(TX)和PA10(RX)。检查一下你的板子原理图,确认串口号和引脚匹配。
原因:权重数据对齐问题。CMSIS-NN要求权重数组按16字节对齐。检查你的链接脚本,确保 .rodata 段对齐到16字节。在Keil中,可以在Options for Target -> Linker -> Misc controls 中添加 --align=16。
原因:MCU时钟配置不对。默认HSI时钟只有16MHz,跑ImageNet这种大模型会慢到怀疑人生。检查SystemInit函数,确保配置了HSE+PLL,把主频跑到216MHz(STM32F746最高主频)。
我曾经有一次折腾了一下午,结果发现是时钟配置里PLL的M、N参数写反了,主频只有32MHz。你想想看,推理速度差了将近7倍,能不慢吗?
4.8 知识结构总览
这一章的内容,我用一张图帮你梳理一下:
4.9 小结
这一章我们做了几件事:
- 从GitHub下载了CMSIS_5官方仓库
- 找到了ImageNet分类例程,理解了文件结构
- 用Keil完成了编译和烧录
- 在开发板上跑通了第一个模型,看到了分类结果
- 分析了核心代码的推理流程
说白了,这一步就是「Hello World」。虽然模型是固定的、输入是写死的,但整个流程——从源码到二进制、从烧录到推理输出——你已经完整走了一遍。后面再换自己的模型、换自己的数据,无非就是改改权重数组和输入预处理,核心流程一模一样。
嗯,到这一步,你已经不是纸上谈兵了。你的开发板,现在是一个能识别图像的AI设备了。