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. 固定顺序获取锁:所有任务都按同样的顺序获取锁。比如先拿锁1,再拿锁2。这样就不会出现循环等待。
  2. 使用超时机制:获取锁时不要无限等待,设置一个超时时间。超时了就放弃,稍后再试。
  3. 尽量少用嵌套锁:能用一个锁解决的问题,别用两个。锁嵌套越多,死锁风险越大。
  4. 使用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);
}

我的经验:在电梯控制系统中,我习惯把所有的共享资源编号,然后规定:所有任务必须按编号从小到大的顺序获取锁。这样,死锁问题基本就杜绝了。另外,我还会在代码里加一个“看门狗”任务,定期检查各个任务的状态,如果发现某个任务长时间卡在锁上,就主动重启它。

好了,这一章的内容就这些。内存管理、资源保护、死锁避免,这三块是电梯控制系统稳定运行的基石。你想想看,如果电梯跑到一半,内存不够了,或者两个任务互相锁死了,那后果不堪设想。所以,这些看似基础的东西,其实是最重要的。

下一章,咱们聊聊任务调度与中断管理,看看怎么让电梯的响应速度更快、更稳。