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,我总结了几条铁律:
- 中断服务程序要短。能做的事越少越好,最好只是发个信号量或消息,具体处理交给任务。
- 任务优先级不要超过5级。优先级太多反而增加调度开销,而且容易出优先级反转的问题。
- 用看门狗监控关键任务。我习惯给保护任务单独设一个硬件看门狗,一旦任务卡死,立刻复位。
- 任务栈大小要留余量。我一般按计算值的1.5倍分配。曾经因为栈溢出导致系统随机重启,查了三天才找到原因。
最后说一句,RTOS不是万能的。如果你的FTU功能很简单,就两三个中断处理,前后台系统完全够用。但一旦涉及多任务、实时性要求高,还是老老实实用RTOS吧。毕竟,电力系统安全无小事,你想想看,一个误动或拒动,可能就是一个变电站停电的事故。
好了,这一章就聊到这儿。下一章咱们深入讲讲FTU的故障检测算法,那才是真正考验功力的地方。