攻克蓝桥杯(4)——第八届蓝桥杯嵌入式省赛电梯调度算法实战解析
1. 电梯调度算法基础与赛题解析
第一次看到第八届蓝桥杯嵌入式省赛的电梯调度题目时,我的内心是崩溃的。题目要求实现一个四层电梯的控制系统,需要处理按键响应、运行方向判断、楼层排序等复杂逻辑。这不仅仅是简单的GPIO控制,更考验我们对经典调度算法的理解和嵌入式实现能力。
电梯调度算法的核心目标是高效响应乘客请求。在资源受限的STM32F103RBT6上实现时,我们需要特别关注几个关键点:首先是内存占用,全局变量不宜过多;其次是实时性,算法不能有太高的时间复杂度;最后是稳定性,要避免死锁等异常情况。
常见的电梯调度算法有SCAN(电梯算法)、LOOK算法、SATF(最短寻道时间优先)等。经过对比分析,我选择了LOOK算法的变种来实现,因为它在保证公平性的同时,具有较好的效率。具体来说,就是电梯会持续朝一个方向运行,直到该方向没有请求时才会调转方向。
2. 硬件平台与外设配置
我使用的是官方指定的CT117E开发板,主控为STM32F103RBT6。在CubeMX中的配置需要特别注意几个关键点:
首先是定时器的配置。我们需要TIM3用于通用计时(处理1秒、6秒等时间逻辑),TIM4产生PWM波控制电机模拟电梯升降,TIM15用于按键消抖。具体参数设置如下:
// TIM3基础配置 htim3.Instance = TIM3; htim3.Init.Prescaler = 7200-1; // 72MHz/7200=10kHz htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 10000-1; // 10kHz/10000=1Hz htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; // TIM4 PWM配置 htim4.Instance = TIM4; htim4.Init.Prescaler = 72-1; // 72MHz/72=1MHz htim4.Init.CounterMode = TIM_COUNTERMODE_UP; htim4.Init.Period = 1000-1; // 1MHz/1000=1kHz PWM htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;GPIO配置方面,四个楼层按键(F1-F4)需要设置为上拉输入模式,LED控制引脚设置为推挽输出。特别注意要开启对应的GPIO时钟和中断(如果需要):
// 按键GPIO配置 GPIO_InitStruct.Pin = F1_Pin|F2_Pin|F3_Pin|F4_Pin; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);3. 核心算法实现细节
3.1 请求队列管理
电梯系统需要维护三个关键数组:目标楼层数组tar_level、上行队列go_up和下行队列go_down。我使用了简单的数组结构来存储这些信息:
uint8_t tar_level[4]; // 目标楼层,最大容量4 uint8_t go_up[3]; // 上行队列 uint8_t go_down[3]; // 下行队列 uint8_t up_cnt = 0; // 上行计数器 uint8_t down_cnt = 0; // 下行计数器当按下楼层按键时,系统需要判断该请求是上行还是下行。这里有一个容易出错的细节:同一楼层的请求在不同情况下可能属于不同方向。例如,当前在1楼按下3楼是上行请求,而当前在4楼按下3楼则是下行请求。
if(HAL_GPIO_ReadPin(F3_GPIO_Port,F3_Pin) == 0 && now_level !=3) { tar_level[cnt] = 3; if(now_level<3) { up_down[cnt]=1; // 上行请求 } else { up_down[cnt]=2; // 下行请求 } cnt++; }3.2 调度策略实现
核心调度逻辑在floor_rank()和up_down_jug()函数中实现。floor_rank()负责将原始请求分类到上行或下行队列,up_down_jug()则决定电梯当前运行方向。
void floor_rank(void) { int t = strlen((char *)up_down); for(int i=0; i<t; i++) { if(up_down[i] == 0x02) { go_down[q++] = tar_level[i]; // 加入下行队列 } else if(up_down[i] == 1) { go_up[p++] = tar_level[i]; // 加入上行队列 } } memset(up_down,0,3); memset(tar_level,0,3); } void up_down_jug(void) { if(strlen((char *)go_up) > 0) { flag_up = 1; flag_down = 0; // 设置上行标志 } else if(strlen((char *)go_down) > 0) { flag_down = 1; flag_up = 0; // 设置下行标志 } else { flag_down = flag_up = 0; // 无请求 } }3.3 运行控制与楼层更新
电梯运行控制是项目中最复杂的部分,需要考虑多种状态转换。我使用了一个状态机模型,主要包含以下几个状态:
- 等待状态(无请求)
- 加速状态(启动初期)
- 匀速运行状态
- 减速状态(接近目标楼层)
- 停靠状态(开门、关门)
楼层更新逻辑在floor_add_dec()函数中实现,每6秒改变一次楼层:
if(sec_6 == 1 && (strlen((char *)go_up)>0 || strlen((char *)go_down)>0)) { if(flag_up && !arrive_tar) { now_level++; if(now_level == go_up[up_cnt]) { arrive_tar = 1; // 到达目标楼层 } } else if(flag_down && !arrive_tar) { now_level--; if(now_level == go_down[down_cnt]) { arrive_tar = 1; // 到达目标楼层 } } }4. 调试技巧与性能优化
在开发过程中,我遇到了几个典型问题及解决方案:
问题1:按键响应不灵敏
- 原因:机械按键存在抖动,直接读取会导致多次触发
- 解决方案:增加300ms的延时消抖
if(HAL_GPIO_ReadPin(F1_GPIO_Port,F1_Pin) == 0) { HAL_Delay(300); // 消抖处理 if(HAL_GPIO_ReadPin(F1_GPIO_Port,F1_Pin) == 0) { // 确认按键按下 } }问题2:电梯运行不流畅
- 原因:主循环中处理太多任务,导致响应延迟
- 优化方案:将部分功能移到定时器中断处理
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim->Instance == TIM3) { time_cnt++; if(time_cnt >= 6000) { // 6秒到达 sec_6 = 1; time_cnt = 0; } } }问题3:LCD显示闪烁
- 原因:频繁刷新导致
- 优化方案:只在数据变化时更新显示
if(last_level != now_level) { sprintf((char *)str1," %d",now_level); LCD_DisplayStringLine(Line4,str1); last_level = now_level; }在资源优化方面,我做了以下改进:
- 将部分字符串常量改为指针引用,减少内存占用
- 使用位域结构体压缩标志位存储
- 合理使用const修饰符,将常量放入Flash而非RAM
- 优化算法时间复杂度,避免多层嵌套循环
5. 完整系统集成与测试
将所有模块集成后,需要进行系统级测试。我设计了以下几种测试场景:
基本功能测试:
- 从1楼依次按下2、3、4楼,验证上行顺序
- 从4楼依次按下3、2、1楼,验证下行顺序
- 混合按下不同楼层,验证调度逻辑
边界条件测试:
- 在当前楼层按下相同楼层(应无响应)
- 快速连续按下多个楼层
- 电梯运行过程中新增请求
压力测试:
- 同时按下所有楼层按键
- 长时间运行测试稳定性
测试过程中发现的一个有趣现象是:当电梯正在上行时,如果按下比当前楼层低的楼层,该请求会被正确加入下行队列,但不会立即响应,而是等完成所有上行请求后再处理。这正好体现了LOOK算法的特点。
6. 工程架构与代码规范
为了提高代码可维护性,我采用了模块化设计:
Elevator_Control/ ├── Inc/ │ ├── elevator.h // 主要数据结构与宏定义 │ ├── io_config.h // GPIO引脚定义 │ └── timer_config.h // 定时器配置 ├── Src/ │ ├── main.c // 主循环与初始化 │ ├── elevator.c // 核心调度算法 │ ├── io_control.c // 按键与LED控制 │ └── lcd_display.c // 显示相关函数 └── Drivers/ // HAL库文件在编码规范方面,我特别注意以下几点:
- 全局变量加前缀"g_"便于识别
- 函数名使用动宾结构,如"get_current_floor()"
- 关键代码段添加详细注释
- 保持一致的缩进风格(4个空格)
- 复杂逻辑拆分为小函数,每个函数只做一件事
7. 常见问题解决方案
在实际开发中,我遇到了几个典型问题,这里分享解决方案:
问题1:电梯运行方向判断错误
- 现象:有时会错误地改变运行方向
- 原因:标志位没有及时清除
- 修复:在完成所有同方向请求后再清除标志位
if(up_cnt == strlen((char *)go_up) && up_cnt != 0) { memset(go_up,0,3); up_cnt = 0; flag_up = 0; // 明确清除标志位 }问题2:楼层显示跳变
- 现象:LCD显示的楼层号偶尔会跳变
- 原因:变量类型不匹配导致溢出
- 修复:统一使用uint8_t类型存储楼层信息
uint8_t now_level = 1; // 明确指定无符号类型问题3:定时器中断冲突
- 现象:有时定时器中断会互相干扰
- 解决方案:合理设置中断优先级
HAL_NVIC_SetPriority(TIM3_IRQn, 1, 0); HAL_NVIC_SetPriority(TIM4_IRQn, 2, 0);8. 进阶优化思路
完成基础功能后,还可以考虑以下优化方向:
- 动态权重调度: 为不同楼层请求设置优先级,例如:
- 长时间等待的请求提高优先级
- 紧急呼叫最高优先级
typedef struct { uint8_t floor; uint32_t wait_time; // 等待时间 uint8_t priority; // 优先级 } ElevatorRequest;- 能耗优化: 根据运行状态调整PWM占空比,在匀速阶段降低能耗
void adjust_pwm(uint8_t mode) { if(mode == ACCELERATE) { __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, 800); // 80%占空比 } else if(mode == CRUISE) { __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, 500); // 50%占空比 } }- 预测算法: 基于历史数据预测可能请求,提前准备响应
- 多电梯协同: 扩展为多电梯系统,实现负载均衡
在资源允许的情况下,还可以增加更多实用功能:
- 语音提示功能
- 故障自检与恢复
- 远程监控接口
- 能耗统计显示
这个项目让我深刻体会到,嵌入式开发不仅仅是写代码,更需要考虑硬件特性、实时性要求和资源限制之间的平衡。调试过程中,逻辑分析仪和STM32CubeMonitor工具帮了大忙,它们可以直观地展示系统运行状态,快速定位问题。
