Skip to content

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 为什么终端只上报、不决策 ​

  1. 算力与内存约束:STM32F407 仅 192KB SRAM,无法承载时序异常检测模型或 LLM 推理。
  2. 安全边界最小化:终端位于物理现场,可被接触、可被篡改;终端能力越小,被攻陷后影响面越小。终端不具备任何数据库写权限与任何 Agent 调用能力。
  3. 单一职责利于审计:终端只产数据,所有判断都在有审计能力的网关侧完成,责任链清晰。
  4. 诊断需要跨设备上下文:单台设备振动 RMS 是否异常,需结合同产线其他设备的基线,终端拿不到该上下文。

终端上唯一的"判断"是阈值粗筛(§5.4),作用仅是打标记降低上行带宽,不产生任何控制动作。

2.3 架构图 ​

text
                  物理现场(不可信区)
 ┌────────────────────────────────────────────────┐
 │ 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 主控 ​

候选优势劣势结论
STM32F407VGT6168MHz 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 400kHzI2C 或 SPI
精度/分辨率±0.5℃±0.1℃16-bit13-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):

  1. 去重力/去直流:重力是低频静态分量,各轴减去窗口均值 → ax_ac = ax - mean(ax)。
  2. 矢量合成:a_mag = sqrt(ax_ac² + ay_ac² + az_ac²),避免单轴敏感性问题。
  3. RMS:rms = sqrt(Σ a_mag² / N),反映振动能量,是 ISO 10816 通用指标。
  4. 峰峰值:pp = max(a_mag) - min(a_mag),捕捉冲击类故障(轴承点蚀、齿面剥落)——此类故障 RMS 上升不明显但峰值突出。
  5. 特征频率(可选):对窗口内 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主控 MCUSTM32F407VGT61—M4F+FPU、192KB SRAM、内置 MAC
2温度传感器DS18B20 防水探头21-Wire / PA0支持离板长线测温、多点组网
3振动传感器MPU6050 模块1I2C1 / PB6-PB71024B FIFO,批量取数降抖动
4以太网模块W5500 模块1SPI1 / PA5-PA7硬件 TCP/IP,MCU 负载低
5晶振8MHz 无源 + 32.768kHz各 1—系统时钟与 RTC 时基
6降压AMS1117-3.31—5V→3.3V
7上拉电阻4.7kΩ2—DS18B20 数据线 / I2C
8调试器 / 串口ST-Link V2 / CH340各 1SWD / USART2烧录调试 / 日志输出
9指示灯LED 红/绿 + 1kΩ各 1GPIO心跳 / 故障指示
10可选TMP117 模块 / ESP-01S各 1I2C / USART3高精度测温对照 / WiFi 备选

4. 软件架构 ​

4.1 分层 ​

text
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.h

4.2 FreeRTOS 任务划分 ​

