从单片机入门者到单片机工程师

2019-07-17 14:40发布

学习单片机也已经有几年了,藉此机会和大家聊一下我学习过程中的一些经历和想法吧。也感谢一线工人提供了这个机会。
    几年前,和众多初学者一样,我接触到了单片机,立刻被其神奇的功能所吸引,从此不能自拔。很多个日夜就这样陪伴着它度过了。期间也遇到过非常多的问题,也一度被这些问题所困惑……等到回过头来,看到自己曾经走过的路,唏嘘不已。经常混迹于
论坛里,也看到了很多初学者发的求助帖子,看到他们走在自己曾走过的弯路上,忽然想到了自己的那段日子,心里竟然莫名的冲动,凡此总总,我总是尽自己所能去回帖。很多时候,都想写一点什么东西出来,希望对广大的初学者有一点点帮助。但总是不知从何处写起。今天借一线工人的台,唱一唱我的戏。“卖弄”也好,“吹嘘”也罢,我只是想认真的写写我这一路走来历经的总总,把其中值得注意,以及经验的地方写出来,权当是我对自己的一个总结吧。而作为看官的你,如果看到了我的错误,还请一定指正,这样对我以及其它读者都有帮助,而至于你如果从中能够收获到些许,那便是我最大的欣慰了。姑妄言之,姑妄听之。  一路学习过来的过程中,帮助最大之一无疑来自于网络了。很多时候,通过网络,我们都可以获取到所需要的学习资料。但是,随着我们学习的深入,我们会慢慢发现,网络提供的东西是有限度的,好像大部分的资料都差不多,或者说是适合大部分的初学者所需,而当我们想更进一步提高时,却发现能够获取到的资料越来越少,相信各位也会有同感,铺天盖地的单片机资料中大部分不是流水灯就是LED,液晶,而且也只是仅仅作功能性的演示。于是有些人选择了放弃,或者是转移到其他兴趣上面去了,而只有少部分人选择了继续摸索下去,结合市面上的书籍,然后在网络上锲而不舍的搜集资料,再从牛人的只言片语中去体会,不断动手实践,慢慢的,也摸索出来了自己的一条路子。当然这个过程必然是艰辛的,而他学会了之后也不会在网络上轻易分享自己的学习成果。如此恶性循环下去,也就不难理解为什么初级的学习资料满天飞,而深入一点的学习资料却很少的原因了。相较于其他领域,单片机技术的封锁更加容易。尽管已经问世了很多年了,有价值的资料还是相当的欠缺,大部分的资料都是止于入门阶段或者是简单的演示实验。但是在实际工程应用中却是另外一回事。有能力的高手无暇或者是不愿公开自己的学习经验。
    很多时候,我也很困惑,看到国外爱好者毫不保留的在网络上发布自己的作品,我忽然感觉到一丝丝的悲哀。也许,我们真的该转变一下思路了,帮助别人,其实也是在帮助自己。啰啰嗦嗦的说了这么多,相信大家能够明白说的是什么意思。在接下来的一段日子里,我将会结合电子工程师之家举办的主题周活动写一点自己的想法。尽可能从实用的角度去讲述。希望能够帮助更多的初学者更上一层楼。而关于这个主题周的最大主题我想了这样的一个名字“从单片机初学者迈向单片机工程师”。名字挺大挺响亮,给我的压力也挺大的,但我会努力,争取使这样的一系列文章能够带给大家一点帮助,而不是看后大跌眼镜。这样的一系列文章主要的对象是初学者,以及想从初学者更进一步提高的读者。而至于老手,以及那些牛XX的人,希望能够给我们这些初学者更多的一些指点哈~@_@
