zoukankan      html  css  js  c++  java
  • 单片机小白与单片机工程师,差得就是这两个思想!

    分层的思想,并不是什么神秘的东西,事实上很多做项目的工程师本身自己也会在用。看了不少帖子都发现没有提及这个东西,然而分层结构确是很有用的东西,参透后会有一种恍然大悟的感觉。如果说我不懂LCD怎么驱动,那好办,看一下datasheet,参考一下阿别人的程序,很快就可以做出来。但是如果不懂程序设计的思想的话,会给你做项目的过程中带来很多很多的困惑。

    参考了市面上各种各样的嵌入式书籍,MCS-51,AVR ,ARM 等都有看过,但是没有发现有哪本是介绍设计思想的,就算有也是凤毛麟角。写程序不难,但是程序怎么样才能写的好,写的快,那是需要点经验积累的。结构化模块化的程序设计的思想,使最基本的要求。

    然而这么将这个抽象的概念运用到工程实践当中恩?那需要在做项目的过程中经历磨难,将一些东西总结出来,抽象升华为理论,对经验的积累和技术的传播都大有裨益。所以在下出来献丑一下,总结一些东西。

     就我个人的经验而谈,有两个设计思想是非常重要的。

    一个就是“时间片轮的设计思想”,这个对实际中解决多任务问题非常有用,通常可以用这个东西来判断一个人是单片机学习者,还是一个单片机工程师。这个必须掌握。(下文将介绍)。

     第二个就是“分层屏蔽的设计思想”即分层思想。下面用扫描键盘程序例子作为引子,引出今天说的东西。

    问题的提出

    单片机学习板一般为了简单起见,将按键分配的很好,例如整个 4*4 的键盘矩阵分配到 P1 口上面,8条控制线,刚好。这样的话程序也非常好写。只需要简单的
    KEY_DAT= P1;


    端口的数据就读进来了。

    诚然,现实中没有这么好的事情。在实际的项目应用当中,单片机引脚的复用相当厉害,这跟那些所谓的单片机学习板就有很大的差别了。

    另外一个原因,一般设计来说,是“软件配合硬件”的设计流程,简单点说就是,先确定好硬件原理图,硬件布线,最后才是软件的开发,因为硬件修改起来比较麻烦,相对来说软件修改的时候比较好改。这个就是中国传统的阴阳平衡哲学原理。硬件设计和软件设计本来就是鱼和熊掌的关系,两者不可兼得。方便了硬件设计,很可能给写软件带来很大的麻烦。

    反过来说,方便了软件设计,硬件设计也会相当的麻烦。如果硬件设计和软件设计同时方便了,那只有两种可能,一是这个设计方案非常简单,二是设计师已经达到了一个非常高的境界。我们不考虑那么多情况,单纯从常用的实际应用的角度来看问题。

     硬件为了布线的方便,很多时候会可能将IO口分配到不同的端口上面,例如上面说的4*4键盘,8根线分别分配到 P0 P1 P2 P3 上面去了。那么,开发板的那些扫描键盘程序可以去见鬼了。怎么扫按键?我想起了我刚开始学习的时候,分成3段非常相似的程序,一个一个按键的扫描的经历......

    或许有人不甘心,“那些东西我花了很长时间学习的,也用的好好的,怎么能说一句不用就不用?”虽然有点残忍,但是我还是想说“兄弟,接受现实吧,现实是残酷的......”

    不过,人区别于低等动物的差别,是人会创造,在碰到困难的时候会想办法解决,于是我们开始了沉思......

    最后我们引入初中数学学的“映射”的概念来解决问题。基本思想就是,将不同端口的按键映射到相同端口上面。

    这样按键扫描程序就分成3个层次了。

    1)最底层的是硬件层,完成端口扫描,20ms延时消抖,将端口的数据映射到一个KEY_DAT寄存器上面,KEY_DAT作为对上层驱动层的一个接口。

    2)中间的一层是驱动层,驱动层只对 KEY_DAT 寄存器的数值进行操作。简单点说,我们无论底层的硬件是怎么接线的,在驱动层都不需要关心,只需要关心 KEY_DAT 这个寄存器的数值是什么就可以了。这样出来的间接效果就是“屏蔽了底层硬件的差异”,所以驱动层写的程序就可以通用了。

    驱动层的另外一个功能是为了上层提供消息接口。我们用了类似window程序的消息的概念。这里可以提供一些按键消息,例如:按下消息,松开消息,长按键消息,长按键的时候的步进消息,等等。

    3)应用层。这里就是根据项目的不同分别写按键功能程序,属于最上层的程序。它使用的是驱动层提供的消息接口。在应用层写程序的思想就是,我不管下层是怎么工作的,我只关心按键消息。有按键消息来的时候我就执行功能,没有消息来的时候,我就什么也不做。

    下面用一个简单的常用的例子,说明我们这个设计思想的用法。

    秒表调整时间的时候,要求按着某个按键不放,时间能连续的向上增加。这个东西很实用,实际的家电中用途很广泛。

    在看下面的东西之前,大家可以想一下,这东西难吗?相信大家都会很响亮的回答,“不难!!”,然而我再问:“这东西麻烦吗?”我相信很多人肯定会说“很麻烦!!” 这不禁让我想起开始学单片机的时候写这种按键的那程序,乱七八糟的结构。如果不相信的话,可以自己用51写一下哦,那样就更加能体会本文说的分层结构的优越性。


    项目要求:

    两个按键,分别分配在P10 和P20,分别是“加”“减”按键,要求长按键的时候实现连续加和连续减的功能。

    实战:

    假设按键上拉,没有按键的时候高电平,有按键的时候低电平,另外,为了突出问题,这里没有将延时消抖的程序写上去,在实际项目中应该加上。C语言函数参数的传递多种多样,这里作为例子,用了最简单的全局变量来传递参数,当然你也可以用 unsigned charReadPort(void) 返回一个读键结果,甚至还可以 void ReadPort(unsigned char*pt) 用一个指针变量传递地址而达到直接修改变量的目的。方法是多种多样的,这个决定于每个人的程序风格。

     1)开始写硬件层程序,完成映射
    #defineKYE_MIN  0X01#defineKEY_PLUS  0X01unsignedchar KeyDat;voidReadPort(void){if (P1 & KEY_PLUS == 0 ){KeyDat |= 0x01 ;}if (P2 & KEY_MIN  == 0 ){KeyDat |= 0x02 ;}}


    C语言应该很容易看懂吧?如果 KEY_PLUS 按下,P10口读到低电平,则 P1 &KEY_PLUS 的结果为 0 (xxxx xxx0 & 0000 0001),满足if 的条件,进入KeyDat |=0x01  是将 KeyDat 的bit0 置一,也就是说,将 KEY_PLUS 映射到 KeyDat 的 bit0

    KEY_MIN是同样的道理映射到 KeyDat 的 bit1,如果 KeyDat 的 bit0 为 1 ,则说明 KEY_PLUS 按下,反则亦然。

    不需要想的很神秘,映射就是这么一回事。如果还有其他按键的话,用同样办法,将他们全部映射到 KeyDat 上面。

    2)驱动层程序编写
    如果将 KeyDat想象成 P1 口,那么这个跟学习板那标准的扫描程序不就是一样了吗?对的,这个就是底层映射的目的了。

    3)应用层程序编写

    根据消息,硬件层是必须分离出来,然而驱动层和应用层的要求就不那么严格了,事实上一些简单的项目没有必要将这两层分离开来,根据实际应用灵活应对就可以了。

    其实这样写程序是很方便移植的,根据板子的不同而适当的修改一下硬件层那个 ReadPort 函数就完成了,驱动层和应用层很多代码可以不经过修改直接用,很能提高开发效率的。当然这个按键程序会存在一定的问题,特别是遇到常闭按键和点触按键的混合使用的场合。这个留给大家自己去想了,反正问题总是能找到解决办法的,尽管方法有好有坏。

    时间片轮设计思想

    先用一个小例子引出今天的主题,想象一下,一个基本的家电控制板,肯定或多或少的会包含 :LED 或者 数码管显示,按键, 继电器或者可控硅的输出 这3部分。数码管需要 10ms到20ms的动态扫描,按键也需要20ms左右的延时消抖,有没有意识到,其实这些时间是同时在进行的。

    回想一下咱们的教科书怎么教 按键 的延时消抖的?没错,死循环,绝对是原地踏步死循环,用指令来计时。这样很自然的引发一个问题,单片机在原地踏步死循环的话,那么其它的工作怎么办?如数码管的动态扫描怎么办?

    唯有等按键扫描之后再进行了,这样出来的效果,数码管肯定会闪烁的,扫描时间过长了,缩短按键消抖时间也不是解决办法,想象如果咱们还有其它很多工作也是同时做的呢?解决办法之一,就是今天的主题,分时扫描的思想。当然不会是唯一的办法,只不过俺一直在用,觉得这个是非常不错的思想,可以解决很多实际问题。大胆妄言一下,分时扫描的思想也是单片机编程最核心的思想了,信不信就由你自己判断了。

    核心思想的实现

    其实是几个步骤

    第一、用RTC中断来计时,RTC的中断时间短一点,我习惯是125us ,为了解红外遥控的码,这个时间是需要的。RTC计时是相当准的,尽量利用。

    第二、在RTC的中断服务程序里面放3个(数量自定)记时器(说白了就是计数器),我的习惯是 2ms 5ms 500ms 这3个是作为基准时间,提供给整个系统来调用的,所以必须准确一点,实际用示波器调一下就OK了,不难。

    第三、在主程序的循环里面放一个专门处理时间的子程序。(注:单片机是不会停的,永远在不断循环的跑,这个跟学校学的貌似有点不同,俺面试的时候被问过这个问题 ….)  将所有的时间处理都放在时间处理子程序里面做,这样是非常方便的,一个单片机系统最起码需要处理 10~20个不同的时间,也需要10~20个计时器了,而且相当多要求同时不同步工作的,如果每个都单独的话是相当的麻烦。

    第四、“程序是跑着来等,而不是站着来等”,这话看来有点玄,一个跟俺一起进去公司的工程师讨论的时候提到的这个问题,俺觉得这个也是分时系统的一个比较重要的思想,所以也这样叫,下面有细说。

    第五、下面用程序来说话,注释尽量详细,可以不用看代码,直接看注释就可以了。

    (一)先中断服务程序部分:

    每 125us 中断一次
    ;-------------------产生几个基准时间---------------------------

    (1) ref_2ms寄存器不断的减1,每次中断减1,一共减 16次,所以这里经过的时间是 125us × 16 = 2ms,这个就是所谓的计时/计数器 了。这样就可以靠一个系统的RTC中断,来实现我们需要的很多个定时时间。

    (2)置2ms 计时结束标志,这个是提供给时间处理程序用的,这是一个计时器的框架,下面的5ms计时完全相同。

    这程序还用了一个块的框架,比较方便的,不过跟今天的主题无关,以后郁闷的时候再上来写写这个。上面的程序就是中断服务程序里面的计时器,分别定时 2ms 5ms 500ms,计时完毕溢出是

    flag_time 标志来记录的,程序通过读这个标志就可以知道定时的时间是否已经到了。

    (二)下面看那个统一的时间服务子程序

    上面用了按键20ms消抖的计时器作为例子,如果理解之后就可以发现,我们可以完全模仿那个计时器而在下面放很多很多的计时器,则每5ms 进来一下,每个计时器都同时在计数了,谁先计算完毕就先关掉自己,置相应的标志给其它程序调用,而对其它计时器完全没有影响!这样,我们可以在这里放很多个计时器了,一般来说,十来二十个是没有问题的,完全满足一个单片机系统对多个时间的需求了。

    单个计时器的结构很简单,先判断允许计时标志是否进入计时,然后一个专用的寄存器在加1或者减1,加/减相应的数值之后也就是相应的时间到了,关掉计时器,置相应需要用到的标志。

    到这里差不多了,俺们需要的时间都可以出来了,这样做是不是非常方便?咱们再来看看在这段时间里单片机在做了什么东西?只有中断计时够 5ms 或者 500ms ,那个溢出标志才有效,才能进入上面的计时程序,其它时间都是在做其它事情。而且进入上面的计时器的时候,可以看出,并不是在那里死循环,只是单纯的加减一下寄存器就退出了,整个过程耗时极其短,看代码不同吧,5us到 20us左右吧,对主程序的执行没有什么影响。

    (三)下面看看具体怎么调用

    最开始谈过的按键的消抖时间处理问题,现在就用上面介绍的办法来看具体怎么解决问题。按键的处理也是重要的基础学问,不过不在本次的讨论范围,所以只是单单的讨论怎么解决时间问题,而对于按键的一些问题,下次有机会继续讨论吧,hoho~~~

    大概是这样的:判断什么时候有健,没有的话跳出,有的话开始延时消抖的计时,第二次进来的时候直接由标志位控制过去判断时间时候够。

    同样是等待,这里就是最后一点所说的,咱这是跑着来等,不是站着来等。跟死循环定时比较,在没有定时到20ms 的这段时间里面单片机在做什么?死循环的话,肯定就是在原地等,什么都不做,而看看上面的程序,他只是判断是否定时够,具体的定时在统一的时间子程序里面做,判断没有到时间的话就跳出了,继续跑其它的程序,直到当时间到了,单片机判断出flag_delay,key_flow 符合条件,开始进入按键处理程序了,在这个期间,单片机都在做其它事情,只是一个主循环跑回来判断一次,所以单片机完全有空跑其它的程序,而没有将时间都耗在消抖上面。

    (四)看看我的主程序循环体

    这个就是我用的循环体了,所有功能都做成子程序形式了,需要就挂上去就可以了,比较方便,这样一个总的循环体,单片机就是在不断的执行这个循环体,如果整个程序都采用上面说的分时扫的思想的话,一周循环回来的时间是相当短的,其实是不是跟电脑的思想有点像呢?电脑再快也并不是同时处理多个任务,而且每次处理一个,然后非常快的速度来循环处理,让我们感觉上他是在同时处理多个程序那样,我想,我最终想表达的思想也就是这个而已。

    在我看来,有这个思想支撑下,单片机的程序变得比较容易上手了,剩下的只是集中精力去用程序来实现我们的思想而已,当然,这里只是说一种可行的办法而已,不是说只有这种办法,如果大家有好的思想也分享一下哦,编写程序是一门艺术,写出来很容易,但是写得好,写得精巧,那就很难了。

    不知道该如何学单片机?那就看看这里吧

    (stm32串口应用)
    http://www.makeru.com.cn/live/detail/1290.html?s=143793
    http://www.makeru.com.cn/live/1392_1164.html?s=143793
    (stm32电机驱动)
    http://www.makeru.com.cn/live/1392_1218.html?s=143793
    (定时器)
    http://www.makeru.com.cn/live/1392_1199.html?s=143793
    (DMA专题讲解)
    http://www.makeru.com.cn/live/1392_1048.html?s=143793

  • 相关阅读:
    Java实现 计蒜客 拯救行动
    Java实现 计蒜客 拯救行动
    Java实现 LeetCode 174 地下城游戏
    Java实现 LeetCode 174 地下城游戏
    Java实现 LeetCode 174 地下城游戏
    Java实现 LeetCode 173 二叉搜索树迭代器
    Java实现 LeetCode 173 二叉搜索树迭代器
    Visual Studio的SDK配置
    怎样使用CMenu类
    mfc menu用法一
  • 原文地址:https://www.cnblogs.com/jinwenyi/p/13231199.html
Copyright © 2011-2022 走看看