07 - STM32 边缘终端设计
课题:基于开源模块化 Agent 的工业预测性运维系统(含安全管控插件层)。本文描述边缘终端(STM32 + FreeRTOS 传感器节点)的选型、软件架构、通信协议与安全设计。
1. 文档定位与关联文档
本文覆盖边缘终端(Edge Node)的硬件选型与 BOM、FreeRTOS 任务划分、数据采集与预处理、MQTT 上报协议、指令下行通道、终端侧安全设计。不在范围内:Agent 编排、安全管控插件策略、MCP 工具实现、数据库表结构。
| 文档 | 关系说明 |
|---|---|
| 01-系统总体方案 | 上游。定义四层架构(Agent / MCP 安全网关 / 通信 / 边缘终端)与整体数据流,本文是其第 4 层的落地展开 |
| 04-MCP安全网关设计 | 下游对接方。定义 mqtt.read_sensor、mqtt.publish_cmd 等工具与指令审计流程;本文的主题与指令 Schema 必须与其一致 |
| 05-数据库设计 | 数据落库契约。sensor_data 表字段(device_id、temp、vibration、ts)是终端 Payload 字段命名的唯一依据 |
一致性约定:终端上报字段名与
sensor_data列名语义一一对应,网关只做命名风格转换(camelCase → snake_case),不做语义改写。
2. 终端在系统中的角色
2.1 为什么需要真实硬件终端
| 维度 | 纯软件模拟 | 真实 STM32 终端 |
|---|---|---|
| 数据真实性 | 人为构造,无噪声/漂移/抖动 | 真实 ADC 量化噪声、单总线时序抖动、温漂,能暴露 Agent 异常检测算法的鲁棒性问题 |
| 时序特性 | time.sleep() 决定,过于理想 | 受 RTOS 调度与网络阻塞影响,存在真实抖动,可验证网关乱序/去重逻辑 |
| 断网行为 | 难模拟缓存补传、LWT、重连风暴 | 可物理拔网线,验证 LWT 触发、环形缓冲补传、seq 连续性 |
| 资源约束 | 无 | 有限 RAM/Flash,验证协议轻量化程度(JSON 体积、TLS 握手开销) |
| 答辩说服力 | 弱 | 强。可现场演示插拔网线、遮挡传感器、下发指令改采样率 |
2.2 为什么终端只上报、不决策
- 算力与内存约束:STM32F407 仅 192KB SRAM,无法承载时序异常检测模型或 LLM 推理。
- 安全边界最小化:终端位于物理现场,可被接触、可被篡改;终端能力越小,被攻陷后影响面越小。终端不具备任何数据库写权限与任何 Agent 调用能力。
- 单一职责利于审计:终端只产数据,所有判断都在有审计能力的网关侧完成,责任链清晰。
- 诊断需要跨设备上下文:单台设备振动 RMS 是否异常,需结合同产线其他设备的基线,终端拿不到该上下文。
终端上唯一的"判断"是阈值粗筛(§5.4),作用仅是打标记降低上行带宽,不产生任何控制动作。
2.3 架构图
物理现场(不可信区)
┌────────────────────────────────────────────────┐
│ STM32 边缘终端 deviceId = stm32-node-001 │
│ DS18B20(1-Wire) MPU6050(I2C) │
│ └── 采集 → 滤波 → 特征 → 打包 │
│ │ FreeRTOS 任务组 │
│ │ MQTT over TCP(一机一密) │
└────────────────┼───────────────────────────────┘
│ iot/{deviceId}/telemetry (上行 QoS1)
│ iot/{deviceId}/cmd (下行 QoS1)
▼
┌────────────────────┐
│ MQTT Broker │ ← 仅接受终端与网关连接
│ Mosquitto :8883 │
└─────────┬──────────┘
│ 网关是唯一订阅者
▼
┌────────────────────────────────────────┐
│ MCP 安全网关(可信区) │
│ ├ mqtt.read_sensor (读工具) │
│ ├ mqtt.publish_cmd (写工具) │
│ ├ 鉴权 / 白名单 / 防重放 / 审计 │
│ └ 字段转换 → business-db │
└──────┬──────────────────────▲──────────┘
│ SQL 写入 │ MCP 工具调用
▼ │
┌──────────────────┐ ┌─────────┴────────────────┐
│ business-db │ │ Agent(不可信区) │
│ sensor_data 表 │ │ 只能经 MCP 网关读/发指令 │
│ (device_id,temp, │ │ ✗ 无 MQTT 凭据 │
│ vibration, ts) │ │ ✗ 无数据库直连 │
└──────────────────┘ └──────────────────────────┘关键约束:Agent 没有任何路径直连 Broker 或 business-db;取数唯一方式是调用 MCP 工具 mqtt.read_sensor,由网关代为订阅/查询后返回。终端只信任网关、不信任 Agent,因此终端侧不存在"接收 Agent 指令"的代码路径。
3. 硬件选型
3.1 主控
| 候选 | 优势 | 劣势 | 结论 |
|---|---|---|---|
| STM32F407VGT6 | 168MHz M4F(带 FPU,振动 FFT 友好)、192KB SRAM、1MB Flash、自带以太网 MAC、LQFP100 易焊接 | 功耗高、成本略高 | 主方案 |
| STM32F103C8T6 | 极便宜、资料多 | 72KB Flash / 20KB SRAM,跑 RTOS+MQTT+TLS 极紧张;无 FPU | 备选(降级演示) |
| STM32L4 系列 | 低功耗优秀,STOP2 为 µA 级 | 算力偏低,以太网需外挂 MAC | 备选(低功耗专项) |
选 F407VGT6 的理由:① 自带 MAC + IEEE1588,网络路径可不依赖 SPI 带宽,与传感器 SPI/I2C 争抢少;② FPU 让振动 RMS、滑动平均与 FFT 特征提取在浮点上无压力,代码可读性远好于定点化改造;③ 192KB SRAM 足以容纳 MQTT 收发缓冲 + 断线补传环形缓冲(预留 32KB);④ LQFP100 便于返修,适合毕设迭代。
原型期暂定:若采购受限,可先用 F103C8T6 + 关闭 TLS(改明文 + HMAC)跑通链路,再迁移 F407。迁移成本主要在 CubeMX 重新生成底层初始化,
App/层代码基本不动。
3.2 传感器
| 维度 | DS18B20(温度) | TMP117(温度) | MPU6050(振动) | ADXL345(振动) |
|---|---|---|---|---|
| 接口 | 1-Wire,可挂多探头 | I2C,地址可配 4 个 | I2C 400kHz | I2C 或 SPI |
| 精度/分辨率 | ±0.5℃ | ±0.1℃ | 16-bit | 13-bit |
| 关键特性 | 可长距离(数米)分散测温 | 走线短,适合板载 | FIFO 1024 字节 | FIFO 32 级 |
| 驱动复杂度 | 时序敏感,需微秒延时 + CRC8 | 标准 I2C,简单 | 标准 I2C | 标准 I2C/SPI |
温度选 DS18B20:工业预测性运维中温度测点常需离板(贴电机外壳、轴承座),其长线能力更贴合;"时序敏感"特性也构成有价值的工程难点(见 §5.2)。TMP117 作为高精度板载对照方案列入 BOM。
振动选 MPU6050:1024 字节 FIFO 是关键——MCU 可按 100Hz 批量取数,而非每 10ms 被打断一次,显著降低 I2C 中断开销与任务抖动。附带陀螺仪,为后续"安装姿态变化导致基线漂移"的补偿留出余地。
如何用加速度计算振动 RMS 与特征频率(三轴加速度 ax,ay,az 单位 g,窗口长度 N):
- 去重力/去直流:重力是低频静态分量,各轴减去窗口均值 →
ax_ac = ax - mean(ax)。 - 矢量合成:
a_mag = sqrt(ax_ac² + ay_ac² + az_ac²),避免单轴敏感性问题。 - RMS:
rms = sqrt(Σ a_mag² / N),反映振动能量,是 ISO 10816 通用指标。 - 峰峰值:
pp = max(a_mag) - min(a_mag),捕捉冲击类故障(轴承点蚀、齿面剥落)——此类故障 RMS 上升不明显但峰值突出。 - 特征频率(可选):对窗口内
a_mag做 N=256 点 FFT(采样 100Hz → 分辨率 0.39Hz),取幅度谱峰值对应频率f_peak。旋转机械故障频率与转频成整数倍关系(1×、2×、BPFO),故f_peak或它与转频的比值是 Agent 侧故障定位的重要输入。原型期可用 CMSIS-DSP 的arm_rfft_fast_f32实现。
终端只上报
vibration(RMS)与可选的vibration_pp、vibration_fpeak,不做任何故障判定。它甚至不知道"多少算异常"的业务含义,只有一个可配置的粗筛阈值。
3.3 联网模块
| 方案 | 说明 | 取舍 |
|---|---|---|
| W5500(SPI 硬件 TCP/IP) | 硬件协议栈,MCU 只做 SPI 搬运,RAM 占用小,链路稳定 | 主方案(工业现场以有线为主,稳定性优先) |
| ESP8266 / ESP32(AT 固件) | WiFi,AT 指令控制,成本低 | 备选(布线受限时;AT 状态机复杂、WiFi 抖动大) |
| 纯 PHY(LAN8720)+ F407 内置 MAC | 带宽最高、最"正统" | 需自行移植 LwIP 以太网驱动,工作量大,原型期不选 |
选 W5500:把 TCP/IP 协议栈下沉到芯片,MCU 侧只需 SPI 读写 + socket 状态机,FreeRTOS 任务模型简单(MQTT 任务阻塞在 recv() 即可),且不会因网络协议栈抢占 CPU 影响 100Hz 采集实时性。
3.4 供电与调试
- 供电:5V DC → AMS1117-3.3 降压至 3.3V,传感器与 MCU 共地;DS18B20 数据线加 4.7kΩ 上拉。
- 调试:SWD 四线(SWDIO/SWCLK/GND/3.3V)+ ST-Link V2。必须保留 SWD 排针,毕设期间需反复烧录与单步调试。
- 日志:USART2(PA2/PA3)接 USB-TTL,115200 8N1。指示:复位按键 + 运行灯(心跳闪烁,肉眼判断卡死)+ 错误灯。
3.5 BOM 表
| # | 器件 | 型号 | 数量 | 接口 | 选型理由 |
|---|---|---|---|---|---|
| 1 | 主控 MCU | STM32F407VGT6 | 1 | — | M4F+FPU、192KB SRAM、内置 MAC |
| 2 | 温度传感器 | DS18B20 防水探头 | 2 | 1-Wire / PA0 | 支持离板长线测温、多点组网 |
| 3 | 振动传感器 | MPU6050 模块 | 1 | I2C1 / PB6-PB7 | 1024B FIFO,批量取数降抖动 |
| 4 | 以太网模块 | W5500 模块 | 1 | SPI1 / PA5-PA7 | 硬件 TCP/IP,MCU 负载低 |
| 5 | 晶振 | 8MHz 无源 + 32.768kHz | 各 1 | — | 系统时钟与 RTC 时基 |
| 6 | 降压 | AMS1117-3.3 | 1 | — | 5V→3.3V |
| 7 | 上拉电阻 | 4.7kΩ | 2 | — | DS18B20 数据线 / I2C |
| 8 | 调试器 / 串口 | ST-Link V2 / CH340 | 各 1 | SWD / USART2 | 烧录调试 / 日志输出 |
| 9 | 指示灯 | LED 红/绿 + 1kΩ | 各 1 | GPIO | 心跳 / 故障指示 |
| 10 | 可选 | TMP117 模块 / ESP-01S | 各 1 | I2C / USART3 | 高精度测温对照 / WiFi 备选 |
4. 软件架构
4.1 分层
App/ 应用层(与硬件/RTOS 解耦):app_sensor / app_dsp / app_mqtt / app_cmd / app_health
Middlewares/ FreeRTOS Kernel + 自研精简 MQTT 客户端 (+ CMSIS-DSP)
Drivers/ STM32 HAL + BSP(bsp_ds18b20 / bsp_mpu6050 / bsp_w5500 / bsp_log)
Core/ CubeMX 生成:时钟、GPIO、中断、main.c、FreeRTOSConfig.h4.2 FreeRTOS 任务划分
| 任务名 | 优先级 | 栈 | 周期/触发 | 职责 |
|---|---|---|---|---|
TaskSensor | 4(最高) | 512 w | 10ms(100Hz) | 从 MPU6050 FIFO 批量读加速度;1Hz 触发 DS18B20 转换读取;原始数据写 q_raw |
TaskDSP | 3 | 1024 w | 事件驱动(q_raw) | 滑动窗口维护、去直流、RMS/峰峰值计算、温度中值+滑动平均;粗筛打标;结果写 q_tx |
TaskMqtt | 3 | 2048 w | 事件驱动(q_tx/网络事件) | W5500 socket 维护、MQTT 连接/订阅/发布、接收 cmd、断线重连、环形缓冲补传 |
TaskCmd | 2 | 1024 w | 事件驱动(q_cmd) | 解析下行指令、白名单校验、防重放校验、执行、发 ACK |
TaskHealth | 1 | 512 w | 1s | 喂硬件 IWDG、统计各任务心跳、上报 status、检测任务卡死并复位 |
TaskLog(可选) | 1 | 512 w | 事件驱动 | 串口日志异步输出,避免高优先级任务被 printf 阻塞 |
优先级取舍:TaskSensor 最高,因 100Hz 采样时序精度直接决定振动特征可信度,错过 FIFO 读取窗口即丢数据。TaskMqtt 与 TaskDSP 同级但低于采集任务——网络阻塞不能反压采集,靠 q_tx 队列深度 + 丢最旧策略保证,而非阻塞等待。TaskHealth 最低,属兜底工作。printf(毫秒级阻塞)放最低优先级的 TaskLog。
4.3 任务间通信
| 机制 | 实例 | 用途 | 为什么用它 |
|---|---|---|---|
| 队列 | q_raw(长 8,raw_sample_t) | TaskSensor → TaskDSP | 解耦生产/消费速率,支持超时等待 |
| 队列 | q_tx(长 16,tx_msg_t) | TaskDSP → TaskMqtt | 同上;满时丢最旧,保证不反压 |
| 队列 | q_cmd(长 4,cmd_t) | TaskMqtt → TaskCmd | 指令处理与网络收发解耦 |
| 事件组 | evt_net(bit0 已连接 / bit1 已订阅 / bit2 需重连) | 网络状态广播 | 一个状态变化可同时唤醒多个等待者,比多个信号量省对象 |
| 互斥量 | mtx_i2c | I2C1 总线共享(MPU6050 / 可选 TMP117) | 防止多任务同时访问总线 |
/* App/app_types.h 节选 */
typedef struct { uint32_t ts_ms; float ax, ay, az; } raw_sample_t;
typedef struct { uint32_t ts_ms; float temp; /* ℃,已滤波 */
float vibration, vibration_pp, vibration_fpeak; /* RMS/峰峰值/主频(Hz) */
uint8_t flag; uint32_t seq; } tx_msg_t; /* 粗筛标记 / 序号 */
typedef struct { char action[24]; char params[128]; uint32_t nonce, cmd_ts, seq; } cmd_t;4.4 调度与数据流
10ms tick 1s tick
│ │
▼ │
┌────────────┐ q_raw ┌────────────┐ q_tx ┌────────────┐
│ TaskSensor │───────▶│ TaskDSP │─────▶│ TaskMqtt │──▶ telemetry ──▶ Broker
│ prio 4 │ (len8) │ prio 3 │(len16)│ prio 3 │
│ 100Hz 采集 │ │ 滤波+特征+粗筛│ │ 连接/发布/补传│
└────────────┘ └────────────┘ └──────┬─────┘
│ ◀── cmd 主题
┌────────────┐ q_cmd ┌────────────┐ │
│ TaskCmd │◀──────│ TaskMqtt │◀────────────┘
│ prio 2 │ (len4) └────────────┘
│ 白名单/防重放│──────▶ 发布 cmd_ack ──▶ Broker
└────────────┘
┌──────────────────────────────────────────────────────┐
│ TaskHealth prio 1,1s:读各任务心跳 → 全部递增才喂 IWDG │
│ 否则不喂狗,等 IWDG 复位(fail-safe);同时发 status │
└──────────────────────────────────────────────────────┘5. 数据采集与预处理
5.1 采样率与窗口
| 参数 | 取值 | 理由 |
|---|---|---|
| 振动采样率 | 100 Hz(10ms) | 覆盖 0~50Hz,满足常见旋转机械转频(3000rpm=50Hz)及 2× 谐波观测;对 I2C 与 CPU 都轻松 |
| 振动窗口 N | 256 点(2.56s) | 与 FFT 点数对齐,频率分辨率 = 100/256 ≈ 0.39Hz |
| 窗口步进 | 128 点(50% 重叠) | 提高特征更新率(每 1.28s 出一次结果),避免窗口切换处特征突变 |
| 温度采样率 | 1 Hz | DS18B20 在 12 位下转换约 750ms,1Hz 已是合理上限;温度是慢变量 |
| 温度滤波窗口 | 5 点 | 兼顾平滑与响应延迟(5s 内跟上真实变化) |
5.2 温度采集与滤波
/* Drivers/BSP/bsp_ds18b20.c 关键流程(微秒时序用 DWT 计数或短临界区实现) */
static int ds18b20_read_one(float *out_temp)
{
if (!ds18b20_reset()) return -1;
ds18b20_write_byte(0xCC); ds18b20_write_byte(0x44); /* SKIP ROM + CONVERT T */
vTaskDelay(pdMS_TO_TICKS(750)); /* 等待转换,不阻塞调度器 */
if (!ds18b20_reset()) return -1;
ds18b20_write_byte(0xCC); ds18b20_write_byte(0xBE); /* READ SCRATCHPAD */
uint8_t sp[9]; for (int i = 0; i < 9; i++) sp[i] = ds18b20_read_byte();
if (ds18b20_crc8(sp, 8) != sp[8]) return -2; /* CRC 失败,丢弃该次读数 */
*out_temp = (float)(int16_t)((sp[1] << 8) | sp[0]) * 0.0625f;
return 0;
}滤波采用两级串联:**中值滤波(窗口 3)**剔除单点脉冲干扰(单总线时序毛刺导致的跳变读数),**滑动平均(窗口 5)**平滑随机噪声输出稳定趋势值。实现即 median3(med_win) 后送入 5 点环形均值。
取舍说明:为什么不用卡尔曼滤波?DS18B20 误差以随机噪声与偶发毛刺为主,缺乏可信的过程/观测噪声模型,调参成本高于收益。中值+均值组合在毕设场景下性价比最高;若需更高级平滑,放 Agent 侧做,终端保持简单。
5.3 振动特征计算
/* App/app_dsp.c —— 每收到 128 个新采样点触发一次(50% 重叠,N=256) */
#define VIB_N 256
static float win_x[VIB_N], win_y[VIB_N], win_z[VIB_N], acc_sq[VIB_N];
static uint16_t win_cnt = 0;
void vib_push(const raw_sample_t *s)
{
win_x[win_cnt % VIB_N] = s->ax; win_y[win_cnt % VIB_N] = s->ay; win_z[win_cnt % VIB_N] = s->az;
win_cnt++;
if (win_cnt < VIB_N || (win_cnt - VIB_N) % (VIB_N / 2) != 0) return; /* 步进 128 */
float mx = win_mean(win_x, VIB_N), my = win_mean(win_y, VIB_N), mz = win_mean(win_z, VIB_N);
float sum_sq = 0, vmax = -1e9f, vmin = 1e9f;
for (int i = 0; i < VIB_N; i++) { /* 去直流 + 矢量合成 */
float ax = win_x[i]-mx, ay = win_y[i]-my, az = win_z[i]-mz;
float mag = sqrtf(ax*ax + ay*ay + az*az);
acc_sq[i] = mag; sum_sq += mag*mag;
if (mag > vmax) vmax = mag;
if (mag < vmin) vmin = mag;
}
tx_msg_t m = {0};
m.vibration = sqrtf(sum_sq / (float)VIB_N); /* RMS */
m.vibration_pp = vmax - vmin; /* 峰峰值 */
#ifdef ENABLE_VIB_FFT
m.vibration_fpeak = vib_dominant_freq(acc_sq, VIB_N, VIB_FS_HZ);
#else
m.vibration_fpeak = 0.0f;
#endif
m.flag = coarse_filter(&m); m.ts_ms = xTaskGetTickCount() * portTICK_PERIOD_MS; m.seq = seq_next();
if (xQueueSend(q_tx, &m, 0) != pdPASS) { /* 满则丢最旧,不反压采集 */
tx_msg_t drop; xQueueReceive(q_tx, &drop, 0); xQueueSend(q_tx, &m, 0); stat.tx_dropped++;
}
}5.4 异常阈值判定(粗筛,非诊断)
终端只做粗筛:目的不是判断"设备是否故障",而是降低上行数据量与触发网关重点关注。
| 标记位 | 名称 | 触发条件 | 含义 |
|---|---|---|---|
0x01 / 0x02 | FLAG_TEMP_HIGH / FLAG_TEMP_LOW | temp > 60℃ / temp < -10℃(默认) | 温度超上限 / 低于下限(探头脱落或断线) |
0x04 | FLAG_VIB_HIGH | vibration > vib_hi_th(默认 0.5g) | 振动 RMS 超阈值 |
0x08 | FLAG_SENSOR_ERR | 传感器读取失败 / CRC 错 | 数据链路异常,数值不可信 |
static uint8_t coarse_filter(const tx_msg_t *m)
{
uint8_t f = 0;
if (m->temp > g_cfg.temp_hi_th) f |= FLAG_TEMP_HIGH;
if (m->temp < g_cfg.temp_lo_th) f |= FLAG_TEMP_LOW;
if (m->vibration > g_cfg.vib_hi_th) f |= FLAG_VIB_HIGH;
if (g_stat.sensor_err_last) f |= FLAG_SENSOR_ERR;
return f;
}设计边界声明:终端不做趋势判断、不做基线对比、不做故障分类、不产生任何告警动作。阈值由网关下发的
set_threshold指令配置,默认值仅作兜底。真正的诊断由 Agent 在拿到连续sensor_data序列后完成。
6. MQTT 协议设计
6.1 Broker 选型
选用 Eclipse Mosquitto(部署在网关侧,容器化)。
| 候选 | 取舍 |
|---|---|
| Mosquitto | 轻量(单进程低内存)、配置简单、支持 TLS 与 password_file、支持主题级 ACL → 选它 |
| EMQX | 功能强(Dashboard、规则引擎、集群),但对本课题过重,且 Web 管理面扩大攻击面 |
| HiveMQ / NanoMQ | 前者需商业授权不适用;后者边缘友好但生态与文档不及 Mosquitto |
关键配置:禁用匿名(allow_anonymous false),启用 password_file,TLS 监听 8883,用 ACL 限制每个终端只能读写自己的主题。
6.2 主题命名规范
| 主题 | 方向 | 发布者 | 订阅者 | 说明 |
|---|---|---|---|---|
iot/{deviceId}/telemetry | 上行 | 终端 | 网关 | 传感器数据报文 |
iot/{deviceId}/status | 上行 | 终端 | 网关 | 心跳、上下线、错误状态 |
iot/{deviceId}/cmd | 下行 | 网关 | 终端 | 指令下发 |
iot/{deviceId}/cmd_ack | 上行 | 终端 | 网关 | 指令执行确认 |
{deviceId} 例:stm32-node-001。ACL 示例(Mosquitto acl_file):
user stm32-node-001 # 终端只能操作自己的主题
topic write iot/stm32-node-001/telemetry
topic write iot/stm32-node-001/status
topic write iot/stm32-node-001/cmd_ack
topic read iot/stm32-node-001/cmd
user gateway # 网关账号可跨设备读写
topic read iot/+/telemetry
topic read iot/+/status
topic read iot/+/cmd_ack
topic write iot/+/cmdACL 里没有 Agent 账号。这是"Agent 不能直接访问 MQTT"在基础设施层的落地——不靠 Agent 自律,而是 Broker 根本不接受它的连接。
6.3 QoS 等级选择
| 主题 | QoS | 理由 |
|---|---|---|
telemetry | 1 | 传感器数据要求"至少一次",允许极少量重复(靠 seq 去重)。QoS 0 会静默丢数据影响 Agent 诊断输入完整性;QoS 2 四次握手对嵌入式端开销大且无必要 |
status | 1 | 心跳与错误状态不能丢 |
cmd | 1 | 指令必须到达;配合 seq + nonce 防重放,重复投递由终端幂等处理 |
cmd_ack | 1 | ACK 丢失会导致网关重发,故 ACK 也需可靠 |
为什么全局不用 QoS 2:QoS 2 需终端持久化消息状态并在重启后恢复,对 Flash 擦写寿命与实现复杂度都不友好。本系统用"QoS 1 + 应用层 seq 去重 + 幂等指令处理"达到等效效果,是常见工程取舍。
6.4 心跳与遗嘱(LWT)
/* App/app_mqtt.c —— CONNECT 报文关键字段 */
mqtt_connect_params_t p = {
.client_id = "stm32-node-001", .keepalive = 30, /* 终端每 10s 发 PINGREQ,留 3 倍余量 */
.clean_session = 0, /* 保留会话,重连后可续订阅 */
.will_topic = "iot/stm32-node-001/status",
.will_payload = "{\"deviceId\":\"stm32-node-001\",\"state\":\"offline\",\"reason\":\"lwt\"}",
.will_qos = 1, .will_retain = 1, /* 保留,新订阅者立刻看到离线状态 */
};| 参数 | 取值 | 说明 |
|---|---|---|
| KeepAlive | 30s | 30s 内无报文则 Broker 判定掉线并发布 LWT |
| 心跳上报 | 每 10s 发 status(state: online) | 网关有明确存活依据,不依赖 TCP 层 |
| LWT | 保留消息,retain=1 | 网关订阅时能立即感知离线事件 |
6.5 断线重连与本地缓存补传
链路状态机:DISCONNECTED ─connect()─▶ TCP_CONNECTED ─MQTT CONNECT─▶ CONNECTED
▲ │
└──── 失败/超时(指数退避 1s,2s,4s,…,30s 封顶)◀── SUBSCRIBE cmd
数据路径:正常 → q_tx 直接发布,成功即丢弃
断线 → q_tx 写入 RAM 环形缓冲 ring_buf(容量 512 条)
重连 → 按 seq 升序补传,每条间隔 ≥20ms 防拥塞;完成后发 status 带 backfill 计数| 策略 | 设计 |
|---|---|
| 重连退避 | 指数退避 1→2→4→8→16→30s,上限 30s,避免重连风暴打垮 Broker |
| 本地缓存 | RAM 环形缓冲 512 条(约 512×64B ≈ 32KB)。不落 Flash:擦写寿命有限,原型期断电丢数据可接受 |
| 溢出策略 | 环满覆盖最旧,累加 stat.backfill_overflow,随下次 status 上报 |
| 补传限速 | 每条间隔 20ms,避免与实时数据抢带宽 |
| 去重依据 | 报文内 seq 单调递增,网关按 (device_id, seq) 去重 |
6.6 Payload JSON Schema
/* telemetry 上行 */
{ "deviceId": "stm32-node-001", "ts": 1757923200, "temp": 42.31, "vibration": 0.1834,
"vibration_pp": 0.9721, "vibration_fpeak": 24.6, "seq": 10427, "flag": 0,
"signature": "3f9a1c7e..." }
/* status 上行 */
{ "deviceId": "stm32-node-001", "ts": 1757923200, "state": "online", "uptime_s": 86412,
"link": "eth-up", "fw_version": "0.3.1", "tx_dropped": 0, "backfill": 0,
"backfill_overflow": 0, "time_synced": true, "reset_cause": "POR" }| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
deviceId | string | 是 | 设备唯一标识,对应 sensor_data.device_id |
ts | integer | 是 | Unix 秒级时间戳(RTC + 网关校时) |
temp | number | 是 | 滤波后温度,℃ |
vibration | number | 是 | 振动 RMS,单位 g,对应 sensor_data.vibration |
vibration_pp / vibration_fpeak | number | 否 | 峰峰值(g) / 主频(Hz),未启用 FFT 时为 0 |
seq | integer | 是 | 单调递增序号,用于去重与防重放 |
flag | integer | 是 | 粗筛标记位(§5.4),0 表示无标记 |
signature | string | 是 | HMAC-SHA256 截断(前 16 字节 hex),见 §7.2 |
deviceId/temp/vibration/ts四个字段由网关直接映射到sensor_data表,命名遵循 05-数据库设计,终端侧不得改写。其余字段为协议元数据,网关解析后不入主表。
7. 终端侧安全设计
7.1 设备身份与认证
| 层次 | 机制 | 原型期落地 | 生产期规划 |
|---|---|---|---|
| 设备身份 | 一机一密:每台终端在 Flash 固定扇区烧入唯一 deviceId + 唯一 psk | ST-Link 烧录时写入指定 Flash 地址;deviceId 同时用于 MQTT ClientID 与主题前缀 | 产线工装写入,密钥存入内部 OTP 或外挂 SE(ATECC608A) |
| MQTT 认证 | username = deviceId,password = HMAC(psk, deviceId ‖ boot_nonce) | 可简化为静态强口令 + 每设备独立账号(password_file) | 动态口令(挑战-应答),避免静态口令被抓包重放 |
| 传输加密 | TLS 1.2/1.3 | 单向 TLS(终端校验 Broker 证书,Broker 不校验证书) | 双向 TLS(mTLS),每设备独立客户端证书 |
- 为什么原型期用单向 TLS?F407 上跑 mTLS 需 TLS 客户端库 + 证书存储 + 握手时非对称运算,握手可能到秒级、RAM 峰值可达数十 KB。单向 TLS 已能防住"被动窃听"这一主要威胁,而"伪造终端"的威胁在实验室环境下可控(物理可达)。mTLS 列为落地期必做项。
- 若 TLS 完全跑不动(如迁移到 F103),降级方案为明文 MQTT + 应用层 HMAC 签名(§7.2),并明确标注该降级不提供机密性、仅提供完整性,不得用于生产。
7.2 报文签名与完整性
/* App/app_crypto.c —— 对关键字段做 HMAC,防中间人篡改 */
static void build_signature(const tx_msg_t *m, char *out_hex, size_t out_len)
{
char canon[160]; /* 规范化串:字段顺序固定,避免歧义 */
snprintf(canon, sizeof(canon), "%s|%lu|%.2f|%.4f|%lu",
g_device_id, (unsigned long)m->ts_ms, m->temp, m->vibration, (unsigned long)m->seq);
uint8_t mac[32];
hmac_sha256(g_psk, g_psk_len, (const uint8_t *)canon, strlen(canon), mac);
bytes_to_hex(mac, 16, out_hex, out_len); /* 取前 16 字节,平衡强度与体积 */
}网关侧重算 HMAC 比对,不匹配则丢弃并记审计日志。即使 TLS 被降级,报文完整性仍可校验。(原型期软件实现 SHA256+HMAC,生产期改用 mbedTLS 的 mbedtls_md_hmac。)
7.3 为什么终端不接收来自 Agent 的直接指令
原型期终端与 Agent 之间不存在任何直接通道。
多层防御,任一层独立即可阻断:
| 层 | 措施 |
|---|---|
| 网络层 | 终端只与 Broker 的 8883 端口建立出站连接,不监听任何入站端口;终端网段与 Agent 网段隔离 |
| Broker ACL | Agent 没有 MQTT 账号(§6.2),Broker 层直接拒绝其连接 |
| 凭据层 | Agent 进程环境内不存在任何 MQTT 凭据;凭据只注入网关进程 |
| 应用层 | 终端 TaskCmd 只处理 iot/{id}/cmd 的报文,而该主题 ACL 仅允许 gateway 账号写入 |
| 审计层 | 网关是唯一能写 cmd 主题的实体,每条指令落库后才发出(§8.1),Agent 的任何指令意图都留痕 |
Agent 控制终端的唯一路径:调用 MCP 工具 mqtt.publish_cmd → 网关鉴权 → 插件层策略校验 → 白名单校验 → 审计落库 → 网关发布 cmd → 终端执行 → ACK 回网关 → 网关更新审计状态。整条链路中 Agent 只是"发起意图",不具备执行能力。
7.4 指令白名单
终端维护动作白名单,不在表内的 action 一律拒绝并回 NACK。白名单硬编码在固件常量表中(不由指令动态修改),避免"改白名单"本身成为攻击面。
/* App/app_cmd.c */
typedef enum { ACT_SET_RATE, ACT_SET_THRESHOLD, ACT_REBOOT, ACT_OTA_BEGIN, ACT_SELF_TEST, ACT__MAX } cmd_action_t;
static const struct { const char *name; uint8_t min_priv; } k_cmd_whitelist[ACT__MAX] = {
[ACT_SET_RATE] = {"set_rate", 1}, [ACT_SET_THRESHOLD] = {"set_threshold", 2},
[ACT_REBOOT] = {"reboot", 3}, [ACT_OTA_BEGIN] = {"ota_begin", 3},
[ACT_SELF_TEST] = {"self_test", 1},
};| action | 参数 | 权限 | 说明 |
|---|---|---|---|
set_rate / self_test | {"hz": 100} / {} | 1 | 修改振动采样率(限 10~200Hz) / 自检传感器、网络、Flash |
set_threshold | {"temp_hi": 60, "vib_hi": 0.5} | 2 | 修改粗筛阈值 |
reboot / ota_begin | {"delay_s": 3} / {"url","sha256"} | 3 | 延时重启 / 进入 OTA 模式(原型期仅占位) |
参数校验:数值型参数必须做范围校验,拒绝越界值(如 hz: 100000 会拖垮任务)。
7.5 防重放
三重校验,全部通过才执行:
| 校验项 | 规则 | 拒绝理由 |
|---|---|---|
cmd_ts 时效 | abs(now - cmd_ts) <= 60s | 过期指令(超时窗口外的重放) |
seq 单调 | cmd.seq > last_cmd_seq | 序号回退 = 重放 |
nonce 去重 | nonce 不在最近 32 条已处理记录中 | 同一指令被重复投递(QoS 1 允许重复) |
static bool anti_replay_ok(const cmd_t *c)
{
uint32_t now = rtc_now_s();
if (now > c->cmd_ts && (now - c->cmd_ts) > CMD_MAX_AGE_S) return false;
if (c->cmd_ts > now && (c->cmd_ts - now) > CMD_MAX_SKEW_S) return false;
if (c->seq <= g_last_cmd_seq) return false;
for (int i = 0; i < NONCE_HIST_LEN; i++) if (g_nonce_hist[i] == c->nonce) return false;
g_nonce_hist[g_nonce_idx] = c->nonce; g_nonce_idx = (g_nonce_idx + 1) % NONCE_HIST_LEN;
g_last_cmd_seq = c->seq;
return true;
}时钟依赖问题:
cmd_ts校验依赖终端 RTC 准确。原型期由网关在status响应中附带校时(终端上报ts,网关比对后下发time_sync指令)。若终端从未校时,退化为仅校验seq+nonce,并在status中标记time_synced: false,让网关提高审计等级。该降级路径必须在答辩中说明。
7.6 固件签名与安全启动(落地期规划)
原型期不做安全启动,但需给出规划以体现设计完整性:
| 阶段 | 措施 |
|---|---|
| 原型期(当前) | 无签名。ST-Link 直接烧录,SWD 不锁定 |
| 阶段一 / 二 | 固件用私钥签名(ECDSA P-256),Bootloader 用固化公钥验签后才跳转 App / 启用读保护(RDP Level 1/2)与写保护,防止经 SWD 读取 psk |
| 阶段三 / 四 | 密钥存入内部 OTP 或外挂安全元件(ATECC608A / STSAFE-A110) / 关闭 SWD 调试口(或改为需密码解锁),防物理调试攻击 |
OTA 流程(原型期占位设计):网关下发 ota_begin(含固件 URL 与 sha256)→ 终端进入 Bootloader → 分块下载到备用 Flash 分区 → 校验 sha256 与签名 → 置更新标志 → 重启由 Bootloader 切换 → 失败自动回滚旧固件。原型期仅实现"接收指令 + 打印日志 + 回 ACK"。
8. 指令下行通道
8.1 完整流程
Agent 意图:把 stm32-node-001 采样率改为 50Hz
│ MCP 工具调用(JSON-RPC)
▼
MCP 安全网关
├ 1. 鉴权:该 Agent 是否被授权操作 stm32-node-001 / set_rate
├ 2. 插件层策略校验:安全管控插件是否允许该动作
├ 3. 参数校验:hz=50 是否在 10~200 范围内
├ 4. 生成 cmd_ts + 单调 seq + nonce
├ 5. 审计落库(谁在何时要求什么,落 cmd_audit 表)
├ 6. 组装指令 JSON
└ 7. 发布 iot/stm32-node-001/cmd (QoS 1) ──▶ MQTT Broker
▼
终端 TaskMqtt 收到 → 入 q_cmd 队列 → TaskCmd
├ 白名单校验 ├ 防重放校验(cmd_ts/seq/nonce) ├ 参数范围校验
├ 执行(改 g_cfg.vib_fs_hz = 50)
└ 发布 iot/stm32-node-001/cmd_ack (QoS 1) ──▶ MQTT Broker
▼
网关订阅 cmd_ack → 更新审计记录状态为 executed / failed审计要求:指令必须先落库再发布,这样即使终端没收到,审计链也是完整的。
8.2 指令 JSON Schema 与 ACK 机制
/* 下行 cmd(网关 → 终端) */
{ "deviceId": "stm32-node-001", "action": "set_rate", "params": { "hz": 50 },
"cmd_ts": 1757923200, "seq": 3312, "nonce": 918273645,
"issuer": "gateway", "trace_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }
/* 上行 cmd_ack(终端 → 网关) */
{ "deviceId": "stm32-node-001", "trace_id": "a1b2c3d4-...", "cmd_seq": 3312,
"action": "set_rate", "result": "ok", "err_code": 0,
"ts": 1757923201, "detail": "vib_fs_hz=50" }| 字段 | 说明 |
|---|---|
deviceId | 目标设备;终端须校验与自身 ID 一致,不一致直接丢弃 |
action / params | 动作名(必须在白名单内) / 动作参数,按 action 定义各自 schema |
cmd_ts / seq / nonce | 网关签发时间(秒级 Unix)/ 网关侧(同设备维度)单调递增序号 / 一次性随机数 |
issuer | 固定为 gateway;终端校验此值,非 gateway 直接拒绝(形式化自证,真正作用在 §7.3 的 ACL 层,此处属纵深防御) |
trace_id | 全链路追踪 ID,贯穿 Agent 调用 → 审计表 → ACK,用于演示"一条指令的完整轨迹" |
result / err_code | ok=0 成功;rejected=1/2/3/4 分别表示不在白名单 / 防重放失败 / 参数越界 / deviceId 不匹配;failed=10/11 表示硬件驱动错误 / 不支持(如 ota_begin 原型期未实现) |
超时与重试:网关维护未确认指令表,超时 10s 未收到 ACK 则重发(最多 3 次,间隔 5s/10s/20s)。终端因有 nonce 去重会识别重复投递,但需重新发送 ACK(避免"指令已执行但 ACK 丢了"导致网关无限重试)——故 TaskCmd 检测到 nonce 重复时返回上一次的执行结果,而非直接丢弃。
9. 开发与烧录
9.1 工具链
| 项目 | 选型 | 说明 |
|---|---|---|
| IDE / 生成器 | STM32CubeMX + STM32CubeIDE | CubeMX 生成 Core/ 与 HAL 初始化,IDE 负责编译调试 |
| 命令行构建 | CMake + arm-none-eabi-gcc | 便于 CI 与无 GUI 构建;提供 CMakeLists.txt |
| 调试器 / 编译器 | OpenOCD 或 ST-Link GDB Server / ARM GNU Toolchain 12.x | 与 IDE 或命令行 gdb 配合;编译器版本原型期暂定 |
| RTOS | FreeRTOS Kernel V10.x | 版本号原型期暂定,以 CubeMX 内置版本为准 |
| 抽象库 / DSP | STM32 HAL / ARM CMSIS-DSP | 不用 LL 库:HAL 可读性更好、与 CubeMX 配合顺畅;CMSIS-DSP 仅在使用 FFT 时引入 |
| MQTT 客户端 | 自研精简版(基于 W5500 socket) | 见下方取舍 |
| 格式化 | newlib-nano 的 snprintf | 默认不支持浮点 %f,需显式开启或自行做整数化格式化 |
MQTT 客户端取舍:不用现成 Paho Embedded C——其嵌入式分支适配 W5500 需改 network 层,且内存管理方式与本项目的静态分配策略不一致。自研精简客户端只需实现 CONNECT / PUBLISH / SUBSCRIBE / PINGREQ / DISCONNECT 五类报文,约 500 行,全部使用静态缓冲区(无 malloc),既符合嵌入式最佳实践,也让"协议实现"本身成为毕设的工作量证明。
9.2 FreeRTOS 移植与调试要点
- 直接使用 FreeRTOS 原生 API(
xTaskCreate、xQueueCreate),不套 CMSIS-RTOS v2 封装,避免调试困难。 configTOTAL_HEAP_SIZE设为 48KB,使用heap_4.c(支持碎片合并);开发期开启configASSERT、configCHECK_FOR_STACK_OVERFLOW = 2与configUSE_MALLOC_FAILED_HOOK。- 禁止在中断中使用非 FromISR API。MPU6050 的 INT 引脚接 EXTI,中断里只做
xQueueSendFromISR。 | 调试方式 | 用途 | | --- | --- | | SWD + ST-Link | 单步、断点、查看q_tx队列内容、任务栈高水位 | | 串口日志(USART2, 115200) | 异步打印状态;放最低优先级TaskLog,不用阻塞式printf| |uxTaskGetStackHighWaterMark()| 定期打印各任务栈余量,用于调栈大小 | |vTaskList()/vTaskGetRunTimeStats()| 打印任务状态与 CPU 占用 | | GPIO 翻转 + 逻辑分析仪 | 测量任务周期抖动(TaskSensor进出各翻转一次 IO) | | MQTT 侧观测 |mosquitto_sub -h <broker> -t 'iot/#' -v直接看报文 |
9.3 目录结构
firmware/stm32-sensor-node/
├── CMakeLists.txt # 命令行构建入口
├── stm32-sensor-node.ioc # CubeMX 工程文件
├── Core/ # CubeMX 生成,尽量不手改
│ ├── Inc/ main.h FreeRTOSConfig.h stm32f4xx_hal_conf.h
│ └── Src/ main.c freertos.c stm32f4xx_it.c system_stm32f4xx.c
├── Drivers/
│ ├── STM32F4xx_HAL_Driver/ # CubeMX 提供
│ └── BSP/ bsp_ds18b20 bsp_mpu6050 bsp_w5500 bsp_log (.c/.h)
├── Middlewares/
│ ├── FreeRTOS/ # Kernel 源码
│ ├── CMSIS/ # 含 CMSIS-DSP(可选)
│ └── mqtt/ mqtt_client.c/.h mqtt_packet.c/.h # 自研精简 MQTT 客户端
├── App/ # 业务代码,与硬件解耦
│ ├── app_types.h app_config.c/.h app_sensor.c/.h app_dsp.c/.h
│ ├── app_mqtt.c/.h app_cmd.c/.h app_health.c/.h
│ ├── app_crypto.c/.h # HMAC-SHA256(原型期软件实现)
│ └── ring_buf.c/.h # 断线补传环形缓冲
└── tools/ flash.sh monitor.sh9.4 构建与烧录
# 命令行构建(在 firmware/stm32-sensor-node/ 下)
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=cmake/arm-none-eabi.cmake
cmake --build build -j
# OpenOCD + ST-Link 烧录
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg \
-c "program build/stm32-sensor-node.elf verify reset exit"
# 或 st-flash(更简洁)
arm-none-eabi-objcopy -O binary build/stm32-sensor-node.elf build/fw.bin
st-flash --reset write build/fw.bin 0x08000000
python -m serial.tools.miniterm COM3 115200 # 串口日志观测10. 与网关的联调验证
10.1 联调步骤清单
| 步骤 | 操作 | 通过判据 |
|---|---|---|
| 1 | 启动 Mosquitto(TLS 8883 + password_file + acl_file) | mosquitto -c mosquitto.conf -v 无报错,8883 处于监听 |
| 2 | 以 gateway 账号用 mosquitto_pub/sub 自测 | 能互相收发,证明 Broker 与 ACL 配置正确 |
| 3 | 校验 ACL:用 agent 账号(或错误口令)尝试连接 | 连接被拒,证明 Agent 无法直连 MQTT |
| 4 | 烧录终端固件(含 deviceId / psk) | 串口打印启动日志,LED 心跳闪烁 |
| 5 | 终端连接 Broker 并发布首条 status | mosquitto_sub -t 'iot/#' -v 看到 {"state":"online"} |
| 6 | 观察 telemetry 主题;网关启动订阅与入库 | 每约 1.28s 收到一条,seq 单调递增无跳变;网关日志显示写入 sensor_data |
| 7 | 查询数据库 | SELECT * FROM sensor_data WHERE device_id='stm32-node-001' ORDER BY ts DESC LIMIT 10; 有数据且值合理 |
| 8 | 通过 MCP 工具读取 | 调用 mqtt.read_sensor({"device_id":"stm32-node-001","limit":10}),返回与数据库一致 |
| 9 | 下发 set_rate 改为 50Hz | 终端串口打印收到指令;cmd_ack 返回 ok;telemetry 周期变为约 2.56s |
| 10 | 防重放验证:重发同一条 cmd(相同 seq/nonce) | 终端回 rejected, err_code: 2,配置不变 |
| 11 | 拔网线测试 | Broker 在 KeepAlive 超时后发布 LWT(state: offline);终端 q_tx 溢出计数增长;插回后自动重连并补传 |
| 12 | 审计校验 | cmd_audit 表中能查到步骤 9 的完整记录(含 trace_id、issuer、结果) |
10.2 常见问题排查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 终端无法连接 Broker | 网络不通 / IP 配错 / W5500 SPI 未通 | ping Broker;打印 PHY 链路与 socket 状态;检查 SPI 时钟极性 |
| TLS 握手失败 | 系统时间不正确(证书校验失败)/ 证书未烧入 | 打印终端 RTC 时间;确认 CA 证书已写入 Flash 且长度正确;检查是否过期 |
| 连上但立刻掉线 | ClientID 冲突(两台设备同 ID)/ KeepAlive 过短 | 检查 deviceId 唯一性;把 KeepAlive 从 30 调到 60 观察 |
telemetry 收不到 | 主题拼写错 / ACL 未授权 / QoS 不匹配 | 用 mosquitto_sub -t 'iot/#' 抓全部;查 Broker 日志的 denied 记录 |
| 报文 JSON 解析失败 / 入库为空 | snprintf 浮点未启用或缓冲截断 / 网关字段映射错(camelCase 未转 snake_case) | 打印原始报文长度与内容,缓冲从 128 加到 256 / 对比 sensor_data 列名与报文键名 |
| 采集值恒为 0 或恒定 | MPU6050 FIFO 未使能 / I2C 地址错 / DS18B20 未加上拉 | 读 MPU6050 的 WHO_AM_I 应为 0x68;检查 DS18B20 复位应答;示波器看 I2C 波形 |
| 温度读数跳变剧烈 | 单总线时序被中断抢占 | 检查读时序是否被高优先级中断打断;必要时在短临界区关中断 |
| 运行一段时间后死机 | 栈溢出 / 堆耗尽 / IWDG 未喂 | 开启栈溢出钩子打印各任务高水位;检查是否有 malloc 泄漏;确认 TaskHealth 未误判 |
| 指令收到但未执行 | 白名单未含该 action / 防重放误拒 | 打印 TaskCmd 判定分支;检查终端 RTC 与网关偏差是否超 60s |
| 重连后数据大量重复 | 补传未按 seq 去重 / ACK 与补传冲突 | 确认网关按 (device_id, seq) 去重;检查补传队列在发布成功后正确出队 |
11. 测试指标
11.1 验收标准
| 指标 | 定义 | 目标值 | 测试方法 |
|---|---|---|---|
| 上报周期抖动 | 连续 1000 条 telemetry 到达间隔的标准差 | ≤ 50 ms(标称 1.28s) | 网关记录到达时间戳算 stddev;终端侧用 GPIO 翻转 + 逻辑分析仪测生成抖动(应 ≤ 10ms) |
| 丢包率 | 运行 24h 后 seq 缺口数 / 总序号跨度 | ≤ 0.5%(有线);≤ 2%(WiFi 备选) | 网关按 (device_id, seq) 统计缺口 |
| 数据准确率 | 入库行数 / 终端 seq 发送总数 | ≥ 99.5% | 对比终端 status 发送计数与数据库行数 |
| 断网恢复时间 | 插回网线到 Broker 收到 state: online 的时长 | ≤ 10 s(含指数退避最坏 ≤ 30s,需说明) | 秒表 + 日志时间戳;连续 20 次取 P95 |
| 断网补传完整性 | 断网期间数据在重连后成功补传的比例 | ≥ 95%(断网 ≤ 10 分钟场景) | 断网 5 分钟后恢复,对比缺口数与溢出计数 |
| 指令端到端时延 | MCP 工具调用发出到收到 cmd_ack | ≤ 2 s(P95) | 网关记录 trace_id 的发起与 ACK 时间戳 |
| 防重放有效性 | 重放攻击被拒绝的比例 | 100% | 脚本重放最近 100 条指令,全部应返回 rejected |
| 功耗 | F407 主方案工作电流(原型期不作硬性要求,实测记录);若迁移 STM32L4 + 间歇上报,平均电流 | ≤ 5 mA(占空比 1%,STOP2 待机) | 万用表串入 5V 回路 / 功耗分析仪长时间测量 |
| 连续运行稳定性 | 无重启、无死机、无内存泄漏的持续时长 | ≥ 72 h | 长跑测试,检查 uptime_s 单调递增、reset_cause 无异常值 |
| 任务栈余量 / CPU 占用 | 各任务高水位最小值 / 空闲任务 CPU 占比 | ≥ 25% 栈未使用 / ≥ 30% 空闲 | uxTaskGetStackHighWaterMark() / vTaskGetRunTimeStats() |
| LWT 生效时间 | 断电/断网到 Broker 发布离线消息 | ≤ 2 × KeepAlive = 60 s | 拔电计时,观察 status 主题 |
11.2 测试脚本与答辩演示
# 1. 抓取 1 小时 telemetry,用于统计周期抖动与丢包
mosquitto_sub -h localhost -p 8883 --cafile ca.crt -u gateway -P "$GW_PASS" \
-t 'iot/+/telemetry' -v -F '%I %t %p' > telemetry_1h.log
# 2. 统计 seq 缺口与周期抖动(解析 '%I 时间戳' 与 JSON 中的 seq)
python3 tools/stat_telemetry.py telemetry_1h.log # 输出:报文数/缺口数/丢包率/平均周期/抖动 std
# 3. 防重放测试:抓一条 cmd 后原样重发(期望 cmd_ack 为 rejected / err_code=2)
mosquitto_pub -h localhost -p 8883 --cafile ca.crt -u gateway -P "$GW_PASS" \
-t 'iot/stm32-node-001/cmd' -q 1 -f replayed_cmd.json答辩演示建议(5 分钟内覆盖全部关键设计点):① mosquitto_sub -t 'iot/#' -v 实时看到终端数据流;② 用 MCP 工具 mqtt.read_sensor 读出同一批数据,说明 Agent 的取数路径;③ 尝试用 Agent 凭据直连 Broker,演示连接被拒(安全模型的可视化证明);④ 物理拔网线 → 看到 LWT 离线消息 → 插回 → 看到补传与 state: online;⑤ 经 MCP 下发 set_rate → 看到 cmd_ack 与上报周期变化;⑥ 重放同一条指令 → 看到 rejected;⑦ 打开 cmd_audit 表,展示指令的完整审计轨迹。
附录:原型期未定项清单
| 项 | 当前状态 | 需确认的内容 |
|---|---|---|
| FreeRTOS 具体版本 | 暂定 V10.x | 以 CubeMX 内置版本为准,实测 configTOTAL_HEAP_SIZE |
| TLS 库选型 | 暂定 mbedTLS | 确认 F407 上 RAM 峰值与握手耗时;若不可接受,明确降级到 HMAC-only 并记录风险 |
| HMAC 实现 | 原型期软件实现 | 确认代码体积与耗时;是否改用硬件加速或简化算法 |
| 振动主频 FFT | 可选(ENABLE_VIB_FFT) | 确认 CMSIS-DSP 引入后 Flash/RAM 占用,决定是否默认开启 |
| OTA 实际下载 | 仅占位 | 是否在毕设周期内实现完整 Bootloader 双分区方案 |
| 时间同步 | 依赖网关校时 | 确认校时指令实现方式与精度,以及未校时的降级行为 |
| WiFi 备选路径 | 未实现 | 是否需在答辩中演示 ESP8266 AT 方案 |