引言
在开发这个储能系统的过程中,我深刻体会到了嵌入式系统中数据管理的复杂性。
当多个采样器(Sampler)需要实时采集数据,多个报告器(Reporter)需要上报数据,而数据处理器(Data Processor)需要协调这一切时,如何设计一个高效、可靠的数据共享机制成为了关键。
这就是我们的 DPR(Data Processor)模块诞生的背景------它不是简单的数据传递,而是一个完整的"内存数据库"架构。
一、DPR架构概览:嵌入式系统的数据中枢
1.1 核心定位
DPR(Data Processor)位于:
modules/data_processor/dpr/
由近 6000 行 C 代码构成,是整个系统的数据中枢。
核心职责
- 数据共享总线:为所有模块提供统一的数据读写接口
- 事件管理中心:处理告警、充电记录、系统事件等
- 配置管理器:管理设备参数、告警配置、历史数据配置
- 数据持久化:与SQLite数据库交互,实现数据的持久化存储
核心代码结构
c
// dpr_loader.c - 主加载器和接口实现 (1718行)
static ERRCDDE_DPR Intern_Get(IN HANDLE hSender, IN INTERGET_TYPE emGetTp,
IN int nDevId, IN int nParamId, IN OUT VAR_VALUE* pBuff, IN int nBuffSize)
static ERRCDDE_DPR Intern_Set(IN HANDLE hSender, IN INTERSET_TYPE emSetTp,
IN OUT void* pBuff, IN int nBuffSize)
// data_processing.c - 数据处理线程 (550行)
DWORD _ThreadEntry_DataProcessing(void* pArgs)
// event_manager.c - 事件管理器 (2119行)
int HandleEvent(CHARGER* pCharger, HANDLE hSender, EVENT_ARG* pEventArg)
// config_mangmt.c - 配置管理 (1988行)
PARAMETER* GetParam_ByPR(PARAM_TYPE emType, int nDevId, int nParamId)
1.2 数据模型设计
DPR的数据模型采用了分层设计:
设备结构
c
typedef struct {
DEVICE_INFO info;
CLUSTER_PARAMETER params_Status; // 状态参数
CLUSTER_PARAMETER params_Control; // 控制参数
CLUSTER_PARAMETER params_Setting; // 设置参数
CLUSTER_ALARM alarms; // 告警列表
} DEVICE;
参数结构
c
typedef struct {
PARAM_BASIC_INFO* pParamInfo;
VAR_VALUE currValue; // 当前值
VAR_VALUE lastValue; // 上次值
time_t tmCurrValueRefreshed; // 当前值刷新时间
time_t tmLastValueChanged; // 上次值变化时间
BOOL bIsValueInvalid; // 值是否无效
BOOL bDisplayAble; // 是否可显示
BOOL bCtrlSetAble; // 是否可控制/设置
} PARAMETER;
设计优势
这种设计的优势在于:
- 类型安全:通过枚举类型区分状态、控制、设置参数
- 时间戳机制:记录值的刷新和变化时间,便于检测数据新鲜度
- 有效性标记:通过bIsValueInvalid标记数据有效性,防止脏读
二、无锁读多写少:原子操作的艺术
2.1 无锁读取设计
c
static ERRCDDE_DPR Intern_Get(IN HANDLE hSender, IN INTERGET_TYPE emGetTp,
IN int nDevId, IN int nParamId, IN OUT VAR_VALUE* pBuff, IN int nBuffSize)
{
PARAMETER* pParam = GetParam_ByPR(PARAM_TYPE_STATUS, nDevId, nParamId);
if(pParam == NULL) {
return ERR_DPR_INVALID_ARG;
}
VAR_VALUE* pVarV = &pParam->currValue;
// 关键:如果当前值无效,使用上次有效值
if(pParam->bIsValueInvalid) {
if(pParam->pParamInfo->emValueType != VALUE_TYPE_BLOB) {
pVarV = &pParam->lastValue;
}
}
int nValSize = pParam->pParamInfo->nValueSize;
// 根据类型进行内存拷贝
if((pParam->pParamInfo->emValueType == VALUE_TYPE_BLOB) ||
(pParam->pParamInfo->emValueType == VALUE_TYPE_STRING)) {
memcpy(pBuff->blobValue, (void*)pVarV->blobValue, nValSize);
} else {
memcpy(pBuff->abyValue, (void*)pVarV->abyValue, 4);
}
return ERR_DPR_OK;
}
设计亮点
- 无锁读取:读操作不需要加锁,直接访问内存
- 双缓冲机制:currValue和lastValue形成双缓冲,当前值无效时回退到上次值
- 快速失败:参数不存在时立即返回错误,避免无效操作
2.2 写操作原子性
写操作虽然较少,但需要保证原子性和一致性:
c
void Dump_ParamValue(PARAMETER* pParm, VAR_VALUE* pNewVal, int nSizeValue, time_t tmStamp)
{
// 更新时间戳
pParm->tmLastValueChanged = pParm->tmCurrValueRefreshed;
pParm->tmCurrValueRefreshed = tmStamp;
pParm->bIsValueInvalid = FALSE;
// 限制拷贝大小,防止越界
nSizeValue = MIN(pParm->pParamInfo->nValueSize, nSizeValue);
nSizeValue = MAX(0, nSizeValue);
if(pParm->pParamInfo->emValueType == VALUE_TYPE_STRING) {
// 先保存旧值到lastValue
memcpy(pParm->lastValue.blobValue, pParm->currValue.blobValue,
(size_t)pParm->pParamInfo->nValueSize);
// 再写入新值
memcpy(pParm->currValue.blobValue, pNewVal->blobValue, (size_t)nSizeValue);
} else if(pParm->pParamInfo->emValueType == VALUE_TYPE_BLOB) {
memcpy(pParm->currValue.blobValue, pNewVal->blobValue, (size_t)nSizeValue);
} else {
// 基本类型(4字节)
memcpy(pParm->lastValue.abyValue, pParm->currValue.abyValue, 4);
memcpy(&pParm->currValue, pNewVal, 4);
}
}
关键点
- 先备份后写入:对于字符串类型,先将当前值备份到lastValue,再写入新值
- 边界检查:使用MIN/MAX宏确保不会越界
- 时间戳原子更新:时间戳的更新顺序保证了数据一致性
2.3 为什么不用锁?
在这个设计中,读操作完全不加锁,写操作也只在必要时加锁(如数据库操作)。原因如下:
- 性能考虑:嵌入式系统CPU资源有限,频繁加锁会导致性能下降
- 数据特性:大部分参数是单生产者(一个采样器)多消费者(多个报告器)模型
- 原子性保证:基本类型(4字节)的读写在ARM架构上是原子的
- 双缓冲容错:即使读到中间状态,也可以通过lastValue恢复
三、生产者-消费者模型
3.1 Sampler(生产者)
采样器(Sampler)作为生产者,采样器负责从硬件设备采集数据,并通过DPR的Set接口写入:
c
// 采样器写入状态参数
INTERSET_DATA setData;
setData.nDevId = 201; // 设备ID
setData.nParamId = 10; // 参数ID
setData.varValue.fValue = 220.5; // 电压值
setData.nValueSize = sizeof(float);
setData.bRawInvalid = FALSE;
// 调用DPR的Set接口
pDPR->Set((HANDLE)pSampler, SETTYPE_RAW_SP, &setData, sizeof(INTERSET_DATA));
在Intern_Set函数中,处理流程如下:
case SETTYPE_RAW_SP:
case SETTYPE_LOGIC_SP:
pParam = GetParam_ByPR(PARAM_TYPE_STATUS, pSetData->nDevId, pSetData->nParamId);
if(pSetData->bRawInvalid) {
pParam->bIsValueInvalid = TRUE;
} else if(Check_ParamValue(pParam, &pSetData->varValue,
pSetData->nValueSize, FALSE) != ERR_PARAMVAL_OK) {
pParam->bIsValueInvalid = TRUE;
} else {
pParam->bIsValueInvalid = FALSE;
Dump_ParamValue(pParam, &pSetData->varValue, pSetData->nValueSize, _NOW_);
}
break;
特点:
- 参数校验:通过Check_ParamValue检查值的范围和类型
- 无效标记:如果数据无效,标记而不是拒绝,保持系统稳定性
- 时间戳自动记录:使用_NOW_宏自动记录当前时间
3.2 告器(Reporter)作为消费者
报告器从DPR读取数据并上报到云端:
c
// 报告器读取状态参数
VAR_VALUE varValue;
ERRCDDE_DPR err = pDPR->Get((HANDLE)pReporter, GETTYPE_SP_VAR,
201, 10, &varValue, sizeof(float));
if(err == ERR_DPR_OK) {
// 上报数据到云端
ReportToCloud(201, 10, varValue.fValue);
}
3.3 数据处理线程
DPR还有一个独立的数据处理线程,负责计算派生参数、刷新告警状态:
c
DWORD _ThreadEntry_DataProcessing(void* pArgs)
{
CHARGER* pCharger = (CHARGER*)pArgs;
while(!pCharger->dataprocessor.bExit) {
Sleep(200); // 200ms周期
// 遍历所有设备
DEVICE* pDev = pCharger->devices.p;
for(n = 0; n < pCharger->devices.nTotal; n++, pDev++) {
// 刷新状态参数(计算派生值)
PARAMETER* pParam = pDev->params_Status.p;
for(i = 0; i < pDev->params_Status.nTotal; i++, pParam++) {
Refresh_StatusParam(pDev, pParam);
}
// 刷新告警状态
ALARM* pAlarm = pDev->alarms.p;
for(i = 0; i < pDev->alarms.nTotal; i++, pAlarm++) {
Refresh_Alarm(pCharger, pDev, pAlarm);
}
}
}
return THREAD_CANCEL_ALL;
}
特点:
- 固定周期:200ms的固定周期保证了实时性
- 表达式计算:通过Refresh_StatusParam计算派生参数(如功率 = 电压 × 电流)
- 告警检测:通过Refresh_Alarm检测告警条件
四、时间戳机制:防脏读核心
4.1 双时间戳设计
c
typedef struct {
time_t tmCurrValueRefreshed; // 当前值刷新时间
time_t tmLastValueChanged; // 上次值变化时间
} PARAMETER;
作用:
- 区分刷新和变化:刷新不一定变化(值可能相同),变化一定刷新
- 检测数据新鲜度:通过比较tmCurrValueRefreshed和当前时间,判断数据是否过期
- 变化率计算:通过tmLastValueChanged计算参数变化频率
4.2 防脏读策略
在数据处理线程中,有一个巧妙的设计:
c
static void Refresh_StatusParam(DEVICE* pDev, PARAMETER* pParam)
{
STATUS_PARAM_INFO* pStatusParamInfo = (STATUS_PARAM_INFO*)pParam->pParamInfo;
if(pStatusParamInfo->emDataSrc == PARAM_SRC_CA) { // 计算参数
pParam->bIsValueInvalid = FALSE;
EXP_CONST result;
if(Exp_Calculate(pDev->info.nDeviceId, szParamName,
pStatusParamInfo->equalExpression.p,
pStatusParamInfo->equalExpression.nTotal,
&result) == EXP_ETYPE_CONST) {
// 先保存旧值
pParam->lastValue.fValue = pParam->currValue.fValue;
pParam->tmLastValueChanged = pParam->tmCurrValueRefreshed;
// 再更新新值和时间戳
pParam->tmCurrValueRefreshed = _NOW_;
pParam->currValue.fValue = (float)result;
} else {
pParam->bIsValueInvalid = TRUE;
}
}
}
核心思想:
- 先读后写:先保存旧值,再写入新值
- 原子标记:通过bIsValueInvalid标记数据有效性
- 时间戳顺序:先更新tmLastValueChanged,再更新tmCurrValueRefreshed
4.3 告警时间管理
c
typedef struct {
ALARM_STATUS emStatus;
time_t tmLastOccured; // 最后发生时间
time_t tmLastCeased; // 最后消除时间
time_t tmLastRestored; // 最后恢复时间(从数据库)
} ALARM;
在告警处理中,有一个防止重复告警的设计:
c
static void Refresh_Alarm(CHARGER* pCharger, DEVICE* pDev, ALARM* pAlarm)
{
time_t tmNow = _NOW_;
// 如果刚从数据库恢复,跳过10秒
if(ABS(tmNow - pAlarm->tmLastRestored) < 10) {
return;
}
// 计算告警条件
if(Exp_Calculate(...) == EXP_ETYPE_CONST) {
bAlarm = (BOOL)result;
if(!bAlarm) {
Alarm_Cease(pCharger, pAlarm);
} else {
Alarm_Begin(pCharger, pAlarm);
}
}
}
👉 防止系统重启后的"告警风暴"
五、DPR vs 消息队列
5.1 DPR vs 消息队列:为什么不用MQ?
在嵌入式系统中,传统的消息队列(如MQTT、ZeroMQ)有以下问题:
- 内存开销大:每条消息都需要分配内存、序列化、反序列化
- 延迟不可控:消息传递有延迟,不适合实时系统
- 状态查询困难:MQ是推送模型,查询当前状态需要额外机制
- 资源消耗高:需要独立的broker进程,占用CPU和内存
5.2 DPR优势
| 特性 | DPR | MQ |
|---|---|---|
| 延迟 | 微秒级(直接内存访问) | 毫秒级(进程间通信) |
| 内存 | 固定大小(预分配) | 动态增长(消息堆积) |
| 查询 | 直接读取 | 要额外机制 |
| 实时性 | 确定性延迟 | 不确定性延迟 |
| 资源消耗 | 低(无额外进程) | 高(需要broker) |
5.3 混合模式(推荐)
c
// 事件队列用于异步通知
typedef struct {
_EVENT_TYPE emEvType;
EVENT_MSG_TYPE emMsgType;
UINT nEvDataSize;
void* pEvData;
HANDLE hSender;
} EVENT_ARG;
// 事件处理
int HandleEvent(CHARGER* pCharger, HANDLE hSender, EVENT_ARG* pEventArg)
{
if(pEventArg->emEvType == EVENT_TYPE_ALARM) {
// 处理告警事件
EVENT_ALARM *pActAlarm = (EVENT_ALARM*)pEventArg->pEvData;
if(pEventArg->emMsgType == EVENT_MSG_BEGIN) {
// 告警开始:写入数据库
sqlite3* data_db = Open_Lock_SqliteDB();
sqlite3_prepare_v2(data_db, "replace into Data_Alarms values (?,?,?,?,?,?,?);",
-1, &stat, NULL);
// ... 绑定参数并执行
Close_UnLock_SqliteDB();
}
}
// 通知所有注册的事件处理器
for(n = 0; n < s_nEventHandlerTotal; n++, pItem++) {
if((pItem->nEvTypeFlag) == pEventArg->emEvType) {
pItem->pfnEvHandler(pItem->hModuleSelf, ...);
}
}
}
策略:
- 数据共享用DPR:高频的数据读写使用共享内存
- 事件通知用队列:低频的事件通知使用消息队列
- 异步持久化:数据库操作放在事件处理中异步执行
六、DPR + AI:边缘智能架构
6.1 应用场景
- 负载预测:根据历史数据预测未来负载
- 故障预警:通过异常检测提前发现故障
- 优化调度:根据电价和负载优化充放电策略
6.2 架构图(逻辑)
┌─────────────────────────────────────────────────────────┐
│ AI推理引擎 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │负载预测 │ │故障检测 │ │优化调度 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └─────────────┴─────────────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │ 特征提取器 │ │
│ └──────┬──────┘ │
└─────────────────────┼──────────────────────────────────┘
│
┌───────▼────────┐
│ DPR数据总线 │
│ ┌──────────┐ │
│ │ 实时数据 │ │
│ │ 历史数据 │ │
│ │ 告警数据 │ │
│ └──────────┘ │
└───────┬────────┘
│
┌─────────────┼─────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│Sampler1 │ │Sampler2 │ │Sampler3 │
└─────────┘ └─────────┘ └─────────┘
6.3 实现示例:实时特征提取
c
// AI特征提取器作为DPR的消费者
typedef struct {
float voltage[100]; // 电压历史
float current[100]; // 电流历史
float temperature[100]; // 温度历史
int index; // 当前索引
} AI_FEATURE_BUFFER;
void AI_FeatureExtractor_Thread(void* pArgs)
{
AI_FEATURE_BUFFER buffer;
memset(&buffer, 0, sizeof(buffer));
while(1) {
Sleep(100); // 100ms采样一次
VAR_VALUE varValue;
// 从DPR读取实时数据
pDPR->Get((HANDLE)pAI, GETTYPE_SP_VAR, 201, 10, &varValue, sizeof(float));
buffer.voltage[buffer.index] = varValue.fValue;
pDPR->Get((HANDLE)pAI, GETTYPE_SP_VAR, 201, 11, &varValue, sizeof(float));
buffer.current[buffer.index] = varValue.fValue;
pDPR->Get((HANDLE)pAI, GETTYPE_SP_VAR, 201, 12, &varValue, sizeof(float));
buffer.temperature[buffer.index] = varValue.fValue;
buffer.index = (buffer.index + 1) % 100;
// 每收集100个样本,进行一次推理
if(buffer.index == 0) {
float features[10];
ExtractFeatures(&buffer, features);
// 调用AI推理引擎
float prediction = AI_Inference(features, 10);
// 将预测结果写回DPR
INTERSET_DATA setData;
setData.nDevId = 200;
setData.nParamId = 100; // AI预测结果参数
setData.varValue.fValue = prediction;
setData.nValueSize = sizeof(float);
pDPR->Set((HANDLE)pAI, SETTYPE_RAW_SP, &setData, sizeof(INTERSET_DATA));
}
}
}
6.4 优势
通过DPR集成AI推理引擎,我们获得了:
- 低延迟:数据不需要上传云端,本地实时推理
- 高可靠:网络断开时仍可正常工作
- 低成本:减少云端计算和带宽成本
- 隐私保护:敏感数据不离开本地设备
七、性能优化实践
7.1 内存池
c
// 在初始化时预分配所有参数内存
BOOL Init_CreateDataProcessor(CHARGER* pCharger)
{
// 预分配设备数组
pCharger->devices.p = NEW(DEVICE, pCharger->devices.nTotal);
// 为每个设备预分配参数数组
DEVICE* pDev = pCharger->devices.p;
for(n = 0; n < pCharger->devices.nTotal; n++, pDev++) {
pDev->params_Status.p = NEW(PARAMETER, pDev->params_Status.nTotal);
pDev->params_Control.p = NEW(PARAMETER, pDev->params_Control.nTotal);
pDev->params_Setting.p = NEW(PARAMETER, pDev->params_Setting.nTotal);
}
}
👉 避免 malloc/free
7.2 批量数据库写入
c
// 在初始化时预分配所有参数内存
static void Refresh_HisParam(CHARGER* pCharger, HISPARAM_INFO* pParam)
{
// 累积到一定数量再写入数据库
if(pParam->nHisValsTotal >= MAX_TEMP_STORED_HISVALS) {
sqlite3* data_db = Open_Lock_SqliteDB();
sqlite3_exec(data_db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
for(n = 0; n < pParam->nHisValsTotal; n++) {
sqlite3_stmt* stat;
sqlite3_prepare_v2(data_db, "replace into Data_HisStatusData values (?,?,?,?,?);",
-1, &stat, NULL);
// ... 绑定参数
sqlite3_step(stat);
sqlite3_finalize(stat);
}
sqlite3_exec(data_db, "COMMIT;", NULL, NULL, NULL);
Close_UnLock_SqliteDB();
pParam->nHisValsTotal = 0;
}
}
...
COMMIT;
提升IO性能
7.3 时间缓存优化
使用缓存的时间戳,避免频繁调用time():
c
#define _NOW_ time(NULL) // 可以优化为缓存的时间戳
// 优化后
static time_t s_cached_time = 0;
static int s_time_update_counter = 0;
#define _NOW_ (++s_time_update_counter % 10 == 0 ? (s_cached_time = time(NULL)) : s_cached_time)
减少系统调用
八、总结
8.1 核心价值
- 统一的数据访问接口:所有模块通过Get/Set接口访问数据,降低耦合
- 高性能的共享内存模型:无锁读取、原子写入,满足实时性要求
- 完善的数据一致性保证:时间戳、双缓冲、有效性标记,防止脏读
- 灵活的事件驱动架构:数据共享与事件通知相结合,兼顾性能和灵活性
- 可扩展的AI集成能力:为边缘智能提供了坚实的数据基础
8.2 适用场景
- 嵌入式实时系统:对延迟和资源消耗敏感
- 多生产者多消费者:多个数据源和多个数据使用者
- 状态查询频繁:需要频繁查询当前状态
- 数据关联复杂:参数之间有复杂的计算关系
8.3 未来优化方向
- 引入版本号(Version)
- lock-free ring buffer
- NUMA优化(高端设备)
- 更强AI融合
一句话总结
DPR本质上就是:
"用共享内存实现的高性能实时数据库 + 事件驱动系统"