共享数据总线(DPR)设计模式——嵌入式系统的“内存数据库”

引言

在开发这个储能系统的过程中,我深刻体会到了嵌入式系统中数据管理的复杂性。

当多个采样器(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;

设计优势

这种设计的优势在于:

  1. 类型安全:通过枚举类型区分状态、控制、设置参数
  2. 时间戳机制:记录值的刷新和变化时间,便于检测数据新鲜度
  3. 有效性标记:通过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;
  }

设计亮点

  1. 无锁读取:读操作不需要加锁,直接访问内存
  2. 双缓冲机制:currValue和lastValue形成双缓冲,当前值无效时回退到上次值
  3. 快速失败:参数不存在时立即返回错误,避免无效操作

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);
      }
  }

关键点

  1. 先备份后写入:对于字符串类型,先将当前值备份到lastValue,再写入新值
  2. 边界检查:使用MIN/MAX宏确保不会越界
  3. 时间戳原子更新:时间戳的更新顺序保证了数据一致性

2.3 为什么不用锁?

在这个设计中,读操作完全不加锁,写操作也只在必要时加锁(如数据库操作)。原因如下:

  1. 性能考虑:嵌入式系统CPU资源有限,频繁加锁会导致性能下降
  2. 数据特性:大部分参数是单生产者(一个采样器)多消费者(多个报告器)模型
  3. 原子性保证:基本类型(4字节)的读写在ARM架构上是原子的
  4. 双缓冲容错:即使读到中间状态,也可以通过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;

特点:

  1. 参数校验:通过Check_ParamValue检查值的范围和类型
  2. 无效标记:如果数据无效,标记而不是拒绝,保持系统稳定性
  3. 时间戳自动记录:使用_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;
  }

特点:

  1. 固定周期:200ms的固定周期保证了实时性
  2. 表达式计算:通过Refresh_StatusParam计算派生参数(如功率 = 电压 × 电流)
  3. 告警检测:通过Refresh_Alarm检测告警条件

四、时间戳机制:防脏读核心

4.1 双时间戳设计

c 复制代码
  typedef struct {
      time_t tmCurrValueRefreshed;  // 当前值刷新时间
      time_t tmLastValueChanged;    // 上次值变化时间
  } PARAMETER;

作用:

  1. 区分刷新和变化:刷新不一定变化(值可能相同),变化一定刷新
  2. 检测数据新鲜度:通过比较tmCurrValueRefreshed和当前时间,判断数据是否过期
  3. 变化率计算:通过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;
          }
      }
  }

核心思想:

  1. 先读后写:先保存旧值,再写入新值
  2. 原子标记:通过bIsValueInvalid标记数据有效性
  3. 时间戳顺序:先更新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)有以下问题:

  1. 内存开销大:每条消息都需要分配内存、序列化、反序列化
  2. 延迟不可控:消息传递有延迟,不适合实时系统
  3. 状态查询困难:MQ是推送模型,查询当前状态需要额外机制
  4. 资源消耗高:需要独立的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, ...);
          }
      }
  }

策略:

  1. 数据共享用DPR:高频的数据读写使用共享内存
  2. 事件通知用队列:低频的事件通知使用消息队列
  3. 异步持久化:数据库操作放在事件处理中异步执行

六、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推理引擎,我们获得了:

  1. 低延迟:数据不需要上传云端,本地实时推理
  2. 高可靠:网络断开时仍可正常工作
  3. 低成本:减少云端计算和带宽成本
  4. 隐私保护:敏感数据不离开本地设备

七、性能优化实践

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 核心价值

  1. 统一的数据访问接口:所有模块通过Get/Set接口访问数据,降低耦合
  2. 高性能的共享内存模型:无锁读取、原子写入,满足实时性要求
  3. 完善的数据一致性保证:时间戳、双缓冲、有效性标记,防止脏读
  4. 灵活的事件驱动架构:数据共享与事件通知相结合,兼顾性能和灵活性
  5. 可扩展的AI集成能力:为边缘智能提供了坚实的数据基础

8.2 适用场景

  • 嵌入式实时系统:对延迟和资源消耗敏感
  • 多生产者多消费者:多个数据源和多个数据使用者
  • 状态查询频繁:需要频繁查询当前状态
  • 数据关联复杂:参数之间有复杂的计算关系

8.3 未来优化方向

  • 引入版本号(Version)
  • lock-free ring buffer
  • NUMA优化(高端设备)
  • 更强AI融合

一句话总结

DPR本质上就是:

"用共享内存实现的高性能实时数据库 + 事件驱动系统"

相关推荐
程序猿online1 小时前
本地mysql密码重置
数据库·mysql
四维迁跃1 小时前
如何排查SQL存储过程死锁_分析死锁日志与索引优化
jvm·数据库·python
m0_741173331 小时前
如何检测SQL注入风险_利用模糊测试技术发现漏洞
jvm·数据库·python
2401_846339561 小时前
CSS如何解决Less与CSS兼容性问题_通过配置文件实现平滑过渡与混合开发
jvm·数据库·python
qq_413847401 小时前
CSS如何控制全屏显示的元素样式
jvm·数据库·python
云动课堂1 小时前
【运维实战】MySQL 8.0 数据库 · 一键自动化部署方案 (适配银河麒麟 V10 / 龙蜥 8 / Rocky Linux 8 / CentOS 8)
linux·运维·数据库
阿正呀1 小时前
CSS粘性定位不生效怎么办_检查父元素高度与overflow属性设置
jvm·数据库·python
2403_883261091 小时前
如何获取DDL语句_DBMS_METADATA.GET_DDL提取对象定义
jvm·数据库·python
m0_613856292 小时前
mysql数据库乱码如何解决_mysql字符集与校对规则配置方法
jvm·数据库·python