Skip to main content

Command Palette

Search for a command to run...

液冷储能系统NCCU 软件系统架构分析文档

Updated
•12 min read•View as Markdown

一、工程概述

本工程是一套运行于嵌入式 Linux 平台(AM335x ARM Cortex-A8,IMMU2/IMMU3 硬件平台)的储能充电站控制系统(NCCU,Network Communication Control Unit),采用纯 C语言编写,面向工业实时控制场景,支持多种工业通信协议,兼顾高可靠性与高可扩展性。


二、顶层目录结构与职责划分

项目根目录

├── main/ # 程序入口与系统主控

├── publib/ # 公共基础库(线程、队列、互斥、日志、数据库)

├── include/ # 全局头文件(数据模型、基础类型、第三方库)

│ ├── base_defines/ # 基础类型与工具宏

│ ├── data_model/ # 核心数据模型定义(CHARGER/DEVICE/SAMPLER/REPORTER等)

│ └── 3rd_party/ # 第三方库(SQLite、curl、MQTT、IEC61850等)

├── comm_ports/ # 通信端口硬件抽象层(HAL),以动态库形式存在

├── data_interfaces/ # 数据访问接口层(DAI),Reporter 访问数据的统一入口

├── moduals/ # 核心业务模块

│ ├── data_processor/ # 数据处理器(DPR):系统核心总线

│ ├── samplers/ # 采样器插件(设备数据采集)

│ └── reporters/ # 报告器插件(对外协议上报)

├── config/ # 系统配置(SQLite格式 System.cfg)

├── extensions/ # 驱动内核模块与系统扩展

├── utilities/ # 辅助工具(日志查看、系统监控、USB升级等)

└── shells/ # 系统启动/停止/运维脚本


三、整体软件架构

3.1 架构模式

本系统采用分层 + 插件化的事件驱动架构,整体可分为五个主要层次:

┌─────────────────────────────────────────────────────────────────┐ │ 外部协议层(Reporter 层) │ │ OCPP1.6 | Modbus TCP | IEC104 | IEC101 | IEC61850 | MQTT | │ │ WebSocket | HTTP/REST | GUI(Qt) | 云平台 | 运维后台 ... │ ├─────────────────────────────────────────────────────────────────┤ │ 数据访问接口层(DAI 层) │ │ DAI_Get / DAI_Set / DAI_Query / DAI_Signal / DAI_EvHandler │ ├─────────────────────────────────────────────────────────────────┤ │ 数据处理器层(DPR 层) │ │ 参数更新 / 报警判定 / 事件分发 / 逻辑计算 / 历史数据持久化 │ ├─────────────────────────────────────────────────────────────────┤ │ 设备采样层(Sampler 层) │ │ Charger | KWH Meter | System | CCU | BMS | PV | AEMS | ... │ ├─────────────────────────────────────────────────────────────────┤ │ 通信端口硬件抽象层(CommPort HAL 层) │ │ 串口 Serial | TCP/IP | UDP | WebSocket | CAN | HTTP/FTP | IPC │ └─────────────────────────────────────────────────────────────────┘ ▲ 公共基础库(publib) iThread | iQueue | iMutex | iLog | iDataBase | iNew


四、核心模块深度分析

4.1 主控模块(main/)

主控模块是整个系统的启动器与调度中心,入口为 main/main.c。

4.1.1 启动流程(main.c)

int main(int argc, char *argv[]) { 
  // Step 1: 命令行参数处理(-pid, -version) 
  Main_ProcessCmdLine(argc, argv); 

  // Step 2: 内存池初始化
  MemoryPool_Init();

  // Step 3: 等待 SQLite 数据库文件锁释放(防止多进程冲突)
  Lock_SqlDB_FileLocker(1000);

  // Step 4: 初始化 SQLite(序列化模式,支持多线程共享同一 handle)
  sqlite3_config(SQLITE_CONFIG_SERIALIZED);

  sqlite3_open_v2(szDBFile, &db, SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE, NULL);

  // Step 5: 创建核心数据结构 CHARGER,挂载事件队列
  Create_Charger(db);

  // Step 6: 打开硬件看门狗
  WatchDog_Open();

  // Step 7: 安装信号处理器
  Install_SigHandlers();

  // Step 8: 设置 longjmp 跳出点(用于异常快速退出)
  int nExitCode = setjmp(s_JumpBufFaultError);

  // Step 9: 初始化线程管理器
  RunThread_ManagerInit(ThreadManager_EventHandler);

  // Step 10: 加载所有业务模块(DPR → DAI → CommPorts → Samplers → Reporters)
  Main_LoadModuals();

  // Step 11: 进入主事件循环
  while(!s_pChargeThis->bExit)
  {
      Do_Proc_HeartBeat();   // 进程心跳
      Queue_Get(hqEvent, &ev_arg, FALSE, 100); // 取事件
      dataprocessor.DoEvent(&ev_arg);           // 分发事件
  }
}

