C语言基础回炉第七天:模块化封装传感器数据
前言
前六天主要是在补 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}
实现时使用 snprintf:
written = 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. 为什么内部辅助函数要用 static?
static 修饰的函数只在当前 .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 网关项目展开,下一步可以学习函数指针和回调,把按键事件、串口接收事件、控制策略切换这些项目场景串起来。
更多推荐


所有评论(0)