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();
}
}
}
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;
// ... 其他状态
}
}
}
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 防止优先级反转的方法
优先级反转,这是个经典问题。简单说就是:低优先级任务占着资源不放,高优先级任务等着用,结果中优先级任务插进来把低优先级任务抢了,高优先级任务就一直等。
电梯系统里,最典型的就是共享资源访问。比如轿厢任务和主控任务都要读取楼层传感器数据,如果不用好互斥机制,就会出问题。
我常用的方法有三种:
- 优先级继承协议:低优先级任务在持有锁时,临时提升到等待该锁的最高优先级任务的优先级。这样中优先级任务就抢不过它了。
- 优先级天花板协议:每个锁都有一个“天花板优先级”,谁拿到锁,谁的优先级就升到天花板。简单粗暴,但有效。
- 关中断:对于极短的操作(比如读一个寄存器),直接关中断。但注意,关中断时间不能超过10微秒,否则会影响实时性。
// 使用优先级继承的互斥锁示例
void read_sensor_data(void) {
// 获取互斥锁(带优先级继承)
mutex_lock(&sensor_mutex);
// 读取传感器
position = read_encoder();
speed = read_speed_sensor();
// 释放锁
mutex_unlock(&sensor_mutex);
}
嗯,还有个细节。如果你用的RTOS不支持优先级继承,那就手动实现一个。或者干脆用消息队列代替共享内存。消息队列天然避免了优先级反转,因为数据是拷贝的,不是共享的。
好了,任务划分和优先级设计就聊到这儿。下一章咱们聊聊电梯的故障诊断与容错设计。那也是个有意思的话题。