关键设计要点:

  • 使用 setjmp/longjmp 实现从任意深度调用栈中安全退出,避免层层返回的复杂性

  • 主循环以 事件队列(hqEvent) 为驱动,每 100ms 轮询一次

  • 使用 SQLite 文件锁 防止多进程同时操作数据库

4.1.2 模块加载顺序(Main_LoadModuals)

static BOOL Main_LoadModuals(void) {

Init_CreateDataProcessor(); // 1. 数据处理器(核心总线)

dataprocessor.LoadConfigAndData(); // 2. 从 SQLite 加载配置

Init_LoadDAI(); // 3. 数据访问接口

Init_Load_AllCommPorts(); // 4. 通信端口(动态库)

Set_SqliteDB_2RunTime_Mode(); // 5. 切换数据库到运行时模式

Init_Load_AllSamplers(); // 6. 采样器(动态库)

Init_Load_AllReporters(); // 7. 报告器(动态库)

}

加载顺序体现了清晰的依赖关系:数据层先于通信层,通信层先于业务层。

4.1.3 看门狗(watch_dog.c)

//打开硬件看门狗设备 
s_hWatchDog = open("/dev/wd", O_RDWR);
ioctl(s_hWatchDog, IOCTL_SET_TIMER, 60); // 60秒超时
ioctl(s_hWatchDog, IOCTL_EN_WD);

// 喂狗(由 ThreadManager 每次心跳时调用)
void WatchDog_Feed(void) {
    ioctl(s_hWatchDog, IOCTL_FEED_WD);
}

同时支持新旧两代 Linux Kernel 的 ioctl 接口(新 WDOG_MAJOR=236,旧 WDOG_MAJOR=241)。

4.1.4 信号处理(sig_handler.c)


4.2 核心数据模型(include/data_model/)

数据模型是整个架构的数据契约,所有模块共享同一套类型定义。

4.2.1 顶层数据结构:CHARGER

  typedef struct tagCharger { 
      CHARGER_INFO info;             // 软件版本、设备模型、参数定义、报警定义 
      CLUSTER_DEVICE devices;        // 设备列表(含参数、报警实例) 
      CLUSTER_SAMPLER samplers;      // 采样器列表 
      CLUSTER_REPORTER reporters;    // 报告器列表 
      CLUSTER_COMMPORT commports;    // 通信端口列表
      BOOL            bExit;         // 全局退出标志(所有线程监听此变量)
      int             nExitCode;     // 退出原因码
      HANDLE          hRunDB;        // 运行时 SQLite 数据库句柄
      HANDLE          hqEvent;       // 主事件队列(MAX=96条)
      BOOL            bIsInitOK;     // 初始化完成标志
      DATA_PROCESSOR  dataprocessor; // 数据处理器(函数指针集合)
      DAI             dai;           // 数据访问接口(函数指针集合)
} CHARGER;

CHARGER 是整个系统的"大脑",贯穿所有模块,通过指针在各层传递。

4.2.2 设备模型层次

CHARGER_INFO

└── CLUSTER_DEVICE_MODEL_INFO 设备模型定义

└── CLUSTER_STATUS_PARAM_INFO 状态参数定义(含显示表达式、范围、枚举)

└── CLUSTER_CONTROL_PARAM_INFO 控制参数定义(含控制条件表达式)

└── CLUSTER_SETTING_PARAM_INFO 设置参数定义(含默认值、同步策略)

└── CLUSTER_ALARM_INFO 报警定义(含触发/屏蔽表达式)

CLUSTER_DEVICE (devices)

└── DEVICE

├── DEVICE_INFO 设备标识(ID、名称、所属模型、所属采样器)

