1. 项目概述与设计思路
作为一名参加过多次蓝桥杯嵌入式竞赛的老选手,今天我想分享第12届真题"车辆管理系统"的完整实现方案。这个项目要求基于STM32G4系列单片机开发一套智能停车场管理系统,实现车辆出入库管理、自动计费、状态显示等核心功能。
整个系统采用模块化设计思路,主要包含以下几个功能模块:
- LCD显示模块:实时展示车位状态和收费信息
- 按键控制模块:实现页面切换和功能操作
- LED指示模块:直观显示剩余车位和系统状态
- 串口通信模块:处理车辆信息收发和计费反馈
在硬件选型上,使用STM32G431RB作为主控芯片,搭配官方提供的CT117E开发板,充分利用其丰富的外设资源。系统软件架构采用"前后台"模式,主循环处理常规任务,中断服务程序响应实时事件。
2. 硬件平台搭建与初始化
2.1 开发环境配置
首先需要搭建开发环境,我使用的是:
- Keil MDK 5.37作为IDE
- STM32CubeMX 6.6.1进行外设配置
- ST-Link V2作为调试工具
- 串口助手用于调试信息输出
在CubeMX中新建工程时,选择STM32G431RB芯片,配置时钟树使HSE通过PLL倍频到170MHz系统时钟。特别注意需要使能以下外设:
- USART1:9600波特率,用于与上位机通信
- TIM6:10ms定时中断,用于按键扫描
- TIM17:PWM输出,控制道闸开关
- GPIO:配置按键输入和LED输出
2.2 外设初始化代码分析
系统初始化主要包含以下几个关键部分:
c复制int main(void)
{
HAL_Init();
SystemClock_Config();
/* 外设初始化 */
MX_GPIO_Init();
MX_TIM6_Init();
MX_TIM17_Init();
MX_USART1_UART_Init();
LCD_Init();
/* 功能模块初始化 */
LCD_SET();
LCD_Data();
led_control(0);
/* 启动中断 */
HAL_TIM_Base_Start_IT(&htim6);
HAL_TIM_PWM_Start(&htim17,TIM_CHANNEL_1);
HAL_UART_Receive_IT(&huart1,(uint8_t *)rx, 22);
while(1) {
// 主循环处理
}
}
时钟配置是系统稳定运行的基础,这里采用外部8MHz晶振通过PLL倍频:
c复制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;
// PLL配置
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 3;
RCC_OscInitStruct.PLL.PLLN = 20;
RCC_OscInitStruct.PLL.PLLP = 2;
RCC_OscInitStruct.PLL.PLLQ = 2;
RCC_OscInitStruct.PLL.PLLR = 2;
HAL_RCC_OscConfig(&RCC_OscInitStruct);
// 系统时钟配置
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2);
}
提示:在竞赛环境中,时钟配置通常已经由组委会提供,但理解其原理对于调试和优化非常重要。PLL参数需要根据具体芯片型号和需求调整。
3. 核心功能模块实现
3.1 LCD显示模块设计
LCD模块负责展示系统状态信息,设计了两类显示页面:
数据页面(Data):显示当前车位状态
c复制void LCD_Data(void)
{
LCD_Clear(Black);
LCD_DisplayStringLine(Line2, (uint8_t *)" Data");
sprintf(text," CNBR:%d",CNBR_Data); // 普通车辆数量
LCD_DisplayStringLine(Line4, (uint8_t *)text);
sprintf(text," VNBR:%d",VNBR_Data); // VIP车辆数量
LCD_DisplayStringLine(Line6, (uint8_t *)text);
sprintf(text," IDLE:%d",IDLE_Data); // 空闲车位
LCD_DisplayStringLine(Line8, (uint8_t *)text);
}
参数页面(Para):显示收费标准
c复制void LCD_Para(void)
{
LCD_Clear(Black);
LCD_DisplayStringLine(Line2, (uint8_t *)" Para");
sprintf(text," CNBR:%.2f",CNBR_Para); // 普通车费率
LCD_DisplayStringLine(Line4, (uint8_t *)text);
sprintf(text," VNBR:%.2f",VNBR_Para); // VIP车费率
LCD_DisplayStringLine(Line6, (uint8_t *)text);
}
注意事项:LCD显示需要处理刷新频率问题,频繁全屏刷新会导致闪烁。实际应用中可以采用局部刷新策略,只更新变化的数据部分。
3.2 按键处理模块实现
按键模块采用状态机设计,实现消抖和多种操作检测:
c复制struct key {
uint8_t stage; // 状态机阶段
uint8_t value; // 当前键值
uint8_t single; // 单击标志
uint8_t change_flag; // 切换标志
};
struct key key[4]={0}; // 4个按键状态
void Key_process(void)
{
// 读取按键状态
key[0].value = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);
key[1].value = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
key[2].value = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);
key[3].value = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
// 状态机处理
for(int i=0 ; i < 4 ; i++) {
switch(key[i].stage) {
case 0: // 初始状态
if(key[i].value == 0) key[i].stage = 1;
break;
case 1: // 消抖确认
if(key[i].value == 0) key[i].stage = 2;
else key[i].stage = 0;
break;
case 2: // 释放检测
if(key[i].value == 1) {
key[i].single = 1;
key[i].change_flag++;
if(key[i].change_flag >= 2) key[i].change_flag = 0;
key[i].stage = 0;
}
break;
}
}
}
按键功能映射表:
| 按键 | 短按功能 | 长按功能 |
|---|---|---|
| KEY1 | 切换Para页面 | 无 |
| KEY2 | 切换Data页面 | 无 |
| KEY3 | 费率增加0.5 | 无 |
| KEY4 | 费率减少0.5 | 开启/关闭PWM |
实操心得:按键消抖时间需要根据实际硬件调整,通常10-20ms为宜。状态机方式比简单延时更高效,能同时检测多种操作。
3.3 LED指示模块设计
LED模块用4个LED表示不同状态组合:
c复制void led_control(uint8_t ucled)
{
led_all_off();
HAL_GPIO_WritePin(GPIOC,ucled<<8,GPIO_PIN_RESET);
led_on();
}
void led_display(void)
{
if(led_flag[0] == 0 && led_flag[1] == 0)
led_control(0x00); // 全灭
else if(led_flag[0] == 0 && led_flag[1] == 1)
led_control(0x01); // LED1亮
else if(led_flag[0] == 1 && led_flag[1] == 0)
led_control(0x02); // LED2亮
else if(led_flag[0] == 1 && led_flag[1] == 1)
led_control(0x03); // LED1+LED2亮
}
LED状态编码规则:
- led_flag[0]:PWM输出状态(0-关闭,1-开启)
- led_flag[1]:车位状态(0-满位,1-有空位)
3.4 串口通信与车辆管理
串口模块处理车辆出入库的核心逻辑:
c复制void uart_process(void)
{
// 解析接收到的数据
int process_flag = sscanf((char *)rx,"%4s:%4s:%12s",
car_now.type,car_now.number,car_now.time_label);
if(process_flag == 3) { // 数据格式正确
car_out(); // 尝试出库
if(inpark_flag == 1) { // 如果未找到出库车辆
car_in(); // 执行入库
LCD_Data(); // 更新显示
}
change_key = 1; // 刷新按键状态
}
else {
error_flag = 1; // 格式错误
}
if(error_flag == 1)
sprintf(tx,"Error\r\n"); // 错误反馈
}
车辆出入库时间计算采用将日期时间转换为秒数的方式:
c复制uint64_t data_to_sensor(struct times i_time)
{
i_time.year += 2000; // 年份补全
uint64_t time_total = (i_time.year-1)*365*24*60*60; // 往年秒数
// 闰年补偿
time_total += ((i_time.year-1)/4 - (i_time.year-1)/100 + (i_time.year-1)/400)*24*60*60;
// 当月之前的天数
for(int i = 0 ; i < i_time.month - 1 ; i++) {
time_total += monthsday[i]*24*60*60;
}
// 闰年2月补偿
if(Leap_year(i_time.year) && i_time.month > 2) {
time_total += 24*60*60;
}
// 当天时间
time_total += (i_time.day-1) * 24*60*60;
time_total += i_time.hour *3600;
time_total += i_time.min *60;
time_total += i_time.second;
return time_total;
}
计费规则实现:
c复制void car_out(void)
{
// 解析时间标签
sscanf(car_now.time_label,"%2d%2d%2d%2d%2d%2d",
&car_now.time.year,&car_now.time.month,&car_now.time.day,
&car_now.time.hour,&car_now.time.min,&car_now.time.second);
// 查找匹配车辆
for(int i = 0 ; i < 8 ; i++) {
if(car[i].place_flag == 1 && (strcmp(car_now.number,car[i].number)==0)) {
inpark_flag = 0;
// 计算停留时间(小时)
uint64_t out_time = data_to_sensor(car_now.time);
uint64_t in_time = data_to_sensor(car[i].time);
hour_cnt = (out_time - in_time)/3600;
// 不足1小时按1小时计算
if((out_time - in_time)%3600 != 0) hour_cnt++;
if(hour_cnt <= 0) hour_cnt = 1;
// 计算费用
if(strcmp(car_now.type,"CNBR")==0) {
CNBR_Data--;
IDLE_Data++;
fee = hour_cnt * CNBR_Para;
car[i].place_flag = 0;
}
else if(strcmp(car_now.type,"VNBR")==0) {
VNBR_Data--;
IDLE_Data++;
fee = hour_cnt * VNBR_Para;
car[i].place_flag = 0;
}
// 生成反馈信息
sprintf(tx,"%s:%s:%d:%.2f\r\n",car_now.type,car_now.number,hour_cnt,fee);
break;
}
}
}
注意事项:时间计算需要考虑闰年和各月份天数差异,实际应用中建议使用RTC模块获取准确时间,避免依赖上位机时间数据。
4. 系统整合与主循环设计
主程序采用事件驱动架构,各功能模块通过标志位交互:
c复制int main(void)
{
// 初始化代码...
while(1) {
keynum = Key_getnum();
switch(keynum) {
case 1: // 切换到Para页面
LCD_Para();
change_flag = 1;
break;
case 2: // 切换到Data页面
LCD_Data();
change_flag = 2;
break;
case 11: // 费率增加
if(change_flag == 1) {
CNBR_Para += 0.5;
VNBR_Para += 0.5;
LCD_Para();
}
break;
case 21: // 费率减少
if(change_flag == 1) {
CNBR_Para -= 0.5;
VNBR_Para -= 0.5;
LCD_Para();
}
break;
case 31: // 开启PWM(开闸)
TIM17->CCR1 = 20;
led_flag[0] = 1;
break;
case 32: // 关闭PWM(关闸)
TIM17->CCR1 = 0;
led_flag[0] = 0;
break;
}
// 更新车位状态指示
if(IDLE_Data != 0) led_flag[1] = 1;
else led_flag[1] = 0;
led_display();
}
}
中断服务程序处理时序关键任务:
c复制// 定时器中断(10ms)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM6) {
Key_process(); // 按键扫描
}
}
// 串口接收中断
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1) {
uart_process(); // 处理车辆信息
if(inpark_flag == 0 || error_flag == 1) {
HAL_UART_Transmit(&huart1,(uint8_t *)tx,strlen(tx),50); // 发送反馈
error_flag = 0;
inpark_flag = 1;
}
HAL_UART_Receive_IT(&huart1,(uint8_t *)rx, 22); // 重新启用接收
}
}
5. 调试技巧与常见问题
5.1 调试方法推荐
-
分段调试法:先确保各模块独立工作正常
- 先调通LCD显示基础内容
- 再测试按键扫描和状态机
- 然后验证串口收发
- 最后整合全部功能
-
调试信息输出:利用串口打印关键变量值
c复制printf("CNBR_Data=%d, VNBR_Data=%d, IDLE_Data=%d\r\n", CNBR_Data, VNBR_Data, IDLE_Data); -
逻辑分析仪使用:检查PWM波形和时序
5.2 常见问题及解决方案
问题1:LCD显示乱码
- 检查初始化序列是否正确
- 确认数据传输时序符合LCD规格书要求
- 检查电源电压是否稳定
问题2:按键响应不灵敏
- 调整消抖时间(修改TIM6周期)
- 检查按键硬件电路(上拉电阻是否合适)
- 确认GPIO模式配置正确(输入带上拉)
问题3:串口数据接收不完整
- 检查波特率是否匹配(双方设置为相同值)
- 确认数据位数、停止位和校验位配置
- 增加接收超时处理机制
问题4:时间计算错误
- 验证闰年判断函数
- 检查月份天数表是否正确
- 测试边界条件(如2月28/29日、12月31日等)
5.3 性能优化建议
-
降低功耗:
- 空闲时进入低功耗模式
- 动态调整CPU频率
- 关闭未使用的外设时钟
-
提高实时性:
- 关键任务放在中断处理
- 优化算法减少计算时间
- 使用DMA传输数据
-
增强鲁棒性:
- 增加数据校验机制
- 实现看门狗定时器
- 添加异常处理流程
这个车辆管理系统虽然基于竞赛题目设计,但涵盖了嵌入式开发的多个关键技术点,包括外设驱动、中断处理、状态机设计、通信协议等。在实际项目中,还可以进一步扩展车牌识别、移动支付、云端同步等功能。