蓝桥杯备赛,第十一届蓝桥杯省赛试题解析。

题目分析

原题

原题参见蓝桥杯大赛=>第十一届真题.zip

另:官方试题汇总链接=>蓝桥杯大赛历届真题

硬件分析

本题中使用到的硬件设备除了MCU外主要有:LCD(液晶显示器)、电位器R37、按键(B1、B2、B3、B4)、LED

其中LCD使用到的引脚如下:

PC0 ~ PC15 => LCD数据线

PB5 => LCD_WR

PB8 => LCD_RS

PB9 => LCD_CS

PA8 => LCD_RD

LED使用到的引脚如下:

LED1 ~ LED8 对应 PC8 ~ PC15,其中锁存使能引脚为 PD2,低电平锁存,高电平释放

注意 PC8 ~ PC15 引脚是LCD与LED共用的,因此在驱动LCD时要先锁存,以在驱动LCD时影响到LED

按键使用到的引脚如下:

B1 => PB0 B2 => PB1

B3 => PB2 B4 => PA0

电位器R37使用到的引脚如下:

R37 => PB15 ADC2-IN-15 => 对应ADC2通道15

除了上述引脚外,题目还需要使用PA6PA7用于PWM输出,对于PA6PA7

PA16 => TIM16-CH1 输出100Hz

PA17 => TIM16-CH1 输出200Hz

注意:题目需要使用80MHz来驱动各个外设,因此需要配置时钟树叶子结点为80MHz,根据需要也可以使能HSE使用外部的24MHz的晶振

软件分析

在本题中我们需要实现如下功能:

  • LED显示(包括控制锁存器)
  • 使用LCD显示用户界面
  • 按键短按检测,并对该事件进行处理
  • 使用ADC读取并转换R37的电阻值
  • PWM输出100Hz与200Hz的脉冲

其中LCD部分的驱动程序将会由官方提供,除此之外还提供I2C的驱动程序

功能实现

STM32CubeMX引脚配置

依照上述硬件分析使能各个引脚即可。

其中PWM部分:

  • PA6 定时器配置:Prescaler = 80-1 Counter Period = 10000 - 1 Pulse = 1000 - 1
  • PA7 定时器配置:Prescaler = 80-1 Counter Period = 5000- 1 Pulse = 500- 1

PA6通过将80MHz的时钟预分频为1MHz的时钟,在将计数周期设置为10000 - 1以得到100Hz的频率,脉宽为10000的方波,PA7同理。

LED部分

代码逻辑为:

  • 将PC8~PC15引脚置于高电平,消除在操作LCD时所设置的引脚电平对LED产生影响
  • 将需要点亮的LED对应的引脚置于低电平
  • 打开锁存器让PC8~PC15与LED“连通”,随后关闭锁存器以防止后续操作LCD时对LED产生影响
#define LED_ALL_PIN (GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15)

void LED_Set(uint8_t led)
{
    HAL_GPIO_WritePin(GPIOC, LED_ALL_PIN, GPIO_PIN_SET);
    HAL_GPIO_WritePin(GPIOC, (uint16_t)led << 8, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);
    HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);
}

LCD部分

代码逻辑为:

  • 通过判断全局变量UI的值,来决定显示哪一个界面
  • 通过标准库字符串格式化函数sprintf来格式化需要显示的字符串
#define UI_DATA 0x00 
#define UI_PARA 0x01

volatile uint8_t UI = UI_DATA;

char ui_str[2][20] = {
    "      Data    ",
    "      Para    "};

char mode_str[2][20] = {
    "    Mode:AUTO    ",
    "    Mode:MANU    "};

void LCD_Show(void)
{
    char str[20] = {};
    LCD_DisplayStringLine(Line0, (u8*)(&ui_str[UI]));
    if (UI == UI_DATA)
    {
        sprintf(str, "    V:%.2fV     ", R37V);
        LCD_DisplayStringLine(Line2, (u8*)str);
        LCD_DisplayStringLine(Line4, (u8*)(&mode_str[Mode]));
    }
    else
    {
        sprintf(str, "    PA6:%d%%    ", PA6V);
        LCD_DisplayStringLine(Line2, (u8*)str);
        sprintf(str, "    PA7:%d%%    ", PA7V);
        LCD_DisplayStringLine(Line4, (u8*)str);
    }
}

按键部分

