3、实时操作系统选型:FreeRTOS任务调度、中断管理、任务间通信(队列/信号量)、时钟节拍配置
好,咱们进入第三章。这一章聊的是实时操作系统的选型,具体到FreeRTOS在电池化成系统里的实战用法。
说实话,我最早做电池化成项目时,用的是裸机循环加定时器中断。那时候觉得RTOS太复杂,没必要。后来项目规模一上来——同时管理几十路充放电通道,还要实时采集电压电流、处理故障保护、跟BMS主控通信……裸机那套根本撑不住。一个中断卡住,整个系统就乱套了。
所以,选FreeRTOS,说白了就是选它的轻量、稳定、社区活跃。而且它完全免费,对嵌入式MCU非常友好。下面我按几个核心模块来讲。
3.1 任务调度:优先级与时间片
FreeRTOS是抢占式调度内核。什么意思?高优先级任务就绪了,低优先级任务立马让路。这在化成系统里特别重要——比如过压保护任务必须最高优先级,哪怕正在处理通信数据,也得先停下来保护电池。
我个人习惯把任务分成三个等级:
- 紧急任务(优先级最高):故障检测、过压/过流保护、温度超限处理。这些任务必须在几毫秒内响应。
- 周期任务(中等优先级):电压电流采集、SOC估算、均衡控制。一般10ms~100ms周期执行一次。
- 后台任务(低优先级):数据记录、日志输出、人机界面刷新。慢一点没关系。
举个例子,我曾在项目中遇到一个问题:均衡控制任务和通信任务优先级设反了。结果均衡指令发出去,通信任务一直抢CPU,均衡迟迟不执行,电池压差越来越大。后来把均衡任务优先级提到通信之上,问题就解决了。
时间片轮转也要注意。同优先级的任务,FreeRTOS默认用时间片(比如1ms)轮流执行。但化成系统里,我建议尽量少用同优先级任务——你想想看,万一两个同优先级任务互相抢时间片,关键采样可能被延迟。能用不同优先级就用不同优先级。
3.2 中断管理:ISR里别干重活
中断是实时系统的命脉。FreeRTOS里,中断服务函数(ISR)要遵循一个铁律:快进快出。
我在项目中见过有人把整个电压采集、滤波、SOC计算全放在中断里。结果中断执行时间太长,导致低优先级中断丢失,系统时钟都乱了。嗯,这里要注意:中断里只做最必要的事——比如读取寄存器、设置标志位、通过队列或信号量通知任务。
FreeRTOS提供了专门的中断安全API:
xQueueSendFromISR():在中断里发送数据到队列xSemaphoreGiveFromISR():在中断里释放信号量vTaskNotifyGiveFromISR():在中断里通知任务
这些函数会返回一个pxHigherPriorityTaskWoken参数。如果中断唤醒了更高优先级的任务,你需要在中断退出前做一次上下文切换。否则,高优先级任务可能不会立即执行。
我曾经踩过一个坑:在ADC中断里用xQueueSendFromISR发送采样数据,但忘了检查pxHigherPriorityTaskWoken。结果数据处理任务优先级比中断低,数据一直积压在队列里,系统响应越来越慢。后来加上切换判断,一切正常。
核心原则:中断里只做“通知”和“传递”,不做“处理”。处理逻辑交给任务。
3.3 任务间通信:队列与信号量
任务之间怎么交换数据?FreeRTOS提供了队列和信号量两种主要方式。
队列(Queue)
队列是“生产者-消费者”模型。一个任务往队列里放数据,另一个任务取数据。队列自带互斥保护,不用担心数据竞争。
在化成系统里,我常用队列传递采集数据:
// 创建队列,每个元素是结构体,长度20
QueueHandle_t xAdcQueue = xQueueCreate(20, sizeof(AdcSample_t));
// 中断中发送
void ADC_IRQHandler(void) {
AdcSample_t sample;
sample.voltage = ADC->DR;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(xAdcQueue, &sample, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// 任务中接收
void vAdcProcessTask(void *pvParameters) {
AdcSample_t sample;
while(1) {
if(xQueueReceive(xAdcQueue, &sample, portMAX_DELAY) == pdPASS) {
// 处理采样数据
ProcessVoltage(sample.voltage);
}
}
}
注意队列长度要合理。我一般设成采样周期的2~3倍,防止突发数据丢失。但也不能太大,否则浪费RAM。
信号量(Semaphore)
信号量更像一个“事件通知”。比如某个任务要等所有通道采集完成才能开始均衡计算。这时可以用二值信号量:
SemaphoreHandle_t xBalanceSemaphore = xSemaphoreCreateBinary();
// 每个通道采集完成后释放信号量
void vChannelDoneCallback(uint8_t ch) {
xSemaphoreGive(xBalanceSemaphore);
}
// 均衡任务等待所有通道完成
void vBalanceTask(void *pvParameters) {
while(1) {
// 等待信号量
if(xSemaphoreTake(xBalanceSemaphore, portMAX_DELAY) == pdPASS) {
// 执行均衡算法
DoCellBalancing();
}
}
}
还有一种计数信号量,适合控制资源访问。比如最多允许3个任务同时访问EEPROM,用计数信号量初始值为3,每次访问前take,访问完give。
我的经验:队列适合传递数据块,信号量适合传递“事件”。别混用。如果你既要传递数据又要通知事件,用队列就够了。
3.4 时钟节拍配置:Tick的学问
FreeRTOS的时钟节拍(Tick)是系统的心跳。所有延时、超时、时间片轮转都依赖它。
配置Tick频率在FreeRTOSConfig.h里:
#define configTICK_RATE_HZ ((TickType_t)1000) // 1ms一个Tick
1000Hz是常用值。但化成系统里,我建议根据实际需求调整:
- 高精度控制(比如PWM调节、快速保护):用1000Hz甚至2000Hz。但Tick越频繁,CPU开销越大。
- 普通采集(100ms周期):用100Hz就够了。省下的CPU时间可以给其他任务。
我曾经在一个低功耗项目里把Tick降到100Hz,结果系统功耗降了30%。但代价是任务延时精度变差——vTaskDelay(1)实际是10ms。所以你要权衡。
还有一个容易忽略的点:Tick中断的优先级。FreeRTOS要求Tick中断优先级不能高于configMAX_SYSCALL_INTERRUPT_PRIORITY。否则中断里调用FreeRTOS API会出问题。我见过有人把Tick中断设成最高优先级,结果系统直接死机。嗯,这个坑踩过就记住了。
警告:不要随意修改Tick中断优先级。保持默认或按FreeRTOS文档配置。否则系统调度会乱套。
3.5 避坑指南:我踩过的几个雷
最后分享几个实战中容易犯的错误:
- 任务栈大小设太小:FreeRTOS不会自动检查栈溢出。我建议每个任务至少分配256字节,复杂任务512字节以上。用
uxTaskGetStackHighWaterMark()监控剩余栈空间。 - 中断里调用非ISR安全函数:比如
printf()、malloc()。这些函数不可重入,在中断里调用会导致死锁。我曾经在中断里打印调试信息,结果系统卡死,查了两天才发现。 - 队列或信号量创建失败:内存不足时
xQueueCreate会返回NULL。一定要检查返回值,否则后续操作会崩溃。 - 忘记配置Tick中断:有些MCU需要手动使能SysTick中断。如果没使能,FreeRTOS无法调度,任务永远不执行。
好了,这一章就到这里。FreeRTOS在化成系统里的应用,说白了就是三个字:快、稳、轻。任务调度要快,中断处理要稳,资源占用要轻。下一章我们聊聊具体的化成控制算法——恒流恒压怎么切换,SOC怎么估算。到时候见。