功耗在哪
在电池供电的IoT设备中,WiFi模块的平均电流是180mA,而MCU只有8mA。
WiFi工作5秒消耗的电量,MCU可以工作超过100秒。在一次智能环境监测项目中,设备标称6个月续航,实际只能撑2个月。排查下来,问题不在硬件选型,而在软件策略------传感器每10秒上报一次数据,不管环境有没有变化。
这让我意识到一个关键问题:传统传感器是死的,不会根据环境变化调整自己的行为。如果传感器能像人一样思考------环境稳定时少采集,环境变化时多采集------续航就能大幅提升。
这就是AI驱动的智能感知的核心思路。
MCU上跑AI
在动手优化之前,先做一个功耗分析。用万用表串联测量,记录各个模块的电流消耗:
| 模块 | 工作电流 | 占空比 | 平均电流 |
|---|---|---|---|
| MCU(STM32L4) | 8mA | 10% | 0.8mA |
| 传感器(BME280) | 0.5mA | 10% | 0.05mA |
| WiFi模块 | 180mA | 5% | 9mA |
| 总计 | - | - | 9.85mA |
问题一目了然:WiFi模块是耗电大户,占总功耗的91%。即使MCU和传感器做得再省电,WiFi一开,其他都是杯水车薪。
这给出了一个明确的优化方向:减少WiFi上报频率。
但减少上报频率有个风险:如果环境突然变化(比如有人开窗通风),传感器没有及时上报,用户体验就差了。
解决方案是:用AI判断环境是否发生显著变化,变化时才上报。
固件集成
我们的MCU是STM32L4,Cortex-M4内核,256KB Flash,64KB RAM。这个资源水平,跑一个简单的神经网络是可行的。
选择一个轻量级方案:用TensorFlow Lite Micro训练一个异常检测模型,输入是最近10次传感器读数,输出是是否需要上报。
模型设计
模型结构非常简单:
python
import tensorflow as tf
from tensorflow.keras import layers, models
def create_anomaly_detector(input_size=10, feature_size=3):
"""
创建异常检测模型
Args:
input_size: 输入序列长度(最近N次读数)
feature_size: 每次读数的特征数(温度、湿度、气压)
Returns:
Keras模型
"""
model = models.Sequential([
layers.Input(shape=(input_size, feature_size)),
layers.Flatten(),
layers.Dense(32, activation='relu'),
layers.Dense(16, activation='relu'),
layers.Dense(1, activation='sigmoid')
])
return model
# 创建模型
model = create_anomaly_detector()
model.summary()
# 编译
model.compile(
optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy']
)
# 模型结构:
# Total params: 1,665
# Trainable params: 1,665
# Model size: ~6.5KB (float32) / ~1.7KB (int8)
这个模型只有1665个参数,量化后约1.7KB,完全可以在STM32L4上运行。
训练数据
训练数据从哪里来?用一个简单的方法:收集1000条正常环境下的传感器读数,标注为正常;再模拟200条异常场景(温度骤变、湿度骤变),标注为异常。
python
import numpy as np
# 模拟正常数据(温度25±2°C,湿度50±10%,气压1013±5hPa)
def generate_normal_data(n_samples=1000):
data = []
for _ in range(n_samples):
temp = 25 + np.random.randn() * 2
humidity = 50 + np.random.randn() * 10
pressure = 1013 + np.random.randn() * 5
data.append([temp, humidity, pressure])
return np.array(data)
# 模拟异常数据(温度骤变)
def generate_anomaly_data(n_samples=200):
data = []
for _ in range(n_samples):
temp = 25 + np.random.choice([-1, 1]) * np.random.uniform(5, 15)
humidity = 50 + np.random.randn() * 10
pressure = 1013 + np.random.randn() * 5
data.append([temp, humidity, pressure])
return np.array(data)
# 生成数据
normal_data = generate_normal_data(1000)
anomaly_data = generate_anomaly_data(200)
# 构建训练集
X_normal = np.array([normal_data[i:i+10] for i in range(len(normal_data)-10)])
y_normal = np.zeros(len(X_normal))
X_anomaly = np.array([anomaly_data[i:i+10] for i in range(len(anomaly_data)-10)])
y_anomaly = np.ones(len(X_anomaly))
X_train = np.concatenate([X_normal, X_anomaly])
y_train = np.concatenate([y_normal, y_anomaly])
# 打乱
indices = np.random.permutation(len(X_train))
X_train = X_train[indices]
y_train = y_train[indices]
print(f"Training data shape: {X_train.shape}") # (1170, 10, 3)
量化与部署
训练完成后,用TensorFlow Lite Converter量化为INT8:
python
import tensorflow as tf
def convert_to_tflite(model, output_path):
"""
将模型转换为TFLite INT8格式
"""
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
def representative_dataset():
for i in range(100):
yield [X_train[i:i+1].astype(np.float32)]
converter.representative_dataset = representative_dataset
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8
tflite_model = converter.convert()
with open(output_path, 'wb') as f:
f.write(tflite_model)
print(f"Model saved to {output_path}")
print(f"Model size: {len(tflite_model) / 1024:.2f} KB")
convert_to_tflite(model, 'anomaly_detector.tflite')
量化后的模型约1.7KB,可以轻松放入STM32L4的Flash。
实测效果
固件使用FreeRTOS,主循环如下:
- 传感器采集数据
- 数据存入环形缓冲区
- 调用AI模型判断是否异常
- 异常则上报,正常则跳过
核心代码
c
// main.c
#include "FreeRTOS.h"
#include "task.h"
#include "bme280.h"
#include "tflite_micro.h"
// 配置
#define SENSOR_INTERVAL_MS 10000 // 传感器采集间隔:10秒
#define BUFFER_SIZE 10 // 环形缓冲区大小
#define ANOMALY_THRESHOLD 0.7f // 异常阈值
// 传感器数据结构
typedef struct {
float temperature;
float humidity;
float pressure;
} SensorData;
// 环形缓冲区
static SensorData data_buffer[BUFFER_SIZE];
static int buffer_index = 0;
static int buffer_count = 0;
// TFLite模型
static const unsigned char g_model_data[] = {
#include "model_data.inc"
};
static tflite::MicroInterpreter* interpreter;
static TfLiteTensor* input_tensor;
static TfLiteTensor* output_tensor;
// 初始化TFLite
void init_tflite() {
static tflite::AllOpsResolver resolver;
static uint8_t tensor_arena[8192];
static tflite::MicroInterpreter static_interpreter(
tflite::GetModel(g_model_data),
resolver,
tensor_arena,
sizeof(tensor_arena)
);
interpreter = &static_interpreter;
interpreter->AllocateTensors();
input_tensor = interpreter->input(0);
output_tensor = interpreter->output(0);
}
// 添加数据到缓冲区
void add_to_buffer(SensorData data) {
data_buffer[buffer_index] = data;
buffer_index = (buffer_index + 1) % BUFFER_SIZE;
if (buffer_count < BUFFER_SIZE) {
buffer_count++;
}
}
// 运行AI推理
float run_inference() {
if (buffer_count < BUFFER_SIZE) {
return 0.0f;
}
// 填充输入张量(INT8量化)
for (int i = 0; i < BUFFER_SIZE; i++) {
int idx = (buffer_index + i) % BUFFER_SIZE;
float temp_norm = (data_buffer[idx].temperature - 0) / 50.0f;
float humid_norm = (data_buffer[idx].humidity - 0) / 100.0f;
float press_norm = (data_buffer[idx].pressure - 900) / 200.0f;
input_tensor->data.int8[i * 3 + 0] = (int8_t)(temp_norm * 127);
input_tensor->data.int8[i * 3 + 1] = (int8_t)(humid_norm * 127);
input_tensor->data.int8[i * 3 + 2] = (int8_t)(press_norm * 127);
}
interpreter->Invoke();
int8_t output_int8 = output_tensor->data.int8[0];
float output_float = (float)output_int8 / 127.0f;
return output_float;
}
// 主任务
void sensor_task(void* pvParameters) {
SensorData data;
while (1) {
bme280_read(&data.temperature, &data.humidity, &data.pressure);
add_to_buffer(data);
float anomaly_score = run_inference();
if (anomaly_score > ANOMALY_THRESHOLD) {
wifi_send_data(&data);
printf("Anomaly detected! Score: %.2f\n", anomaly_score);
} else {
printf("Normal. Score: %.2f (skipped reporting)\n", anomaly_score);
}
vTaskDelay(pdMS_TO_TICKS(SENSOR_INTERVAL_MS));
}
}
int main() {
bme280_init();
wifi_init();
init_tflite();
xTaskCreate(sensor_task, "sensor", 2048, NULL, 1, NULL);
vTaskStartScheduler();
return 0;
}
这段代码的核心逻辑是:传感器每10秒采集一次数据,存入环形缓冲区。当缓冲区满(10条数据)时,调用AI模型判断是否异常。如果异常分数超过0.7,才上报数据。
关键设计点:
- 环形缓冲区:用固定大小的数组存储最近10次读数,避免动态内存分配
- INT8量化:输入数据从float转换为int8,减少计算量和内存占用
- 阈值判断:用0.7作为异常阈值,可根据实际场景调整
续航从2个月变成8个月
优化后的功耗对比:
| 模块 | 优化前 | 优化后 | 说明 |
|---|---|---|---|
| 传感器采集 | 每10秒 | 每10秒 | 不变 |
| WiFi上报 | 每10秒 | 每5分钟(平均) | AI过滤90%上报 |
| 平均电流 | 9.85mA | 1.2mA | 降低88% |
| 续航(2000mAh) | 2个月 | 8个月 | 提升4倍 |
更重要的是,用户体验没有下降。异常场景(温度骤变、开窗通风)仍然能及时上报,只是正常场景不再频繁上报了。
这个案例说明:低功耗设计不只是硬件选型,更是智能算法和系统架构的结合。AI不是只能跑在云端,它也可以跑在MCU上,让传感器变得聪明起来。
经验总结
开发过程中遇到几个典型问题,记录一下,后来者可以少走弯路。
内存不够用
调用AllocateTensors()时直接报错。tensor arena只分配了4KB,模型需要约8KB。改成8KB后解决,但这也提醒我们:在MCU上做AI,内存规划必须留足余量,至少比理论需求多50%。
c
static uint8_t tensor_arena[8192]; // 从4096改为8192
量化后输出跑偏
同样的输入,浮点模型和量化模型输出差异很大。排查发现representative_dataset只用了100条数据,覆盖不了正常工况的完整分布。后来增加到500条,按不同时间段采样,误差明显改善。
python
def representative_dataset():
for i in range(500): # 从100增加到500
yield [X_train[i:i+1].astype(np.float32)]
WiFi重试吃掉电量
AI判断需要上报,但WiFi连接失败,重试了5次才成功。WiFi模块连接时功耗180mA,5次重试就是几十秒的高功耗状态,电池直接尿崩。
最后实现了带超时和退避的连接策略,连不上就存本地,下次再发:
c
bool wifi_send_data(SensorData* data) {
int retries = 0;
int backoff_ms = 1000;
while (retries < MAX_RETRIES) {
if (wifi_connect_with_timeout(5000)) {
return wifi_post_data(data);
}
vTaskDelay(pdMS_TO_TICKS(backoff_ms));
backoff_ms *= 2;
retries++;
}
save_to_local_storage(data); // 存本地,下次再试
return false;
}
阈值怎么定
一开始用固定阈值0.7,结果误报率很高------环境温度白天晚上差10度,全被判成异常。后来改成自适应阈值,根据最近一段时间的误报率动态调整,稳定了很多。
c
float adaptive_threshold = 0.7f;
void update_threshold(float false_positive_rate) {
if (false_positive_rate > 0.1f) {
adaptive_threshold += 0.05f;
} else if (false_positive_rate < 0.01f) {
adaptive_threshold -= 0.05f;
}
adaptive_threshold = CLAMP(adaptive_threshold, 0.5f, 0.9f);
}