news 2026/6/26 1:21:43

C语言基础回炉第七天:模块化封装传感器数据

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 个核心函数:

  • SensorPacketInit
  • SensorPacketValidate
  • SensorPacketToJson
  • SensorPacketFromLine

运行脚本:

& "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; }

这里最重要的是两点:

  1. 先判断空指针。
  2. 不让结构体处于未初始化状态。

在嵌入式里,未初始化数据很危险。尤其是控制类字段,比如舵机脉宽,如果随机值被拿去输出 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 网关项目展开,下一步可以学习函数指针和回调,把按键事件、串口接收事件、控制策略切换这些项目场景串起来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/26 1:20:49

java伪共享问题的稳定解法

背景 用户咨询了一个java中cpu缓存伪共享场景&#xff0c; 他通过padding多个long 字段隔离 2 个volatile字段&#xff0c;但是实测效果没有提升。 这是个比较有趣的场景&#xff0c;在 jdk8 有更稳定的方案去解决伪共享带来的性能问题。 下面我们展开介绍 伪共享问题是什么用…

作者头像 李华
网站建设 2026/6/26 1:13:57

ZFX山海证券:出金细节管理体现平台对用户资金体验的重视

对投资者来说&#xff0c;出金体验往往是衡量平台服务质量的重要环节。从处理效率来看&#xff0c;ZFX山海证券更强调让用户在可理解的流程中完成提款。在资料完整、账户状态正常的情况下&#xff0c;相关申请会进入标准审核流程&#xff0c;服务人员也会根据节点及时跟进。从提…

作者头像 李华
网站建设 2026/6/26 1:13:43

AScript如何实现LINQ语法

electMany/Where/Join/GroupJoin/GroupBy/OrderBy/OrderByDescending/Select等方法。 Queryable/Enumerable扩展方法已通过AddFunc方式注入到了CSharpLang语言中&#xff1a; 1 // IEnumerable<T>扩展方法 2 AddFunc(typeof(System.Linq.Enumerable)); 3 // IQueryable…

作者头像 李华
网站建设 2026/6/26 1:13:07

Appium跨界Windows桌面自动化测试:统一技术栈实战指南

1. 项目概述&#xff1a;当Appium遇上Windows桌面提到Appium&#xff0c;绝大多数测试工程师和自动化开发者的第一反应就是移动端自动化测试。没错&#xff0c;从Android到iOS&#xff0c;Appium凭借其跨平台、支持多语言的特性&#xff0c;早已成为移动端UI自动化的首选框架。…

作者头像 李华