4、FTU软件架构:嵌入式实时操作系统(RTOS)选型、任务调度机制、前后台系统与RTOS对比

各位做FTU开发的同行,咱们今天聊聊软件架构。说实话,FTU这玩意儿看着不起眼,但软件架构选错了,后面调试能让你怀疑人生。我最早做FTU时,用的就是前后台系统,那时候觉得够用就行。直到有一次现场出了个故障,保护动作慢了200毫秒,差点酿成大祸。从那以后,我对RTOS的敬畏心就上来了。

4.1 前后台系统 vs RTOS:到底差在哪?

先说说前后台系统。说白了,就是一个大循环(后台)加中断(前台)。代码结构大概这样:

void main(void)
{
    while(1)
    {
        // 后台:轮询所有任务
        scan_io();          // 扫描IO
        check_protection(); // 检查保护逻辑
        comm_task();        // 通信任务
        // ... 其他任务
    }
}

// 前台:中断服务
void TIMER_IRQHandler(void)
{
    // 处理定时中断
    adc_sample();
}

这种架构有什么问题?我举个例子。假设你正在执行check_protection(),这时候来了个通信中断。嗯,中断优先级高,CPU立刻跑去处理通信。等通信处理完回来,保护逻辑可能已经执行到一半了。如果这时候恰好发生了故障,你说危不危险?

RTOS就不一样了。它把每个功能模块拆成独立的任务,由调度器统一管理。每个任务有自己的优先级,高优先级的任务可以抢占低优先级的。就像这样:

// RTOS下的任务定义
void task_protection(void *arg)
{
    while(1)
    {
        // 保护逻辑,最高优先级
        check_fault();
        vTaskDelay(1);  // 让出CPU
    }
}

void task_comm(void *arg)
{
    while(1)
    {
        // 通信任务,中等优先级
        process_comm();
        vTaskDelay(10);
    }
}

void task_display(void *arg)
{
    while(1)
    {
        // 显示任务,最低优先级
        update_lcd();
        vTaskDelay(100);
    }
}

核心区别一句话总结:前后台系统是「谁抢到算谁的」,RTOS是「谁重要谁先上」。

4.2 RTOS选型:我踩过的坑

选RTOS这事儿,我吃过不少亏。最早用FreeRTOS,觉得开源免费挺好。结果有一次做低功耗FTU,发现它的tickless模式在Cortex-M0上跑得不太稳定。后来换了uC/OS-II,稳定是稳定,但商业授权费不便宜。

我个人建议,选型时重点看这几个维度:

评估维度 具体要求 我的经验
实时性 中断响应时间 < 10μs FTU保护动作要求ms级,但中断响应必须μs级
内存占用 RAM < 32KB,ROM < 128KB 我见过有人用uC/OS-III,光内核就吃掉64KB ROM
任务数量 支持16-32个任务即可 FTU一般8-12个任务就够用
生态支持 有中文文档、社区活跃 FreeRTOS社区最活跃,遇到问题好解决

我的推荐:如果项目预算充足,用RT-Thread。国产的,文档全,而且针对电力行业有专门优化。如果预算紧张,FreeRTOS加一些裁剪也够用。

4.3 任务调度机制:优先级怎么定?

任务调度是RTOS的灵魂。FTU里常见的调度方式有三种:

  • 抢占式调度:高优先级任务随时可以打断低优先级任务。保护任务必须用这个。
  • 时间片轮转:同优先级任务轮流执行。适合通信、显示这类非紧急任务。
  • 协作式调度:任务主动让出CPU。嗯,这个在FTU里基本不用,太不可控了。

优先级怎么定?我有个原则:越紧急、越重要的任务,优先级越高。具体来说:

// FTU任务优先级分配(数值越小优先级越高)
#define PRIO_PROTECTION    0   // 保护逻辑,最高
#define PRIO_FAULT_DETECT  1   // 故障检测
#define PRIO_SAMPLE        2   // 采样任务
#define PRIO_COMM          3   // 通信任务
#define PRIO_DISPLAY       4   // 显示任务
#define PRIO_IDLE          5   // 空闲任务,最低

这里有个坑要注意。我曾经把采样任务优先级设得比保护逻辑还高,结果采样频繁打断保护逻辑,导致保护动作时间不稳定。后来才明白,采样可以用DMA,根本不需要CPU频繁介入。

避坑指南:我曾经在项目中把通信任务优先级设得太高,结果通信数据量大的时候,保护任务被饿死了。记住:通信可以等,保护不能等!

4.4 任务间通信:怎么传数据?

任务之间要交换数据,比如采样任务要把数据传给保护任务。RTOS提供了几种机制:

  • 消息队列:适合传递结构化数据。比如采样值、故障标志。
  • 信号量:适合做同步。比如通知保护任务「采样完成,可以计算了」。
  • 事件标志组:适合多条件触发。比如「过流且零序电流超标」才启动保护。
  • 共享内存:效率最高,但要注意互斥访问。

我个人习惯用消息队列。举个例子:

// 定义消息结构
typedef struct {
    uint16_t ia;    // A相电流
    uint16_t ib;    // B相电流
    uint16_t ic;    // C相电流
    uint16_t i0;    // 零序电流
} sample_msg_t;

// 创建消息队列
QueueHandle_t xSampleQueue = xQueueCreate(10, sizeof(sample_msg_t));

// 采样任务发送数据
void task_sample(void *arg)
{
    sample_msg_t msg;
    while(1)
    {
        msg.ia = read_adc(ADC_CH_A);
        msg.ib = read_adc(ADC_CH_B);
        msg.ic = read_adc(ADC_CH_C);
        msg.i0 = read_adc(ADC_CH_0);
        
        // 发送到队列,如果满了就丢弃旧数据
        xQueueOverwrite(xSampleQueue, &msg);
        vTaskDelay(1);  // 1ms采样周期
    }
}

// 保护任务接收数据
void task_protection(void *arg)
{
    sample_msg_t msg;
    while(1)
    {
        // 阻塞等待新数据
        if(xQueueReceive(xSampleQueue, &msg, portMAX_DELAY) == pdTRUE)
        {
            // 执行保护逻辑
            check_overcurrent(msg.ia, msg.ib, msg.ic);
            check_earth_fault(msg.i0);
        }
    }
}

小技巧:消息队列的深度不要设太大。FTU里10个深度足够了。设太大反而浪费RAM,要知道嵌入式设备内存金贵着呢。

4.5 实际项目中的经验总结

做了这么多年FTU,我总结了几条铁律:

  1. 中断服务程序要短。能做的事越少越好,最好只是发个信号量或消息,具体处理交给任务。
  2. 任务优先级不要超过5级。优先级太多反而增加调度开销,而且容易出优先级反转的问题。
  3. 用看门狗监控关键任务。我习惯给保护任务单独设一个硬件看门狗,一旦任务卡死,立刻复位。
  4. 任务栈大小要留余量。我一般按计算值的1.5倍分配。曾经因为栈溢出导致系统随机重启,查了三天才找到原因。

最后说一句,RTOS不是万能的。如果你的FTU功能很简单,就两三个中断处理,前后台系统完全够用。但一旦涉及多任务、实时性要求高,还是老老实实用RTOS吧。毕竟,电力系统安全无小事,你想想看,一个误动或拒动,可能就是一个变电站停电的事故。

好了,这一章就聊到这儿。下一章咱们深入讲讲FTU的故障检测算法,那才是真正考验功力的地方。