友情提示: 此问题已得到解决,问题已经关闭,关闭后问题禁止继续编辑,回答。
19条回答
秀才与兵
2019-07-18 08:09
硬件连接很简单,四个独立按键分别接在P3^0------P3^3四个I/O上面。
因为51单片机I/O口内部结构的限制,在读取外部引脚状态的时候,需要向端口写1.在51单片机复位后,不需要进行此操作也可以进行读取外部引脚的操作。因此,在按键的端口没有复用的情况下,可以省略此步骤。而对于其它一些真正双向I/O口的单片机来说,将引脚设置成输入状态,是必不可少的一个步骤。
下面的程序代码初始化引脚为输入。
void KeyInit(void)
{
    io_key_1 = 1 ;
    io_key_2 = 1 ;
    io_key_3 = 1 ;
    io_key_4 = 1 ;            
}
根据按键硬件连接定义按键键值
#define KEY_VALUE_1              0x0e
#define KEY_VALUE_2              0x0d
#define KEY_VALUE_3                0x0b
#define KEY_VALUE_4                0x07
#define KEY_NULL                    0x0f
下面我们来编写按键的硬件驱动程序。
根据第一章所描述的按键检测原理,我们可以很容易的得出如下的代码:
static uint8 KeyScan(void)
{
    if(io_key_1 == 0)return KEY_VALUE_1 ;
    if(io_key_2 == 0)return KEY_VALUE_2 ;
    if(io_key_3 == 0)return KEY_VALUE_3 ;
    if(io_key_4 == 0)return KEY_VALUE_4 ;
    return KEY_NULL ;
}
其中io_key_1等是我们按键端口的定义,如下所示:
sbit io_key_1 = P3^0 ;
sbit io_key_2 = P3^1 ;
sbit io_key_3 = P3^2 ;
sbit io_key_4 = P3^3 ;

KeyScan()作为底层按键的驱动程序,为上层按键扫描提供一个接口,这样我们编写的上层按键扫描函数可以几乎不用修改就可以拿到我们的其它程序中去使用,使得程序复用性大大提高。同时,通过有意识的将与底层硬件连接紧密的程序和与硬件无关的代码分开写,使得程序结构层次清晰,可移植性也更好。对于单片机类的程序而言,能够做到函数级别的代码重用已经足够了。
在编写我们的上层按键扫描函数之前,需要先完成一些宏定义。
//定义长按键的TICK数,以及连_发间隔的TICK数
#define KEY_LONG_PERIOD        100
#define KEY_CONTINUE_PERIOD    25

//定义按键返回值状态(按下,长按,连_发,释放)
#define KEY_DOWN                0x80
#define KEY_LONG                    0x40
#define KEY_CONTINUE            0x20
#define KEY_UP                  0x10

//定义按键状态
#define KEY_STATE_INIT            0
#define KEY_STATE_WOBBLE            1
#define KEY_STATE_PRESS            2
#define KEY_STATE_LONG            3
#define KEY_STATE_CONTINUE      4
#define KEY_STATE_RELEASE        5

接着我们开始编写完整的上层按键扫描函数,按键的短按,长按,连按,释放等等状态的判断均是在此函数中完成。对照状态流程转移图,然后再看下面的函数代码,可以更容易的去理解函数的执行流程。完整的函数代码如下:

