2017-05-07 17:31:01 a123rou 阅读数 1971
  • MFC上位与STM32下位通讯精讲

    本课程主要介绍C++类库MFC上位机与STM32单片机的RS232、RS422、RS485、USB、LWIP以太网、CAN等接口进行稳定通信。课程主要从MFC和STM32基础开始,以编写上位机以及下位机为主,非常注重实践。

    9342 人正在学习 去看看 王凯杰

说到单片机入门,很多小伙伴都是从郭天祥的《新概念51单片机C语言教程--入门提高开发拓展全攻略》这本书入门,郭天祥书中的按键扫描程序是用延时来进行软件消抖,后面在网络上学习了“MCU起航”的按键扫描程序的正规用法后,将状态机与按键扫描给融合进来进行使用。总结一下,日后可以方便回顾知识和能给一些需要的朋友提供思路参考一下。

						
		if(KEY == 0)		//如果按键被按下
		{
			delay_ms(10);	//延时一段时间
			if(KEY == 0)
			{
				LED = ~LED;			//LED状态翻转
				while(KEY == 0);	//key被按下低电平,一直在这儿等待,等再次按下再退出这个状态
			}		
郭天祥书中的按键检测程序基本流程是

1.检测按键是否被触发

2.延时消抖

3.再次检测确实被触发

4执行功能

5.等待按键释放



先给大家介绍一下什么是状态机?

简单讲就是一组状态,各个状态之间,依据一定的条件,(如输入一个1 或者是 0)存在一定的转换,(从状态X转换到状态Y)。



接下来按键检测这个程序来结合状态机实际程序来说明

/***************************************************
*				定时器0初始化函数,16位定时器
*				约2ms溢出一次,开启溢出中断
****************************************************/
void timer0_init(void)
{
	TMOD = 0x01;		//模式设置,0b0000 0001,定时器0,工作于模式1  16位(M1=0,M0=1)
	ET0 = 1;			 	//开定时器0中断
	TH0 = 0xf8;		 	//定时器溢出值设置,每隔2ms发起一次中断。
	TL0 = 0xcb;
	TR0 = 1;			 	//定时器0开始计时
	EA = 1;					//开总中断
}
先初始化化定时器的配置,设置成2ms作为系统的时钟节拍。

如果其他程序也需要计时,判断进入2ms中断的累计次数就可以获取时间;

/***************************************************
*				中断子程序,每2ms中断一次
*				cnt_2ms加一,同时给TH0和TL0赋初值
****************************************************/
void time0() interrupt 1
{	
	static unsigned char cnt_2ms = 0;
	TH0 = 0xf8;		 			//定时器溢出值设置,每隔2ms发起一次中断。
	TL0 = 0Xcb;
	cnt_2ms++;;					//该计数单元数值加一
	if(cnt_2ms == 5)		//调用按键扫描子函数
	{
		cnt_2ms = 0;
		key_scan();
	}
}




中断函数,每2ms进入一次中断,按键扫描函数10ms扫描一次,即累计5次进入一次扫描

/***************************************************
*	按键扫描子函数,状态机方式
*	约10ms调用一次本函数,10ms的间隔起到去抖的效果
****************************************************/
void key_scan(void)
{
	static unsigned char state_cnt = 0;				//switch语句的状态变量,局部静态变量
	switch(state_cnt)
	{
		case 0x00:	if(KEY == 0)								//有按键被按下
									{
										state_cnt = 0x01;
										break;
									}
		case 0x01:	if(KEY == 0)								//确实被按下
									{
										state_cnt = 0x02;
										LED =~LED;							//执行按键对应的操作
										break;
									}
								 else												//毛刺
									{
										state_cnt = 0x00;
										break;
									}
		case 0x02:	if(KEY == 1)								//按键被释放
									{
										state_cnt = 0x00;			//一个完整的按键触发、执行、释放的过程完成
										break;
									}
		default:		break;
	}
}


首先是无按键按下状态,10ms扫描一次目前的状态,如果还是没有被按下,继续保持无按键被按下状态,break退出扫描函数。

10ms后再次扫描一下按键,如果确实按键被按下,改变状态为等待释放状态,如果没有被按下则回到开始的无按键按下状态break退出扫描函数。

10ms后再次扫描按键,如果检测到按键被释放,则状态跳转到一开始的无按键按下状态,break退出扫描函数。

注意:

static unsigned char state_cnt = 0;

switch语句的状态变量,局部静态变量

如果默认设置为auto的局部变量在跳出函数后再次进入key_scan()后,state_cnt 的值为0,不能保持之前推出时state_cnt的值

设置成static,局部静态变量,在跳出函数后,再次进入key_scan(),state_cnt任然保持之前的值,即保存住了状态。




记第一篇博客

之前一直对电子这方面感兴趣,说要学单片机,结果懒癌加上拖延症一直没有开始学习,今年开学后买了开发板和书便下定决心学习单片机,从郭天祥的书入的门,刚开始只是跟着书上的例程在开发板上跑一跑,在笔记本上记上一点笔记,后面遇见问题时在网上搜索,一些前辈把自己遇见的问题和一些经验总结在博客上,在我遇见问题时起了很大的帮助。

后面又学习了刘平老师的教程,推荐我们可以多写写博客,把我们自己的读书笔记,学习笔记,项目笔记,或者电路的调试总结,知识归纳,也可以把自己生活,工作的点点滴滴,经验,感悟拿出来和大家分享。

因为这样的文章不仅自己受益,同样也能让他人受益。假如我有一个苹果,与N个人交换,我还是只有一个苹果,假如我有的是一份资料,与N个人交换,我将有N+1份资料。

负责我们的老师也让我们做完一个小项目后,写一篇文档总结一下,锻炼了我们制作文档的能力,也让我们再次梳理了一下这段时间的学习成果。

最后再次感谢在网络上把自己遇到的问题错误和经验总结的前辈们,感谢你们。


2019-12-17 15:17:23 u010858987 阅读数 10
  • MFC上位与STM32下位通讯精讲

    本课程主要介绍C++类库MFC上位机与STM32单片机的RS232、RS422、RS485、USB、LWIP以太网、CAN等接口进行稳定通信。课程主要从MFC和STM32基础开始,以编写上位机以及下位机为主,非常注重实践。

    9342 人正在学习 去看看 王凯杰

一. 什么是状态机

我们以生活中的小区的停车系统为例:停车杆一般没车的是不动的(初态),有车来的时候需要抬杆(状态1),车通过需要放杆(状态2),如果在放杆的过程中突然有车,又需要抬杆(状态3)。。。就是说我们需要将不同的情况划分为不同的状态,每个状态对应相应的操作。

这只是状态机的一个很简单的举例。如果单个状态也是一个状态机的话,那么就会形成状态机套状态机,有点类似于C++里的父类和子类。其实在我们日常的程序设计中,处处都有状态机的影子,不管是我们嵌入式编程,游戏开发,服务端开发等等都要用到。

网上关于状态机的介绍很多,如果有想详细了解的可以去找找相关资料。

二. 用状态机的思想实现按键扫描

按键有按下和松开的状态,还有长按和短按的状态,以及单个按键和组合按键的状态等等。我们来看看如何用状态机的思想实现它,这里会有两种处理方式,第一种是利用一个定时器中断,每隔ms扫描一次;第二种是直接在while(1)循环里检测是否有按键按下。处理的思路都差不多,下面看看实现的代码。

/**********************************key.c*********************************/
//读取按键对应的管脚电平
static u16 GetKey(keysTypedef_t *keyS)
{
    uint8_t i = 0; 
    uint16_t readKey = 0;              
    
    //GPIO Cyclic scan
    for(i = 0; i < keys.keyTotolNum; i++)
    {
        if(!HAL_GPIO_ReadPin((GPIO_TypeDef*)keyS->singleKey[i].keyPort,keyS->singleKey[i].keyGpio))
        {
            G_SET_BIT(readKey, keyS->singleKey[i].keyNum); //读取到按键电平,设置对应的按键返回值为1
        }
    }
    
    return readKey;
}

// 判断按键是否按键按下,是长按还是短按
uint16_t readKeyValue(keysTypedef_t *keyS)
{
    static uint8_t keyCheck = 0;    //当前按键的值
    static uint8_t keyState = 0;
    static uint16_t keyLongCheck = 0;
    static uint16_t keyPrev = 0;      //上一次按键的值

    uint16_t keyPress = 0;
    uint16_t keyReturn = 0;
    
    keyCountTime ++;
    
    if(keyCountTime >= 10)          //keyCountTime 1MS+1  
    {
        keyCountTime = 0;
        keyCheck = 1;
    }
    if(1 == keyCheck)              //10ms时间到
    {
        keyCheck = 0;
        keyPress = GetKey();
        switch (keyState)
        {
            case 0:
                if(keyPress != 0)
                {
                    keyPrev = keyPress;
                    keyState = 1;
                }
                break;
                
            case 1:  
                if(keyPress == keyPrev)     //确认按键按下
                {
                    keyState = 2;
                    keyReturn= keyPrev | KEY_DOWN;
                }
                else                //按键松开,判断为抖动
                {
                    keyState = 0;
                }
                break;
                
            case 2:

                if(keyPress != keyPrev)  //按键松开,判定为短按
                {
                    keyState = 0;
                    keyLongCheck = 0;
                    keyReturn = keyPrev | KEY_UP;
                    return keyReturn;
                }
                if(keyPress == keyPrev)  //按键未松开
                {
                    keyLongCheck++;
                    if(keyLongCheck >= 300)    //按下了3S,判定为长按
                    {
                        keyLongCheck = 0;
                        keyState = 3;
                        keyReturn= keyPress | KEY_LONG;
                        return keyReturn;
                    }
                }
                break;

            case 3:     //长按键释放
                if(keyPress != keyPrev)
                {
                    keyState = 0;
                }
                break;
        }
    }
    return  NO_KEY;
}


void keyHandle(keysTypedef_t *keyS)
{
    uint8_t i = 0;
    uint16_t key_value = 0;

    key_value = readKeyValue();

    if(!key_value) return;
    
    //Check short press button
   //Check short press button
    if(key_value & KEY_UP)
    {
        //Valid key is detected
        for(i = 0; i < keyS->keyTotolNum; i++)
        {
            if(G_IS_BIT_SET(key_value, keyS->singleKey[i].keyNum)) 
            {

                //添加对应按键短按的处理
            }
        }
    }

    //Check short long button
    if(key_value & KEY_LONG)
    {
        //Valid key is detected
        for(i = 0; i < keyS->keyTotolNum; i++)
        {
            if(G_IS_BIT_SET(key_value, keyS->singleKey[i].keyNum))
            {
                //添加对应按键长按的处理
            }
        }
    }
} 

以上代码主要是在定时中断中处理按键,开辟一个1ms的定时器,在定时中断函数里调用keyHandle,每过10ms检测一次按键的状态,每个状态都存储起来并返回,不用死等。

这种中断的方式适合按键可能在任意时刻按下的场景。那还有一种场景是:通过按键开启整个系统的工作状态。比如家里的电磁炉,你只有按下开关键,再按下菜单键才能选择对应的功能和温度,针对这种场景完全可以在while循环里调用keyhandle。

uint16_t readKeyValue(keysTypedef_t *keyS)
{
    static uint8_t keyCheck = 0;
    static uint8_t keyState = 0;
    static uint16_t keyLongCheck = 0;
    static uint16_t keyPrev = 0;      //last key

    uint16_t keyPress = 0;
    uint16_t keyReturn = 0;
  
    keyPress = GetKey();
    switch (keyState)
    {
            case 0:
                if(keyPress != 0)
                {
                    keyPrev = keyPress;
                    keyState = 1;
                }
                break;
                
            case 1:
                if(keyPress == keyPrev)
                {
                    keyState = 2;
                    keyReturn= keyPrev | KEY_DOWN;
                    PressTimerStart =1;   //开启定时器计时
                }
                else                //Button lift, jitter, no response button
                {
                    keyState = 0;
                }
                break;
                
            case 2:

                if(keyPress != keyPrev)  //按键已经松开
                {  
                    pressTimerStart =0;  //停止按键计时
                    if(pressCount>=30){  //按下超过30ms,判断为短按
                        keyState = 0;
                        pressCount =0;
                        keyReturn = keyPrev | KEY_UP;
                        return keyReturn;
                    }
                    else{    //不足30ms,不做处理
                        keyState = 0;
                        pressCount =0;
                    }
                }
                else{
                     Keystate =3;

                }
            case 3:
                if(keyPress == keyPrev)
                {  
                   if(pressCount>=30000){  //按下超过3s
                      keyState = 4;
                      pressTimerStart =0;
                      pressCount =0;
                      keyReturn= keyPress | KEY_LONG;
                      return keyReturn;

                    }
                }
                else{
                        keyState = 0;
                        pressTimerStart =0;
                        pressCount =0;
                        keyReturn = keyPrev | KEY_UP;
                        return keyReturn;
                }
                break;
      }
      return  NO_KEY;
}

OK,以上是两种检测每次单个按键按下的情况,那么多个按键同时按下怎么办呢?

看下面的代码,通过按键返回值判断是那几个按键同时按下。


//读取按键电平状态
u16 Key_Read(void)
{
    u16 KeyReadVal =0;
    if(Key1Press)  
		 keyReadVal|= 1;
    if(Key2Press)  
		 keyReadVal|= 2;      
    if(!READ_KEY3)  
		 keyReadVal|= 4;
    if(!READ_KEY4)  
		 keyReadVal|= 8;
	
    return KeyReadVal;

}

MCU端比较常用的多按键检测方法暂时就遇到这么多,后续遇到会继续补充。

2019-10-30 21:46:37 qq_44629109 阅读数 169
  • MFC上位与STM32下位通讯精讲

    本课程主要介绍C++类库MFC上位机与STM32单片机的RS232、RS422、RS485、USB、LWIP以太网、CAN等接口进行稳定通信。课程主要从MFC和STM32基础开始,以编写上位机以及下位机为主,非常注重实践。

    9342 人正在学习 去看看 王凯杰

单片机独立按键使用程序 (51单片机)

独立按键是单片机中很重要的一个器件,在这篇文章里,通过这个用独立按键控制LED灯的小程序来介绍独立按键开关的使用。

#include<reg52.h>
typedef unsigned int u16;
typedef unsigned char u8;
sbit key=P3^1; 
sbit led=P2^0;  

void delay(u16 num)//延时函数
{
 u16 x,y;
 for(x=num;x>0;x--)
  for(y=110;y>0;y--)
 {
  ;
 }
}
void main(void)
{
 led=1;
 while(1)
{
 if(key==0)
 {
  
  delay(10);//消抖程序
  if(key==0)
  {
  led=~led;//取反
  }
  while(!key);//使灯在开关下一次按下之前不发生变化,不跳出大循环
 }
}
}

两次按下按键的结果图
1

在这里插入图片描述

运行视频
在这里插入图片描述

2016-05-24 11:50:24 huohongpeng 阅读数 7212
  • MFC上位与STM32下位通讯精讲

    本课程主要介绍C++类库MFC上位机与STM32单片机的RS232、RS422、RS485、USB、LWIP以太网、CAN等接口进行稳定通信。课程主要从MFC和STM32基础开始,以编写上位机以及下位机为主,非常注重实践。

    9342 人正在学习 去看看 王凯杰

单片机的葵花宝典

 

霍宏鹏 著

 

目录

第1章 单片机初试牛刀 1

1.1 一个LED闪烁的故事 1

1.2 两个LED同时闪烁的故事 1

1.3 两个LED不同时闪烁的故事 1

1.4 思考题 3

第2章 状态机的通俗解释 3

2.2 状态机具体化 4

第3章 状态机在单片机上的应用 5

3.1 代码实现步骤 5

3.2 应用代码详解 5

第4章 简单的举例 8

4.1 实例概述 8

4.2 实例状态图 8

4.3 ADC部分实现 8

4.4 总结 10

 

第1章 单片机初试牛刀

 

1.1 一个LED闪烁的故事

   用单片机实现1个LED闪烁功能,要求LED亮500ms, 灭300ms。这个问题对于学过单片机的人来说是一个入门级的东西,实现起来简单的不能再简单了,就像C语言入门时的“HelloWorld”。好让我们来实现以下这个功能。

int main()

{
    while(1)
    {
         LedOn();
         DelayMs(500);
         LedOff();
         DelayMs(300);
    }
}

 

1.2 两个LED同时闪烁的故事

   根据上一节内容我们让LED完成了闪烁功能,在这一节当中,我们来让两个LED完成同时闪烁的功能,这对于一些工程来讲,还是一样入门级的东西,好,让我们来一起实现它。

int main()
{
    while(1)
    {
         Led1On();
         Led2On();
         DelayMs(500);
         Led1Off();
         Led2Off();
         DelayMs(300);
    }
}

 

1.3 两个LED不同时闪烁的故事

   上面两个例子,看起来实际上是一个故事,因为他们只是在延时的前后加上了LED的开关动作。这一节我们来尝试一下,让两个LED不同时闪烁,比如,LED1开200ms,关500ms,

LED2开300ms,关400ms。乍得一听,不知如何下手。对,有的小伙伴可能想起来了,用定时器来实现啊,嗯,没错可以这样做。那好我们来试试吧。在看下面的代码之前建议看到的小伙伴,想一想如果让你来自己实现这一功能你会怎么实现。

 

 

 

#define LED_ON (unsigned char)(0x01)

#define LED_OFF (unsigned char)(0x00)

#define LED1_ON_TIME_MS (unsigned short)(200)

#define LED1_OFF_TIME_MS (unsigned short)(500)

#define LED2_ON_TIME_MS (unsigned short)(300)

#define LED2_OFF_TIME_MS (unsigned short)(400)

 

int main()

{

/*初始化定时器中断间隔为*/

TimerMsInit(1);

 

while(1);

}

 

/*定时器中断函数*/

void TimerMsInterrupt(void)

{

if(Led1Flag == LED_ON)

{

          if(Led1Count > LED1_ON_TIME_MS)

          {

              Led1Count = 0;

              Led1Off();

              Led1Flag = LED_OFF;

          }

}

else

{

          if(Led1Count > LED1_OFF_TIME_MS)

          {

              Led1Count = 0;

              Led1On();

              Led1Flag = LED_ON;

          }

}

if(Led2Flag == LED_ON)

{

          if(Led2Count > LED2_ON_TIME_MS)

          {

              Led2Count = 0;

              Led2Off();

              Led2Flag = LED_OFF;

          }

}

else

{

          if(Led2Count > LED2_OFF_TIME_MS)

          {

              Led2Count = 0;

              Led2On();

              Led2Flag = LED_ON;

         }

}

 

Led1Count++;

Led2Count++;

}

 

    好,到这里我们已经把两个LED不同时闪烁的代码实现了,所有小伙伴都是想这么实现吗?或者还是在看到这段段码的时候根本没有想到怎么实现?其实在中断函数中已经有了“状态机”的模型,只是不太容易看出来。很多写单片机程序的人不太知道状态机是干什么的,不要着急,我们在下一个章节会详细介绍。其实到这里还是没有引出我们的重点内容,好吧,只能给聪明的小伙伴留一个思考题了。如果思考题没有想出来可以带着疑问去看第2章内容。

 

1.4 思考题

  单片机有串口1个,灯2个分别为LED1和LED2,按键2个分别为KEY1和KEY2。要求如下:

1 在KEY1按下的一瞬间,串口发送”KEY1 DOWN”

2 在KEY1 按下时,LED1亮起,KEY1按下的时间长短任意。如果KEY1一直处于

  按下状态那么LED1一直亮起。

3 在KEY1抬起的一瞬间,串口发送”KEY1 UP”

4 在KEY1抬起时,LED1熄灭

5 在KEY2按下的一瞬间,串口发送”KEY2 DOWN”

6 在KEY2 按下时,LED2亮起,KEY2按下的时间长短任意。如果KEY2一直处于

  按下状态那么LED2一直亮起。

7 在KEY2抬起的一瞬间,串口发送”KEY2 UP”

8 在KEY2抬起时,LED2熄灭

9 两个按键按下抬起,互不影响,且能立即反映做出动作。(最重要的要求)

10 为了简化问题,这里不考虑按键去抖。

 

   小伙伴们,脑洞大开想一想这个问题,几个很简单的外设,这样的要求是不是在你们买的开发板的示例程序中从来没有见到过?不要每天沉溺于高级外设蓝牙、USB等等,单片机的逻辑还是很重要滴!

 

 

第2章 状态机的通俗解释

2.1 状态机概念  

    按照最标准的概念介绍状态机可能很多人会不理解,所以想来想去还是用最通俗的语言来解释这一概念,配合一个简单的例子来介绍。

    其实,状态机就是把一件事情分为几个过程来实现,每个过程对应一个状态。比如一个人一天的生活吧,当然人每天的生活非常复杂,这里我只举几个比较常见的事情,以吃饭为主,这样对于像我这样爱吃的小伙伴可能更容易理解。好了,来大致介绍一下吧,比如生活包括做饭、吃饭、洗碗、睡觉、看电视、散步。其中这些都是生活中的某一状态那么我们画一个状态图来理解一下吧。

 

图2.1

 

   通过上图可以看到,椭圆中就代表一个状态,饿了、吃饱了等代表进入到下一个状态的条件,也就是说满足一个条件就可以进入到下一个状态。

 

2.2 状态机具体化

     好了,有了上一节的描述,小伙伴们对状态机应该有了一个模糊的概念,那么下面我们来将上一章的思考题,具体的变为几个状态,根据实际情况来画一下状态图。

    对于上一节的思考题,LED1,KEY1和LED2,KEY2可以看做相同的情况,那么我们就用其中一个来说明。

 

图2.2

    根据上图我们可以看出,一共将整个过程分为了四个状态,分别为检测按键是否按下、LED打开、检测按键是否抬起、串口输出LED关闭。而触发进入到下一状态的条件包括按键按下、处理完成、按键抬起,其中处理完成实际上就是无条件执行的,也就是说,不管怎样,LED打开后就是要进入到等待按键抬起状态。

    状态图有什么要求呢?总结以下几点:1 每个状态中不可以有延时。2 必须能够从上一个状态进入到下一个状态,也就是不能永远停留在一个状态。

    好了,这一章就说这么多吧,下一章,我们将用代码来实现上面的例子。

 

 

第3章 状态机在单片机上的应用

3.1 代码实现步骤

   根据上一章,我们可以看到状态机的状态图,而这一章我们将状态图进一步转化为实际的代码示例。转化的过程实际上只需要定义一个变量来指示整个状态就可以了。另一方面,从状态图中可以看到,如果把整个状态当成一个模块,那么这么模块应该在while(1)中无限循环下去。好了,来看具体的程序代码吧。

3.2 应用代码详解

 

#define KEY_STAT_CHECK_DOWN (unsigned char)(0x00) /*检测按键是否按下状态*/

#define KEY_STAT_CHECK_UP (unsigned char)(0x01) /*检测按键是否抬起状态*/

#define KEY_STAT_DOWN_HANDLE (unsigned char)(0x02) /*按键按下处理*/

#define KEY_STAT_UP_HANDLE (unsigned char)(0x03) /*按键抬起处理*/

 

unsigned char Key1Stat; /*定义KEY1模块的状态变量用于记录状态*/

unsigned char key2stat; /*定义KEY2 模块的状态变量用于记录状态*/

 

void Key1Moudle(unsigned char *pstat); /*KEY1模块处理函数*/

void Key2Moudle(unsigned char *pstat); /*KEY2模块处理函数*/

void Key1StatCheckDown(unsigned char *pstat); /*KEY1的按键按下检测函数*/

void Key1StatCheckUp(unsigned char *pstat); /*KEY1的按键抬起检测函数*/

void Key1StatDownHandle(unsigned char *pstat); /*KEY1的按键按下处理函数*/

void Key1StatUpHandle(unsigned char *pstat); /*KEY1的按键抬起处理函数*/

 

void Key2StatCheckDown(unsigned char *pstat); /*KEY2的按键按下检测函数*/

void Key2StatCheckUp(unsigned char *pstat); /*KEY2的按键抬起检测函数*/

void Key2StatDownHandle(unsigned char *pstat); /*KEY2的按键按下处理函数*/

void Key2StatUpHandle(unsigned char *pstat); /*KEY2的按键抬起处理函数*/

 

 

int main()

{

while(1)

{

    /*按键1模块处理*/

         Key1Moudle(&Key1Stat);

         /*按键2模块处理*/

         Key2Moudle(&Key1Stat);

}

}

 

 

void Key1Moudle(unsigned char *pstat)

{

/*根据当前状态判断执行哪一个状态处理函数*/

switch(*stat)

{

         case KEY_STAT_CHECK_DOWN : Key1StatCheckDown(pstat); break;

    case KEY_STAT_DOWN_HANDLE : Key1StatDownHandle(pstat); break;

         case KEY_STAT_CHECK_UP : Key1StatCheckUp(pstat); break;

         case KEY_STAT_UP_HANDLE : Key1StatUpHandle(pstat); break;

         /*当没有此状态,默认设置为检测按键是否按下状态*/

         default :

              *pstat = KEY_STAT_CHECK_DOWN;

              break;

}

return;

}

 

 

 

void Key2Moudle(unsigned char *pstat)

{

/*根据当前状态判断执行哪一个状态处理函数*/

switch(*stat)

{

         case KEY_STAT_CHECK_DOWN : Key2StatCheckDown(pstat); break;

    case KEY_STAT_DOWN_HANDLE : Key2StatDownHandle(pstat); break;

         case KEY_STAT_CHECK_UP : Key2StatCheckUp(pstat); break;

         case KEY_STAT_UP_HANDLE : Key2StatUpHandle(pstat); break;

         /*当没有此状态,默认设置为检测按键是否按下状态*/

         default :

              *pstat = KEY_STAT_CHECK_DOWN;

              break;

}

return;

}

 

void Key1StatCheckDown(unsigned char *pstat)

{

/*判断按键是否按下*/

if(ReadKey1() == 0x00)

{ /*将状态设置为 按键按下处理状态*/

         *pstat =  KEY_STAT_DOWN_HANDLE;

}

return;

}

 

void Key1StatDownHandle(unsigned char *pstat)

{

/*KEY1 按下状态处理*/

UartSend(“KEY1 DOWN”);

Led1On();

 

/*处理完成后,设置到 检测按键是否抬起状态*/

*pstat = KEY_STAT_CHECK_UP;

return;

}

 

void Key1StatCheckUp(unsigned char *pstat)

{

/*判断按键是否抬起*/

if(ReadKey1() != 0x00)

{ /*将状态设置为 按键抬起处理状态*/

         *pstat =  KEY_STAT_UP_HANDLE;

 

     }

return;

}

 

void Key1StatUpHandle(unsigned char *pstat)

{

/*KEY1 按下状态处理*/

UartSend(“KEY1 UP”);

Led1Off();

 

/*处理完成后,设置到 检测按键是否按下状态*/

*pstat = KEY_STAT_CHECK_DOWN;

return;

}

 

void Key2StatCheckDown(unsigned char *pstat)

{

/*判断按键是否按下*/

if(ReadKey2() == 0x00)

{ /*将状态设置为 按键按下处理状态*/

         *pstat =  KEY_STAT_DOWN_HANDLE;

}

return;

}

 

void Key1StatDownHandle(unsigned char *pstat)

{

/*KEY2 按下状态处理*/

UartSend(“KEY2 DOWN”);

Led2On();

 

/*处理完成后,设置到 检测按键是否抬起状态*/

*pstat = KEY_STAT_CHECK_UP;

return;

}

 

void Key2StatCheckUp(unsigned char *pstat)

{

/*判断按键是否抬起*/

if(ReadKey2() != 0x00)

{ /*将状态设置为 按键抬起处理状态*/

         *pstat =  KEY_STAT_UP_HANDLE;

 

     }

return;

}

 

void Key1StatUpHandle(unsigned char *pstat)

{

/*KEY2 按下状态处理*/

UartSend(“KEY2 UP”);

Led2Off();

 

/*处理完成后,设置到 检测按键是否抬起状态*/

*pstat = KEY_STAT_CHECK_DOWN;

return;

}

 

 

 

第4章 简单的举例

4.1 实例概述

     这一章我们来一个实战的练习,以片外ADC为例子。那么现在来介绍一下片外ADC的采集过程。通常片外单芯片ADC采集。大致过程为:空闲状态->设置ADC开始采集->等待ADC采集完成->读取ADC采集数据,这里需要说明,片外这些ADC不会采集的特别快,采集一次需要的时间远比片上ADC的时间要长,如果单片机单纯的等待ADC采集完成,那么单片机的效率会变极低。所以采用状态机的方式,将ADC作为1个模块来完成。为了体现状态机

方式的编程技巧,在第3章讲述的例子上,加上ADC功能,加上ADC后系统按键反映必须还是非常灵敏,不能出现迟钝等现象,这就要求,ADC模块中不能添加延时。

    下面描述一下ADC需要实现功能。ADC需要1s采集一次,采集后的数值保存到变量中。当然,请各位一定要明白,通篇文章中的程序代码都是功能示意性代码,并非完整代码。

4.2 实例状态图

 

图4.1

 

4.3 ADC部分实现

#define ADC_ACQUISITION_PERIOD_MS (unsigned short)(1000)/*ADC 采集周期*/

 

#define ADC_STAT_CHECK_TIME (unsigned char)(0x00) /*检查是否到达ADC采集时间*/

#define ADC_STAT_START (unsigned char)(0x01) /*启动ADC采集*/

#define ADC_STAT_CHECK_COMPLETE (unsigned char)(0x02) /*检查ADC是否采集完成*/

#define ADC_STAT_CHECK_READ_DATA (unsigned char)(0x03) /*读取ADC采集的数据*/

 

typedef struct

{

unsigned short CountMs; /*ADC采集周期计时*/

unsigned short AdcData; /*ADC 采集的数据*/

unsigned char  stat;    /*ADC 模块状态变量*/

}AdcStructTypeDef;

 

AdcStrcutTypeDef AdcStruct;

 

/*将第3章的main函数增加1条ADC模块*/

int main()

{

while(1)

{

    /*按键1模块处理*/

         Key1Moudle(&Key1Stat);

         /*按键2模块处理*/

         Key2Moudle(&Key1Stat);

         /*ADC模块采集*/

         AdcModule(&AdcStruct);

}

}

 

/*定时器1ms中断函数*/

void TimerInterruptMs(void)

{

    /*在定时器中,改变ADC的计数变量*/

pAdcStruct->CountMs = 0

}

 

void AdcMoudle(AdcStrcutTypeDef *pAdcStruct)

{

/*根据当前状态判断执行哪一个状态处理函数*/

switch(pAdcStrcut->stat)

{

         case ADC_STAT_CHECK_TIME      : AdcStatCheckTime(pAdcStruct);

         case ADC_STAT_START           : AdcStatStart(pAdcStruct);

         case ADC_STAT_CHECK_COMPLETE  : AdcStatCheckComplete(pAdcStruct);

         case ADC_STAT_CHECK_READ_DATA : AdcStatCheckReadData(pAdcStruct);

         /*默认情况下设置状态为 ADC检查是否到采集时间*/

         default :

              pAdcStruct->stat = ADC_STAT_CHECK_TIME;

              break;

}

}

 

void AdcStatCheckTime(AdcStrcutTypeDef *pAdcStruct)

{

if(pAdcStruct->CountMs < ADC_ACQUISITION_PERIOD_MS)

{ /*采集周期没有到达直接返回*/

         return;

}

/*清零计时器*/

pAdcStruct->CountMs = 0;

/*设置状态为 启动ADC采集*/

pAdcStruct->stat = ADC_STAT_START;

 

return;

}

void AdcStatStart(AdcStrcutTypeDef *pAdcStruct)

{

/*设置ADC开始转换*/

AdcStartCmd();

/*设置状态为 检查ADC是否采集完成*/

pAdcStruct->stat = ADC_STAT_CHECK_COMPLETE;

 

return;

}

void AdcStatCheckComplete(AdcStrcutTypeDef *pAdcStruct)

{ /*读取ADC状态,判断是否采集完成*/

if(AdcReadCompleteStat() == 0x00)

{

         /*没有采集完成直接返回*/

         return;

}

 

/*设置状态为 检查ADC是否采集完成*/

pAdcStruct->stat = ADC_STAT_CHECK_READ_DATA;

}

void AdcStatCheckReadData(AdcStrcutTypeDef *pAdcStruct)

{

    /*读取ADC采集的数据到变量*/

pAdcStruct->AdcData = AdcReadData();

/*设置状态为 检查是否到达ADC采集时间*/

pAdcStruct->stat = ADC_STAT_CHECK_TIME;

}

 

4.4 总结

    通过第3节的例子可以发现,当使用状态机编程时,如果在已有的模块上增加一个模块是非常容易的,而且对其他模块并没有干扰。

在资源有限的单片机中,没有操作系统,没有多任务,如果使用传统的方法,很难实现复杂的功能。而使用状态机编程,不像操作系统那样需要大量的单片机RAM,FLASH等资源,可以将复杂的问题,分解为多个状态,从而实现简单的编程,充分利用单片机的资源利用率,也使单片机做出复杂的项目,提供了强大的技术方法。

全篇通过简单的例子讲解了,单片机下的编程,在实际项目应用中,可能会遇到更复杂的问题,只要学会了方法,解决在复杂的项目工程也是一样。最后,大家学习愉快,工作顺利。

  

 

  以上内容仅供参考,如有错误请批评指正。

2018-10-13 16:46:13 qq_40825345 阅读数 372
  • MFC上位与STM32下位通讯精讲

    本课程主要介绍C++类库MFC上位机与STM32单片机的RS232、RS422、RS485、USB、LWIP以太网、CAN等接口进行稳定通信。课程主要从MFC和STM32基础开始,以编写上位机以及下位机为主,非常注重实践。

    9342 人正在学习 去看看 王凯杰

单片机按键驱动小技巧

平时在开发简单的工程应用时,会应用引脚资源少一些的MCU,这时要做稍复杂一些的用户交互功能选项时,就需要单个按键实现更多的功能,这时就需要按键实现短按、长按、连按功能,以下说明这些功能的实现方法,应用场合在按键资源不足无法使用矩阵键盘的情况下。

业务逻辑说明(异步非阻塞方式实现)

在这里插入图片描述

实现代码(异步非阻塞方式实现)

给出的示例代码以STC15F系单片机实现,硬件环境为触摸IC输出信号,实际可以应用到轻触按键等任何需要消抖的按键需求;
硬件配置

sbit touchPad_1 = P1^6;
sbit touchPad_2 = P1^7;
sbit touchPad_3 = P1^5;

单次扫描:

u8 touchPadScan_oneShoot(void){	

	u8 valKey_Temp = 0;
	
	if(!touchPad_1)valKey_Temp |= 0x01;
	if(!touchPad_2)valKey_Temp |= 0x02;
	if(!touchPad_3)valKey_Temp |= 0x04;
	
	return valKey_Temp;
}

实践步骤-1):首先初始化定时器用于对按键按下时间以及连按间隔时间进行计时

