基于STM32CubeMX的电梯调度系统实战开发指南
在嵌入式系统开发领域,蓝桥杯竞赛一直是检验学习者实战能力的重要舞台。其中电梯调度题目因其综合性强、贴近实际应用场景而备受关注。本文将带领读者从零开始,使用STM32CubeMX和HAL库完整实现一个四层电梯调度系统,涵盖外设配置、算法实现到调试优化的全流程。
1. 开发环境搭建与基础配置
1.1 硬件平台选择与初始化
CT117E开发板作为蓝桥杯嵌入式竞赛的官方平台,集成了STM32F103系列MCU和丰富的外设接口。开发前需准备:
- STM32CubeMX v6.x或更高版本
- Keil MDK-ARM开发环境
- ST-Link/V2调试器
- 四层电梯模拟模块(按键、LED指示灯等)
关键外设配置表:
| 外设类型 | 功能说明 | 配置参数 |
|---|---|---|
| GPIO | 楼层按键输入 | 上拉输入模式 |
| TIM3 | 系统计时基准 | 1kHz时钟,中断使能 |
| TIM4 | PWM生成 | 通道1,72MHz/7200=10kHz |
| RTC | 实时时钟 | LSE时钟源,日历模式 |
// CubeMX生成的时钟配置代码片段 void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; // 配置HSE振荡器 RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; HAL_RCC_OscConfig(&RCC_OscInitStruct); // 配置系统时钟 RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_SYSCLK; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2); }1.2 HAL库使用注意事项
使用HAL库开发时需特别注意:
- 中断优先级配置:确保关键外设(如定时器)具有适当优先级
- 延时函数使用:避免在中断服务例程中直接调用HAL_Delay()
- 外设初始化顺序:先初始化时钟和外设,再启用中断
提示:调试阶段可启用HAL库的assert功能,有助于快速定位配置错误。
2. 电梯调度核心算法实现
2.1 请求队列管理机制
电梯系统需要高效处理来自不同楼层的请求。我们采用以下数据结构:
// 电梯状态数据结构 typedef struct { uint8_t current_floor; // 当前楼层(1-4) uint8_t target_floors[4]; // 目标楼层队列 uint8_t direction; // 0-停止 1-上行 2-下行 uint8_t door_status; // 0-关闭 1-开启 } ElevatorState; // 请求处理函数 void process_floor_request(uint8_t floor) { if(floor == elevator.current_floor) return; // 检查是否已存在相同请求 for(int i=0; i<4; i++) { if(elevator.target_floors[i] == floor) return; } // 添加到请求队列 for(int i=0; i<4; i++) { if(elevator.target_floors[i] == 0) { elevator.target_floors[i] = floor; break; } } // 自动确定运行方向 if(floor > elevator.current_floor) { elevator.direction = 1; } else if(floor < elevator.current_floor) { elevator.direction = 2; } }2.2 扫描调度算法优化
针对四层电梯场景,我们实现优化的扫描算法:
- 上行阶段:
- 收集所有高于当前楼层的请求
- 按升序依次服务
- 下行阶段:
- 收集所有低于当前楼层的请求
- 按降序依次服务
- 方向切换规则:
- 完成当前方向所有请求后检查反向请求
- 无请求时保持静止状态
void elevator_scheduler(void) { static uint32_t last_move_time = 0; // 电梯运行状态机 switch(elevator.direction) { case 0: // 停止状态 if(find_next_request()) { elevator.direction = (next_floor > elevator.current_floor) ? 1 : 2; } break; case 1: // 上行状态 if(++elevator.current_floor == next_floor) { serve_floor(elevator.current_floor); if(!find_next_request_in_direction()) { elevator.direction = find_opposite_request() ? 2 : 0; } } last_move_time = HAL_GetTick(); break; case 2: // 下行状态 if(--elevator.current_floor == next_floor) { serve_floor(elevator.current_floor); if(!find_next_request_in_direction()) { elevator.direction = find_opposite_request() ? 1 : 0; } } last_move_time = HAL_GetTick(); break; } // 超时保护 if((HAL_GetTick() - last_move_time) > 60000) { elevator.direction = 0; } }3. 关键外设驱动实现
3.1 按键检测与消抖处理
采用状态机方式实现可靠的按键检测:
// 按键状态定义 typedef enum { KEY_IDLE, KEY_PRESSED, KEY_CONFIRMED, KEY_RELEASED } KeyState; // 按键检测状态机 void detect_key_press(uint16_t pin, GPIO_TypeDef* port) { static KeyState key_state = KEY_IDLE; static uint32_t press_time = 0; switch(key_state) { case KEY_IDLE: if(HAL_GPIO_ReadPin(port, pin) == GPIO_PIN_RESET) { press_time = HAL_GetTick(); key_state = KEY_PRESSED; } break; case KEY_PRESSED: if(HAL_GPIO_ReadPin(port, pin) == GPIO_PIN_SET) { key_state = KEY_IDLE; } else if((HAL_GetTick() - press_time) > 50) { key_state = KEY_CONFIRMED; process_floor_request(get_floor_by_pin(pin)); } break; case KEY_CONFIRMED: if(HAL_GPIO_ReadPin(port, pin) == GPIO_PIN_SET) { key_state = KEY_RELEASED; } break; case KEY_RELEASED: key_state = KEY_IDLE; break; } }3.2 PWM控制电机驱动
使用TIM4生成PWM信号控制电梯模拟电机:
// 电机控制函数 void control_elevator_motor(uint8_t direction) { switch(direction) { case 1: // 上行 HAL_GPIO_WritePin(EL_DOWN_GPIO_Port, EL_DOWN_Pin, GPIO_PIN_RESET); HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1); __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, 5000); // 50%占空比 break; case 2: // 下行 HAL_GPIO_WritePin(EL_UP_GPIO_Port, EL_UP_Pin, GPIO_PIN_RESET); HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1); __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, 3000); // 30%占空比 break; default: // 停止 HAL_TIM_PWM_Stop(&htim4, TIM_CHANNEL_1); break; } }4. 系统集成与调试技巧
4.1 状态可视化实现
通过LCD实时显示电梯运行状态:
void update_lcd_display(void) { char lcd_buf[20]; // 显示当前楼层 sprintf(lcd_buf, "Floor: %d", elevator.current_floor); LCD_DisplayStringLine(LINE1, (uint8_t*)lcd_buf); // 显示运行方向 const char* dir_str = ""; switch(elevator.direction) { case 1: dir_str = "UP "; break; case 2: dir_str = "DOWN"; break; default: dir_str = "STOP"; break; } LCD_DisplayStringLine(LINE2, (uint8_t*)dir_str); // 显示目标楼层队列 sprintf(lcd_buf, "Targets: %d %d %d %d", elevator.target_floors[0], elevator.target_floors[1], elevator.target_floors[2], elevator.target_floors[3]); LCD_DisplayStringLine(LINE3, (uint8_t*)lcd_buf); }4.2 常见问题排查指南
开发过程中遇到的典型问题及解决方案:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 按键响应不稳定 | 消抖处理不当 | 增加硬件滤波电容,优化软件消抖算法 |
| 电梯运行卡顿 | 定时器配置错误 | 检查TIM3时钟配置,确认中断优先级 |
| LCD显示异常 | 初始化时序问题 | 确保LCD初始化延迟足够,检查数据线连接 |
| PWM输出不稳定 | 自动重装载值设置不当 | 调整ARR和PSC寄存器值,确保频率合适 |
注意:调试复杂状态机时,建议使用LED指示灯或串口打印关键状态变量,有助于快速定位逻辑错误。
5. 工程优化与扩展思路
5.1 性能优化策略
- 中断优化:
- 将时间关键代码放入定时器中断
- 使用DMA传输减轻CPU负担
// 定时器中断处理示例 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim->Instance == TIM3) { static uint16_t counter = 0; if(++counter >= 100) { // 100ms时间基准 counter = 0; elevator_scheduler(); } detect_all_keys(); } }5.2 功能扩展方向
紧急停止功能:
- 添加硬件急停按钮
- 实现安全中断处理
能耗统计模块:
- 记录运行时间和启停次数
- 估算能耗并显示
网络监控接口:
- 通过串口或蓝牙传输状态数据
- 开发上位机监控界面
在完成基础功能后,可以尝试将工程移植到FreeRTOS等实时操作系统,实现更复杂的任务调度和资源管理。实际测试中发现,合理的任务划分能显著提高系统响应速度,例如将按键扫描、状态更新和电机控制分别放在不同优先级的任务中处理。