void GetKey(uint8 *pKeyValue)
{
    static uint8 s_u8KeyState = KEY_STATE_INIT ;
    static uint8 s_u8KeyTimeCount = 0 ;
    static uint8 s_u8LastKey = KEY_NULL ;  //保存按键释放时候的键值
    uint8 KeyTemp = KEY_NULL ;

    KeyTemp = KeyScan() ;        //获取键值

    switch(s_u8KeyState)
    {
        case KEY_STATE_INIT :
                {
                    if(KEY_NULL != (KeyTemp))
                    {
                        s_u8KeyState = KEY_STATE_WOBBLE ;
                    }
                }
        break ;

        case KEY_STATE_WOBBLE :      //消抖
                {
                    s_u8KeyState = KEY_STATE_PRESS ;     
                }
        break ;

        case KEY_STATE_PRESS :
                {
                    if(KEY_NULL != (KeyTemp))
                    {
                        s_u8LastKey = KeyTemp ; //保存键值,以便在释放按键状态返回键值
                        KeyTemp |= KEY_DOWN ;  //按键按下
                        s_u8KeyState = KEY_STATE_LONG ;
                    }
                    else
                    {
                        s_u8KeyState = KEY_STATE_INIT ;
                    }
                }
        break ;

        case KEY_STATE_LONG :
                {
                    if(KEY_NULL != (KeyTemp))
                    {
                        if(++s_u8KeyTimeCount > KEY_LONG_PERIOD)
                        {
                            s_u8KeyTimeCount = 0 ;
                            KeyTemp |= KEY_LONG ;  //长按键事件发生
                            s_u8KeyState = KEY_STATE_CONTINUE ;
                        }
                    }
                    else
                    {
                        s_u8KeyState = KEY_STATE_RELEASE ;
                    }
                }
        break ;

        case KEY_STATE_CONTINUE :
                {
                    if(KEY_NULL != (KeyTemp))
                    {
                        if(++s_u8KeyTimeCount > KEY_CONTINUE_PERIOD)
                        {
                            s_u8KeyTimeCount = 0 ;
                            KeyTemp |= KEY_CONTINUE ;
                        }
                    }
                    else
                    {
                        s_u8KeyState = KEY_STATE_RELEASE ;
                    }
                }
        break ;

        case KEY_STATE_RELEASE :
                {
                    s_u8LastKey |= KEY_UP ;
                    KeyTemp = s_u8LastKey ;
                    s_u8KeyState = KEY_STATE_INIT ;
                }
        break ;

        default : break ;
    }
    *pKeyValue = KeyTemp ; //返回键值     
}
关于这个函数内部的细节我并不打算花过多笔墨去讲解。对照着按键状态流程转移图,然后去看程序代码,你会发现其实思路非常清晰。最能让人理解透彻的,莫非就是将整个程序自己看懂,然后想象为什么这个地方要这样写,抱着思考的态度去阅读程序,你会发现自己的程序水平会慢慢的提高。所以我更希望的是你能够认认真真的看完,然后思考。也许你会收获更多。
不管怎么样,这样的一个程序已经完成了本章开始时候要求的功能:按下,长按,连按,释放。事实上,如果掌握了这种基于状态转移的思想,你会发现要求实现其它按键功能,譬如,多键按下,功能键等等,亦相当简单,在下一章,我们就去实现它。
在主程序中我编写了这样的一段代码,来演示我实现的按键功能。
void main(void)
{     
    uint8 KeyValue = KEY_NULL;
    uint8 temp = 0 ;
      LED_CS11 = 1 ; //流水灯输出允许
    LED_SEG = 0 ;
    LED_DIG = 0 ;
    Timer0Init() ;
    KeyInit() ;
    EA = 1 ;
    while(1)
    {
        Timer0MainLoop() ;
        KeyMainLoop(&KeyValue) ;
         
        if(KeyValue == (KEY_VALUE_1 | KEY_DOWN)) P0 = ~1 ;
        if(KeyValue == (KEY_VALUE_1 | KEY_LONG)) P0 = ~2 ;
        if(KeyValue == (KEY_VALUE_1 | KEY_CONTINUE)) { P0 ^= 0xf0;}
        if(KeyValue == (KEY_VALUE_1 | KEY_UP)) P0 = 0xa5 ;
    }

}
    按住第一个键,可以清晰的看到P0口所接的LED的状态的变化。当按键按下时候,第一个LED灯亮,等待2 S后第二个LED亮,第一个熄灭,表示长按事件发生。再过500 ms 第5~8个LED闪烁,表示连按事件发生。当释放按键时候,P0口所接的LED的状态为:
灭亮灭亮亮灭亮灭,这也正是P0 = 0xa5这条语句的功能。
第四章----渐明渐暗的灯

一周热门 更多>