
前言前六天主要是在补 C 语言基础和嵌入式常用数据处理能力位运算、字符串、链表、数组、环形缓冲区、UART 帧解析。今天继续往前走不再停留在单个函数练习而是开始练真实项目里更常见的写法把一组相关数据和操作封装成一个.h/.c模块。今天的练习目录是workplace/exercises/day07_sensor_packet_module这次练的是一个传感器数据模块SensorPacket。它对应我自己的项目链路STM32 ADC 数据 - SensorPacket - [SENSOR] JSON 字符串 - UART/DTU 发送 - PC 网关解析 - Web/MQTT 输出也就是说今天不是单纯写一个“能跑的函数”而是尝试把项目里的传感器数据格式集中管理起来。一、今天的完成结果今天完成了sensor_packet.c里的 4 个核心函数SensorPacketInitSensorPacketValidateSensorPacketToJsonSensorPacketFromLine运行脚本 D:\BaiduNetdiskDownload\精通嵌入式C语言\workplace\exercises\day07_sensor_packet_module\run_day07.ps1实际输出Building day07 sensor packet module Running test_sensor_packet.exe test_sensor_packet passed All day07 sensor packet module exercises passed这说明今天的模块初始化、字段范围检查、JSON 打包、串口文本行解析都已经通过当前测试。二、为什么今天要学.h/.c模块化前几天的练习大多是单文件一个.c文件里写函数、写测试、直接编译运行。这样适合训练基础但真实项目不会一直这样写。在 STM32 项目里如果所有 ADC 处理、UART 打包、命令解析、OLED 显示、舵机控制都塞进一个文件后面会很难维护。稍微改一个字段就可能影响多个地方。模块化的基本思路是.h 文件告诉别人这个模块能用什么 .c 文件实现这个模块具体怎么做今天的文件分工是sensor_packet.h 对外接口 sensor_packet.c 具体实现 test_sensor_packet.c 测试用例 run_day07.ps1 编译和运行脚本sensor_packet.h里放的是结构体、错误码和函数声明sensor_packet.c里放的是函数实现和内部辅助函数。三、SensorPacket数据结构今天定义的核心结构体是typedef struct { int temp; int light; int mode; int servo; } SensorPacket;这四个字段正好对应项目里经常需要上传的数据temp温度light光照mode工作模式servo舵机 PWM 脉宽用结构体集中描述这些字段比在多个函数里散落四个独立变量更清晰。以后如果上报格式要加字段比如设备 ID、电机状态、报警标志也可以围绕这个结构体继续扩展。四、错误码比直接打印更适合模块接口今天没有让函数直接printf报错而是统一返回错误码#define SENSOR_PACKET_OK 0 #define SENSOR_PACKET_ERR_NULL -1 #define SENSOR_PACKET_ERR_RANGE -2 #define SENSOR_PACKET_ERR_BUF_SMALL -3 #define SENSOR_PACKET_ERR_FORMAT -4这样做的好处是模块本身只负责判断结果不决定上层怎么处理。比如STM32 端可以在错误时点亮 LED 或丢弃本次上报。PC 网关端可以打印日志或返回 API 错误。测试程序可以直接判断返回值是否符合预期。这比在底层函数里直接打印一句“error”更可控。五、初始化函数给结构体一个确定状态SensorPacketInit的作用是把结构体设置成默认值int SensorPacketInit(SensorPacket *packet) { if (packet NULL) { return SENSOR_PACKET_ERR_NULL; } packet-temp 0; packet-light 0; packet-mode 0; packet-servo 1500; return SENSOR_PACKET_OK; }这里最重要的是两点先判断空指针。不让结构体处于未初始化状态。在嵌入式里未初始化数据很危险。尤其是控制类字段比如舵机脉宽如果随机值被拿去输出 PWM就可能导致错误动作。六、范围检查项目数据不能只看格式今天写了一个内部辅助函数static int IsRange(int value, int min_value, int max_value) { return value min_value value max_value; }它前面的static很重要表示这个函数只在sensor_packet.c内部可见。外部模块不需要知道IsRange的存在只需要调用SensorPacketValidate。范围检查规则是temp : -40 到 125 light : 0 到 100 mode : 0 到 2 servo : 500 到 2500对应代码int SensorPacketValidate(const SensorPacket *packet) { if (packet NULL) { return SENSOR_PACKET_ERR_NULL; } if (!IsRange(packet-temp, -40, 125)) { return SENSOR_PACKET_ERR_RANGE; } if (!IsRange(packet-light, 0, 100)) { return SENSOR_PACKET_ERR_RANGE; } if (!IsRange(packet-mode, 0, 2)) { return SENSOR_PACKET_ERR_RANGE; } if (!IsRange(packet-servo, 500, 2500)) { return SENSOR_PACKET_ERR_RANGE; } return SENSOR_PACKET_OK; }项目里不能只判断“有没有数据”还要判断“数据是否合理”。例如光照百分比不应该超过 100舵机脉宽也不应该超过安全范围。七、JSON 打包必须检查缓冲区大小今天的输出格式是[SENSOR] {light:74,temp:27,mode:0,servo:1500}实现时使用snprintfwritten snprintf(buf, buf_size, [SENSOR] {\light\:%d,\temp\:%d,\mode\:%d,\servo\:%d}, packet-light, packet-temp, packet-mode, packet-servo);这里不能用不受限制的sprintf。因为在 MCU 里输出缓冲区通常是固定大小。如果没有检查长度就可能写越界。判断方式是if ((size_t)written buf_size) { return SENSOR_PACKET_ERR_BUF_SMALL; }snprintf的返回值是“本来需要写入的字符数”不包含结尾的\0。如果返回值大于等于缓冲区大小就说明放不下完整字符串。这个细节很适合面试时讲因为它能体现内存边界意识。八、从[SENSOR]行解析回结构体PC 网关端收到的是一行文本例如[SENSOR] {light:74,temp:27,mode:0,servo:1500}今天先用sscanf解析固定格式fields sscanf(marker, [SENSOR] {\light\:%d,\temp\:%d,\mode\:%d,\servo\:%d}, parsed.light, parsed.temp, parsed.mode, parsed.servo);然后判断是否解析出 4 个字段if (fields ! 4) { return SENSOR_PACKET_ERR_FORMAT; }最后再调用SensorPacketValidate。这一步不能省因为格式正确不代表数据合理。今天这个解析器不是完整 JSON 解析器它只适合当前固定格式练习。但对 Day7 来说重点不是造一个通用 JSON 库而是理解模块接口、错误码、输入检查和项目数据流。九、和自己项目的关系这个模块可以直接对应到我的两个项目方向。STM32 端可以这样理解ADC 采样值 - 换算成 temp/light - 填入 SensorPacket - SensorPacketToJson - UART/DTU 发送PC 网关端可以这样理解串口读到一行 [SENSOR] 文本 - SensorPacketFromLine - SensorPacketValidate - 存入历史数据 - Web API / MQTT 发布这样做的好处是数据格式集中。以后字段变化时不需要在 STM32 端、PC 网关端、测试代码里到处找散落的字符串拼接逻辑。十、今天必须能口述的问题1..h和.c分别放什么.h文件放对外可见的内容比如结构体、宏、错误码、函数声明。.c文件放具体实现和内部细节。这样其他文件只需要包含头文件就知道怎么调用模块而不需要关心内部怎么实现。2. 为什么内部辅助函数要用staticstatic修饰的函数只在当前.c文件内可见可以隐藏实现细节减少命名冲突也避免外部模块绕过正式接口直接调用内部函数。3. 为什么要返回错误码返回错误码可以让调用者决定怎么处理错误。底层模块不应该随便打印或直接退出因为 STM32 端、PC 端、测试程序对错误的处理方式可能完全不同。4. 为什么 JSON 打包要检查buf_size嵌入式系统常用固定长度缓冲区如果不检查大小就写字符串可能造成越界写入破坏栈、全局变量或其他数据。snprintf能限制写入长度但仍然要检查返回值确认字符串是否完整写入。总结Day7 完成了一个新的 C 语言模块化练习把传感器数据封装成SensorPacket并实现初始化、范围校验、JSON 打包和[SENSOR]行解析。脚本已经通过test_sensor_packet passed All day07 sensor packet module exercises passed今天的重点不是多写几个函数而是从“函数能跑”推进到“模块能被项目使用”。后续继续围绕 C 语言硬功和自己的 STM32/PC 网关项目展开下一步可以学习函数指针和回调把按键事件、串口接收事件、控制策略切换这些项目场景串起来。