├── CLUSTER_PARAMETER params_Status 运行时状态参数值

├── CLUSTER_PARAMETER params_Control 运行时控制参数值

├── CLUSTER_PARAMETER params_Setting 运行时设置参数值

└── CLUSTER_ALARM alarms 运行时报警状态

4.2.3 参数值(PARAMETER)

typedef struct tagA_Param {

PARAM_BASIC_INFO* pParamInfo; // 指向参数定义(静态配置)

HANDLE hDevice; // 所属设备

VAR_VALUE currValue; // 当前值

BOOL bIsValueInvalid; // 当前值是否有效

VAR_VALUE lastValue; // 上一次值(用于变化检测)

time_t tmCurrValueRefreshed; // 最后更新时间

time_t tmLastValueChanged; // 最后变化时间

} PARAMETER;


4.3 公共基础库(publib/)

publib 是整个系统的"地基",编译为 libpub.so 动态库,供所有模块链接使用。

4.3.1 线程管理(iThread.c)

系统实现了一套 带心跳监控的线程管理框架:

线程管理器内部结构

typedef struct tagThreadManager {

int nThreadEntries; // 当前分配的线程槽数(动态扩展,步长32)

RUN_THREAD_ENTRY *pThreadEntries; // 线程条目数组

int volatile nRunningThreads; // 当前运行中的线程数

RUN_THREAD_EVENT_HANDLER pfnEventHandler; // 事件处理回调(main 注册)

pthread_mutex_t hSyncLock; // 保护线程表的互斥锁

} RUN_THREAD_MANAGER;

每个线程条目

typedef struct tagThreaEntry {

DWORD dwThreadId; // 高16位为魔术字,低16位为索引

char szThreadName[16];

pthread_t hSystemThread;

DWORD volatile dwThreadHeartbeat; // 心跳计数器

int volatile nStatus; // RUN_THREAD_IS_RUNNING / TO_QUIT / IS_INVALID

} RUN_THREAD_ENTRY;

心跳机制:

  • 每个业务线程必须在循环中调用 RUN_THREAD_HEARTBEAT()

  • ThreadManager 每 500ms 检查一次所有线程心跳

  • 若某线程 60 秒无心跳响应,触发 THREAD_EVENT_NO_RESPONSE 回调

  • 主控接收到回调后设置 bExit=TRUE,触发系统重启

main.c 中的线程事件处理器

static DWORD ThreadManager_EventHandler(DWORD dwThreadEvent, HANDLE hThread, const char *pszThreadName)

{

WatchDog_Feed(); // 每次心跳时喂狗

if(THREAD_EVENT_NO_RESPONSE == dwThreadEvent) {

s_pChargeThis->bExit = TRUE; s_pChargeThis->nExitCode = SYS_EXIT_BY_THREAD;

}

return THREAD_CONTINUE_RUN;

}

4.3.2 线程安全队列(iQueue.c)

typedef struct tagQueue {

int nSize; // 单个元素大小

int nCapacity; // 队列容量

char *pBuffer; // 环形缓冲区

pthread_mutex_t hSyncLock;// 互斥锁

int nCount; // 当前元素数 int nHead; // 头指针

int nTail; // 尾指针

HANDLE hCountSem; // 信号量(用于阻塞等待)

} QUEUE;

队列支持:

  • 阻塞模式(bBlock=TRUE,带超时等待)

  • 非阻塞模式(bBlock=FALSE,立即返回)

  • 可配置的自动扩容(nIncStep > 0)

4.3.3 互斥锁与信号量(iMutex.c)

对 pthread_mutex_t 和 POSIX 信号量进行封装,提供带超时的 Mutex_Lock,避免死锁。


4.4 通信端口硬件抽象层(comm_ports/)

4.4.1 HAL 接口定义(comm_port.h)

所有通信端口驱动必须导出统一的 6 个函数接口:

动态库必须导出的符号名称

#define SYM_HAL_COMMOPEN "HAL_CommOpen"

#define SYM_HAL_COMMACCEPT "HAL_CommAccept"

#define SYM_HAL_COMMREAD "HAL_CommRead"

#define SYM_HAL_COMMWRITE "HAL_CommWrite"

#define SYM_HAL_COMMCONTROL "HAL_CommControl"

