大家好,我是賀老師,嵌入式 AI 工程師,《嵌入式AI:讓單片機學會思考》主理人,專注AI在MCU上的落地實踐。
很多人一看到“嵌入式AI要學 C++”,第一反應就是去補模板、繼承、多態、STL、設計模式。真到 MCU 上部署模型時,最常用、最關鍵的并不是這些。
嵌入式AI里真正決定你能不能把模型接起來、把輸入輸出處理對、把內存控制住的,主要是幾類基礎但非常硬的內容:數組、指針、引用、const、數據類型、結構體、類、靜態內存和函數封裝。
這些內容如果掌握得扎實,模型輸入組織、特征處理、推理調用、結果解析就會順很多;如果這些地方不穩,模型訓練得再好,部署端一樣會出問題。
在嵌入式AI里,數組最常見的用途有三種:存原始采樣數據、存預處理后的特征數據、存模型輸入輸出緩沖區。
float acc_x[128];
float acc_y[128];
float acc_z[128];如果模型輸入要求三軸數據按連續內存排布,常見寫法是:
float input[128 * 3];
for (int i = 0; i < 128; i++) {
input[i * 3 + 0] = acc_x[i];
input[i * 3 + 1] = acc_y[i];
input[i * 3 + 2] = acc_z[i];
}這里必須真正理解兩件事。第一,數組本質上是一段連續內存。第二,你必須清楚每個元素在內存里的位置,不然模型輸入順序很容易拼錯。
實戰建議:嵌入式AI里數組長度通常明確、固定。窗口長度是 128,就直接寫成 128。項目早期優先追求穩定和可控,不要一開始就把輸入緩沖區設計成復雜的動態大小。
在 MCU 項目里,很多函數不會直接傳一個大數組對象,而是傳數組首地址。也就是傳指針。
void Normalize(float* data, int len, float mean, float std)
{
for (int i = 0; i < len; i++) {
data[i] = (data[i] - mean) / std;
}
}調用時:
float input[128];
Normalize(input, 128, 0.5f, 0.2f);這里 float* data 指向數組首地址,函數內部直接修改原始緩沖區。這種寫法在預處理階段極常見。
更完整一點的輸入輸出接口通常會這樣寫:
void ExtractFeature(const int16_t* raw, int raw_len, float* feature, int feature_len)
{
for (int i = 0; i < feature_len && i < raw_len; i++) {
feature[i] = raw[i] / 32768.0f;
}
}這里有輸入指針,有輸出指針,也有長度參數。嵌入式里只要用指針,就要同時把長度管住,否則越界問題非常難查。
如果一個參數確定存在,而且函數要直接修改它,引用通常比指針更干凈。
bool RunInference(const float* input, int len, int& predicted_label, float& score)
{
if (input == nullptr || len <= 0) {
return false;
}
predicted_label = 2;
score = 0.91f;
return true;
}調用時:
int label;
float prob;
if (RunInference(input, 128, label, prob)) {
// 使用 label 和 prob
}這個接口比 int* 和 float* 更清楚,因為它表達的意思很明確:這兩個輸出對象一定存在,不允許為空。
嵌入式AI項目里,很多錯誤不是算法錯誤,而是某個函數無意中改了本不該改的數據。const 就是用來防這種問題的。
extern const unsigned char g_model[];
extern const int g_model_len;
void Process(const float* input, int len);
class ModelInfo
{
public:
int GetInputSize() const
{
return input_size_;
}
private:
int input_size_ = 128;
};最實用的規則很簡單:
const 就加。const T* 或 const T&。const。嵌入式AI里最怕的一類錯誤,就是類型用錯。尤其是 float、int8_t、uint8_t、int16_t 這幾類。
int16_t adc_buffer[256];
float feature[64];
int8_t model_input[128];
uint8_t image_input[96 * 96];量化時不能直接強轉:
model_input[i] = (int8_t)feature[i]; // 這種寫法通常不對應該帶著縮放關系去做:
int8_t QuantizeFloatToInt8(float x, float scale, int zero_point)
{
int value = (int)(x / scale) + zero_point;
if (value > 127) value = 127;
if (value < -128) value = -128;
return (int8_t)value;
}只要項目稍微完整一點,結構體就一定會用到。最常見的是打包一幀采樣數據、一次推理結果或者一組配置參數。
struct ImuSample
{
int16_t ax;
int16_t ay;
int16_t az;
int16_t gx;
int16_t gy;
int16_t gz;
};
struct InferenceResult
{
int label;
float score;
uint32_t time_ms;
};結構體最大的價值就是:讓函數參數更清楚,模塊之間傳數據更穩,不再到處散落著很多相關變量。
嵌入式AI里最常用的類,通常都不復雜。它們最重要的作用,是把“內部狀態 + 外部操作”封裝在一起。
class SlidingWindow
{
public:
SlidingWindow() : count_(0) {}
void Push(float value)
{
if (count_ < kSize) {
buffer_[count_++] = value;
} else {
for (int i = 0; i < kSize - 1; i++) {
buffer_[i] = buffer_[i + 1];
}
buffer_[kSize - 1] = value;
}
}
const float* Data() const
{
return buffer_;
}
int Size() const
{
return count_;
}
private:
static const int kSize = 128;
float buffer_[kSize];
int count_;
};這類簡單類非常適合用在滑窗緩沖區、特征提取器、模型調用器這類模塊上。目的不是為了“寫得高級”,而是為了讓模塊邊界更清楚。
很多桌面程序喜歡用 new、delete、malloc、free,但在 MCU 項目里,尤其是 AI 項目里,更推薦直接靜態分配。
static uint8_t tensor_arena[8 * 1024];
static int8_t input_buffer[128];
static float feature_buffer[64];靜態分配的好處非常直接:內存占用可預估、生命周期清楚、不容易出現內存碎片、系統更穩。
下面這段代碼就是典型錯誤:
float* GetBuffer()
{
float temp[128];
return temp;
}因為 temp 是局部數組,函數退出后這段內存就不再有效。更穩妥的寫法,要么由外部傳入緩沖區,要么明確使用靜態對象:
void FillBuffer(float* out, int len)
{
for (int i = 0; i < len; i++) {
out[i] = 0.0f;
}
}嵌入式AI里很多詭異問題,最后都能追到生命周期沒理清楚。
很多性能問題并不是推理本身慢,而是你前處理階段反復復制數組。能原地處理就原地處理。
void NormalizeInPlace(float* data, int len, float mean, float std)
{
for (int i = 0; i < len; i++) {
data[i] = (data[i] - mean) / std;
}
}如果一路從原始采樣拷到臨時數組 1,再拷到臨時數組 2,再拷到模型輸入,RAM 和運行時間都會被浪費掉。
不要把所有邏輯都塞進 main.cpp。更好的做法,是按數據流拆分。
bool ReadSensor(ImuSample& sample);
void UpdateWindow(const ImuSample& sample);
void BuildFeature(const ImuSample* window, int win_len, float* feature, int feature_len);
bool RunModel(const float* feature, int feature_len, int& label, float& score);
void HandleResult(int label, float score);這樣一來,采樣、預處理、特征、推理、后處理各自邊界都很清楚。出問題時,你也能很快定位到底是哪一段出了問題。
不推薦這種含糊寫法:
void Process(float* a, int b, float* c, int d);更推薦這樣:
bool BuildInputFeature(const int16_t* raw_data,
int raw_len,
float* feature_out,
int feature_len);變量名本身就把職責寫清楚了。后面維護時,代碼可讀性會高很多。
嵌入式AI最怕“悄悄出錯”。表面程序在跑,實際上輸入長度錯了、緩沖區沒填滿、輸出索引讀錯了。
bool RunModel(const float* input, int len, int& label)
{
if (input == nullptr) {
return false;
}
if (len != 128) {
return false;
}
label = 0;
return true;
}如果項目更完整一點,可以直接定義狀態碼:
enum class Status
{
Ok = 0,
NullPointer,
InvalidLength,
ModelError
};然后把函數統一改成返回 Status,這樣調試時會清楚很多。
自己寫一個最小工程,把下面這條鏈完整打通:
把這條鏈真正寫通,比繼續看很多概念更有效。
學習嵌入式AI,不需要先把 C++ 學成“大而全”。最值得優先掌握的,是那些直接影響模型部署的硬能力。
把這幾層練扎實了,后面無論接哪種 MCU 側 AI 框架,都會輕松很多。

END