▼點擊下方名片,關注公眾號,獲取更多精彩內容▼
這里是《賀老師講嵌入式AI》,我《嵌入式AI:讓單片機學會思考》課程主理人,專注AI在MCU上的落地實踐。
本文手把手記錄從新建 STM32CubeMX/STM32CubeIDE 工程,到導入 sinemodel.tflite,再到在 STM32F407 上運行 CubeAI 推理并通過串口顯示正確正弦波的完整過程。
本實驗最終要實現的效果是:STM32F407 上電后初始化 GPIO、USART1 和 X-CUBE-AI 模型,MCU 內部自動生成正弦模型輸入 x,周期性運行模型推理,然后通過串口把理論正弦值、模型推理值和誤差發送給上位機顯示。

sinemodel.tflite | |
ApplicationTemplate | |
0x7E,長度 0x08,幀尾 0x7F |

打開 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 更敏感,所以第一步就要選對芯片。
在 System Core -> SYS 中,把 Debug 設置為:
Serial Wire這樣會占用 PA13 和 PA14 作為 SWD 調試口。

本實驗模型非常小,只有 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 提高主頻。
本實驗使用 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 是熄滅。

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在 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導入模型后,點擊

開始分析

分析完成



模型導入后,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
量化模型中,真實浮點值和 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 解析,必須先反量化。
報告中還有一句很重要:
(*) 'input'/'output' buffers are allocated in the activations buffernetwork.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 不是同一個。
在 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() 做周期調度。
上位機協議如下:
0x7E | |
0x040x08 表示 16 位,0x10 表示 32 位或 float | |
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);
}
因為實際下載使用的是 Release,所以要確認 Release 能編譯通過,而不是只看 Debug。
Project Explorer -> 右鍵工程
Build Configurations -> Set Active -> Release
Project -> Build Project本工程最終 Release 編譯成功,大小大致如下:
text data bss dec hex
23400 1692 4776 29868 74ac

上位機設置:
串口:對應 USB 轉串口端口
波特率:115200
數據位:8
校驗位:None
停止位:1
精度:16 位
幀頭:0x7E
幀尾:0x7F