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

2019-07-17 14:40发布

学习单片机也已经有几年了,藉此机会和大家聊一下我学习过程中的一些经历和想法吧。也感谢一线工人提供了这个机会。
    几年前,和众多初学者一样,我接触到了单片机,立刻被其神奇的功能所吸引,从此不能自拔。很多个日夜就这样陪伴着它度过了。期间也遇到过非常多的问题,也一度被这些问题所困惑……等到回过头来,看到自己曾经走过的路,唏嘘不已。经常混迹于
论坛里,也看到了很多初学者发的求助帖子,看到他们走在自己曾走过的弯路上,忽然想到了自己的那段日子,心里竟然莫名的冲动,凡此总总,我总是尽自己所能去回帖。很多时候,都想写一点什么东西出来,希望对广大的初学者有一点点帮助。但总是不知从何处写起。今天借一线工人的台,唱一唱我的戏。“卖弄”也好,“吹嘘”也罢,我只是想认真的写写我这一路走来历经的总总,把其中值得注意,以及经验的地方写出来,权当是我对自己的一个总结吧。而作为看官的你,如果看到了我的错误,还请一定指正,这样对我以及其它读者都有帮助,而至于你如果从中能够收获到些许,那便是我最大的欣慰了。姑妄言之,姑妄听之。  一路学习过来的过程中,帮助最大之一无疑来自于网络了。很多时候,通过网络,我们都可以获取到所需要的学习资料。但是,随着我们学习的深入,我们会慢慢发现,网络提供的东西是有限度的,好像大部分的资料都差不多,或者说是适合大部分的初学者所需,而当我们想更进一步提高时,却发现能够获取到的资料越来越少,相信各位也会有同感,铺天盖地的单片机资料中大部分不是流水灯就是LED,液晶,而且也只是仅仅作功能性的演示。于是有些人选择了放弃,或者是转移到其他兴趣上面去了,而只有少部分人选择了继续摸索下去,结合市面上的书籍,然后在网络上锲而不舍的搜集资料,再从牛人的只言片语中去体会,不断动手实践,慢慢的,也摸索出来了自己的一条路子。当然这个过程必然是艰辛的,而他学会了之后也不会在网络上轻易分享自己的学习成果。如此恶性循环下去,也就不难理解为什么初级的学习资料满天飞,而深入一点的学习资料却很少的原因了。相较于其他领域,单片机技术的封锁更加容易。尽管已经问世了很多年了,有价值的资料还是相当的欠缺,大部分的资料都是止于入门阶段或者是简单的演示实验。但是在实际工程应用中却是另外一回事。有能力的高手无暇或者是不愿公开自己的学习经验。
    很多时候,我也很困惑,看到国外爱好者毫不保留的在网络上发布自己的作品,我忽然感觉到一丝丝的悲哀。也许,我们真的该转变一下思路了,帮助别人,其实也是在帮助自己。啰啰嗦嗦的说了这么多,相信大家能够明白说的是什么意思。在接下来的一段日子里,我将会结合电子工程师之家举办的主题周活动写一点自己的想法。尽可能从实用的角度去讲述。希望能够帮助更多的初学者更上一层楼。而关于这个主题周的最大主题我想了这样的一个名字“从单片机初学者迈向单片机工程师”。名字挺大挺响亮,给我的压力也挺大的,但我会努力,争取使这样的一系列文章能够带给大家一点帮助,而不是看后大跌眼镜。这样的一系列文章主要的对象是初学者,以及想从初学者更进一步提高的读者。而至于老手,以及那些牛XX的人,希望能够给我们这些初学者更多的一些指点哈~@_@
友情提示: 此问题已得到解决,问题已经关闭,关闭后问题禁止继续编辑,回答。
19条回答
秀才与兵
2019-07-18 10:07
初始化定时器0
void Timer0Init(void)
{
    TMOD &= 0xf0 ;  
    TMOD |= 0x01 ;      //定时器0工作方式1
    TH0  =    0xf8 ;      //定时器初始值
    TL0  =  0xcc ;  
    TR0  = 1 ;  
    ET0  = 1 ;  
}
在定时器0中断处理程序中,设置时标消息。
void Time0Isr(void) interrupt 1
{
    TH0  =    0xf8 ;            //定时器重新赋初值
    TL0  =  0xcc ;
    g_bSystemTime2Ms = 1 ;    //2MS时标标志位置位
}
然后我们开始编写数码管的动态扫描函数。
新建一个C源文件,并包含相应的头文件。
#include <reg52.h>
#include "MacroAndConst.h"
#include "Timer.h"
先开辟一个数码管显示的缓冲区。动态扫描函数负责从这个缓冲区中取出数据,并扫描显示。而其它函数则可以修改该缓冲区,从而改变显示的内容。
uint8 g_u8LedDisplayBuffer[8] = {0} ; //显示缓冲区
然后定义共阳数码管的段码表以及相应的硬件端口连接。
code uint8 g_u8LedDisplayCode[]=
{
    0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,
    0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8E,
    0xbf, //'-'号代码
} ;

