你有没有过这样的经历?正在赶项目、存数据时,市电突然中断,屏幕瞬间变黑——但服务器、电脑却能平稳衔接,继续正常运行。

这背后,全靠UPS不间断电源的“默默守护”。而支撑UPS精准响应、稳定工作的核心,正是它的“大脑”——嵌入式固件。

今天,我们就来拆解一套商用UPS的真实固件源码,看看这个“电力守护者”的软件架构,是如何在极度有限的资源下,实现全方位的电力管理的。

文章来源于微信公众号:光储能源站

一、写在前面:资源受限下的极致设计

我们拆解的这套固件,来自XXX的在线式UPS,运行在一颗极其“朴素”的16位单片机上——Renesas R8C/2D,仅拥有64KB Flash(相当于一张老式软盘的容量)、5KB RAM(不到手机内存的千万分之一)。

就是在这样苛刻的资源限制下,它要同时完成6大核心任务:

  • ⚡ 逆变器输出控制(电压、频率、相位同步,保证供电稳定)
  • 🔋 电池充放电管理(延长电池寿命,精准把控电量)
  • 🛡️ 多重保护逻辑(过压、欠压、过流等,避免设备损坏)
  • 📺 LCD/LED面板显示(实时反馈UPS工作状态)
  • 📡 RS232/SNMP通信协议(支持本地+网络监控)
  • ⏱️ 实时任务调度(多个任务有序协作,不卡顿、不遗漏)

看似不可能完成的任务,答案就藏在它的软件架构里。

二、整体架构:四层金字塔,各司其职不混乱

这套软件的架构就像一座金字塔,从底到顶分为四层,每层职责清晰、层层调用,既保证了解耦,又提升了可维护性。

这种分层设计的优势,用一张表就能看明白:

优势 说明
解耦 上层不关心底层实现,底层变化不影响上层(比如换单片机,只需修改驱动层)
复用 驱动层和模块层可直接移植到其他UPS项目,减少重复开发
测试 每层可独立测试,出现问题能快速定位(比如显示异常,直接查驱动层)
维护 新人接手时,按层理解即可,无需通读全部代码

三、底层:自研RTOS内核,极致精简才够用

3.1 为什么不直接用FreeRTOS?

很多嵌入式开发者会问:市面上有FreeRTOS、μC/OS等成熟内核,为什么还要自己写?

答案很简单:资源不够用

这颗MCU只有5KB RAM,而FreeRTOS光内核本身就要占用几KB,再加上任务栈、变量,根本不够用。于是工程师们量身定制了一个极简RTOS内核,整个内核只有4个文件,不到500行代码!

它的核心数据结构——任务控制块(TCB),也精简到了极致:

// 核心数据结构:任务控制块
typedef struct{
    OS_STK  *OSTCBStkPtr;       // 栈指针(任务切换的关键)
    INT16U   TimerPeriod;       // 定时器周期
    INT16U   TimerCnt;          // 定时器计数
    TASK_EVENT OSEvent;         // 事件标志
    TASK_EVENT OSEventBitMask;  // 事件掩码
} OS_TCB;

3.2 优先级查找:O(1)的效率魔法

任务调度的核心问题是:在多个就绪任务中,谁先占用CPU?

传统做法是遍历所有任务找最高优先级,时间复杂度是O(n)——任务越多,响应越慢。但这个自研内核用了一个巧妙的查表法,直接把时间复杂度降到O(1)。

// 优先级查找表:输入4位值,输出最低位1的位置
const INT8U OSUnMapTbl[16] = {            
    255, 0, 1, 0,    // 0000→无, 0001→0, 0010→1, 0011→0
    2  , 0, 1, 0,    // 0100→2, 0101→0, 0110→1, 0111→0
    3  , 0, 1, 0,    // ...
    2  , 0, 1, 0
};

// 就绪表是一个16位整数,每一位代表一个任务是否就绪
// 例如:OSRdyMap = 0x0022 = 0b00100010
//       表示优先级1和5的任务就绪,应选择优先级1