任务名优先级栈周期/触发职责
TaskSensor4(最高)512 w10ms(100Hz)从 MPU6050 FIFO 批量读加速度;1Hz 触发 DS18B20 转换读取;原始数据写 q_raw
TaskDSP31024 w事件驱动(q_raw)滑动窗口维护、去直流、RMS/峰峰值计算、温度中值+滑动平均;粗筛打标;结果写 q_tx
TaskMqtt32048 w事件驱动(q_tx/网络事件)W5500 socket 维护、MQTT 连接/订阅/发布、接收 cmd、断线重连、环形缓冲补传
TaskCmd21024 w事件驱动(q_cmd)解析下行指令、白名单校验、防重放校验、执行、发 ACK
TaskHealth1512 w1s喂硬件 IWDG、统计各任务心跳、上报 status、检测任务卡死并复位
TaskLog(可选)1512 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_i2cI2C1 总线共享(MPU6050 / 可选 TMP117)防止多任务同时访问总线
c
/* 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 调度与数据流 ​

text
   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 都轻松
振动窗口 N256 点(2.56s)与 FFT 点数对齐,频率分辨率 = 100/256 ≈ 0.39Hz
窗口步进128 点(50% 重叠)提高特征更新率(每 1.28s 出一次结果),避免窗口切换处特征突变
温度采样率1 HzDS18B20 在 12 位下转换约 750ms,1Hz 已是合理上限;温度是慢变量
温度滤波窗口5 点兼顾平滑与响应延迟(5s 内跟上真实变化)

5.2 温度采集与滤波 ​

c
/* 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 振动特征计算 ​

c
/* 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 / 0x02FLAG_TEMP_HIGH / FLAG_TEMP_LOWtemp > 60℃ / temp < -10℃(默认)温度超上限 / 低于下限(探头脱落或断线)
0x04FLAG_VIB_HIGHvibration > vib_hi_th(默认 0.5g)振动 RMS 超阈值
0x08FLAG_SENSOR_ERR传感器读取失败 / CRC 错数据链路异常,数值不可信
c
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):

text
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/+/cmd

ACL 里没有 Agent 账号。这是"Agent 不能直接访问 MQTT"在基础设施层的落地——不靠 Agent 自律,而是 Broker 根本不接受它的连接。

6.3 QoS 等级选择 ​

主题QoS理由
telemetry1传感器数据要求"至少一次",允许极少量重复(靠 seq 去重)。QoS 0 会静默丢数据影响 Agent 诊断输入完整性;QoS 2 四次握手对嵌入式端开销大且无必要
status1心跳与错误状态不能丢
cmd1指令必须到达;配合 seq + nonce 防重放,重复投递由终端幂等处理
cmd_ack1ACK 丢失会导致网关重发,故 ACK 也需可靠

为什么全局不用 QoS 2:QoS 2 需终端持久化消息状态并在重启后恢复,对 Flash 擦写寿命与实现复杂度都不友好。本系统用"QoS 1 + 应用层 seq 去重 + 幂等指令处理"达到等效效果,是常见工程取舍。

6.4 心跳与遗嘱(LWT) ​

c
/* 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,                  /* 保留,新订阅者立刻看到离线状态 */
};
参数取值说明
KeepAlive30s30s 内无报文则 Broker 判定掉线并发布 LWT
心跳上报每 10s 发 status(state: online)网关有明确存活依据,不依赖 TCP 层
LWT保留消息,retain=1网关订阅时能立即感知离线事件

6.5 断线重连与本地缓存补传 ​

text
链路状态机: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 ​

json
/* 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" }
字段类型必填说明
deviceIdstring是设备唯一标识,对应 sensor_data.device_id
tsinteger是Unix 秒级时间戳(RTC + 网关校时)
tempnumber是滤波后温度,℃
vibrationnumber是振动 RMS,单位 g,对应 sensor_data.vibration
vibration_pp / vibration_fpeaknumber否峰峰值(g) / 主频(Hz),未启用 FFT 时为 0
seqinteger是单调递增序号,用于去重与防重放
flaginteger是粗筛标记位(§5.4),0 表示无标记
signaturestring是HMAC-SHA256 截断(前 16 字节 hex),见 §7.2

deviceId / temp / vibration / ts 四个字段由网关直接映射到 sensor_data 表,命名遵循 05-数据库设计,终端侧不得改写。其余字段为协议元数据,网关解析后不入主表。

7. 终端侧安全设计 ​

7.1 设备身份与认证 ​

层次机制原型期落地生产期规划
设备身份一机一密:每台终端在 Flash 固定扇区烧入唯一 deviceId + 唯一 pskST-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 报文签名与完整性 ​

c
/* 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 ACLAgent 没有 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。白名单硬编码在固件常量表中(不由指令动态修改),避免"改白名单"本身成为攻击面。

c
/* 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 允许重复)
c
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 完整流程 ​

text
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 机制 ​

json
/* 下行 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_codeok=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 + STM32CubeIDECubeMX 生成 Core/ 与 HAL 初始化,IDE 负责编译调试
命令行构建CMake + arm-none-eabi-gcc便于 CI 与无 GUI 构建;提供 CMakeLists.txt
调试器 / 编译器OpenOCD 或 ST-Link GDB Server / ARM GNU Toolchain 12.x与 IDE 或命令行 gdb 配合;编译器版本原型期暂定
RTOSFreeRTOS Kernel V10.x版本号原型期暂定,以 CubeMX 内置版本为准
抽象库 / DSPSTM32 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 目录结构 ​

text
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.sh

9.4 构建与烧录 ​

bash
# 命令行构建(在 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 并发布首条 statusmosquitto_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 测试脚本与答辩演示 ​

bash
# 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 方案