基于前期我们学习,实现了LED点亮、按键控制LED、USB串口设置、屏幕显示、摄像头显示、预处理,这期学习把训练模型部署到STM32开发板上,实现对手写数字的识别。
一、在pycharm中转化权重文件
python训练模型.pkl权重文件要转化为stm32cubemx-ai能够加载的文件,最常见的是.onnx文件,转化代码(也可以自己找转化代码)如下:
python
import os
import torch
#注意这里转化的过程中你生成的pkl文件的所在路径确保正确
#生成的.onnx文件存储的位置可以任意选择
PKL_PATH = r"E:\python\PythonProject\ch05\model\mnist_cnn.pkl"
ONNX_PATH = r"E:\python\PythonProject\ch05\mnist_cnn.onnx"
assert os.path.exists(PKL_PATH), f"文件不存在: {PKL_PATH}"
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# ---- 1. 加载整个模型 ----
model = torch.load(PKL_PATH, map_location=device)
print("加载类型:", type(model))
# 兼容被 DataParallel 包过的情况(这个 type 显示,保险起见留着)
if hasattr(model, "module"):
model = model.module
# ---- 2. 推理模式 + 上设备 ----
model.eval()
model.to(device)
# ---- 3. 输入形状:按上面推断结果改 ----
batch_size = 1
input_shape = (1, 28, 28) # ← 按实际改,MNIST 一般是 (1, 28, 28)
dummy = torch.randn(batch_size, *input_shape).to(device)
# ---- 4. 导出 ----
torch.onnx.export(
model, # 直接用 model,不要 .module
dummy,
ONNX_PATH,
opset_version=12,
do_constant_folding=True,
input_names=["input"],
output_names=["output"],
dynamic_axes={
"input": {0: "batch_size"},
"output": {0: "batch_size"},
},
)
print("导出完成:", ONNX_PATH)
以上在pycharm中的操作全部完成
二、STM32CubeMX中AI配置
1.打开摄像头显示、以及预处理的文件找到.ioc文件

1.找到stm32cubemx左边的Middleware and Software Packs(中间件及安装包)

2.下载X-CUBE-AI
找到X-CUBE-AI,若之前没有下载,先进行下载点击X-CUBE-AI的左边部分有下载箭头或者双击找到X-CUBE-AI没有下载的情况下有Install点击自动下载两种方法选一即可

3.加载X-CUBE-AI
双击进入在STMicroelectronics.X-CUBE-AI中勾选Core,Device Application勾选Application Template(应用模版)选择好点击ok
Application Template(应用模版):生成 app_x-cube-ai.c框架,演示怎么填输入跑推理读输出
SystemPerformance(系统性能) :提供测试的 形成测试的代码 主要测试模型执行的CPU的周期或者负载等 主要看速度是否够快
Validation(目标检测): 把测试集编进固件,在板子上跑推理并比对结果 看精度是否下降 主要是验证的

4.加载权重模型
点击+弹出network 选择ONNX,点击Browse找到在pycharm脚本中生成的.ONNX的文件,由于模型比较小,其他选项不需勾选,直接点击Analyze
各个选项代表的意义如下:
Compression:权重压缩方式,用来减小Flash占用。
Optimization:代码生成优化目标;Balanced时间/RAM 平衡、Time速度优先、RAM内存优先。
Complexity:模型计算量(MACC,乘加次数)
Used Flash:估算占用的Flash(权重+代码)
Used Ram:估算占用的RAM(激活张量+工作区)
Achieved compression:实际压缩率
Analysis status:分析状态
Validation metrics:验证结果(准确率、误差、混淆情况)
Analyze:分析模型
Validate on desktop:在PC上跑验证(快,不用烧板子)
Validate on target:烧到板子上跑验证(慢,但能验证真实量化结果)


5.进入分析页面,分析完成点击OK


6.分析成功后,显示各个相关的分析数据

7.保存文件以及生成文件