INT8U OSFindHighPrio(void) {
    if(OSRdyMap & 0x000F)  // 检查优先级0-3
        return OSUnMapTbl[OSRdyMap & 0x000F];
    else if(OSRdyMap & 0x00F0)  // 检查优先级4-7
        return OSUnMapTbl[(OSRdyMap>>4) & 0x000F] + 4;
    // ...以此类推
}

一次查表,就能直接找到最高优先级的任务,哪怕有16个任务,响应速度也不会变慢——这就是嵌入式开发中“极致优化”的体现。

四、驱动层:硬件的“翻译官”,移植起来超简单

驱动层的核心职责,就是把复杂的硬件操作(比如操作LCD、读取AD值),封装成简单的函数接口,让上层软件不用关心硬件细节。

4.1 驱动目录结构

4.2 驱动设计模式:三文件分离

每个驱动都遵循“配置-端口-实现”的三文件分离模式,这也是它能快速移植的关键。以EEPROM驱动为例:

// 1. 配置文件:定义硬件参数(eep_config.h)
#define EEPROM_PAGE_SIZE    64
#define EEPROM_ADDR         0xA0

// 2. 硬件端口映射:定义引脚(eep_hard_ports.h)
#define EEPROM_SCL_PIN      P3_0
#define EEPROM_SDA_PIN      P3_1

// 3. 驱动实现:提供接口函数(eep_driver.c)
void EEPROM_Init(void);
void EEPROM_Write(INT16U addr, INT8U *data, INT16U len);
void EEPROM_Read(INT16U addr, INT8U *data, INT16U len);

如果要把这套驱动移植到另一颗单片机,只需修改“硬件端口映射”文件,不用动核心的读写逻辑——极大降低了移植成本。

五、模块层:UPS的“智能核心”,算法都在这里

模块层是整个系统的“大脑中枢”,包含了UPS所有的“智能决策”,比如逆变器如何调节电压、电池如何充电、出现故障如何保护。

5.1 模块划分:各司其职,互不干扰

模块 文件 核心职责
逆变器 module_inverter.c 输出电压调节、频率控制、相位同步
电池 module_battery.c 充电控制、容量计算、状态监测
母线 module_bus.c 直流母线电压控制
旁路 module_bypass.c 旁路切换逻辑(市电异常时快速切换)
其他模块 市电/负载/输出/保护/ECO 市电检测、负载计算、故障保护、节能模式

5.2 模块设计原则:高内聚、低耦合

每个模块都遵循“自己的事自己做,不插手别人的事”的原则,模块之间只通过接口函数通信,绝不直接访问对方的变量。以逆变器模块为例:

// 模块内部结构(module_inverter.c)

// 1. 私有参数(只能本模块访问,对外隐藏)
static INT16U wInverterVoltHigh;    // 过压阈值
static INT16U wInverterVoltLow;     // 欠压阈值

// 2. 输出变量(供其他模块读取,不能直接修改)
INT16U wRInverterVoltNew;           // 实时输出电压

// 3. 功能函数(内部实现,对外隐藏)
static void sRInverterVoltRegu(...) // PI电压调节器
{
    // 增量式PI算法,实现电压精准控制
    dwDelt = ((wRKp + wRKi) * wRVoltError - wRKp * wRVoltError0) >> 14;
    wRNewSinAmp += dwDelt;
}

// 4. 接口函数(对外暴露,供其他模块调用)
INT16U swGetRInverterVoltNew(void)  // 获取实时电压
{
    return wRInverterVoltNew;
}

void sSetInverterVoltSet(INT16U volt)  // 设置目标电压
{
    wInverterVoltSet = volt;
}

这种设计的好处是:如果逆变器的控制算法需要修改,只需改动本模块,不会影响电池、保护等其他模块——后期维护起来极其方便。

5.3 逆变器控制:双PI参数的“智慧调节”

逆变器的核心是电压调节,既要快速响应电压变化,又要保证输出稳定。这套固件用了“双PI参数”的设计,完美解决了这个问题:

