参考
第八届蓝桥杯嵌入式省赛程序设计题解析(基于HAL库)_蓝桥杯嵌入式第八届-CSDN博客
一、CubeMx 配置
第六届 蓝桥杯 嵌入式 省赛 -CSDN博客,一样的配置,把 .ioc 文件拷贝过来就行。
二、代码编写
1. 头文件
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
//#include "adc.h"
#include "rtc.h"
#include "tim.h"
//#include "usart.h"
#include "gpio.h"
#include <stdio.h> // 提供 sprintf()
#include <string.h> // 提供 strlen()
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
//#include "lcd.h"
#include "i2c_hal.h"
/* USER CODE END Includes */
2. 全局变量
用户要定义的变量,后面缺啥补啥
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
RTC_TimeTypeDef H_M_S_Time;
RTC_DateTypeDef Y_M_D_Date;
/* USER CODE END PTD */
/* USER CODE BEGIN PV */
//*减速变量
__IO uint32_t uwTick_Key_Set_Point = 0;//控制Key_Proc的执行速度
__IO uint32_t uwTick_Led_Set_Point = 0;//控制Led_Proc的执行速度
__IO uint32_t uwTick_Lcd_Set_Point = 0;//控制Lcd_Proc的执行速度
__IO uint32_t uwTick_Usart_Set_Point = 0;//控制Usart_Proc的执行速度
//*按键扫描专用变量
uint8_t ucKey_Val, unKey_Down, ucKey_Up, ucKey_Old;
//*LED专用变量
uint8_t ucLed;
//*LCD显示专用变量
uint8_t Lcd_Disp_String[21];//最多显示20个字符
//用户自定义变量区
/* USER CODE END PV */
3. 函数声明
//main.h
/* USER CODE BEGIN EFP */
void Key_Proc(void);
void Led_Proc(void);
void Lcd_Proc(void);
//void Usart_Proc(void);
void Elev_Proc(void);
/* USER CODE END EFP */
4. 主函数
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
//MX_ADC1_Init();
//MX_ADC2_Init();
MX_TIM2_Init();
MX_TIM6_Init();
//MX_USART1_UART_Init();
MX_RTC_Init();
MX_TIM3_Init();
MX_TIM15_Init();
MX_TIM17_Init();
/* USER CODE BEGIN 2 */
LCD_Init();
LCD_Clear(White);
LCD_SetBackColor(White);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
Key_Proc();
Led_Proc();
Lcd_Proc();
//Usart_Proc();
Elev_Proc();
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
5. 函数实现
5.1 按键
按键永远是逻辑的开端。
重新审题:
4个按键代表4个楼层。
按下后点亮对应的LED灯(LD1-LD4)。
如果此时再按该楼层,则按键无效。
一次可以设置多个目标楼层。
在最后一次按键按完 1 秒之后,模拟电机开始运行。这句话其实隐含了,只能在空闲状态下进行楼层选择。
- 首先,空闲状态下有 1秒的按键判断。
- 那么,按键每次按下,都要刷新这 1秒的时间判断。
- 其次,由于是可以多目标选择,那么逻辑上4个LED灯是求或的。
- 需要加个前提,不是当前平台。
- 举个例子:如果当前在2楼,且空闲状态,此时按平台2,是没有效果的。
- 最后,LED的高4位不要动,因为要作为流水灯的。
void Key_Proc(void)
{
if((uwTick - uwTick_Key_Set_Point)<50) return;//减速函数
uwTick_Key_Set_Point = uwTick;
ucKey_Val = Key_Scan();
unKey_Down = ucKey_Val & (ucKey_Old ^ ucKey_Val);
ucKey_Up = ~ucKey_Val & (ucKey_Old ^ ucKey_Val);
ucKey_Old = ucKey_Val;
if(state == ELEV_IDLE)//空闲状态
{
if(unKey_Down == 1)//B1按压
{
if(ucPlat != 1) ucSet |= 0x01; //ucPlat的1234表示当前所处的平台
} //ucSet用于记录几号按键按下去了。只用低4位,分别对应LD4~LD1,按键B4~B1
else if(unKey_Down == 2)//B2按压
{
if(ucPlat != 2) ucSet |= 0x02;
}
else if(unKey_Down == 3)//B3按压
{
if(ucPlat != 3) ucSet |= 0x04;
}
else if(unKey_Down == 4)//B4按压
{
if(ucPlat != 4) ucSet |= 0x08;
}
ucLed &= 0xF0;
ucLed |= ucSet;
if(unKey_Down != 0)//当有按键按下去,启动计时
{
uwTick_Set_Point = uwTick;
}
}
}
5.2 流水灯
题目没说灯的流动方向和上下方向的关系。这里就设定:
上升的时候,灯从右往左
下降的时候,灯从左往右
枚举方向
typedef enum {
DIR_UP = 1,
DIR_DOWN = 2
} Direction;
static Direction dir = DIR_UP;
void Led_Proc(void)
{
if((uwTick - uwTick_Led_Set_Point)<200) return;//减速函数
uwTick_Led_Set_Point = uwTick;
LED_Disp(ucLed);
}
uint8_t Flow = 0x10;//流水的变量
void UpdateFlowLED(Direction dir) {
ucLed &= 0x0F;
ucLed |= Flow;
if (dir == DIR_UP) {
Flow = (Flow >> 1);
if(Flow == 0x08)
Flow = 0x80;
} else {
Flow = (Flow << 1);
if(Flow == 0x00)
Flow = 0x10;
}
LED_Disp(ucLed);
}
//void LED_Disp(uint8_t ucLed)在gpio.c中定义了
5.3 LCD
void Lcd_Proc(void)
{
if((uwTick - uwTick_Lcd_Set_Point)<100) return;//减速函数
uwTick_Lcd_Set_Point = uwTick;
//开机屏幕测试代码
sprintf((char *)Lcd_Disp_String, " Current Platform");
LCD_DisplayStringLine(Line1, Lcd_Disp_String);
sprintf((char *)Lcd_Disp_String, " %1d",(unsigned int)ucPlat);
LCD_DisplayStringLine(Line3, Lcd_Disp_String);
//*RTC内容显示
HAL_RTC_GetTime(&hrtc, &H_M_S_Time, RTC_FORMAT_BIN);//读取日期和时间必须同时使用
HAL_RTC_GetDate(&hrtc, &Y_M_D_Date, RTC_FORMAT_BIN);
sprintf((char *)Lcd_Disp_String, " %02d-%02d-%02d",(unsigned int)H_M_S_Time.Hours,(unsigned int)H_M_S_Time.Minutes,(unsigned int)H_M_S_Time.Seconds);
LCD_DisplayStringLine(Line6, Lcd_Disp_String);
}
5.4 状态机
stateDiagram
[*] --> Idle
Idle --> DoorClosing: 有目标楼层
DoorClosing --> DoorClosed: 关门完成
DoorClosed --> Moving: 启动电机
Moving --> Arrived: 到达目标层
Arrived --> DoorOpening: 停止电机
DoorOpening --> DoorOpened: 开门完成
DoorOpened --> Idle: 无其他目标
DoorOpened --> WaitingNext: 有其他目标
WaitingNext --> DoorClosing: 等待结束
枚举状态
typedef enum {
ELEV_IDLE, // 空闲状态(等待按键)
ELEV_DOOR_CLOSING, // 关门中
ELEV_DOOR_CLOSED, // 门已关
ELEV_MOVING, // 运行中(上下行)
ELEV_ARRIVED, // 到达目标层
ELEV_DOOR_OPENING, // 开门中
ELEV_DOOR_OPENED, // 门已开
ELEV_WAITING_NEXT // 等待下一目标
} ElevState;
static ElevState state = ELEV_IDLE;
PWM 宏定义
/* USER CODE BEGIN PD */
// 硬件配置宏(便于修改)
#define DOOR_PWM_TIM htim17 //PA7
#define DOOR_PWM_CHANNEL TIM_CHANNEL_1
#define MOTOR_PWM_TIM htim3 //PA6
#define MOTOR_PWM_CHANNEL TIM_CHANNEL_1
/* USER CODE END PD */
实现
// 主状态机函数
void Elev_Proc(void) {
if (!ucSet) return; // 无目标层时退出
switch (state) {
//------------------ 空闲状态 ------------------//
case ELEV_IDLE:
if ((uwTick - uwTick_Set_Point) >= 1000) {
state = ELEV_DOOR_CLOSING;
}
break;
//------------------ 关门过程 ------------------//
case ELEV_DOOR_CLOSING:
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // 关门方向
__HAL_TIM_SET_COMPARE(&DOOR_PWM_TIM, DOOR_PWM_CHANNEL, 250); // 50%占空比
HAL_TIM_PWM_Start(&DOOR_PWM_TIM, DOOR_PWM_CHANNEL);
LCD_ShowMessage(Line8, "Door Closing");
stateEnterTime = uwTick;
state = ELEV_DOOR_CLOSED;
break;
case ELEV_DOOR_CLOSED:
if ((uwTick - stateEnterTime) >= 4000) {
HAL_TIM_PWM_Stop(&DOOR_PWM_TIM, DOOR_PWM_CHANNEL);
LCD_ShowMessage(Line8, "Door Closed");
state = ELEV_MOVING;
}
break;
//------------------ 运行过程 ------------------//
case ELEV_MOVING: {
uint8_t targetMask = (1 << (ucPlat - 1));
if (ucSet > targetMask) {
// 上行
dir = DIR_UP;
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
__HAL_TIM_SET_COMPARE(&MOTOR_PWM_TIM, MOTOR_PWM_CHANNEL, 800); // 80%占空比
HAL_TIM_PWM_Start(&MOTOR_PWM_TIM, MOTOR_PWM_CHANNEL);
LCD_ShowMessage(Line8, "Elev Upping");
} else if (ucSet < targetMask) {
// 下行
dir = DIR_DOWN;
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
__HAL_TIM_SET_COMPARE(&MOTOR_PWM_TIM, MOTOR_PWM_CHANNEL, 600); // 60%占空比
HAL_TIM_PWM_Start(&MOTOR_PWM_TIM, MOTOR_PWM_CHANNEL);
LCD_ShowMessage(Line8, "Elev Downing");
}
stateEnterTime = uwTick;
state = ELEV_ARRIVED;
break;
}
case ELEV_ARRIVED:
if ((uwTick - stateEnterTime) >= 6000) {
// 更新楼层
ucPlat += (dir == DIR_UP) ? 1 : -1;
sprintf((char *)Lcd_Disp_String, " %1d",(unsigned int)ucPlat);
LCD_DisplayStringLine(Line3, Lcd_Disp_String);
sprintf((char *)Lcd_Disp_String, "Elev Runned 1 Floor ");
LCD_DisplayStringLine(Line8, Lcd_Disp_String);
// 检查是否到达目标层
if (ucSet & (1 << (ucPlat - 1))) {
HAL_TIM_PWM_Stop(&MOTOR_PWM_TIM, MOTOR_PWM_CHANNEL);
state = ELEV_DOOR_OPENING;
} else {
// 未到达,继续移动
stateEnterTime = uwTick;
state = ELEV_MOVING;
}
} else {
// 运行中流水灯效果
UpdateFlowLED(dir);
HAL_Delay(300);
}
break;
//------------------ 开门过程 ------------------//
case ELEV_DOOR_OPENING:
HAL_Delay(300);
sprintf((char *)Lcd_Disp_String, " ");
LCD_DisplayStringLine(Line3, Lcd_Disp_String);
HAL_Delay(300);
sprintf((char *)Lcd_Disp_String, " %1d",(unsigned int)ucPlat);
LCD_DisplayStringLine(Line3, Lcd_Disp_String);
HAL_Delay(300);
sprintf((char *)Lcd_Disp_String, " ");
LCD_DisplayStringLine(Line3, Lcd_Disp_String);
HAL_Delay(300);
sprintf((char *)Lcd_Disp_String, " %1d",(unsigned int)ucPlat);
LCD_DisplayStringLine(Line3, Lcd_Disp_String);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // 开门方向
__HAL_TIM_SET_COMPARE(&DOOR_PWM_TIM, DOOR_PWM_CHANNEL, 300); // 60%占空比
HAL_TIM_PWM_Start(&DOOR_PWM_TIM, DOOR_PWM_CHANNEL);
LCD_ShowMessage(Line8, "Door Opening");
stateEnterTime = uwTick;
state = ELEV_DOOR_OPENED;
break;
case ELEV_DOOR_OPENED:
if ((uwTick - stateEnterTime) >= 4000) {
HAL_TIM_PWM_Stop(&DOOR_PWM_TIM, DOOR_PWM_CHANNEL);
LCD_ShowMessage(Line8, "Door Opened");
// 清除当前目标层标志
ucSet &= ~(1 << (ucPlat - 1));
ucLed &= 0xF0;
ucLed |= ucSet;
LED_Disp(ucLed);
state = (ucSet) ? ELEV_WAITING_NEXT : ELEV_IDLE;
}
break;
//------------------ 等待下一目标 ------------------//
case ELEV_WAITING_NEXT:
if ((uwTick - stateEnterTime) >= 2000) {
LCD_ShowMessage(Line8, " ");
state = ELEV_DOOR_CLOSING; // 继续下一目标
}
break;
}
}
5.5 PWM
计数器输出频率
计数器占空比
题目要求PA6,也就是TIM3配成1kHz 。
PA7,也就是TIM17配置成2kHz。最好ARR配置成 1000,会更规范点。
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 800);//修改占空比的基本操作 D=0.8
HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_1); //PA6 1khz 绿色波形
/**
* @brief Set the TIM Capture Compare Register value on runtime without calling another time ConfigChannel function.
* @param __HANDLE__ TIM handle.
* @param __CHANNEL__ TIM Channels to be configured.
* This parameter can be one of the following values:
* @arg TIM_CHANNEL_1: TIM Channel 1 selected
* @arg TIM_CHANNEL_2: TIM Channel 2 selected
* @arg TIM_CHANNEL_3: TIM Channel 3 selected
* @arg TIM_CHANNEL_4: TIM Channel 4 selected
* @arg TIM_CHANNEL_5: TIM Channel 5 selected
* @arg TIM_CHANNEL_6: TIM Channel 6 selected
* @param __COMPARE__ specifies the Capture Compare register new value.
* @retval None
*/
#define __HAL_TIM_SET_COMPARE(__HANDLE__, __CHANNEL__, __COMPARE__) \
(((__CHANNEL__) == TIM_CHANNEL_1) ? ((__HANDLE__)->Instance->CCR1 = (__COMPARE__)) :\
((__CHANNEL__) == TIM_CHANNEL_2) ? ((__HANDLE__)->Instance->CCR2 = (__COMPARE__)) :\
((__CHANNEL__) == TIM_CHANNEL_3) ? ((__HANDLE__)->Instance->CCR3 = (__COMPARE__)) :\
((__CHANNEL__) == TIM_CHANNEL_4) ? ((__HANDLE__)->Instance->CCR4 = (__COMPARE__)) :\
((__CHANNEL__) == TIM_CHANNEL_5) ? ((__HANDLE__)->Instance->CCR5 = (__COMPARE__)) :\
((__HANDLE__)->Instance->CCR6 = (__COMPARE__)))