在嵌入式软件开发中,模块化设计是保证代码可维护性和可扩展性的关键。下面传感器这段代码展示了一种经典且实用的跨文件数据共享模式:通过静态全局变量与访问函数结合,实现模块内部数据的封装与外部只读访问。
#ifndef __SENSOR_H
#define __SENSOR_H
#include "rtthread.h"
#define SENSOR_POLL_TIME (60 * 15)
struct temp_data{
float temp;
rt_uint32_t timestamp;
};
struct temp{
rt_uint8_t cnt;
struct temp_data temp_data[10];
};
struct humidity_data{
float humidity;
rt_uint32_t timestamp;
};
struct humidity{
rt_uint8_t cnt;
struct humidity_data humidity_data[10];
};
struct temp *temp_get_data(void);
void temp_clear_data(void);
struct humidity *humidity_get_data(void);
void humidity_clear_data(void);
void sensor_cache_for_gps(void);
const struct temp* sensor_get_gps_temp(void);
const struct humidity* sensor_get_gps_humidity(void);
void sensor_clear_gps_temp(void);
void sensor_clear_gps_humidity(void);
#endif
#include "sensor.h"
#include "rtthread.h"
#include "dev_sht4.h"
struct sensor{
struct rt_thread thread;
rt_uint8_t stack[0x200];
rt_device_t sht4;
struct temp temp;
struct humidity humidity;
};
static struct sensor app;
static rt_device_t sht4_dev = RT_NULL;
static struct temp gps_temp_cache = {0};
static struct humidity gps_humidity_cache = {0};
void sensor_cache_for_gps(void)
{
struct sht4_data data;
rt_device_t dev = rt_device_find("sht4");
if (dev == RT_NULL) {
gps_temp_cache.cnt = 0;
gps_humidity_cache.cnt = 0;
return;
}
rt_device_open(dev, RT_DEVICE_FLAG_RDWR);
rt_device_read(dev, 0, &data, sizeof(data));
rt_device_close(dev);
gps_temp_cache.cnt = 1;
gps_temp_cache.temp_data[0].temp = data.t_degC;
gps_temp_cache.temp_data[0].timestamp = time(RT_NULL);
gps_humidity_cache.cnt = 1;
gps_humidity_cache.humidity_data[0].humidity = data.rh_pRH;
gps_humidity_cache.humidity_data[0].timestamp = gps_temp_cache.temp_data[0].timestamp;
}
const struct temp* sensor_get_gps_temp(void)
{
return &gps_temp_cache;
}
const struct humidity* sensor_get_gps_humidity(void)
{
return &gps_humidity_cache;
}
void sensor_clear_gps_temp(void)
{
gps_temp_cache.cnt = 0;
}
void sensor_clear_gps_humidity(void)
{
gps_humidity_cache.cnt = 0;
}
struct temp *temp_get_data(void)
{
struct sht4_data data;
if(app.sht4 == RT_NULL)
{
app.sht4 = rt_device_find("sht4");
if(app.sht4 == RT_NULL)
{
rt_kprintf("find sht4 failed\r\n");
app.humidity.cnt = 0;
app.temp.cnt = 0;
return &app.temp;
}
rt_device_init(app.sht4);
}
rt_device_open(app.sht4, RT_DEVICE_FLAG_RDWR);
rt_device_read(app.sht4, 0, &data, sizeof(data));
rt_device_close(app.sht4);
app.temp.cnt = 1;
if(data.t_degC > -0.1 && data.t_degC < 0.1)
{
app.temp.temp_data[0].temp = data.t_degC;
}
app.temp.temp_data[0].temp = data.t_degC;
app.temp.temp_data[0].timestamp = time(RT_NULL);
app.humidity.cnt = 1;
app.humidity.humidity_data[0].humidity = data.rh_pRH;
app.humidity.humidity_data[0].timestamp = app.temp.temp_data[0].timestamp;
return &app.temp;
}
void temp_clear_data(void)
{
app.temp.cnt = 0;
}
struct humidity *humidity_get_data(void)
{
return &app.humidity;
}
void humidity_clear_data(void)
{
app.humidity.cnt = 0;
}
void sensor_thread(void *param)
{
while(1)
{
rt_thread_mdelay(10000);
struct temp *temp = temp_get_data();
if(temp != RT_NULL)
{
rt_kprintf("temp: %f, humidity: %f\r\n", temp->temp_data[0].temp, app.humidity.humidity_data[0].humidity);
}
}
}
int sensor_app_init(void)
{
app.sht4 = rt_device_find("sht4");
if (app.sht4 == RT_NULL)
{
rt_kprintf("sht4 device not found!\n");
return -RT_ERROR;
}
if (rt_device_init(app.sht4) != RT_EOK)
{
rt_kprintf("sht4 init failed!\n");
return -RT_ERROR;
}
rt_device_open(app.sht4, RT_DEVICE_FLAG_RDWR);
rt_device_close(app.sht4);
// rt_err_t ret = rt_thread_init(&app.thread,
// "sensor",
// sensor_thread,
// RT_NULL,
// app.stack,
// sizeof(app.stack),
// 21,
// 20);
// if(ret == RT_EOK)
// ret = rt_thread_startup(&app.thread);
rt_kprintf("sensor init success\r\n");
return RT_EOK;
}
MSH_CMD_EXPORT_ALIAS(sensor_app_init, sensor_app_init, sensor app init);
一、代码回顾
在 sensor.c 中:
static struct temp gps_temp_cache = {0};
static struct humidity gps_humidity_cache = {0};
const struct temp* sensor_get_gps_temp(void)
{
return &gps_temp_cache;
}
const struct humidity* sensor_get_gps_humidity(void)
{
return &gps_humidity_cache;
}
在 msg.c 中(或其他需要数据的文件):
#include "sensor.h"
const struct temp *temp_cache = sensor_get_gps_temp();
const struct humidity *humidity_cache = sensor_get_gps_humidity();
二、为什么要这样设计?
1. 需求背景
温湿度传感器数据需要在多个模块(如GPS上报、WiFi上报)中使用。
但由于GPS工作时发热会影响传感器读数,所以需要在开启GPS前主动读取并缓存数据,之后各模块直接使用这份缓存数据。
数据由 sensor.c 模块统一管理,其他模块不应直接操作缓存变量,而应通过接口获取。
2. 不使用全局变量的理由
如果直接定义全局变量(如在 sensor.h 中声明 extern struct temp gps_temp_cache;),会带来以下问题:
命名冲突:全局符号污染,容易与其他模块同名变量冲突。
耦合度高:任何地方都可以随意修改该变量,难以追踪数据变化,容易引入bug。
缺乏封装 :内部数据结构(如 struct temp 的成员)暴露给所有调用者,一旦结构体定义变化,所有使用的地方都需要修改。
无法控制访问权限:无法限制只读或只写,也无法在访问时添加额外逻辑(如数据有效性检查)。
三、本设计模式的优点
1. 信息隐藏与封装
使用 static 关键字将 gps_temp_cache 和 gps_humidity_cache 限制在 sensor.c 文件作用域内,外部文件无法直接访问。
对外只提供获取指针的接口函数,且返回类型为 const 限定,强制调用者只能读取数据而不能修改,保证了缓存数据的一致性。
2. 接口稳定性
即使将来 struct temp 的内部成员发生变化(如增加字段、修改数据类型),只要接口函数的声明不变,调用方代码就无需修改。例如,若需要将温度值由 float 改为 int 表示,只需调整 sensor.c 内部实现,而 msg.c 的调用代码完全不受影响。
3. 延迟绑定与灵活性
可以在接口函数中添加额外的逻辑,例如检查数据是否有效、是否需要重新读取等。比如 sensor_get_gps_temp() 内部可以判断缓存是否过期,若过期则自动更新,调用方完全无感。
未来若需要从多个传感器获取数据,或改为通过消息队列传递,只需修改接口实现,无需改动调用方。
4. 符号隔离
静态全局变量不会出现在符号表中,避免了链接时的命名冲突。多个模块可以各自定义同名的静态变量而互不干扰。
四、深入理解:指针返回与数据生命周期
1. 返回指针 vs 返回值
返回指针(如 const struct temp*)避免了数据拷贝,效率高,尤其适用于结构体较大的情况。
但是需要注意生命周期问题:返回的指针指向的是模块内部的静态存储区,其生命周期与整个程序相同。调用方获取指针后,只要模块未重新初始化,指针始终有效。这比返回局部变量的指针安全得多。
若返回的是动态分配的内存,则调用方需负责释放,增加了复杂性。此处使用静态变量,简单可靠。
2. 常量限定 const
返回 const struct temp* 表明调用者不能修改指针指向的内容,符合"缓存数据只读"的语义。
如果确实需要修改数据(例如清空缓存),应提供专门的修改接口(如 sensor_clear_gps_temp()),而不是直接暴露指针。
3. 数据同步与线程安全
在多线程或RTOS环境中,如果多个任务同时调用这些接口,可能存在数据竞争。本例中的缓存由单个线程(GPS控制线程)在开启GPS前更新,其他线程(如网络上报线程)只读,因此是安全的。如果需要更复杂的并发控制,可以在接口内部加锁。
五、与其他数据共享方式的对比
| 方式 | 优点 | 缺点 |
|---|---|---|
| 直接全局变量 | 简单直接 | 耦合高,难维护,无法控制访问权限 |
| 静态全局变量 + 接口函数 | 封装性好,接口稳定,只读控制 | 需要编写额外接口函数,稍微增加代码量 |
| 动态分配 + 传递指针 | 灵活,可动态创建销毁 | 需手动管理内存,易泄漏 |
| 全局结构体 + 注册回调 | 松耦合,适合事件驱动 | 实现复杂,不适合简单数据共享 |
六、实际应用中的扩展
这种模式在嵌入式系统中非常普遍,例如:
驱动层提供传感器数据:static sensor_data_t s_data; const sensor_data_t* driver_get_data(void);
系统状态管理:static sys_status_t s_status; const sys_status_t* sys_get_status(void);
配置参数管理:static config_t s_config; const config_t* config_get(void);
甚至可以将接口进一步抽象,提供多个获取函数(如按索引获取、按名称获取),内部实现可以是数组、链表等,但对外隐藏细节。
七、总结
这段代码完美体现了模块化设计中的信息隐藏原则:
数据(静态全局变量)归模块内部所有;
接口(函数)提供受控的访问途径;
调用者通过接口间接获取数据,无需关心数据来源和存储方式。
这种做法的核心价值在于降低模块间的耦合,提高代码的可维护性和可复用性。掌握这种设计模式,对于构建大型嵌入式软件系统至关重要。今后在需要跨文件共享数据时,可以优先考虑这种"静态变量 + 访问函数"的方式,而不是直接使用全局变量。