void sRInverterVoltRegu(INT16U wRTrackVolt, ...)
{
    // 计算目标电压与实际电压的误差
    wRVoltError = wRTrackVolt - wRInverterVoltNew;

    // 关键:根据误差大小切换PI参数
    if(wRVoltError >= wRErrorLimit) {
        // 误差大(比如市电突然中断):用快速响应参数
        wRKp = wRKp2;  // 较大的比例系数,快速拉近误差
        wRKi = wRKi2;  // 较大的积分系数,加快响应速度
    } else {
        // 误差小(电压接近稳定):用精细调节参数
        wRKp = wRKp1;  // 较小的比例系数,避免波动
        wRKi = wRKi1;  // 较小的积分系数,精准稳定
    }

    // 增量式PI计算,调整正弦波幅值(控制输出电压)
    dwDelt = ((wRKp + wRKi) * wRVoltError - wRKp * wRVoltError0) >> 14;
    wRNewSinAmp += dwDelt;
}

简单说:误差大时“猛踩油门”,快速响应;误差小时“轻踩刹车”,精准稳定——这就是UPS输出电压能保持平稳的核心原因。

六、任务层:九大任务协作,有序不混乱

模块层提供了核心算法,而任务层则负责调度这些算法,让它们在合适的时间执行。系统共有9个任务,按优先级从高到低排列,确保关键任务优先执行。

6.1 任务全景图

优先级 任务名 周期 核心职责
0(最高) sTaskModeSwitch 1秒 UPS模式切换(市电↔电池↔旁路)
1 sTaskOnOff 1秒 开关机控制,保障设备安全启停
2 sTaskUtility 4毫秒 市电质量检测(电压、频率是否正常)
3 sTaskPanel 500毫秒 LCD/LED面板显示更新,反馈工作状态
4 sTaskOutput 10毫秒 输出电压控制,调用逆变器模块算法
5 sTaskMisc 4毫秒 风扇、温度等杂项控制,保障设备散热
6 sTaskUart1 10毫秒 SNMP网络通信,支持远程监控
7 sTaskSavePara 1秒 参数保存到EEPROM,防止断电丢失
8(最低) sTaskUart0 事件驱动 RS232本地通信,支持本地调试

6.2 任务的工作方式:无限循环+事件驱动

每个任务都是一个无限循环,不会主动退出,而是通过“等待事件”来触发执行——这样既能保证实时性,又能节省CPU资源。以模式切换任务为例:

void sTaskModeSwitch(void)
{
    while(1) {
        // 等待定时器事件(每1秒触发一次)
        OSMaskEventPend(OS_EVENTID_TIMER);

        // 执行模式切换逻辑
        if(市电正常 && 当前是电池模式) {
            切换到市电模式(); // 市电恢复,切换回市电供电
        }
        else if(市电异常 && 当前是市电模式) {
            切换到电池模式(); // 市电中断,切换到电池供电
        }

        // 任务结束,自动让出CPU,给其他任务执行
    }
}

6.3 任务间通信:事件标志“握手”

任务之间不会直接调用,而是通过“事件标志”来通信,这样能最大限度降低耦合。比如市电检测任务发现市电异常,会通知模式切换任务:

// 任务A:市电检测任务(sTaskUtility)
void sTaskUtility(void)
{
    while(1) {
        OSMaskEventPend(OS_EVENTID_TIMER); // 每4毫秒检测一次

        if(检测到市电异常()) {
            // 发送“市电异常”事件,通知模式切换任务
            OSEventSend(PRIO_MODE_SWITCH, OS_EVENTID_UTILITY_FAIL);
        }
    }
}

// 任务B:模式切换任务(sTaskModeSwitch)
void sTaskModeSwitch(void)
{
    while(1) {
        // 等待定时器事件 或 市电异常事件
        event = OSMaskEventPend(OS_EVENTID_TIMER | OS_EVENTID_UTILITY_FAIL);

        if(event & OS_EVENTID_UTILITY_FAIL) {
            切换到电池模式(); // 收到事件,立即执行切换
        }
    }
}

七、中断服务程序:实时响应的“紧急通道”

有些事情必须“立即响应”,不能等任务轮询——比如AD采样、PWM生成,这就是中断的职责。中断就像一条“紧急通道”,一旦触发,CPU会暂停当前任务,优先执行中断程序。

7.1 核心中断列表