sbit io_led_seg_cs = P1^4 ;
sbit io_led_bit_cs = P1^5 ;

#define LED_PORT P0

再分别编写送数码管段码函数,以及位选通函数。
static void SendLedSegData(uint8 dat)
{
    LED_PORT      = dat ;   
    io_led_seg_cs = 1 ;        //开段码锁存,送段码数据
    io_led_seg_cs = 0 ;
}

static void SendLedBitData(uint8 dat)
{
    uint8 temp ;
    temp = (0x01 << dat ) ;  //根据要选通的位计算出位码
    LED_PORT      = temp ;
    io_led_bit_cs = 1 ;          //开位码锁存,送位码数据   
    io_led_bit_cs = 0 ;        
}

下面的核心就是如何编写动态扫描函数了。
如下所示:


void LedDisplay(uint8 * pBuffer)
{
    static uint8 s_LedDisPos = 0 ;
    if(g_bSystemTime2Ms)
    {
        g_bSystemTime2Ms = 0 ;

        SendLedBitData(8) ;        //消隐,只需要设置位选不为0~7即可

        if(pBuffer[s_LedDisPos] == '-')      //显示'-'号
        {
            SendLedSegData(g_u8LedDisplayCode[16]) ;            
        }
        else   
        {
            SendLedSegData(g_u8LedDisplayCode[pBuffer[s_LedDisPos]]) ;
        }

        SendLedBitData(s_LedDisPos);

        if(++s_LedDisPos > 7)
        {
            s_LedDisPos = 0 ;   
        }   
    }        
}

函数内部定义一个静态的变量s_LedDisPos,用来表示扫描数码管的位置。每当我们执行该函数一次的时候,s_LedDisPos的值会自加1,表示下次扫描下一个数码管。然后判断g_bSystemTime2Ms时标消息是否到了。如果到了,就开始执行相关扫描,否则就直接跳出函数。SendLedBitData(8) ;的作用是消隐。因为我们的系统的段选和位选是共用P0口的。在送段码之前,必须先关掉位选,否则,因为上次位选是选通的,在送段码的时候会造成相应数码管的点亮,尽管这个时间很短暂。但是因为我们的数码管是不断扫描的,所以看起来还是会有些微微亮。为了消除这种影响,就有必要再送段码数据之前关掉位选。
if(pBuffer[s_LedDisPos] == '-')      //显示'-'号这行语句是为了显示’-’符号特意加上去的,大家可以看到在定义数码管的段码表的时候,我多加了一个字节的代码0xbf:
code uint8 g_u8LedDisplayCode[]=
{
    0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,
    0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8E,
    0xbf, //'-'号代码
} ;
通过SendLedSegData(g_u8LedDisplayCode[pBuffer[s_LedDisPos]]) ;送出相应的段码数据后,然后通过SendLedBitData(s_LedDisPos);打开相应的位选。这样对应的数码管就被点亮了。
if(++s_LedDisPos > 7)
{
    s_LedDisPos = 0 ;   
}
然后s_LedDisPos自加1,以便下次执行本函数时,扫描下一个数码管。因为我们的系统共有8个数码管,所以当s_LedDisPos > 7后,要对其进行清0 。否则,没有任何一个数码管被选中。这也是为什么我们可以用
    SendLedBitData(8) ;        //消隐,只需要设置位选不为0~7即可
对数码管进行消隐操作的原因。
   
下面我们来编写相应的主函数,并实现数码管上面类似时钟的效果,如显示10-20-30
即10点20分30秒。
Main.c
#include <reg52.h>
#include "MacroAndConst.h"
#include "Timer.h"
#include "Led7Seg.h"

sbit io_led = P1^6 ;


void main(void)
{
        io_led = 0 ;      //发光二极管与数码管共用P0口,这里禁止掉发光二极管的锁存输出
        Timer0Init() ;
        g_u8LedDisplayBuffer[0] = 1 ;
        g_u8LedDisplayBuffer[1] = 0 ;
        g_u8LedDisplayBuffer[2] = '-' ;
        g_u8LedDisplayBuffer[3] = 2 ;
        g_u8LedDisplayBuffer[4] = 0 ;
        g_u8LedDisplayBuffer[5] = '-' ;
        g_u8LedDisplayBuffer[6] = 3 ;
        g_u8LedDisplayBuffer[7] = 0 ;
        EA = 1 ;
        while(1)
        {
            LedDisplay(g_u8LedDisplayBuffer) ;
        }
}
将整个工程进行编译,看看效果如何





