4、任务划分与优先级设计:电梯核心任务与优先级分配策略

好,咱们接着聊。任务划分和优先级设计,这玩意儿说难不难,说简单也不简单。我见过太多项目,代码写得花里胡哨,结果一上电,电梯门关到一半卡住了,或者楼层信号丢了。说白了,就是任务没分清楚,优先级没排明白。

电梯系统是个典型的实时控制系统。你想想看,一个轿厢在高速运行,门在开合,外头还有人按按钮。这些事必须同时干,还得干得漂亮。那怎么办?靠RTOS的任务调度。

4.1 电梯核心任务划分

我个人习惯,先把电梯系统拆成四个大块:主控、轿厢、门控、通讯。每个块再往下拆。

任务模块 核心职责 典型周期/触发方式
主控任务 楼层决策、方向判断、停靠逻辑 事件触发 + 10ms轮询
轿厢任务 速度曲线计算、位置追踪、平层校准 1ms定时中断
门控任务 门电机控制、防夹检测、门状态机 5ms轮询
通讯任务 外呼面板、内呼面板、群控交互 20ms轮询 + 中断

嗯,这里要注意。主控任务不是跑得越快越好。我见过有人把主控放在1ms的定时器里跑,结果CPU全耗在决策上了,门控反而没时间处理。得不偿失。

4.2 主控任务:大脑级别的存在

主控任务,说白了就是电梯的脑子。它负责接收所有请求,然后决定:往哪走?停不停?

我在项目中遇到过一个问题:电梯在16楼,有人按了1楼,又有人按了18楼。主控任务如果处理不好,电梯就会在中间来回晃悠。后来我加了个“方向锁定”机制——一旦决定了上行,就先把所有下行请求缓存起来,等到了最高点再处理。

// 主控任务核心逻辑(伪代码)
void main_control_task(void) {
    while(1) {
        // 等待事件触发(按钮、传感器、定时)
        wait_event(EVENT_FLOOR_REQ | EVENT_TIMER_10MS);
        
        // 收集所有请求
        collect_all_requests();
        
        // 方向决策
        if (current_direction == UP) {
            // 只处理上行请求
            if (has_up_request_above()) {
                keep_going_up();
            } else {
                // 检查是否有下行请求
                if (has_down_request_below()) {
                    change_direction(DOWN);
                } else {
                    idle_at_current_floor();
                }
            }
        }
        
        // 平层判断
        if (is_at_floor() && has_request_for_this_floor()) {
            open_door();
        }
        
        // 喂狗
        feed_watchdog();
    }
}
我的小技巧:主控任务里别做太多计算。把速度曲线、位置追踪这些活儿扔给轿厢任务。主控只管“去哪儿”,不管“怎么去”。

4.3 轿厢任务:速度与位置的双重把控

轿厢任务,这是最考验实时性的地方。电梯能不能平稳停靠,全靠它。

我记得有一次,客户投诉电梯停靠时总是“咯噔”一下。查了半天,发现是轿厢任务的优先级设低了,被通讯任务抢了CPU。结果速度曲线计算延迟了几毫秒,电机响应跟不上,就产生了顿挫感。

轿厢任务的核心是速度曲线生成。我一般用S型曲线,加减速平滑,乘客体验好。

// 轿厢速度曲线计算(简化版)
void car_speed_task(void) {
    static float current_speed = 0;
    static float target_speed = 0;
    static float accel = 0.5;  // 加速度 m/s²
    
    while(1) {
        // 每1ms执行一次
        wait_event(EVENT_TIMER_1MS);
        
        // 获取当前位置
        position = get_encoder_position();
        
        // 计算目标速度(根据剩余距离)
        float remaining = target_floor_position - position;
        if (remaining < DECEL_DISTANCE) {
            target_speed = calculate_decel_speed(remaining);
        }
        
        // 速度平滑过渡
        if (current_speed < target_speed) {
            current_speed += accel * 0.001;  // 1ms步进
        } else {
            current_speed -= accel * 0.001;
        }
        
        // 输出给变频器
        set_speed_command(current_speed);
        
        // 平层检测
        if (abs(remaining) < LEVELING_THRESHOLD) {
            signal_floor_reached();
        }
    }
}
注意:轿厢任务绝对不能阻塞!我曾经见过有人在这里加了printf调试,结果电梯直接冲过层。调试信息请用DMA或者环形缓冲区,别直接打印。

4.4 门控任务:安全第一

门控任务,看着简单,其实坑最多。电梯事故里,门相关的事故占了很大比例。

门控任务的核心是状态机。开门、关门、防夹、重开门,每个状态都要处理得滴水不漏。

// 门控状态机
typedef enum {
    DOOR_CLOSED,
    DOOR_OPENING,
    DOOR_OPEN,
    DOOR_CLOSING,
    DOOR_REOPENING,
    DOOR_FAULT
} door_state_t;

