目录
学习目标:
掌握led按键lcd显示屏
学习内容:
Led模块
1.了解led硬件模块的配置,从而用cubemx配置底层
这里需要注意的有控制Led的引脚(PC8~PC15),控制锁存器的引脚(PD2),Led为共阳极连接。
在获取了这些信息后,可以开始配置底层
配置Led的引脚为推挽输出模式,并将初始电平拉高,在上电时让led熄灭,由于
推挽输出与开漏输出模式的区别:
推挽输出:引脚可以驱动高电平或低电平,适⽤于多数数字输出应用。
开漏输出:引脚只能拉低电平,适⽤于需要外部上拉电阻的应⽤,⽐如I2C总线
推挽输出和开漏输出的内部区别就是两个mos管的导通与截至,开漏输出只有低电平和高阻态两个模式,无法拉高电平,使用时需要外接上拉电阻
上拉电阻和下拉电阻的原理:就是和单片机内部的电阻形成并联,使其相对的阻值减小,从而形成高低电平。
2.底层代码编写
#include "led_app.h"
uint8_t ucled[8]={0};//led状态数组,1为亮
void led_disp(uint8_t *ucled)
{
uint8_t temp=0x00;//用于读取ucled的数值
static uint8_t temp_old=0xff;
for(uint8_t i=0;i<8;i++)
{
temp|=(ucled[i]<<(7-i));//写入时寄存器时,寄存器最高位对应的led是左边第一个led,所以这里ucled数组的值需要顺序调换一下
}
if(temp!=temp_old)
{
// 将 GPIOC 低字节清零,高字节更新为新的 temp 值
GPIOC->ODR &= 0x00ff; // 清除 GPIOC 高字节
GPIOC->ODR |= ~(temp << 8); // 设置 GPIOC 高字节为 temp 的相反值
GPIOD->BSRR |= 0x01 << 2; // 设置 GPIOD 第2位,BSRR可以拉高也可以拉低
GPIOD->BRR |= 0x01 << 2; // 重置 GPIOD 第2位,BRR寄存器只能将位拉低
temp_old = temp; // 更新记录的旧状态
}
}
void led_proc()
{
led_disp(ucled);
}
代码编写思路:建立一个自己使用的led状态数组,每次都用temp去读取状态数组的值,并且将其整合为8位二进制,这里读取数据进行移位操作时需要注意,状态数组的第一个元素默认对应的是从左往右的第一个灯,而第一个灯是LD8在寄存器中的控制位是最高位,判断temp和temp_old然后对寄存器和锁存器进行赋值完成点灯。
KEY按键模块:
1.了解按键模块硬件配置
按下后输入低电平。
配置底层:
这里要配置为输入模式,并且要配置上拉电阻。
2。代码编写:
·
#include "key_app.h"
uint8_t key_val,key_down,key_up,key_old=0;
uint8_t key_read()
{
uint8_t temp=0;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0)==GPIO_PIN_RESET)
temp=1;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1)==GPIO_PIN_RESET)
temp=2;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2==GPIO_PIN_RESET))
temp=3;
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)==GPIO_PIN_RESET)
temp=4;
return temp;
}
void key_proc()//功能函数
{
key_val=key_read();
key_down=key_val&(key_val^key_old);
key_up=~key_val&(key_val^key_old);
key_old=key_val;
}
和单片机的部分基本一致,只是这里用到了HAL_GPIO_ReadPin这个hal函数去读取io端口的电平高低,使用原来的消抖函数程序去消抖。
Lcd显示屏模块
1.了解硬件底层
全部配置为输出模式。
介绍引脚(只需要了解即可,底层直接引用即可):
分为:数据信号,数据是否可以进入信号,数据命令区分信号,写使能,读使能信号。
2.底层代码编写
1.导入底层代码
fonts供lcd使用即可不用放在bsp_app中。
2.编写底层代码,lcd_app.c
#include "lcd_app.h"
/**
* @brief 格式化字符串并显示在指定的LCD行上。
*
* 该函数接受一个行号和一个格式化字符串(类似于printf),
* 格式化字符串后,将其显示在LCD的指定行上。
*
* @param Line 要显示字符串的LCD行号。
* @param format 格式化字符串,后跟要格式化的参数。
*
* 该函数内部使用 `vsprintf` 来格式化字符串,然后
* 调用 `LCD_DisplayStringLine` 在LCD上显示格式化后的字符串。
*
* 示例用法:
* @code
* LcdSprintf(0, "Temperature: %d C", temperature);
* @endcode
*/
void LcdSprintf(uint8_t Line, char *format,...)
{
char String[21]; // 缓冲区用于存储格式化后的字符串
va_list arg; // 参数列表用于存储可变参数
va_start(arg, format); // 使用格式化字符串初始化参数列表
vsprintf(String, format, arg); // 格式化字符串并存储在缓冲区中
va_end(arg); // 清理参数列表
LCD_DisplayStringLine(Line,String); // 在LCD的指定行显示格式化后的字符串
}
void lcd_proc(void)
{
}
背住直接打即可
进阶:按键状态机!
简单来说,按键机就是根据不同的状态对按键的按下和松开进行一些状态转化和其他的操作。
按键的状态有松开,按下,一直按着,三种状态。
状态机结构体
一个好的结构体可以帮助程序更好的封装,也可以帮助实现更多的操作,结构体其实就是用来表示一个按键的不同的属性,比如按键id,按键状态,按键的引脚,按键按下的次数等等
状态机初始化
状态机初始化就是对要使用的一些按键的一些属性定义一下,填补一下结构体。
状态机具体使用例程及个人理解(在代码中)
typedef struct
{
GPIO_TypeDef *gpiox; // 指向 GPIO 端口的指针,例如 GPIOA、GPIOB 等。
uint16_t pin; // 指定 GPIO 引脚,例如 GPIO_PIN_0、GPIO_PIN_1 等。
uint16_t ticks; // 用于计时的变量,通常用于去抖动处理。
uint8_t level; // 当前按键的电平状态,高电平或低电平。
uint8_t id; // 按键的唯一标识符,可以用于区分不同的按键。
uint8_t state; // 按键的当前状态,可能用于表示按键是否被按下或释放。
uint8_t debouce_cnt; // 按键去抖动计数器,用于防止按键抖动引起的误触发。
uint8_t repeat; // 按键重复按下的次数。
} button;
button btns[4]; // 按键数组
// 按键初始化函数
void key_init(void)
{
// 初始化第一个按键
btns[0].gpiox = GPIOB; // 指定GPIO端口
btns[0].pin = GPIO_PIN_0; // 指定引脚
btns[0].level = 1; // 设置初始电平
btns[0].id = 0; // 设置按键ID
// 初始化第二个按键
btns[1].gpiox = GPIOB;
btns[1].pin = GPIO_PIN_1;
btns[1].level = 1;
btns[1].id = 1;
// 初始化第三个按键
btns[2].gpiox = GPIOB;
btns[2].pin = GPIO_PIN_2;
btns[2].level = 1;
btns[2].id = 2;
// 初始化第四个按键
btns[3].gpiox = GPIOA;
btns[3].pin = GPIO_PIN_0;
btns[3].level = 1;
btns[3].id = 3;
}
// 按键任务处理函数
void key_task(button *btn)
{
// 读取按键当前电平
uint8_t gpio_level = HAL_GPIO_ReadPin(btn->gpiox, btn->pin);
// 如果按键状态大于 0,则递增计时器
// 就是说按键处于按下状态,对其按下的时间进行计时,通过判断ticks的值可以判断长按等操作
if (btn->state > 0)
btn->ticks++;
// 如果当前电平与按键记录的电平不同,进行去抖动处理
// 这里开始进行消抖操作
//读取按键值的时候,不可避免地会遇到按键电平抖动的情况,可以通过延时函数进行消抖,也可以通过定时器中断消抖,这里就是用类似定时器中断的方式进行消抖,在任务调度器中函数每10ms调用一次,只有经过三次调度,电平的值都是不变的且和原来的电平值不同,才会改变按键的电平属性,达到消抖的效果
if (btn->level != gpio_level)
{
// 计数达到 3 次,确认电平变化
if (++(btn->debouce_cnt) >= 3)
{
btn->level = gpio_level; // 更新电平
btn->debouce_cnt = 0; // 重置去抖动计数器
}
}
else
{
btn->debouce_cnt = 0; // 电平没有变化,重置去抖动计数器
}
// 按键状态机
switch (btn->state)
{
case 0: // 初始状态,按键没有按下
if (btn->level == 0) // 等待按键按下,按键按下后电平level就变为你0
{
btn->ticks = 0; // 重置计时器,将计时参数置0
btn->repeat = 1; // 初始按键重复计数,刚开始按下记为按下一次,可以用来判断连击的操作,双击,三击
btn->state = 1; // 进入按键按下状态
}
break;
case 1: // 按键按下状态
if (btn->level != 0) // 等待按键松开,按键值为1表示松开,因为按键接地的。
{
if (btn->ticks >= 30) // 按键长按,进行长按任务
{
ucLed[btn->id] ^= 1; // 执行长按后的操作
btn->state = 0; // 返回初始状态
}
else
{
btn->ticks = 0; // 重置计时器
btn->state = 2; // 进入按键释放状态
}
}
else if (btn->ticks >= 30) // 按键长按
{
//这个分支内没有进行状态的改变,需要注意
btn->repeat = 0; // 防止释放的时候再次触发单击事件
}
break;
case 2: // 按键释放状态
if (btn->ticks >= 15) // 计时器达到阈值,这里是为了判断双击三击操作,只要按键按下的时间过长则判断为没有连击
{
btn->state = 0; // 返回初始状态
if(btn->repeat == 1)//单击
{
ucLed[btn->id+4] ^= 1; // 点亮对应的LED
}
else if(btn->repeat == 2)//双击
{
ucLed[btn->id] ^= 1; // 点亮对应的LED
}
}
else
{
if (btn->level == 0) // 按键再次按下,判读为进行了连击操作
{
btn->repeat++; // 递增重复计数
btn->ticks = 0; // 重置计时器
btn->state = 1; // 返回按键按下状态
}
}
break;
}
}
// 按键状态处理函数
void key_state(void)
{
for (uint8_t i = 0; i < 4; i++) // 遍历所有按键
{
key_task(&btns[i]); // 处理每个按键的状态
}
}