动起来

既然我们想要模拟一个时钟,那么时钟肯定是要走动的,不然还称为什么时钟撒。下面我们在前面的基础之上,添加一点相应的代码,让我们这个时钟走动起来。
我们知道,之前我们以及设置了一个扫描数码管用到的2 ms时标。 如果我们再对这个时标进行计数,当计数值达到500,即500 * 2 = 1000 ms 时候,即表示已经逝去了1 S的时间。我们再根据这个1 S的时间更新显示缓冲区即可。听起来很简单,让我们实现它吧。
首先在Timer.c中声明如下两个变量:
bit g_bTime1S = 0 ;                      //时钟1S时标消息
static uint16 s_u16ClockTickCount = 0 ;  //对2 ms 时标进行计数

再在定时器中断函数中添加如下代码:
    if(++s_u16ClockTickCount == 500)
    {
        s_u16ClockTickCount = 0 ;
        g_bTime1S = 1 ;        
    }
从上面可以看出,s_u16ClockTickCount计数值达到500的时候,g_bTime1S时标消息产生。然后我们根据这个时标消息刷新数码管显示缓冲区:
void RunClock(void)
{
    if(g_bTime1S )
    {
        g_bTime1S = 0 ;
        if(++g_u8LedDisplayBuffer[7] == 10)
        {
            g_u8LedDisplayBuffer[7] = 0 ;
            if(++g_u8LedDisplayBuffer[6] == 6)
            {
                g_u8LedDisplayBuffer[6] = 0 ;
                if(++g_u8LedDisplayBuffer[4] == 10)
                {
                    g_u8LedDisplayBuffer[4]    = 0 ;
                    if(++g_u8LedDisplayBuffer[3] == 6)
                    {
                        g_u8LedDisplayBuffer[3] = 0 ;
                        if(    g_u8LedDisplayBuffer[0]<2)
                        {
                            if(++g_u8LedDisplayBuffer[1]==10)
                            {
                                g_u8LedDisplayBuffer[1] = 0 ;
                                g_u8LedDisplayBuffer[0]++;
                            }  
                        }
                        else
                        {
                            if(++g_u8LedDisplayBuffer[1]==4)
                            {
                                g_u8LedDisplayBuffer[1] = 0 ;
                                g_u8LedDisplayBuffer[0] = 0 ;
                            }  
                        }   
                    }
                }
            }

        }
    }        
}
这个函数的作用就是对每个数码管缓冲位的值进行判断,判断的标准就是我们熟知的24小时制。如秒的个位到了10 就清0,同时秒的十位加1….诸如此类,我就不一一详述了。
同时,我们再编写一个时钟初始值设置函数,这样,可以很方便的在主程序开始的时候修改时钟初始值。
void SetClock(uint8 nHour, uint8 nMinute, uint8 nSecond)
{
    g_u8LedDisplayBuffer[0] = nHour / 10 ;
    g_u8LedDisplayBuffer[1] = nHour % 10 ;
    g_u8LedDisplayBuffer[2] = '-' ;
    g_u8LedDisplayBuffer[3] = nMinute / 10 ;
    g_u8LedDisplayBuffer[4] = nMinute % 10 ;
    g_u8LedDisplayBuffer[5] = '-' ;
    g_u8LedDisplayBuffer[6] = nSecond / 10 ;
    g_u8LedDisplayBuffer[7] = nSecond % 10 ;            
}
然后修改下我们的主函数如下:
void main(void)
{
    io_led = 0 ;      //发光二极管与数码管共用P0口,这里禁止掉发光二极管的锁存输出
    Timer0Init() ;
    SetClock(10,20,30) ;  //设置初始时间为10点20分30秒
    EA = 1 ;
    while(1)
    {
        LedDisplay(g_u8LedDisplayBuffer) ;
        RunClock();
    }
}
编译好之后,下载到我们的实验板上,怎么样,一个简单的时钟就这样诞生了。


至此,本章所诉就告一段落了。至于如何完成数码管的闪烁显示,就像本章开头所说的那个数码管时钟的功能,就作为一个思考的问题留给大家思考吧。
同时整个LED篇就到此结束了,在以后的文章中,我们将开始学习如何编写实用的按键扫描程序。

一周热门 更多>