不会AI的嵌入式工程师,手把手教你系统跑通第一个STM32 AI模型(多图预警,建议收藏备用)
这里是《贺老师讲嵌入式AI》,我《嵌入式AI:让单片机学会思考》课程主理人,专注AI在MCU上的落地实践。
本文手把手记录从新建 STM32CubeMX/STM32CubeIDE 工程,到导入 sinemodel.tflite,再到在 STM32F407 上运行 CubeAI 推理并通过串口显示正确正弦波的完整过程。
1. 实验目标
本实验最终要实现的效果是:STM32F407 上电后初始化 GPIO、USART1 和 X-CUBE-AI 模型,MCU 内部自动生成正弦模型输入 x,周期性运行模型推理,然后通过串口把理论正弦值、模型推理值和误差发送给上位机显示。

|
项目 |
配置 |
|---|---|
|
MCU |
STM32F407VET6 |
|
工具链 |
STM32CubeIDE |
|
STM32CubeMX |
6.16.1 |
|
STM32Cube FW_F4 |
V1.28.3 |
|
X-CUBE-AI |
10.2.0 |
|
模型文件 |
sinemodel.tflite |
|
CubeAI 应用模板 |
ApplicationTemplate |
|
USART1 |
PA9 / PA10,115200,8N1 |
|
LED |
PB12 / PB13,低电平点亮 |
|
上位机协议 |
帧头 |

2. 新建 STM32CubeIDE 工程
打开 STM32CubeIDE,选择 File -> New -> STM32 Project。在 MCU/MPU Selector 中搜索:
STM32F407VET6

选择对应芯片后新建工程,如下所示:
Project Name: STM32F407_TinyML
Targeted Language: C
Targeted Binary Type: Executable
Targeted Project Type: STM32Cube
Toolchain/IDE: STM32CubeIDE


容易踩坑:芯片型号要和板子一致
STM32F407 系列有不同封装和 Flash/RAM 规格。如果芯片型号选错,可能导致下载失败、链接脚本不匹配、引脚不存在,甚至工程能编译但烧录后运行异常。模型部署对 Flash 和 RAM 更敏感,所以第一步就要选对芯片。
3. 基础外设配置
3.1 配置调试接口
在 System Core -> SYS 中,把 Debug 设置为:
Serial Wire
这样会占用 PA13 和 PA14 作为 SWD 调试口。

3.2 配置时钟
本实验模型非常小,只有 321 MACC。为了先把部署链路跑通,可以使用默认 HSI 16 MHz,不必一开始就配置到最高主频。
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;

调试建议
先用简单时钟把模型部署、串口协议和上位机显示跑通。等链路确认无误后,再切换 HSE + PLL 提高主频。
3.3 配置 LED
本实验使用 PB12 和 PB13 作为 LED 输出:
PB12 -> LED1
PB13 -> LED2
由于开发板 LED 是低电平点亮,所以 GPIO 默认输出应该设置为 GPIO_PIN_SET,也就是上电默认熄灭。
#define LED1_GPIO_Port GPIOB
#define LED1_Pin GPIO_PIN_12
#define LED2_GPIO_Port GPIOB
#define LED2_Pin GPIO_PIN_13
#define LED_ON GPIO_PIN_RESET
#define LED_OFF GPIO_PIN_SET
#define LED_BLINK_HALF_PERIOD_MS 500U
容易踩坑:低电平点亮不是低电平熄灭
很多开发板 LED 一端接 VCC,MCU 引脚输出低电平时灌电流点亮。因此 GPIO_PIN_RESET 是点亮,GPIO_PIN_SET 是熄灭。

3.4 配置 USART1
USART1 用于给上位机发送波形数据:
PA9 -> USART1_TX
PA10 -> USART1_RX
Baud Rate: 115200
Word Length: 8 Bits
Parity: None
Stop Bits: 1
Hardware Flow Control: None

容易踩坑:文本调试和二进制协议不能混在一起
最终上位机要解析二进制帧。如果同一串口里还在输出 printf("hello\r\n"),就会破坏帧结构。工程中保留文本调试函数,但默认关闭:
#define UART_TEXT_DEBUG_ENABLED 0U
4. 添加 X-CUBE-AI 并导入模型
在 STM32CubeMX 的 Software Packs 中启用 X-CUBE-AI:
Software Packs -> Select Components -> X-CUBE-AI