#define SYM_HAL_COMMCLOSE "HAL_CommClose"

通信端口对象(函数指针集合)

typedef struct tagCommPort {

COMMPORT_INFO info;

COMM_OPEN_PROC Open;

COMM_CLOSE_PROC Close;

COMM_ACCEPT_PROC Accept;

COMM_READ_PROC Read;

COMM_WRITE_PROC Write;

COMM_CONTROL_PROC Control;

HANDLE hLib; // dlopen 句柄

} COMMPORT;

4.4.2 动态加载机制(comm_loader.c)

static BOOL Load_ACommPort(COMMPORT* pCommPort) {

// 通过 dlopen 加载 .so 文件,路径由配置数据库决定

pCommPort->hLib = LoadDynamicLibrary( MakeFullFileName(szFullLibPath, getenv("NCCU_APP_PATH"), "comm_port", pCommPort->info.szPortDriver),

ITEM_OF(pszSymName),

pszSymName, // 需要查找的符号名数组

pfnProc, // 将函数地址写入 COMMPORT 对应成员

TRUE // 要求所有符号必须存在

);

}

4.4.3 支持的通信端口类型


4.5 采样器层(moduals/samplers/)

采样器负责从物理设备采集数据并回写到数据处理器,每个采样器运行在独立线程中。

4.5.1 采样器数据模型(sampler_model.h)

typedef struct tagSampler { 

  SAMPLER_INFO info; // ID、名称、驱动库名、采样周期(ms) 
  COMMPORT *pUsedCommPort; // 使用的通信端口 
  HANDLE hSamplerLib; // dlopen 动态库句柄 
  HANDLE hSampleThread; // 采样线程句柄 
  HANDLE hDPR; // 数据处理器句柄(用于 Get/Set) 
  void *pDataHeap; // 采样器私有数据堆  
  // 动态加载的函数指针(插件接口)
  SAMPLER_INIT_PROC    Init;        // 初始化
  SAMPLER_SAMPLE_PROC  Sample;      // 采样主函数
  SAMPLER_CONTROL_PROC Control;     // 控制执行
  SAMPLER_NOTIFY_PROC  Notify;      // 接收 DPR 通知
  HANDLE          hqControlCommand; // 控制命令队列(异步接收来自 DPR 的控制命令)
  BYTE            emWorkStatus;     // 工作状态(工作中/断线/不工作)
  double          tmLastSampled;    // 最后采样时间
  BOOL            bJustControlled;  // 刚执行了控制命令,需立即重采样
  BOOL            bExit;            // 线程退出标志
} SAMPLER;

4.5.2 采样器线程主循环(sampler_loader.c)

static DWORD __Thread_ASampler(void* pArgs) { 
    SAMPLER* pSampler = (SAMPLER*)pArgs; 
    while(!pSampler->bExit) { RUN_THREAD_HEARTBEAT(); // 向 ThreadManager 汇报心跳      
    // 1. 优先处理控制命令队列(最多连续处理6条)
    while(Queue_Get(pSampler->hqControlCommand, &cmd, FALSE, 100) == OK)
      {
          pSampler->Control(hSamplerSelf, PARAM_TYPE_CONTROL,
                            cmd.data.nDevId, cmd.data.nParamId,
                            &cmd.data.varValue, cmd.data.nValueSize, INFINITE);
          // 控制成功后,回写结果到 DPR
          pDPR->Set(pSampler, SETTYPE_RAW_BACK_CTRL, &cmd.data, sizeof(INTERSET_DATA));
      }

      // 2. 按采样周期触发采样
      if(IsTimeToSample(pSampler, GetCurrentTime()))
      {
          pSampler->Sample(hSamplerSelf);  // 调用插件的 Sample 函数
      }
  }
}

4.5.3 已实现的采样器插件


4.6 数据处理器层(moduals/data_processor/dpr/)

数据处理器(DPR)是系统的中央总线与大脑,负责:

  1. 配置加载:从 SQLite 读取所有设备模型、参数定义、报警定义

  2. 数据管理:维护所有设备参数的实时值(线程安全)

  3. 事件分发:向注册了感兴趣事件的所有模块广播事件

  4. 报警判定:基于表达式引擎计算报警触发/屏蔽条件

  5. 历史数据持久化:将关键数据写入 SQLite