8、生成工程
如果我们的 CubeMX 工程放置配置路径中没有中文,生成代码后会弹出类似下图的提示窗口,点击 【Open Project】 就打开 MDK 工程(如果是中文路径则会报错)。

到此为止CubeMX任务就结束了。
二、STM32系统C程序设计
打开生成的C代码项目已经假设在你的计算机安装了MDK,如果MDK还没有安装请自行下载并安装。Keil Product Downloads
注意!不要下载最新版,有可能存在问题!
(一)用MDK打开工程
在MDK的keil编程界面中打开工程后的主界面如下图:

(二)AI部署在Keil中生成的文件
一共四个文件,四个文件分别是:
network.c :神经网络推理实现。包含各层计算、前向推理、AI 运行时 API(不要改动)。
network_data.c :模型权重和偏置数据。.onnx 里的权值被转成 C 数组放在这里,文件通常很大(不要改动)。
network_data_params.c :网络参数描述。包含张量尺寸、输入输出信息、激活内存、内存池配置等(不要改动)。
app_x-cube-ai.c :应用层代码。主要负责:启动 AI、喂输入、取输出、后处理(这个文件可以改)。

(三)配置模型调用预处理、AI推理程序
在app_x-cube-ai.c添加一下代码:
主要是连接"图像预处理"和"AI 推理"的全局声明与编译期检查代码。
c
#include "image_preprocess.h"
#include <math.h>
#if MODEL_INPUT_PIXELS != AI_NETWORK_IN_1_SIZE
#error Image and model input sizes must match
#endif
int32_t AI_Digit = -1;
ai_float AI_DigitScores[AI_NETWORK_OUT_1_SIZE];
添加位置如下:

X-CUBE-AI ApplicationTemplate 中"填输入"和"取结果"的两个环节的代码如下:
c
int acquire_and_process_data(ai_i8* data[])
{
ai_float *input = (ai_float *)data[0];
if (input == NULL)
return -1;
for (uint32_t i = 0; i < AI_NETWORK_IN_1_SIZE; ++i) {
ai_float pixel = (ai_float)Image_Binary28[i];
#if DIGIT_AI_INVERT_INPUT
pixel = 255.0f - pixel;
#endif
/* Match training ToTensor(): convert to float and scale to [0, 1]. */
input[i] = pixel / 255.0f;
}
return 0;
}
int post_process(ai_i8* data[])
{
const ai_float *output = (const ai_float *)data[0];
int32_t best = 0;
if (output == NULL)
return -1;
for (uint32_t i = 0; i < AI_NETWORK_OUT_1_SIZE; ++i) {
if (!isfinite(output[i]))
return -1;
AI_DigitScores[i] = output[i];
if (output[i] > output[best])
best = (int32_t)i;
}
/* Class indices represent digits only if training labels are ordered 0..9. */
AI_Digit = best;
return 0;
}
(四)改写main.c中的代码
1.在main.c的头文件加入预处理的文件:
c
#include "image_preprocess.h"
2.配置按键按下拍照的代码:
c
static GPIO_PinState key_last = GPIO_PIN_RESET;
static uint8_t screen_frozen = 0;
static uint8_t photo_pending = 0;
3.配置CRC 寄存器代码如下:
在 AI runtime 创建神经网络之前,先打开并配置硬件 CRC,本工程没有使用 HAL 的 CRC 驱动,所以这里直接操作 CRC 寄存器
c
static void MX_CRC_Init(void)
{
__HAL_RCC_CRC_CLK_ENABLE();
__HAL_RCC_CRC_FORCE_RESET();
__HAL_RCC_CRC_RELEASE_RESET();
CRC->CR = 0U;
CRC->INIT = 0xFFFFFFFFU;
CRC->POL = 0x04C11DB7U;
CRC->CR = CRC_CR_RESET;
}
4.在主函数添加显示函数:
主要配置 LCD中显示识别结果的位置
c
/* USER CODE BEGIN 2 */
MX_CRC_Init();
MX_X_CUBE_AI_Init();
LCD_Disp_Init();
LCD_ClaerBUF();
LCD_ShowString(88, 32, 72, 16, 16, (uint8_t *)"Digit: -");
DCMI_FrameIsReady = 0;
HAL_Delay(20); /* Allow the MCO camera clock to settle. */
if (Camera_Init_Device(&hi2c1, FRAMESIZE_56X56) != Camera_OK)
{
LCD_ShowString(0, 0, 160, 16, 16, (uint8_t *)"CAM INIT ERROR");
Error_Handler();
}
Camera_StartFrame();
/* USER CODE END 2 */
5.在while循环中添加以下代码:
主要是取帧,通过取得帧预处理后送给加载的权重文件进行识别,显示识别的结果
c
/* USER CODE BEGIN 3 */
GPIO_PinState key_now = HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13);
if ((key_last == GPIO_PIN_RESET) && (key_now == GPIO_PIN_SET))
{
if (screen_frozen)
{
screen_frozen = 0;
photo_pending = 0;
}
else
{
photo_pending = 1;
}
}
key_last = key_now;
if (!DCMI_FrameIsReady && (HAL_GetTick() - Camera_CaptureTick) > 2000U)
{
LCD_ShowString(0, 0, 160, 16, 16, (uint8_t *)"CAM FRAME TIMEOUT");
Error_Handler();
}
if (Camera_CaptureError)
{
LCD_ShowString(0, 0, 160, 16, 16, (uint8_t *)"DCMI DMA ERROR");
Error_Handler();
}
if (DCMI_FrameIsReady)
{
if (!screen_frozen)
{
memcpy(IMGBUF[1], IMGBUF[0], sizeof(IMGBUF[0]));
Image_PreprocessFrame(IMGBUF[1]);
MX_X_CUBE_AI_Process();
(void)CDC_SendCameraFrame(Image_Binary28);
LCD_DispImg(0);
{
/* Keep the result on the right half of the 160x80 LCD. */
uint8_t result[] = {'D', 'i', 'g', 'i', 't', ':', ' ', '-', 0};
if (AI_Digit >= 0 && AI_Digit <= 9)
result[7] = (uint8_t)('0' + AI_Digit);
LCD_ShowString(88, 32, 72, 16, 16, result);
}
if (photo_pending)
{
photo_pending = 0;
screen_frozen = 1;
}
}
/* LCD_DispImg is skipped while frozen, so clear the flag here too. */
DCMI_FrameIsReady = 0;
Camera_StartFrame();
}
}
/* USER CODE END 3 */
(五)在lcd.c中改写代码
摄像头整帧预览显示以及识别结果显示
c
static void show_camera_preview(void)
{
ST7735_FillRGBRect(&st7735_pObj, (80 - IMGBUFWIDTH) / 2,
(ST7735Ctx.Height - IMGBUFHEIGHT) / 2,
IMGBUF[1], IMGBUFWIDTH, IMGBUFHEIGHT);
}
void LCD_DispImg(uint8_t x_img)
{
(void)x_img;
if (DCMI_FrameIsReady)
{
show_camera_preview();
LCD_ShowString(125, 40, ST7735Ctx.Width, 16, 16, TXTBUF[2]);
LCD_ShowString(125, 58, ST7735Ctx.Width, 16, 16, TXTBUF[3]);
DCMI_FrameIsReady = 0;
}
}
三、编译下载
单击编译按钮等一段时间系统编译完成会有如上图的错误或者警告信息(都是0最好)。编译通过以后,通过项目设置窗口的Debug标签确认程序下载器是否正确连接并安装好驱动程序

如果下载器没问题单击download下载按钮,下载器一边闪烁一边把编好的程序下载到STM32的FLASH当中,按下复位键开发板就可以工作了
有问题欢迎留言,我再学新东西的时候和刚入门的小伙伴一起分享...