void door_control_task(void) {
    while(1) {
        wait_event(EVENT_TIMER_5MS);
        
        switch(door_state) {
            case DOOR_CLOSED:
                if (door_open_request) {
                    start_motor(OPEN_DIRECTION);
                    door_state = DOOR_OPENING;
                }
                break;
                
            case DOOR_OPENING:
                if (is_door_fully_open()) {
                    stop_motor();
                    door_state = DOOR_OPEN;
                    // 开始关门计时
                    start_timer(DOOR_HOLD_TIME);
                }
                // 防夹检测(开门时也要检测)
                if (is_obstacle_detected()) {
                    // 反向?不,开门时遇到障碍继续开
                    // 但需要记录日志
                    log_obstacle();
                }
                break;
                
            case DOOR_CLOSING:
                if (is_obstacle_detected()) {
                    // 防夹!立即反向
                    start_motor(OPEN_DIRECTION);
                    door_state = DOOR_REOPENING;
                }
                if (is_door_fully_closed()) {
                    stop_motor();
                    door_state = DOOR_CLOSED;
                    signal_door_locked();
                }
                break;
                
            // ... 其他状态
        }
    }
}
关键点:门控任务的防夹检测必须放在最高优先级的中断里处理。别问我为什么,我曾经因为防夹检测延迟了20ms,夹坏了一个快递员的包裹。从那以后,防夹检测我都是独立硬件中断 + 独立任务。

4.5 通讯任务:别让它拖后腿

通讯任务,负责跟外头的人打交道。外呼面板、内呼面板、群控系统,全得靠它。

通讯任务有个特点:它不要求特别高的实时性,但要求可靠性。你按了按钮,电梯得知道。如果通讯丢了,乘客就会觉得电梯坏了。

我一般用CAN总线或者RS485。通讯协议里加个超时重传机制,确保数据不丢。

// 通讯任务(CAN总线示例)
void comm_task(void) {
    can_message_t msg;
    
    while(1) {
        wait_event(EVENT_TIMER_20MS);
        
        // 发送心跳包
        send_heartbeat();
        
        // 接收所有待处理消息
        while(can_receive(&msg)) {
            switch(msg.id) {
                case CAN_ID_OUTER_CALL:
                    // 外呼请求
                    add_floor_request(msg.data.floor, msg.data.direction);
                    break;
                case CAN_ID_INNER_CALL:
                    // 内呼请求
                    add_car_request(msg.data.floor);
                    break;
                case CAN_ID_GROUP_CTRL:
                    // 群控指令
                    handle_group_command(msg.data);
                    break;
            }
        }
        
        // 检查通讯超时
        check_comm_timeout();
    }
}

4.6 优先级分配策略:谁该跑得快?

优先级分配,说白了就是决定谁先跑。电梯系统里,安全相关的任务必须跑得快,非安全相关的可以慢点。

我常用的优先级分配策略是这样的:

优先级 任务/中断 理由
最高(0) 防夹检测中断 人身安全,必须立即响应
高(1) 轿厢任务(速度控制) 实时性要求极高,影响平层精度
中(2) 主控任务 决策逻辑,可以容忍几毫秒延迟
中低(3) 门控任务 门动作本身有机械延迟,5ms内响应即可
低(4) 通讯任务 20ms轮询足够,丢包可以重传
最低(5) 日志/诊断任务 不紧急,有空再跑
我的经验:别把优先级设得太满。留一两个空闲优先级,万一以后要加功能呢?我曾经因为优先级用完了,不得不重构整个调度器,那叫一个痛苦。

4.7 防止优先级反转的方法

优先级反转,这是个经典问题。简单说就是:低优先级任务占着资源不放,高优先级任务等着用,结果中优先级任务插进来把低优先级任务抢了,高优先级任务就一直等。

电梯系统里,最典型的就是共享资源访问。比如轿厢任务和主控任务都要读取楼层传感器数据,如果不用好互斥机制,就会出问题。

我常用的方法有三种:

  1. 优先级继承协议:低优先级任务在持有锁时,临时提升到等待该锁的最高优先级任务的优先级。这样中优先级任务就抢不过它了。
  2. 优先级天花板协议:每个锁都有一个“天花板优先级”,谁拿到锁,谁的优先级就升到天花板。简单粗暴,但有效。
  3. 关中断:对于极短的操作(比如读一个寄存器),直接关中断。但注意,关中断时间不能超过10微秒,否则会影响实时性。
// 使用优先级继承的互斥锁示例
void read_sensor_data(void) {
    // 获取互斥锁(带优先级继承)
    mutex_lock(&sensor_mutex);
    
    // 读取传感器
    position = read_encoder();
    speed = read_speed_sensor();
    
    // 释放锁
    mutex_unlock(&sensor_mutex);
}
避坑指南:我曾经在一个项目里用了普通的二值信号量保护共享数据,结果高优先级任务等了整整200ms才拿到数据。电梯在那一瞬间失去了位置信息,差点冲顶。从那以后,我所有共享资源都用优先级继承互斥锁。

嗯,还有个细节。如果你用的RTOS不支持优先级继承,那就手动实现一个。或者干脆用消息队列代替共享内存。消息队列天然避免了优先级反转,因为数据是拷贝的,不是共享的。

好了,任务划分和优先级设计就聊到这儿。下一章咱们聊聊电梯的故障诊断与容错设计。那也是个有意思的话题。