4.6.1 DPR 接口(dataprocessor_model.h)

typedef struct tagDataProc { 
  DATAPROC_INFO info; 
  HANDLE hThread; 
  BOOL bExit;  
  // 供 main 调用的接口
  LOADCFG_PROC        LoadConfigAndData;  // 从 DB 加载所有配置
  STARTSTOPDATA_PROC  DoDataProcessing;   // 启动数据处理线程
  PROCESSEVENT_PROC   DoEvent;            // 处理来自主队列的事件

  // 供 Sampler 调用的接口
  INTER_GET           Get;   // 采样器读取参数值
  INTER_SET           Set;   // 采样器推送采样结果
  CALCULATE_EXPRESSION CalculateExp; // 计算逻辑表达式
  REGISTER_EVHAND     Register_EvHandler; // 注册事件处理器
} DATA_PROCESSOR;

4.6.2 内部数据流向(Set 接口的类型枚举)

typedef enum tagInternalSetType { 
    SETTYPE_RAW_SP, // 采样器推送原始状态参数值 
    SETTYPE_LOGIC_SP, // 逻辑计算出的状态参数值 
    SETTYPE_INTERN_SETT, // 设置参数同步到其他采样器 
    SETTYPE_RAW_ST, // 设置参数本地刷新(不同步) 
    SETTYPE_INTERN_CTRL, // 控制参数分发到对应采样器 
    SETTYPE_SENDMSG, // 发送消息到 Reporter/DPR 
    SETTYPE_RAW_BACK_CTRL, // 控制执行结果回写 
    SETTYPE_RAW_BACK_SETT, // 设置执行结果回写
    SETTYPE_RESTORE_SETTINGS_FROMDB, // 从 SD 卡恢复配置
    SETTYPE_CLEAR_FACTOY_DEBUG_DATA, // 清除工厂调试数据 
} INTERSET_TYPE;

4.7 数据访问接口层(data_interfaces/DAI)

DAI 是 Reporter 访问系统数据的标准接口,将 DPR 的内部实现与 Reporter 完全解耦。

4.7.1 DAI 接口集合(dai_model.h)

typedef struct tagDAI { 
    DAI_GET Get; // 读取设备信息、参数值、报警状态 
    DAI_SET Set; // 设置参数(控制命令/设置参数) 
    DAI_QUERY Query; // 历史数据查询(充电记录、报警、事件) 
    DAI_SIGNAL Signal; // 向主事件队列投递事件 
    DAI_REGISTER_EVHAND Regsiter_EvHandler; // 注册事件监听器 
} DAI;

4.7.2 DAI_GET 支持的查询类型

typedef enum tagDAIType { 
    DAITYPE_DEV_TOTAL_R, // 设备总数 
    DAITYPE_DEV_INFO_R, // 设备信息(按索引) 
    DAITYPE_PARAM_TOTAL_R, // 参数总数(状态/控制/设置) 
    DAITYPE_PARAM_INFO_R, // 参数定义信息 
    DAITYPE_ALARM_TOTAL_R, // 报警总数 
    DAITYPE_ALARM_INFO_R, // 报警定义信息 
    DAITYPE_STAT_PARAMVALUE_R, // 读取实时状态参数值 
    DAITYPE_SETT_PARAMVALUE_RW, // 读写设置参数 
    DAITYPE_CTRL_PARAMVALUE_RW, // 写控制参数(发控制命令) 
    DAITYPE_ALARMSTAT_R, // 读取报警状态 
    DAITYPE_GETLOCALLIST_VERSION, // OCPP 本地授权列表版本 
    DAITYPE_COMMPORT_R, // 读取通信端口信息 
    DAITYPE_VIRTUAL_PARAMVALUE_W, // Reporter 写入虚拟参数                    
    DAITYPE_COMM_DATA_DUMP, // 通信数据转储 
    ... 
} DAI_TYPE;

4.8 报告器层(moduals/reporters/)

报告器负责实现对外协议,向上级系统、后台或用户界面上报数据和处理指令。每个报告器运行在独立线程,通过 DAI 接口访问数据,通过事件机制感知系统状态变化。

4.8.1 报告器数据模型(reporter_model.h)