定义按键按下计时值以及连按间隔时间进行计时值

u16	xdata touchPadActCounter	= 0;  //触摸计时
u16	xdata touchPadContinueCnt	= 0;  //连按间隔计时

定时器初始化

void appTimer0_Init(void){	//50us 中断@24.000M

	AUXR |= 0x80;		
	TMOD &= 0xF0;		
	TL0   = 0x50;		
	TH0   = 0xFB;	
	TF0   = 0;	
	ET0	  = 1;	//开中断
	PT0   = 0;	//高优先级中断
	
	TR0   = 1;		
}

计时运行

void timer0_Rountine (void) interrupt TIMER0_VECTOR{
	
	u8 code period_1ms 		= 20;
	static u8 counter_1ms 	= 0; 
	
	//****************1ms专用**********************************************/
	if(counter_1ms < period_1ms)counter_1ms ++;
	else{
	
		/*触摸动作时间计时*/
		if(touchPadActCounter)touchPadActCounter --;
		
		/*连按间隔时间计时*/
		if(touchPadContinueCnt)touchPadContinueCnt --;
	}
}

实践步骤-2):具体业务逻辑实现,本代码段为异步进行,即在主程序大循环内一直调用即可.
相关宏及数据结构

#define	timeDef_touchPressContinue	350		//连按间隔时间设置,单位:ms