中断 触发时机 核心职责
isr_sine_point.c 每个PWM周期 AD采样、正弦波生成(最核心的中断)
isr_zero_cross.c 过零点 频率/相位测量,保证输出与市电同步
isr_uart.c 串口收发 通信数据缓冲,避免数据丢失
isr_500us_tick.c 每500μs 系统滴答、任务调度,保证任务周期准确

7.2 中断与任务的配合:快慢分离

中断负责“快速采集、快速响应”,任务负责“慢速处理、逻辑决策”,两者配合,既保证了实时性,又避免了CPU资源浪费:

// 中断:快速采集(必须快,不能占用太多时间)
void isr_sine_point(void)
{
    // 读取AD值(电压、电流),快速完成
    wInvVoltAD = ADC_read();
    wInvCurrAD = ADC_read();

    // 生成PWM信号,控制逆变器输出
    PWM_update(wSinTable[wIndex]);
}

// 任务:慢速处理(可以稍慢,负责算法逻辑)
void sTaskOutput(void)
{
    while(1) {
        OSMaskEventPend(OS_EVENTID_TIMER); // 每10毫秒执行一次

        // 电压调节算法(调用模块层函数)
        sRInverterVoltRegu(wTargetVolt, ...);
    }
}

八、数据流:从AD采样到PWM输出的完整闭环

我们可以追踪一个完整的控制闭环,看看UPS是如何精准控制输出电压的——整个闭环在几十微秒内完成,确保电压稳定:

九、通信协议:UPS的“对外语言”

UPS需要与外界通信——接受监控、配置参数,甚至远程关机,这就需要一套标准的通信协议。

9.1 VPCP协议:UPS的核心通信协议

这套固件实现了自定义的VPCP协议,帧结构清晰,便于解析:

常用命令也很简洁,比如:

#define CMD_GET_STATUS      0x01    // 获取UPS工作状态
#define CMD_GET_VOLTAGE     0x02    // 获取输出电压
#define CMD_SET_VOLTAGE     0x03    // 设置目标电压
#define CMD_SHUTDOWN        0x04    // 远程关机命令

9.2 双串口设计:本地+网络双监控

系统设计了两个串口,分别负责本地和网络监控,满足不同场景需求:

十、启动流程:从上电到运行,一步都不能少

UPS上电后,不是直接进入工作状态,而是要经过一系列初始化流程,确保系统稳定启动:

十一、设计亮点总结:极致优化,稳字当头

这套固件最让人佩服的,不是复杂的算法,而是在资源极度受限的情况下,做到了“极致优化”和“稳定可靠”,总结下来有三大亮点:

11.1 资源优化:每1字节都用在刀刃上

优化点 手法 效果
RTOS内核 自研极简版,仅保留核心功能 仅占几百字节,节省大量RAM
任务栈 静态分配,共享空间 所有任务栈总共仅1.5KB
优先级查找 查表法 时间复杂度O(1),响应更快
常量存储 放在Flash中,不占用RAM 最大化节省宝贵的RAM资源

11.2 可靠性设计:多重保障,杜绝故障

  • 看门狗:防止程序跑飞,出现异常自动复位
  • 参数校验:读取EEPROM数据时,验证有效性,避免错误参数导致故障
  • 多重保护:涵盖过压、欠压、过流、短路、过温等,全方位保护UPS和负载
  • 故障记录:自动记录历史故障,便于后期诊断和维护

11.3 可维护性设计:分工清晰,便于迭代

  • 分层架构:四层结构,职责清晰,新人易上手
  • 模块化:高内聚、低耦合,修改一个模块不影响其他模块
  • 接口封装:隐藏底层实现细节,上层调用更简单
  • 配置分离:参数集中管理,修改参数无需改动核心代码

十二、写在最后

拆解完这套UPS固件,我最大的感受是:好的嵌入式软件,从来不是“炫技”,而是“务实”。

没有华丽的框架,没有复杂的设计模式,只有几百行精简的内核、清晰的分层架构、精准的算法优化——工程师们用最朴素的代码,在极其有限的资源下,实现了稳定可靠的电力保护功能。

如果这篇文章对你有帮助,欢迎点赞、在看、转发,让更多人看到。

欢迎关注微信公众号:光储能源站

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