typedef struct tagReporter { 
    REPORTER_INFO info; // ID、名称、驱动库名、配置字符串 
    void *pDataHeap; // 报告器私有数据 
    HANDLE hSamplerLib; // 动态库句柄 
    COMMPORT *pUsedCommPort; // 使用的通信端口 
    HANDLE hThread; // 报告器主线程 
    BOOL bExit; // 退出标志 
    HANDLE hqEvent; // 事件队列(接收 DPR 的事件推送) 
    DAI *pDAI; // 数据访问接口指针 
    INIT_PROC Init; // 初始化函数(启动线程) 
} REPORTER;

4.8.2 已实现的报告器插件


4.9 事件驱动机制

事件机制是各层间解耦通信的核心手段。

4.9.1 事件类型(event_model.h)

typedef enum tagEvent_Type { 
    EVENT_TYPE_ALARM, // 报警触发/消除 
    EVENT_TYPE_CFG_CHANGED, // 配置变更 
    EVENT_TYPE_CHARGING, // 充电状态变化 
    EVENT_TYPE_USER_AUTHORIZE, // 用户授权 
    EVENT_TYPE_SYSTEM_EXIT, // 系统退出 
    EVENT_TYPE_COMM_FAIL, // 通信失败 
    EVENT_TYPE_CHARGEREC_UPDATE, // 充电记录更新 
    EVENT_TYPE_FIRMWARE_UPDATE, // 固件升级状态 
    EVENT_TYPE_DIAGNOSTIC_UPLOAD, // 诊断日志上传         
    EVENT_TYPE_METERVAL_UPDATE_IN_CHARGING,// 充电过程中电表值更新     
    EVENT_TYPE_BMSRUNDATA_UPDATE, // BMS 运行数据更新 
    EVENT_TYPE_HEALTHDATA_UPDATE, // 健康数据更新 
    EVENT_TYPE_REFRESH_ALARM, // 刷新报警 
    ... 
} _EVENT_TYPE;

4.9.2 事件对象(EVENT_ARG)

typedef struct tagEventArg { 
    _EVENT_TYPE emEvType; // 事件类型 
    _EVENT_MSG emMsgType; // 消息阶段(BEGIN/STATUS_CHANGED/END/RESTORE) 
    UINT nEvDataSize; // 数据大小 
    void *pEvData; // 事件数据指针 
    BOOL bHighPriority; // 高优先级(插队到队头) 
    BOOL bBlockMode; // 阻塞模式(等待处理完成) 
    HANDLE hSender; // 发送者句柄 
    EVENT_RECEIPT pfnReceipt; // 处理完成回调 
} EVENT_ARG;

4.9.3 事件流向

Sampler (数据变化/报警触发) 
│ DPR->Set(SETTYPE_SENDMSG) 
▼ 
DPR 检测到状态变化 → 生成 EVENT_ARG 
│ Queue_Put(s_pChargeThis->hqEvent) 
▼ 
主事件循环 (main.c) 取出事件 
│ dataprocessor.DoEvent(&ev_arg) 
▼ 
DPR 广播给所有已注册的 EVENT_HANDLER 
│ 遍历 EVENT_HANDLER_REGISTER 列表 
├──▶ Reporter A 的 hqEvent (OCPP 关心 CHARGING 事件) 
├──▶ Reporter B 的 hqEvent (IEC104 关心 ALARM 事件) 
└──▶ Reporter C 的 hqEvent (GUI 关心所有事件)

4.10 配置数据库(SQLite)

系统配置采用 SQLite 数据库文件(config/System.cfg),核心表结构如下:

SQLite 运行策略:

  • 启动阶段用 序列化模式(SQLITE_CONFIG_SERIALIZED)安全加载配置

  • 切换到运行时后调用 Set_SqliteDB_2RunTime_Mode(),优化为高并发模式

  • 通过 文件锁 防止多进程冲突(系统升级时其他进程可能操作数据库)


五、系统可靠性设计

5.1 多重故障保护机制

硬件看门狗(WDT)

│ 每次 ThreadManager 心跳时喂狗(60秒内必须喂一次)

│ ThreadManager 心跳监控(每 60 秒检查所有线程)

│ 每个业务线程循环调用 RUN_THREAD_HEARTBEAT()

│

信号处理(SIGSEGV/SIGILL/SIGBUS 等) → 记录日志 → longjmp 退出 → WDT 超时 → 系统 reboot

