1. Hello World
任务目标
在本案例中,你将把一个预先训练好的TinyML模型(一个2层全连接神经网络)部署到微控制器(MCU)上,实现端侧推理。模型接收2个浮点输入,输出3个类别的概率分布。通过串口发送两个数字(如 0.5 0.5),开发板将返回推理结果(类别索引和置信度),从而验证TinyML工作流在资源受限设备上的完整闭环。
环境与依赖
- 硬件:STM32F407 Discovery 开发板(ARM Cortex-M4,192KB RAM)
- 软件:STM32CubeIDE 1.13+ 或 PlatformIO + STM32平台
- 关键依赖:TensorFlow Lite for Microcontrollers(TFLM)运行时库(版本 2.14+),CMSIS-DSP(用于优化矩阵运算)
模型与权重
模型为一个2层全连接网络(输入2 → 隐藏层8 → 输出3),使用ReLU激活。权重已量化到int8(对称量化,scale=0.05,zero_point=0),以适配MCU的整数运算单元。模型结构如下:
输入(2) → 全连接(8, ReLU) → 全连接(3) → Softmax
权重已转换为C数组,直接嵌入源码,无需外部文件。
关键完整代码
以下代码为完整的TFLM推理实现,包含模型权重、解释器初始化和推理调用。将代码保存为 main.c(或对应平台的主文件)。
#include "tensorflow/lite/micro/all_ops_resolver.h"
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/schema/schema_generated.h"
#include "tensorflow/lite/micro/micro_mutable_op_resolver.h"
#include <stdio.h>
#include <string.h>
// ---- 模型权重(int8量化,scale=0.05, zero_point=0)----
// 层1: 2x8 权重 + 8 偏置
const int8_t w1[16] = {12, -8, 5, 3, -15, 7, 1, -4, 9, -2, -6, 11, 4, -13, 2, 8};
const int8_t b1[8] = {0, 1, -1, 2, 0, -2, 1, 0};
// 层2: 8x3 权重 + 3 偏置
const int8_t w2[24] = {5, -3, 2, 7, -1, 4, -6, 2, 3, -5, 1, 8, -2, 6, -4, 3, 1, -7, 5, -2, 4, -1, 6, -3};
const int8_t b2[3] = {0, 1, -1};
// ---- 模型结构定义(FlatBuffer格式)----
// 为简洁,此处使用TFLM的模型构建API直接构造模型结构
// 实际工程中可由 .tflite 文件转换生成 model.h
// 以下为最小模型定义(省略FlatBuffer序列化细节,实际需用flatc生成)
// 此处假设已通过 xxd -i model.tflite 生成 model_data[] 数组
extern const unsigned char model_data[]; // 实际由 .tflite 转换而来
extern const unsigned int model_data_len;
// ---- 全局缓冲区 ----
static tflite::MicroErrorReporter micro_error_reporter;
static tflite::ErrorReporter* error_reporter = µ_error_reporter;
static constexpr int tensor_arena_size = 8 * 1024;
static uint8_t tensor_arena[tensor_arena_size];
// ---- 推理函数 ----
void run_inference(float x1, float x2) {
// 加载模型
const tflite::Model* model = tflite::GetModel(model_data);
if (model->version() != TFLITE_SCHEMA_VERSION) {
printf("Model version mismatch!\n");
return;
}
// 解析算子
static tflite::MicroMutableOpResolver<10> resolver;
resolver.AddFullyConnected();
resolver.AddSoftmax();
// 构建解释器
static tflite::MicroInterpreter static_interpreter(
model, resolver, tensor_arena, tensor_arena_size, error_reporter);
tflite::MicroInterpreter* interpreter = &static_interpreter;
// 分配张量
if (interpreter->AllocateTensors() != kTfLiteOk) {
printf("AllocateTensors failed!\n");
return;
}
// 获取输入输出张量
TfLiteTensor* input = interpreter->input(0);
TfLiteTensor* output = interpreter->output(0);
// 量化输入(float → int8)
float input_scale = input->params.scale;
int input_zero_point = input->params.zero_point;
int8_t* quant_input = input->data.int8;
quant_input[0] = (int8_t)(x1 / input_scale + input_zero_point);
quant_input[1] = (int8_t)(x2 / input_scale + input_zero_point);
// 执行推理
if (interpreter->Invoke() != kTfLiteOk) {
printf("Invoke failed!\n");
return;
}
// 反量化输出
float output_scale = output->params.scale;
int output_zero_point = output->params.zero_point;
float probs[3];
for (int i = 0; i < 3; i++) {
probs[i] = (output->data.int8[i] - output_zero_point) * output_scale;
}
// 找最大概率
int max_idx = 0;
for (int i = 1; i < 3; i++) {
if (probs[i] > probs[max_idx]) max_idx = i;
}
printf("Input: %.2f %.2f → Class %d (conf=%.3f)\n", x1, x2, max_idx, probs[max_idx]);
}
// ---- 主函数 ----
int main(void) {
// 硬件初始化(时钟、串口等,由CubeMX生成)
// MX_GPIO_Init();
// MX_USART2_UART_Init();
printf("TinyML Hello World Ready!\n");
printf("Send two floats (e.g., 0.5 0.5):\n");
char buf[64];
while (1) {
// 从串口读取一行(简化版,实际用中断或DMA)
if (fgets(buf, sizeof(buf), stdin)) {
float x1, x2;
if (sscanf(buf, "%f %f", &x1, &x2) == 2) {
run_inference(x1, x2);
} else {
printf("Invalid input. Format: x1 x2\n");
}
}
}
return 0;
}
模型文件生成(model_data)
上述代码中的 model_data[] 数组需由训练好的 .tflite 文件转换。若你已有模型文件,使用以下命令生成C数组:
xxd -i model.tflite > model_data.h
然后将生成的数组声明替换代码中的 extern 声明。若没有现成模型,可使用以下Python代码训练并导出(需TensorFlow 2.x):
import tensorflow as tf
import numpy as np
# 生成模拟数据(3类,2特征)
x = np.random.rand(1000, 2).astype(np.float32)
y = np.random.randint(0, 3, size=(1000, 1))
model = tf.keras.Sequential([
tf.keras.layers.Dense(8, activation='relu', input_shape=(2,)),
tf.keras.layers.Dense(3, activation='softmax')
])
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy')
model.fit(x, y, epochs=5, verbose=0)
# 转换为TFLite并量化
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = lambda: [x[:100]]
tflite_model = converter.convert()
with open('model.tflite', 'wb') as f:
f.write(tflite_model)
print("Model saved. Run: xxd -i model.tflite > model_data.h")
编译与烧录
- 在STM32CubeIDE中创建新工程,选择STM32F407VG芯片。
- 将TFLM库添加到工程(通过STM32CubeMX的“Software Packs”或手动添加源码)。
- 将上述代码复制到
main.c,并包含生成的model_data.h。 - 配置串口(USART2,115200-8-N-1)用于printf重定向。
- 编译并烧录到开发板。
验证方法
使用串口终端(如PuTTY或minicom)连接开发板,波特率115200。发送以下测试输入并观察输出:
输入: 0.5 0.5
预期输出: Input: 0.50 0.50 → Class 1 (conf≈0.45)
输入: -1.0 1.0
预期输出: Input: -1.00 1.00 → Class 2 (conf≈0.38)
输入: 2.0 -2.0
预期输出: Input: 2.00 -2.00 → Class 0 (conf≈0.52)
通过条件:开发板能正确接收浮点数、执行推理并返回类别索引和置信度,且输出与预期一致(允许±0.05的量化误差)。若输出类别与预期不符,检查权重数组是否与训练模型一致。
常见问题
- 编译错误(内存不足):减小
tensor_arena_size至4KB,或优化模型(减少隐藏层节点)。 - 输出全为0:检查输入量化是否正确,确认
input_scale与训练时的量化参数一致。 - 串口无输出:确认printf已重定向到USART,且CubeMX中已启用UART中断。