typedef enum{

	press_Null = 1, //无按键
	press_Short, //短按
	press_ShortCnt, //连按
	press_LongA, //长按A
	press_LongB, //长按B
}keyCfrm_Type;

普通长短按触发逻辑封装

void touchPad_functionTrigNormal(u8 statusPad, keyCfrm_Type statusCfm){ //普通长短按触发

	switch(statusCfm){
	
		case press_Short:{
			
#if(DEBUG_LOGOUT_EN == 1)				
			{ //用后注释,否则占用大量代码空间
				u8 xdata log_buf[64];
				
				sprintf(log_buf, "touchPad:%02X, shortPress.\n", (int)statusPad);
				PrintString1_logOut(log_buf);
			}
#endif	
			switch(statusPad){
				
				case 1:
				case 2:
				case 4:{
					
					swCommand_fromUsr.actMethod = relay_flip;
					swCommand_fromUsr.objRelay = statusPad;
					EACHCTRL_realesFLG = statusPad; //互控
					devActionPush_IF.push_IF = 1; //推送使能
					
				}break;
					
				default:{}break;
			}
			
		}break;
		
		case press_ShortCnt:{
			
#if(DEBUG_LOGOUT_EN == 1)				
			{ //用后注释,否则占用大量代码空间
				u8 xdata log_buf[64];
				
				sprintf(log_buf, "touchPad:%02X, cntPress.\n", (int)statusPad);
				PrintString1_logOut(log_buf);
			}
#endif	
			
			switch(statusPad){
				
				case 1:
				case 2:
				case 4:{
					
					swCommand_fromUsr.actMethod = relay_flip;
					swCommand_fromUsr.objRelay = statusPad;
					
				}break;
					
				default:{}break;
			}
			
		}break;
		
		case press_LongA:{
			
#if(DEBUG_LOGOUT_EN == 1)				
			{ //用后注释,否则占用大量代码空间
				u8 xdata log_buf[64];
				
				sprintf(log_buf, "touchPad:%02X, longPress_A.\n", (int)statusPad);
				PrintString1_logOut(log_buf);
			}
#endif	
		
			switch(statusPad){
			
				case 1:{
					
				
				}break;
					
				case 2:{}break;
					
				case 4:{
				
					devStatusChangeTo_devHold(1); //设备网络挂起
				
				}break;
					
				default:{}break;
			}
			
		}break;
			
		case press_LongB:{
			
#if(DEBUG_LOGOUT_EN == 1)				
			{ //用后注释,否则占用大量代码空间
				u8 xdata log_buf[64];
				
				sprintf(log_buf, "touchPad:%02X, longPress_B.\n", (int)statusPad);
				PrintString1_logOut(log_buf);
			}
#endif	
		
			switch(statusPad){
			
				case 1:{}break;
					
				case 2:{}break;
					
				case 4:{}break;
					
				default:{}break;
			}
			
		}break;
			
		default:{}break;
	}
}