进程心跳(Do_Proc_HeartBeat) → 通知 sys_hand 进程,app 进程仍在存活 → sys_hand 若检测 app 消失,可触发 SIGUSR1 信号通知

5.2 优雅退出机制

longjmp 机制:从任意深度快速返回主函数 
void Main_Exit(int nExitCode) { 
    // 通知所有 Sampler/Reporter 退出 
    for(n = 0; n < nTotal; n++) pSampler->bExit = TRUE;  
    WatchDog_Close(FALSE); // 不禁用看门狗(让 WDT 触发重启)
    longjmp(s_JumpBufFaultError, nExitCode); // 跳回 main 的 setjmp 点
}

// 在 setjmp 接收点: 
    int nExitCode = setjmp(s_JumpBufFaultError); 
    if(nExitCode != 0) { 
        RunThread_ManagerExit(10000, TRUE); // 等待最多 10 秒让所有线程退出         
        _exit(nExitCode); 
}

六、构建系统

6.1 构建体系(Makefile + include.mk)

交叉编译工具链

GCC_PREFIX = arm-linux-gnueabihf- # ARM 目标平台

CC = ccache $(GCC_PREFIX)gcc # 使用 ccache 加速编译

构建目标:

- libpub.so 公共基础库

- libcomm_xxx.so 各通信端口驱动(8种)

- sampler_xxx.so 各采样器插件(12种)

- reporter_xxx.so 各报告器插件(15种)

- nccu_app 主程序可执行文件

平台差异

ifeq ($(NCCU_CROSS_PLATFORM), arm)

CFLAGS += -D_HAS_FLASH_MEM=1 # 启用看门狗、闪存支持

endif

ifeq ($(MU_TYPE), mu3)

CFLAGS += -DIMMU3 # IMMU3 硬件平台差异

endif

6.2 第三方依赖


七、架构优缺点总结

优点

不足与值得关注的点


八、架构图总览

┌────────────────────────────────────────────────────────────────────────┐ │ NCCU 软件系统架构 │ │ │ │ ┌──────────────────────── 硬件平台 ─────────────────────────────┐ │ │ │ AM335x ARM Cortex-A8 | IMMU2 / IMMU3 | Linux 内核 │ │ │ │ 硬件看门狗 /dev/wd | CAN Bus | RS485 | 触摸屏 │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │ │ ┌── main/ 主控层 ────────────────────────────────────────────────┐ │ │ │ main.c: 启动序列 | 事件主循环 | longjmp 退出机制 │ │ │ │ sig_handler.c: 信号处理 watch_dog.c: 硬件看门狗控制 │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │ 统一使用

●P│ ┌── publib/ 基础库层(libpub.so)────────────────────────────────┐ │ │ │ iThread: 线程管理+心跳 iQueue: 线程安全队列 iMutex: 互斥锁 │ │ │ │ iLog/iLog4c: 日志框架 iDataBase: SQLite封装 iNew: 内存池 │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌── include/data_model/ 数据模型层(共享类型定义)─────────────────┐ │ │ │ CHARGER → DEVICE → PARAMETER(状态/控制/设置) → ALARM │ │ │ │ DATA_PROCESSOR(函数指针集) │ DAI(函数指针集) │ EVENT_ARG │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ ┌──────────────┬───────────────────┬──────────────────┐ │ │ ┌──▼──────────┐ ┌─▼──────────────┐ ┌──▼────────────────┐│ │ │ │ DPR(核心 │ │ Sampler 层 │ │ Reporter 层 ││ │ │ │ 数据总线) │ │ 采样器插件 │ │ 报告器插件 ││ │ │ │ 参数管理 │ │ charger.so │ │ ocpp.so ││ │ │ │ 事件分发 │ │ kwhmeter.so │ │ modbus.so ││ │ │ │ 报警判定 │ │ system.so │ │ iec104.so ││ │ │ │ 历史持久化 │ │ ccu.so ... │ │ mqtt.so ... ││ │ │ └──────────┘ └────────┬───────┘ └────────┬──────────┘│ │ │ ▲ DPR.Get/Set │ │ DAI.Get/Set/Query │ │ ┌──┴─────────── data_interfaces/DAI 层 ──┘ │ │ │ └──────────────────────────────────────────────────┘ │ │ │ │ │ ┌── comm_ports/ 通信端口 HAL 层(动态库)────────────────────────┐ │ │ │ serial.so │ tcpip.so │ udp.so │ websocket.so │ │ │ │ httpftp.so │ can.so │ ipc.so │ websocket_ex.so │ │ │ └──────────────────────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────────────────────────┘