代码逻辑为:

  • 通过自定义结构体来存放按键的相关信息:端口号、引脚号、以及状态
  • 每隔10ms左右获取一次引脚电平,如果上一次电平为低,当前电平为高时判断为按键被按下一次
  • 如果B1被按下全局变量Key_Click被设置为0x01,B2被按下设置为0x02,B3被按下设置为0x04,B4被按下设置为0x08
  • 通过一个指针数组来存放所有按键的处理函数,按某个按键被按下,Key_Click被赋值,通过调用Key_Handle函数来匹配并调用该按键的处理函数,同时清除Key_Click变量
#define KEY_MAX_SUM 4
#define KEY_LONG_DOWN 60

typedef void (*KeyCallback)(void);

struct KeyInfo
{
    GPIO_TypeDef *port;
    uint16_t pin;
    uint8_t status;
};

struct KeyInfo KeyList[KEY_MAX_SUM] = {
    {GPIOB, GPIO_PIN_0, 0xff},
    {GPIOB, GPIO_PIN_1, 0xff},
    {GPIOB, GPIO_PIN_2, 0xff},
    {GPIOA, GPIO_PIN_0, 0xff}
};

KeyCallback KeyHadnle[KEY_MAX_SUM] = {
    B1_Short,
    B2_Short,
    B3_Short,
    B4_Short
};

void Key_Scan(void)
{
    for (size_t i = 0; i < KEY_MAX_SUM; i++)
    {
        if(HAL_GPIO_ReadPin(KeyList[i].port, KeyList[i].pin) == GPIO_PIN_SET)
        {
            if((KeyList[i].status & 0x01) == 0x00)
            {
                // 短按
                Key_Click = 0x01<<i;
                KeyList[i].status = 0xff;
            }
        }
        else
        {
            KeyList[i].status <<= 1;
        }
    }
}

void Key_Handle(void)
{
    for (size_t i = 0; i < KEY_MAX_SUM; i++)
    {
        if(Key_Click == 0x01 << i)
        {
            KeyHadnle[i]();
            Key_Click = 0x00;
        }
    }
    
}

void B1_Short(void)
{
    UI = (UI == UI_DATA)? UI_PARA : UI_DATA;
}

void B2_Short(void)
{
    if(UI == UI_PARA)
    {
        PA6V = (PA6V + 10 > 90)? 10 : PA6V + 10;
        PWM_Refresh();
    }
}

void B3_Short(void)
{
    if(UI == UI_PARA)
    {
        PA7V = (PA7V + 10 > 90)? 10 : PA7V + 10;
        PWM_Refresh();
    }    
}

void B4_Short(void)
{
    Mode = (Mode == MODE_AUTO)? MODE_MANU : MODE_AUTO;
}

ADC部分

代码逻辑:

  • 启动ADC规则通道转换
  • 读取ADC转换后的值并加以处理,更新至全局变量R37V
  • 判断是否在AUTO模式,以更新PA6VPA7V的值并刷新PWM输出
  • 关闭ADC规则通道转换
volatile float R37V = 0.0;

volatile uint8_t PA6V = 10;
volatile uint8_t PA7V = 20;

void Status_Update(void)
{
    // 电压采集
    HAL_ADC_Start(&hadc2);
    R37V = (HAL_ADC_GetValue(&hadc2)  / 4095.0) * 3.3;
    HAL_ADC_Stop(&hadc2);

    // 是否在AUTO模式
    if(Mode == MODE_AUTO)
    {
        PA6V = (uint8_t)((R37V * 100.0) / 3.3);
        PA7V = (uint8_t)((R37V * 100.0) / 3.3);
        // 更新PWM
        PWM_Refresh();
    }
}

PWM部分

代码逻辑:

  • 启动PWM输出
  • 根据PA6VPA7V的值更新PWM占空比
void PWM_Start(void)
{
    HAL_TIM_PWM_Start(&htim16, TIM_CHANNEL_1);
    HAL_TIM_PWM_Start(&htim17, TIM_CHANNEL_1);
}

void PWM_Refresh()
{
    __HAL_TIM_SET_COMPARE(&htim16, TIM_CHANNEL_1, PA6V * 100);
    __HAL_TIM_SET_COMPARE(&htim17, TIM_CHANNEL_1, PA7V * 50);
}

完整代码

参见Github链接蓝桥杯第十一届省赛

Last modification:April 9th, 2021 at 07:37 pm