连按触发逻辑封装

void touchPad_functionTrigContinue(u8 statusPad, u8 loopCount){	//连按触发
	
	EACHCTRL_realesFLG = statusPad; //最后一次按下触发互控同步
	devActionPush_IF.push_IF = 1; //最后一次按下触发推送使能
	
#if(DEBUG_LOGOUT_EN == 1)				
	{ //用后注释,否则占用大量代码空间
		u8 xdata log_buf[64];
		
		sprintf(log_buf, "touchPad:%02X, %02Xtime pressOver.\n", (int)statusPad, (int)loopCount);
		PrintString1_logOut(log_buf);
	}
#endif	

	switch(statusPad){
	
		case 1:{
		
			switch(loopCount){
			
				case 3:{
				
				}break;
				
				case 4:{
				
					usrZigbNwkOpen(); //开放网络
					
				}break;
					
				default:{}break;
			}
			
		}break;
			
		case 2:{
		
			switch(loopCount){
			
				case 3:{}break;
					
				default:{}break;
			}
			
		}break;
			
		case 4:{
		
			switch(loopCount){
			
				case 3:{}break;
					
				case 4:{
				
					devHoldStop_makeInAdvance(); //设备网络挂起提前结束
				
				}break;
					
				default:{}break;
			}
			
		}break;
			
		default:{}break;
	}
}