九、关键设计模式总结

┌────────────┬────────────────────────────────────────────────────────────────────────────────────┐ │ 设计模式 │ 在系统中的体现 │ ├────────────┼────────────────────────────────────────────────────────────────────────────────────┤ │ 插件模式 │ Sampler/Reporter/CommPort 均以 .so 形式存在,dlopen 动态加载,主程序不依赖具体实现 │ ├────────────┼────────────────────────────────────────────────────────────────────────────────────┤ │ HAL 抽象 │ CommPort 统一 6 个函数接口(Open/Accept/Read/Write/Control/Close),屏蔽硬件差异 │ ├────────────┼────────────────────────────────────────────────────────────────────────────────────┤ │ 观察者模式 │ Reporter 通过 Register_EvHandler 向 DPR 注册感兴趣的事件类型,DPR 广播时精准投递 │ ├────────────┼────────────────────────────────────────────────────────────────────────────────────┤ │ 命令模式 │ 控制命令封装为 INTER_QUEUE_CMD 投入采样器命令队列,异步执行 │ ├────────────┼────────────────────────────────────────────────────────────────────────────────────┤ │ 门面模式 │ DAI 作为统一门面,Reporter 无需了解 DPR 内部实现,仅调用 Get/Set/Query/Signal │ ├────────────┼────────────────────────────────────────────────────────────────────────────────────┤ │ 策略模式 │ DATA_PROCESSOR 和 DAI 均为函数指针结构体,可替换不同实现策略 │ ├────────────┼────────────────────────────────────────────────────────────────────────────────────┤ │ 心跳守卫 │ ThreadManager + WDT 构成双重"死锁/卡死"守卫,确保系统自愈 │ └────────────┴────────────────────────────────────────────────────────────────────────────────────┘


以上即为本工程的完整软件架构分析文档。整个系统体现了工业嵌入式软件的典型设计哲学:以稳定性为第一优先级,以可扩展性为第 二优先级,通过插件化和接口抽象实现协议的灵活扩展,同时通过多重硬件与软件守卫机制保障 7×24 小时不间断运行。

More from this blog

BMS Fault Diagnosis: Reliable Fault Detection Using a Two-State State Machine and Filtering Delay

前言 电池管理系统(BMS)在储能、电动汽车、UPS等高压直流应用中承担着核心的安全守卫职责。一个错误的故障判断——无论是漏报还是误 报——都可能带来严重后果:漏报导致过充过放损毁电芯甚至起火,误报导致系统频繁保护停机、影响生产和用户体验。 如何在噪声环境下做到"不该报的不报,该报的必须报"?本文基于真实固件代码,深入剖析其故障诊断体系的设计思路与实现细节。 一、两态故障状态机:简单却不简陋 1

Jul 14, 20268 min read

Integrated PV-Storage-Load Forecasting: Empowering EMS with Predictive Intelligence

光伏-储能-负荷联合预测:给 EMS 装上"预知能力" 本文结合我正在维护的一套工业储能管理系统的真实工程实践,聊聊如何把预测能力嵌入 EMS 决策链路。代码片段均来自项目实际文件,不是伪代码。 一、为什么 EMS 需要预测? 我在调试这套系统的 MQTT 指令链路时,发现一个有意思的现象:EMS 的充放电决策完全依赖当前时刻的 SOC 和预设的时段计划表。翻开 ems_management

Jun 8, 20264 min read

电力需求响应——SEMS/REMS层级架构下的多站协调控制

作者:储能系统固件开发工程师日期:2026-04-14项目背景:工商业储能管理系统(BESS),支持川崎项目多站联合需求响应,SEMS为站级本地EMS,REMS为上级远程EMS 一、为什么需要两级EMS? 最初,项目只有一个站级EMS(站级能量管理系统,SEMS)。SEMS负责控制本站的PCS充放电,执行时段计划、防逆流、防过载,运行得还算平稳。 然而,当客户在川崎项目提出"多个储能站联合响应

Apr 14, 20267 min read

BMS

14 posts