4. 内存与资源管理:电梯控制系统的“血液”与“交通规则”
各位同学,咱们接着聊。电梯控制系统里,内存和资源管理,说白了就是给系统“输血”和“定规矩”。血供不上,或者交通乱了套,电梯就得趴窝。这一章,我重点讲讲内存分配策略、共享资源保护,还有怎么避免死锁。这些都是我这些年踩坑踩出来的经验。
4.1 内存分配策略:静态 vs 动态,选哪个?
先问个问题:你写代码时,变量是提前声明好,还是运行时再申请?这就是静态分配和动态分配的区别。
静态分配,就是编译时就把内存定死了。比如我定义一个全局数组:
/* 静态分配:电梯楼层状态表,最多64层 */
uint8_t floor_status[64];
这种方式的优点很明显:确定性强。系统一启动,内存就摆在那了,不会出现“哎呀,内存不够了”的尴尬。我在做第一代电梯控制器时,全部用的静态分配。为什么?因为那时候的MCU资源紧张,跑个RTOS都费劲,动态分配容易出幺蛾子。
但静态分配也有缺点:浪费。你想想看,如果一栋楼只有10层,你非得预留64层的空间,那54层的内存就白白浪费了。这在资源受限的嵌入式系统里,挺心疼的。
动态分配呢,就是运行时按需申请。比如:
/* 动态分配:根据实际楼层数申请内存 */
uint8_t *floor_status = (uint8_t*)malloc(building_floors * sizeof(uint8_t));
动态分配的好处是灵活。楼高10层,我就申请10个字节;楼高50层,我就申请50个。不浪费。但坏处也明显:不确定性。你申请时,内存可能不够;释放后,还可能产生碎片。
我的建议:在电梯控制这种硬实时系统里,优先用静态分配。如果非要用动态分配,那一定要在系统初始化阶段一次性申请完,运行期间不再动态申请释放。这叫“池化”思想。
我个人习惯,把任务栈、消息队列、事件标志组这些核心资源,全部静态分配。只有一些非关键的日志缓存,才考虑用动态。嗯,这里要注意:RTOS里的动态内存分配,一定要用线程安全的版本,比如FreeRTOS的pvPortMalloc(),而不是标准C的malloc()。
避坑指南:我曾经在一个项目里,用了标准C的malloc,结果在中断服务函数里调用,直接导致系统崩溃。后来查了半天,才发现malloc不是可重入的。从那以后,我所有RTOS项目里,动态内存只用系统提供的API。
4.2 共享资源保护:互斥锁与信号量
电梯系统里,很多资源是共享的。比如电梯的位置信息,多个任务都要读;比如门控信号,只能有一个任务去操作。这时候,就需要保护机制。
互斥锁(Mutex),说白了就是“一把钥匙开一把锁”。谁拿到钥匙,谁才能进房间。出来时把钥匙还回去。典型的用法:
/* 互斥锁保护电梯位置信息 */
static SemaphoreHandle_t xMutex;
void vTaskUpdatePosition(void *pvParameters) {
for(;;) {
/* 获取互斥锁 */
if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
/* 安全地更新电梯位置 */
g_elevator_position = get_encoder_value();
/* 释放互斥锁 */
xSemaphoreGive(xMutex);
}
}
}
void vTaskReadPosition(void *pvParameters) {
for(;;) {
if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
/* 安全地读取电梯位置 */
uint32_t pos = g_elevator_position;
xSemaphoreGive(xMutex);
/* 用pos做后续处理 */
}
}
}
信号量(Semaphore)呢,更像是一个“计数器”。比如,电梯的按钮事件,来了一个事件,信号量加1;处理一个事件,信号量减1。如果信号量为0,任务就等着。
/* 二值信号量:通知门控任务关门 */
static SemaphoreHandle_t xDoorSemaphore;
void vTaskDoorControl(void *pvParameters) {
for(;;) {
/* 等待关门信号 */
if(xSemaphoreTake(xDoorSemaphore, portMAX_DELAY) == pdTRUE) {
/* 执行关门动作 */
close_door();
}
}
}
/* 在某个中断或任务中发送信号 */
void vSendCloseDoorSignal(void) {
xSemaphoreGive(xDoorSemaphore);
}
你可能会问:互斥锁和信号量到底啥区别?我简单总结一下:
| 特性 | 互斥锁(Mutex) | 信号量(Semaphore) |
|---|---|---|
| 本质 | 所有权机制 | 计数机制 |
| 谁可以释放 | 只有持有者可以释放 | 任何任务都可以释放 |
| 典型用途 | 保护共享资源 | 任务同步、事件通知 |
| 优先级继承 | 支持(防止优先级反转) | 不支持 |
| 递归锁定 | 支持(同一任务可多次获取) | 不支持 |
注意:互斥锁一定要在同一个任务里获取和释放。我曾经见过一个新手,在任务A里获取锁,在任务B里释放,结果系统直接死锁。记住:谁拿的钥匙,谁还。
4.3 避免死锁的方法:别让系统“僵”住
死锁,就是两个或多个任务互相等待对方释放资源,结果谁也动不了。就像两个人过独木桥,谁也不让谁,最后都过不去。
我在项目中遇到过最典型的死锁场景:任务A持有锁1,等待锁2;任务B持有锁2,等待锁1。结果两个任务都卡死了。
怎么避免?我总结了几个实用方法:
- 固定顺序获取锁:所有任务都按同样的顺序获取锁。比如先拿锁1,再拿锁2。这样就不会出现循环等待。
- 使用超时机制:获取锁时不要无限等待,设置一个超时时间。超时了就放弃,稍后再试。
- 尽量少用嵌套锁:能用一个锁解决的问题,别用两个。锁嵌套越多,死锁风险越大。
- 使用RTOS提供的死锁检测:有些RTOS有死锁检测功能,比如FreeRTOS的
configUSE_RECURSIVE_MUTEXES。
举个例子,固定顺序获取锁:
/* 错误示范:可能死锁 */
void taskA(void) {
xSemaphoreTake(lock1, portMAX_DELAY);
xSemaphoreTake(lock2, portMAX_DELAY);
/* ... */
xSemaphoreGive(lock2);
xSemaphoreGive(lock1);
}
void taskB(void) {
xSemaphoreTake(lock2, portMAX_DELAY);
xSemaphoreTake(lock1, portMAX_DELAY); /* 顺序反了! */
/* ... */
xSemaphoreGive(lock1);
xSemaphoreGive(lock2);
}
/* 正确示范:统一顺序 */
void taskA(void) {
xSemaphoreTake(lock1, portMAX_DELAY);
xSemaphoreTake(lock2, portMAX_DELAY);
/* ... */
xSemaphoreGive(lock2);
xSemaphoreGive(lock1);
}
void taskB(void) {
xSemaphoreTake(lock1, portMAX_DELAY); /* 先拿锁1 */
xSemaphoreTake(lock2, portMAX_DELAY); /* 再拿锁2 */
/* ... */
xSemaphoreGive(lock2);
xSemaphoreGive(lock1);
}
我的经验:在电梯控制系统中,我习惯把所有的共享资源编号,然后规定:所有任务必须按编号从小到大的顺序获取锁。这样,死锁问题基本就杜绝了。另外,我还会在代码里加一个“看门狗”任务,定期检查各个任务的状态,如果发现某个任务长时间卡在锁上,就主动重启它。
好了,这一章的内容就这些。内存管理、资源保护、死锁避免,这三块是电梯控制系统稳定运行的基石。你想想看,如果电梯跑到一半,内存不够了,或者两个任务互相锁死了,那后果不堪设想。所以,这些看似基础的东西,其实是最重要的。
下一章,咱们聊聊任务调度与中断管理,看看怎么让电梯的响应速度更快、更稳。