具体业务逻辑

void touchPad_Scan(void){

	static u8 touchPad_temp = 0;
	static bit keyPress_FLG = 0;
	
	static bit funTrigFLG_LongA = 0;
	static bit funTrigFLG_LongB = 0;
	
	code	u16	touchCfrmLoop_Short 	= timeDef_touchPressCfm,	//消抖时间,即短按确认时间,单位:ms
				touchCfrmLoop_LongA 	= timeDef_touchPressLongA,	//长按A确认时间,单位:ms
				touchCfrmLoop_LongB 	= timeDef_touchPressLongB,	//长按B确认时间,单位:ms
				touchCfrmLoop_MAX	 	= 60000;//计时封顶
	
	static u8 pressContinueGet = 0;
	       u8 pressContinueCfm = 0;
	
	u16 conterTemp = 0; //时差计算缓存,所有计时都为倒计时,初值为touchCfrmLoop_MAX,所以确认时间时,需要进行减法运算
	
	if(touchPadScan_oneShoot()){
		
		if(!keyPress_FLG){
		
			keyPress_FLG = 1;
			touchPadActCounter = touchCfrmLoop_MAX;
			touchPadContinueCnt = timeDef_touchPressContinue;  //连按间隔判断时间更新
			touchPad_temp = touchPadScan_oneShoot();
		}
		else{
			
			if(touchPad_temp == touchPadScan_oneShoot()){
				
				conterTemp = touchCfrmLoop_MAX - touchPadActCounter;	
				if(conterTemp > touchCfrmLoop_LongA && conterTemp <= touchCfrmLoop_LongB){
				
					if(!funTrigFLG_LongA){
					
						funTrigFLG_LongA = 1;
						touchPad_functionTrigNormal(touchPad_temp, press_LongA);
					}
				}
				if(conterTemp > touchCfrmLoop_LongB && conterTemp <= touchCfrmLoop_MAX){
				
					if(!funTrigFLG_LongB){
					
						funTrigFLG_LongB = 1;
						touchPad_functionTrigNormal(touchPad_temp, press_LongB);
					}
				}
			}
		}
	}else{
		
		if(keyPress_FLG){
		
			conterTemp = touchCfrmLoop_MAX - touchPadActCounter;
			if(conterTemp > touchCfrmLoop_Short && conterTemp <= touchCfrmLoop_LongA){
			
				if(touchPadContinueCnt)pressContinueGet ++;
				if(pressContinueGet <= 1)touchPad_functionTrigNormal(touchPad_temp, press_Short); 
				else touchPad_functionTrigNormal(touchPad_temp, press_ShortCnt);
				beeps_usrActive(3, 50, 3);
			}
		}
	
		if(!touchPadContinueCnt && pressContinueGet){
		
			pressContinueCfm = pressContinueGet;
			pressContinueGet = 0;
			
			if(pressContinueCfm >= 2){
				touchPad_functionTrigContinue(touchPad_temp, pressContinueCfm);
				pressContinueCfm = 0;
			}
			
			touchPad_temp = 0;
		}

		funTrigFLG_LongA = 0;
		funTrigFLG_LongB = 0;
			
		touchPadActCounter = 0;
		keyPress_FLG = 0;
	}
}

至此,业务逻辑程序代码实现结束,以上代码针对按键功能对原工程进行剥离整理,若有需要,可查看博主原工程更为详细完整代码:https://github.com/Nepenthes/LB_ZIGB_devNode_sw_3bit/blob/master/Proj_sw_devZigbNode/Sensor/usrKin.c
同时,以上示例中的打印输出为串口引脚印射输出,与实际通信串口内部串口寄存器共用,这样做可以节省一点内存,具体实现也看查看原工程.

以上完成后便可以将驱动在前台大循环进行调用:
在这里插入图片描述
功能效果(图片):
在这里插入图片描述
功能效果(视频):
https://www.bilibili.com/video/av33750150?from=search&seid=12846664617436496314

没有更多推荐了,返回首页