单片机为什么能够运行神经网络?从 C 函数理解 AI推理
这里是《贺老师讲嵌入式AI》,我是《嵌入式AI:让单片机学会思考》课程主理人,专注AI在MCU上的落地实践。
很多人第一次听到“单片机跑神经网络”,脑子里会自动浮现两个疑问:单片机没有操作系统、没有显卡、没有 Python,它凭什么能跑 AI?答案并不神秘:部署到 MCU 上的神经网络,本质上就是一段固定的数值计算流程。
一、先把神经网络当成一个 C 函数
在单片机工程里,我们每天都在写函数。读取 ADC 是函数,计算温度是函数,判断按键状态也是函数。神经网络推理也可以先按这个方式理解:给它一段输入数组,它按照固定顺序计算,最后返回一段输出数组。
比如一个姿态识别模型,输入可能是 1 秒钟的三轴加速度数据;一个故障检测模型,输入可能是 256 个采样点的振动波形;一个关键词识别模型,输入可能是一小段音频特征。模型并不会在单片机上重新学习,它只会把这段输入按训练后确定下来的参数算一遍。
推理阶段最关键的一句话:权重和偏置已经确定,MCU 只负责把输入代入固定计算流程,得到输出结果。
如果用 C 语言写一个极简接口,它可能长这样:
void model_run(const int8_t *input, int8_t *output) { /* 真实工程里,这里通常由 TFLite Micro、STM32Cube.AI、 CMSIS-NN 或厂商推理库生成/调用。 */ dense_layer_0(input, hidden_buffer); relu(hidden_buffer); dense_layer_1(hidden_buffer, output); }这段代码没有“思考”的过程,只有确定的输入、确定的参数和确定的计算顺序。它和普通业务函数的差别,不在于执行方式突然变神秘,而在于函数内部的参数不是程序员手写出来的规则,而是训练阶段从数据中拟合出来的数值。
二、权重和偏置是什么?先别想复杂,就是常量数组
神经网络里经常出现两个词:权重和偏置。它们听起来像算法术语,落到固件里其实就是一批数字。权重决定输入特征之间怎么组合,偏置用于调整计算结果的基准位置。训练结束后,这些数字会被导出成模型文件,再被转换工具变成 MCU 能链接进去的数据。
最小的例子可以从一个全连接层看起。假设输入有 3 个数,输出有 2 个数,那么每个输出都要和 3 个输入做乘加,再加上一个偏置:
static const int8_t W[2][3] = { { 12, -7, 4 }, { -3, 9, 11 } }; static const int32_t B[2] = { 128, -64 }; void dense_3_to_2(const int8_t x[3], int32_t y[2]) { for (int out = 0; out < 2; out++) { int32_t acc = B[out]; for (int in = 0; in < 3; in++) { acc += (int32_t)x[in] * (int32_t)W[out][in]; } y[out] = acc; } }这就是很多神经网络算子的核心影子:数组读取、乘法、加法、循环、结果写回。卷积层比这个例子多了窗口滑动和通道循环,深度可分离卷积多了不同的计算组织方式,激活函数会把结果限制到某个范围,但底层仍然离不开“按固定顺序读数组并做数值计算”。
所以单片机能够运行神经网络,并不是因为它具备了某种抽象智能,而是因为神经网络推理可以被编译成 MCU 能执行的指令:读 Flash 里的权重,读 RAM 里的输入和中间结果,用 CPU/DSP/NPU 做乘加,最后把输出写回 RAM。
三、输入输出不是随便塞进去的数组,而是有格式约束的接口
普通 C 函数只要参数类型对得上,很多时候就能运行。模型推理函数也有输入输出接口,但它对数据格式更敏感。输入长度、排列顺序、量化比例、预处理方式必须和训练时保持一致,否则模型会正常运行,却输出不可信的结果。
输入长度
训练时用 128 个采样点,板端就不能临时改成 100 个点。长度变了,后面的权重矩阵维度就对不上。
数据顺序
训练时按 ax、ay、az 排列,板端不能变成 az、ay、ax。顺序错了,模型会把同一组物理信号理解成另一种特征。
数值范围
训练时做过归一化或量化,板端也要执行同样的缩放。尤其是 int8 模型,零点和缩放系数不能随意处理。
输出含义
输出不是天然等于动作命令。分类分数、异常分数、回归值需要经过阈值、连续帧确认或状态机再进入业务逻辑。
这也是很多 MCU AI 项目失败的位置:模型函数本身可以跑,串口也能打印结果,但输入数据和训练数据不是同一种格式。由于模型只会执行固定计算,它没有办法自动识别“这次输入的单位变了”“这次窗口没对齐”“这次零点没处理”。这些问题必须由固件的数据链路负责。
一个更接近工程现场的推理入口,通常不是简单传入一段数组,而是包含采样、窗口、预处理、推理和后处理:
bool run_vibration_model(float adc_voltage) { ring_buffer_push(adc_voltage); if (!window_ready()) { return false; } build_window_feature(input_tensor); quantize_to_int8(input_tensor, model_input); model_run(model_input, model_output); float score = dequantize_score(model_output[FAULT_CLASS]); return state_machine_update(score); }这段代码里,真正的模型推理只是中间一环。前面要保证采样窗口稳定,后面要保证输出结果不会直接触发危险动作。单片机能跑神经网络,和单片机能把神经网络用好,是两个层级的问题。
四、模型和普通函数的区别,不是能不能运行,而是参数从哪里来
普通函数通常是工程师根据经验写出来的。比如温度超过 80℃ 报警,电流超过 2A 限流,振动 RMS 超过阈值提示异常。这类函数的规则清晰、可解释、可手工调整,适合边界明确的问题。
模型函数的计算流程同样固定,但内部参数来自训练数据。它不要求工程师把每条规则都写出来,而是用大量样本把“哪些组合更像某一类状态”压缩进权重和偏置里。对于多传感器、多特征、边界模糊、阈值难以稳定覆盖的问题,这种方式更容易表达复杂模式。
普通函数更像“人工写规则”
程序员明确写出判断条件,例如 if (rms > threshold)。好处是行为清楚,坏处是复杂场景下规则会越来越多,并且很难覆盖边界状态。
模型函数更像“训练得到参数”
程序员确定输入格式和运行链路,训练过程确定权重和偏置。部署后,MCU 仍然执行固定代码,只是这段代码内部包含大量由数据生成的常量参数。
用一句工程化的话总结:普通函数的核心资产是代码逻辑,模型函数的核心资产是参数。代码逻辑决定它怎么计算,模型参数决定它对输入模式的判断倾向。部署到单片机后,这两者都必须变成可编译、可链接、可运行、可测试的固件内容。
五、为什么 RAM、Flash 和算子支持会决定模型能不能跑
既然神经网络推理可以看成 C 函数,接下来就能解释 MCU 部署最常见的三个限制。
Flash 决定模型参数能不能放下。权重和偏置通常以常量数组形式存在,模型越大,参数越多,占用的 Flash 或外部 Flash 就越多。INT8 量化后,参数体积通常会明显下降,这也是 MCU 部署经常优先使用量化模型的原因。
RAM 决定中间结果能不能周转。输入张量、输出张量、中间激活值、临时缓冲区都要占 RAM。很多模型看起来权重不大,但运行时中间激活很大,最终卡在 RAM,而不是卡在 Flash。
算子支持决定固定流程能不能生成有效代码。如果模型里用了部署框架不支持的层,转换工具就无法把它稳定变成 MCU 端可执行流程。工程上经常要反过来设计模型结构,优先使用 MCU 推理库支持成熟、优化充分的算子。
这三个限制对应到 C 函数的角度非常直观:常量表放哪里,临时数组放哪里,函数里的每一种计算有没有可用实现。只要这三件事成立,神经网络就不再是“云端算法”,而是一段可以被 MCU 调度的本地计算任务。
单片机能运行神经网络,并不是因为单片机突然拥有了“智能”。它能运行,是因为推理阶段已经被压缩成确定的输入、确定的参数、确定的计算流程和确定的输出接口。
最后落到工程:不要先问模型高级不高级,先问它能不能变成一个可靠函数
做 MCU AI 项目时,最实用的判断方式不是先讨论网络名字,而是把它拆回固件问题:输入数组怎么来,权重和偏置放在哪里,中间缓冲够不够,推理函数多久跑完,输出结果怎么进入状态机。
如果这些问题都能回答清楚,单片机运行神经网络就不是一句口号,而是一个可以验证的工程链路。如果这些问题回答不清楚,即使模型在电脑上准确率很高,到了板端也可能只是一个能编译、能打印、但无法稳定用于产品判断的 demo。