启用后,在左侧会出现 X-CUBE-AI 相关配置页面。导入模型:
D:\model\sinemodel.tflite
模型名称保持为:
network

当前工程 .ioc 中对应的关键配置如下:
STMicroelectronics.X-CUBE-AI.10.2.0.ModelStructureFile-...=D\:\\model\\sinemodel.tflite
STMicroelectronics.X-CUBE-AI.10.2.0.ModelName-...=network
STMicroelectronics.X-CUBE-AI.10.2.0.ModelKind-...=TFLITE
STMicroelectronics.X-CUBE-AI.10.2.0.ModelCompression-...=None
5.模型分析
导入模型后,点击
![]()
开始分析

分析完成

6. Generate Code 后编译成功


7. 读取 CubeAI 生成报告,确认量化参数
模型导入后,X-CUBE-AI 会生成报告:
X-CUBE-AI\App\network_generate_report.txt
本模型报告中的关键信息:
input 1/1 : int8(1x1), QLinear(0.024573976,-128,int8)
output 1/1 : int8(1x1), QLinear(0.008472007,4,int8)
macc : 321
weights : 420 B
activations: 64 B
截图如下:

7.1 QLinear(scale, zero_point) 的含义
量化模型中,真实浮点值和 int8 值之间的关系是:
real_value = (quantized_value - zero_point) * scale
反过来,如果有一个浮点输入,要送进 int8 模型,就要做:
quantized_value = round(real_value / scale + zero_point)
本模型输入参数为:
scale = 0.024573976
zero_point = -128
因此输入量化代码是:
static int8_t sine_quantize_input(float value)
{
float qf = (value / SINE_AI_INPUT_SCALE) + (float)SINE_AI_INPUT_ZERO_POINT;
int32_t q = (int32_t)((qf >= 0.0f) ? (qf + 0.5f) : (qf - 0.5f));
if (q > 127)
{
q = 127;
}
else if (q < -128)
{
q = -128;
}
return (int8_t)q;
}
本模型输出参数为:
scale = 0.008472007
zero_point = 4
因此输出反量化代码是:
static float sine_dequantize_output(int8_t value)
{
return ((float)value - (float)SINE_AI_OUTPUT_ZERO_POINT) * SINE_AI_OUTPUT_SCALE;
}
容易踩坑:不要把 int8 模型当 float 模型使用
如果报告显示输入是 int8,就必须按量化参数写入 int8。输出也是 int8,不能直接当 float 解析,必须先反量化。
8. 理解 CubeAI 输入输出 buffer
报告中还有一句很重要:
(*) 'input'/'output' buffers are allocated in the activations buffer
network.h 中也能看到:
#define AI_NETWORK_INPUTS_IN_ACTIVATIONS (4)
#define AI_NETWORK_OUTPUTS_IN_ACTIVATIONS (4)
这意味着输入输出 buffer 由 CubeAI 放在 activations 内存池里。ApplicationTemplate 初始化后会拿到这些地址:
ai_input = ai_network_inputs_get(network, NULL);
ai_output = ai_network_outputs_get(network, NULL);
for (int idx=0; idx < AI_NETWORK_IN_NUM; idx++) {
data_ins[idx] = ai_input[idx].data;
}
for (int idx=0; idx < AI_NETWORK_OUT_NUM; idx++) {
data_outs[idx] = ai_output[idx].data;
}
之后采集函数只需要写入:
data[0][0] = (ai_i8)sine_quantize_input(sine_input_x);
推理完成后读取输出:
sine_model_y = sine_dequantize_output((int8_t)data[0][0]);
容易踩坑:不要自己随便换输入输出数组
如果 X-CUBE-AI 配置了 allocate-inputs 和 allocate-outputs,输入输出已经在 activations buffer 中。手动换成另一个数组,可能导致 ai_network_run() 使用的 buffer 和你写入的 buffer 不是同一个。
9. 编写周期推理逻辑
在 X-CUBE-AI/App/app_x-cube-ai.c 的 USER CODE 区域中定义输入变化参数:
#define SINE_AI_FRAME_PERIOD_MS 20U
#define SINE_AI_INPUT_STEP 0.05f
#define SINE_AI_TWO_PI 6.28318530717958647692f
static float sine_input_x = 0.0f;
每次推理后让 x 增加:
static void sine_advance_input(void)
{
sine_input_x += SINE_AI_INPUT_STEP;
if (sine_input_x >= SINE_AI_TWO_PI)
{
sine_input_x -= SINE_AI_TWO_PI;
}
}
当前配置下,每 20 ms 推理一次,每次 x 增加 0.05 rad,一圈约 126 个点,一个完整正弦周期大约 2.5 秒,适合上位机观察。
void MX_X_CUBE_AI_Process(void)
{
uint32_t now = HAL_GetTick();
if ((sine_ai_ready != 0U) && ((now - sine_last_frame_tick) >= SINE_AI_FRAME_PERIOD_MS))
{
sine_last_frame_tick = now;
if (acquire_and_process_data(data_ins) == 0)
{
if (ai_run() == 0)
{
post_process(data_outs);
}
}
}
}
调度建议
不要在主循环中用长时间 HAL_Delay() 阻塞。AI 推理、LED 和串口发送都可以用 HAL_GetTick() 做周期调度。
10. 设计上位机串口协议
上位机协议如下:
|
字段 |
含义 |
|---|---|
|
帧头 |
0x7E |
|
长度 |
0x04
表示 8 位, |
|
曲线1 |
理论正弦值 |
|
曲线2 |
模型推理值 |
|
曲线3 |
推理值 - 理论值 |
|
曲线4 |
绝对误差 |
|
帧尾 |
0x7F |
本实验最终选择 16 位模式:
长度 = 0x08
四路数据 = 每路 2 字节
整帧长度 = 1 + 1 + 2 * 4 + 1 = 11 字节
帧格式:
0x7E 0x08 C1_L C1_H C2_L C2_H C3_L C3_H C4_L C4_H 0x7F
采用小端序发送:
static void sine_protocol_put_i16(uint8_t *buffer, int16_t value)
{
uint16_t raw = (uint16_t)value;
buffer[0] = (uint8_t)(raw & 0xFFU);
buffer[1] = (uint8_t)((raw >> 8) & 0xFFU);
}
最终发送函数:
static void sine_protocol_send_frame(int16_t curve1, int16_t curve2, int16_t curve3, uint16_t curve4)
{
uint8_t frame[11];
frame[0] = SINE_PROTOCOL_HEADER;
frame[1] = SINE_PROTOCOL_UINT16_LEN;
sine_protocol_put_i16(&frame[2], curve1);
sine_protocol_put_i16(&frame[4], curve2);
sine_protocol_put_i16(&frame[6], curve3);
sine_protocol_put_u16(&frame[8], curve4);
frame[10] = SINE_PROTOCOL_TAIL;
HAL_UART_Transmit(&huart1, frame, sizeof(frame), 100U);
}

11. Release 编译并下载
因为实际下载使用的是 Release,所以要确认 Release 能编译通过,而不是只看 Debug。
Project Explorer -> 右键工程
Build Configurations -> Set Active -> Release
Project -> Build Project
本工程最终 Release 编译成功,大小大致如下:
text data bss dec hex
23400 1692 4776 29868 74ac


12. 上位机显示设置
上位机设置:
串口:对应 USB 转串口端口
波特率:115200
数据位:8
校验位:None
停止位:1
精度:16 位
帧头:0x7E
帧尾:0x7F
|
曲线 |
含义 |
判断标准 |
|---|---|---|
|
曲线1 |
理论正弦值 |
应为平滑正弦波 |
|
曲线2 |
模型推理值 |
应与曲线1基本重合,但允许模型近似误差 |
|
曲线3 |
推理误差 |
应围绕 0 上下变化 |
|
曲线4 |
绝对误差 |
应始终为正值 |

欢迎有不同意见的“老板们”和同学们,在评论区讨论、交流!
更多推荐


所有评论(0)