在上一篇文章中,转载了一种上流的方法,使用状态机、函数指针、结构体、结构体指针等,异常的上流,但是有些bug。
目录
首先记录一下调试的过程。
矩阵调试与其他思路
调试
首先源代码的主函数中
u8 Key_Value;
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
MX_TIM2_Init();
MX_TIM3_Init();
while (1)
{
if(App_LEAVE != currState->loop()) //(检测是否可以离开当前的状态,不可以就会推出当前状态)
{ //就是调用currstate的loop函数,得到返回值
return 0;
}
// Button released (检测到了松开,就执行动作)
if(currState == &colScanningPressed)
{
printf("KEY:%d \r\n",Key_Value);
}
// Next state(执行完当前状态就应该把状态转换,到下个状态)
//(可以看出,currState,作为一个指针结构体,就是一个指针的作用,存储当前状态用于判断)
currState = currState == &rowScanning ? &colScanning : currState == &colScanning ? &colScanningPressed : &rowScanning;
//如果切换到下一个状态,则调用其 enter() 函数
currState->enter();
}
这时候,在
uint8_t colScanningLoop()
{
if(GPIO_PIN_RESET == checkPressedLow(COL0_GPIO_Port,COL0_Pin))
{
// Key_Value |= 0;printf("KEY:%d \r\n",Key_Value);
return App_LEAVE;
}
if(GPIO_PIN_RESET == checkPressedLow(COL1_GPIO_Port,COL1_Pin))
{
Key_Value |= 1; //低两位存储列值
// printf("KEY:%d \r\n",Key_Value);
return App_LEAVE;
}
if(GPIO_PIN_RESET == checkPressedLow(COL2_GPIO_Port,COL2_Pin))
{
Key_Value |= 2; //低两位存储列值
// printf("KEY:%d \r\n",Key_Value);
return App_LEAVE;
}
if(GPIO_PIN_RESET == checkPressedLow(COL3_GPIO_Port,COL3_Pin))
{
Key_Value |= 3; //低两位存储列值
// printf("KEY:%d \r\n",Key_Value);
return App_LEAVE;
}
return App_STAY;
}
这里的printf已经可以正常输出了,但是main中的printf就是没有,或者说,需要好多次才有概率触发一次。
于是调试一波,emmm,if(App_LEAVE != currState->loop()) //(检测是否可以离开当前的状态,不可以就会推出当前状态) { //就是调用currstate的loop函数,得到返回值 return 0; }
这里直接写在main函数中的, return都是直接退出函数的,所以这里只要没有检测到按键就会一直退出main函数重进, 于是改一下
把if改成这个while循环即可。
while(App_LEAVE != currState->loop());
但是还有问题,就是现在按一次可能会输出好几次,也有按键消抖了,但还是不稳。
那么有一种解决方法是,在if(currState == &colScanningPressed) { printf("KEY:%d \r\n",Key_Value); }
这里面发送完之后加一个while判断,只要按键值不改变就不退出,但是这样就存在一个问题,假如我就是要按两次同一按键就会出现bug
所以嘛,拆分了一下这一句currState = currState == &rowScanning ? &colScanning : currState == &colScanning ? &colScanningPressed : &rowScanning;
如下:
currState=&rowScanning;
currState->enter();
while(App_LEAVE != currState->loop());
// if(0) //(检测是否可以离开当前的状态,不可以就会推出当前状态)
// { //就是调用currstate的loop函数,得到返回值
// return 0;
// }
// Button released (检测到了松开,就执行动作)
currState= &colScanning;
currState->enter();
currState->loop();
currState= &colScanningPressed;
currState->enter();
while(App_LEAVE != currState->loop());
printf("KEY:%d \r\n",Key_Value);
把那个条件语句改成了三步实现,但还是没有解决这个问题。
最后想想,按键消抖那里可能不太严格,还得回那里去看看。
//按键消抖要做好
GPIO_PinState checkPressedLow(GPIO_TypeDef *port, uint16_t pin)
{
if(GPIO_PIN_RESET == HAL_GPIO_ReadPin(port, pin))
{
HAL_Delay(DEBOUNCE_DELAY);
if(GPIO_PIN_RESET == HAL_GPIO_ReadPin(port, pin))
return HAL_GPIO_ReadPin(port, pin);
}
if(GPIO_PIN_SET == HAL_GPIO_ReadPin(port, pin))
{
HAL_Delay(DEBOUNCE_DELAY);
if(GPIO_PIN_SET == HAL_GPIO_ReadPin(port, pin))
return HAL_GPIO_ReadPin(port, pin);
}
return GPIO_PIN_SET;
}
经过这个改善之后终于可以了。
补充一点改善代码简洁度
对于这个函数
uint8_t colScanningPressedLoop()
{
int col = 3 & Key_Value; //读取列值
if(0 == col)
{
if(GPIO_PIN_SET == HAL_GPIO_ReadPin(COL0_GPIO_Port, COL0_Pin))
{
return App_LEAVE;
}
}
else if(1 == col)
{
if(GPIO_PIN_SET == HAL_GPIO_ReadPin(COL1_GPIO_Port, COL1_Pin))
{
return App_LEAVE;
}
}
else if(2 == col)
{
if(GPIO_PIN_SET == HAL_GPIO_ReadPin(COL2_GPIO_Port, COL2_Pin))
{
return App_LEAVE;
}
}
else
{
if(GPIO_PIN_SET == HAL_GPIO_ReadPin(COL3_GPIO_Port, COL3_Pin))
{
return App_LEAVE;
}
}
return App_STAY;
}
其实写复杂了,可以改成switch case
会更简洁一点
其他矩阵键盘扫描的思路
行列扫描
很经典的一种扫描思路,就是用8根线,一个字节的值,这是原来的
https://blog.csdn.net/weixin_40973138/article/details/86607562
源代码在这键盘扫描源代码
以上两种方法都不能检测按键同时按下,比如同时按两个,你能在行读取的值中有两个0,列读取的值中,两个0,而且还不固定,这样就无法读取。
暴力for轮询扫描
这种就是我先写一个函数,可以判断(i,j)的按键有没有被按下。这个函数中也是,先让第i行上拉输入,第j列输出0同时其他三列输出为1,这样读取值为0是按下,反之没按下。
然后每次都两个for循环嵌套不断轮询。
同时,这个还可以 解决两个按键同时按下的问题,并且可以设定优先级 。
思路是这样的:
假如扫描到第7个按键的时候我按下了第1个和第8个,就只检测到了第8个,而且就算同时检测到两个,也无优先级可言。
所以,我设置一个key_table[16] = {0},每一位对应一个按键是否被按下,,然后, 轮询三次,或者轮询多少ms之后再输出,每次轮询都把当前按下的按键所对应在数组中的值置1, 最后输出这个数组,
处理的时候,设置一个指针,指向数组头,只要指针指向地址的值为1,就switch case 指针的值(偏移量),执行相应动作,不为1就指针加加。
另外记录一下UI界面的思路
一直想理一下,在做一个项目的时候,人机交互这种界面怎么去做,怎么实现这个控制逻辑。下面理一下思路
main函数while扫描1(两个按键多种界面)
main函数中的while(1)就不干别的了,只做按键扫描和屏幕显示。
以一个工程为例。首先我们只有两个按键,有三种模式,每种模式(模式123),用一个变量mode表示,每个模式有若干种子模式(子模式123…),用一个变量zmode表示,我在显示屏的右上角就会显示Mab(其中ab分别为模式与子模式的号),比如M13就是第1种模式的第3种子模式。
那好只有两个按键,怎么控制呢,
用伪代码的形式来表示
mode 模式
zmode 子模式
while(1){
if(key1 ==0)
{
mode++;
mode %=3;这样控制在3中模式之内
}
if(key2 == 0)
{
zmode ++;
zmode %= 3;
}
switch(mode)
{
case 1: {
switch(zmode)
{
case 1:屏幕显示函数等操作
case 2:
case 3:
这里需要注意的一点是,这三个case不加break,
那么就可以实现的效果是,
模式1可以显示三种,模式2只显示两种,模式3只显示一种东西,就是这种效果。
}
}
case 2:{ switch(zmode) }
...
}
}
就是这样一种嵌套的switch来实现,前面只扫描按键值,
后面来做渲染,然后不停的循环
问题1——改进
现在,我在模式M13,然后改到了模式2会自动是子模式3,即M23。因为他们两个是分离的两个变量。
那假如每次mode的值改了的时候,zmode都会从1开始 ,那是不是可以解决呢,
NO,现在就有另外一个问题。
本来是M13,现在调成了模式M21,等我重新调回模式1 的时候,又变成了模式M11,这样就不够人性化,所以,我们进一步改进,使用二维数组
int mode
int zmodetabel[3]={0,0,0};
while(1){
if(key1 == 0)
{
mode++;
mode%=3;
while(!key1);
}
if(key2 == 0)
{
!!!!!!!!!!!!!!!!!!!!!!
zmodetabel[mode]++;mode对应的那个值加加
!!!!!!!!!!!!!!!!!!!!
zmodetabel[mode]%=3;
}
switch(mode)
{
case 1: {
switch(zmodetable[mode])
{
case 1:屏幕显示函数等操作
case 2:
case 3:
这里需要注意的一点是,这三个case不加break,
那么就可以实现的效果是,
模式1可以显示三种,模式2只显示两种,模式3只显示一种东西,就是这种效果。
}
}
case 2:{ switch(zmode) }
...
}
}
就是这样一种嵌套的switch来实现,前面只扫描按键值,
后面来做渲染,然后不停的循环
补充2
现在两个按键,还可以加入长按的逻辑,加入第三个逻辑,增加一个变量 Pmode,显示长按的界面
if(key1 == 0)
{
while(key == 0)
{time++;
delay
if delay >= maxtime
Pmode = 1;
}
此时这个长按的界面,如果进入之前我的模式是M12
但此时不需要调模式,如果此时单击按键1的话,
回去的时候可能就是M2 或者M3,主模式就变了,
所以此时在逻辑上需要改一下
if(Pmode == 0)
{
只有Pmode = 0才会执行加一的操作
i++;
if(i>maxi)
i = 0;
}
}
然后,就可以在刚才的switch case外面再套一个,
对Pmode进行switch case
同时此时也可以继续使用key2 ,继续进行switch的嵌套
main函数while扫描2(两个按键多种界面)
这种就是,至少需要四个按键,
key1,进入一个状态,不停地等待,卡在这里不出去,里面key2进行设置,key3进行确定,key4进行退出。
示例
int main(void)
{
//u16 kkk=445*5;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //矢
delay_init();
LED_Init();
Beep_Init();
uart_init();
KEY_Init();
EXTIX_Init();
GPIO_Stepper_Init();
TIM2_Int_Init(MOTOR1_FRE,0);
TIM3_Int_Init(MOTOR1_FRE,0);
TIM4_Int_Init(MOTOR1_FRE,0);
TIM5_Int_Init(MOTOR1_FRE,0);
MOTOR1_PUL_H;
OLED_GPIO_Init();
OLED_Init();
Item.i=1;Item.j1=1;Item.j2=1;Item.k=1;Item.k2=3;Item.L=0,Item.D=50;
OLED_CLS();
OLED_P8x16Str(0,0,"Reseting...");
delay_ms(200);
Stepper_Reset();
delay_ms(200);
OLED_CLS();
OLED_P8x16Str(0,0,"WELCOME...");
while(1)
{
if(!KEY2){
show_setting();
delay_ms(300);
while(1){
if(!KEY2){
Item.i++;
if(Item.i>2)Item.i=1;
OLED_CLS();
show_setting();
delay_ms(300);
}
if(!KEY3){
if(Item.i==1)Item.j1++;
if(Item.i==2)Item.j2++;
if(Item.j1>2)Item.j1=1;
if(Item.j2>4)Item.j2=1;
show_setting();
delay_ms(300);
}
if(!KEY4){
if(Item.i==1){Item.k++;delay_ms(300);}
if(Item.i==2){Item.k2++;delay_ms(300);}
if(Item.k>2)Item.k=1;
if(Item.k2>15)Item.k2=1;
show_setting();
}
if(!KEY5){
if(Item.i==1){Item.L++;}
if(Item.j2==3){Item.D+=10;}
if(Item.L>15)Item.L=0;
if(Item.D>90)Item.D=0;
show_setting();
delay_ms(300);
}
if(!KEY1){
OLED_P8x16Str(0,0," WAITING ");
break;
}
}
}
广播的方式
使用全局变量,不断的按键扫描,然后更改全局变量的值,在按键动作函数中进行switchcase执行各种函数
另外,也可以使用外部中断,中断中什么也不干,就只给一个变量赋值,然后在主函数中不断的进行判断,比如中断中使key1= 1
main中
if(key1 = 1)
{
key1 = 0;一定要先清零才行
…
}