怎样找到一份深度学习的工作

长按上图二维码关注更多信息 有问题 找皇冠署理 做调研 更专业 &&&&聚焦最新、最热、最有价值的工业资讯,追踪全球最热皇冠现金署理市场皇冠直营现金网,提供最全面实时调研数据,全面提升您小我私家及企业的焦点竞争力,一切尽在皇冠署理数据。在微信民众账号中搜索&皇冠署理网&,或用手机扫描左方二维码。
全文链接:关注51Testing
十年云计算老兵零基础进军深度学习方法论
发表于: 10:46 &作者:曹亚孟 & 来源:51Testing软件测试网采编
推荐标签:
  人工智能是当今的热议行业,深度学习是热门中的热门,浪尖上的浪潮,但对传统IT从业人员来说,人工智能到处都是模型、算法、矢量向量,太晦涩难懂了。所以本文作者写了这篇入门级科普,目标是让IT从业者能看清读懂深度学习技术的特点,希望读者能够从中受益,顺利找到自己心仪的工作。  第一.人工智能的天时地利人和  行业的成熟要靠从业者的奋斗(人和),也要考虑大环境和历史的进程(天时和地利)。  人工智能技术的井喷并不是单纯的技术进步,而是软件、硬件、数据三方面共同努力水到渠成的结果,深度学习是AI技术的最热分支,也是受这三方面条件的限制。  AI软件所依赖的算法已经存在很多年了,神经网络是50年前提出的技术,CNN/RNN等算法比大部分读者的年龄都要大。AI技术一直被束之高阁,是因为缺乏硬件算力和海量数据。随着CPU、GPU、FPGA硬件的更新,几十年时间硬件算力扩充了万倍,硬件算力被逐渐解放。随着硬盘和带宽的降价提速,20年前全人类都没几张高清照片,现在单个公司的数据量就能达到EB级。大数据技术只能读写结构化日志,要读视频和图片必须用AI,人类已经盯不过来这么多摄像头了。  我们只有从心里把AI技术请下神坛,才能把它当做顺手的工具去用。AI的技术很深理论很晦涩,主要是这个行业刚刚发芽还未分层,就像20年前IT工程师需要全面掌握技能,现在的小朋友们连字符集都不用关注。  第二.关联度模型  深度学习有两步工作,先要训练生成模型,然后使用模型去推测当前的任务。  比如说我用100万张图片标记好这是猫还是狗,AI把图片内各个片段的特征提取出来,生成一个猫狗识别模型。然后我们再给这个模型套上接口做成猫狗检测程序,每给这个程序一张照片它就能告诉你有多大几率是猫多大几率是狗。  这个识别模型是整个程序中最关键的部分,可以模糊的认为它就是一个密封黑盒的识别函数。以前我们写程序都是做if-then-else因果判断,但图像特征没有因果关系只看关联度,过去的工作经验反而成了新的认知障碍,还不如就将其当做黑盒直接拿来用。  接下来我放一个模型训练和推测的实验步骤截图,向大家说明两个问题:  需要用客户的现场数据做训练才能出模型,训练模型不是软件外包堆人日就行,很难直接承诺模型训练结果。  训练模型的过程很繁琐耗时,但并不难以掌握,其工作压力比DBA在线调试小多了,IT工程师在AI时代仍有用伍之地。  第三.动手实验  本节较长,如果读者对实验步骤和结果没兴趣,而是直接想看我的结论,也可以跳过这一节。  这个实验是Nvidia提供的入门培训课程——ImageClassificationwithDIGITS–Trainingamodel。  我们的实验很简单,用6000张图片去训练AI识别0-9这几个数字。  训练样本数据是6000张标号0-9的小图片,其中4500张是用来做训练(train),1500张是验证(val)训练结果。  实验数据准备  训练图片很小也很简单,如下图预览,就是一堆数字:  —下图是01样本图片—   我做的图片是官方教程提供了个白底红字的“2”.  —下图是02测试图片—  制作数据集  首先我们要做一个图片识别的数据集,数据集文件放在“/data/train_small”目录下,图片的类型选择“Grayscale”,大小选28×28,其他都选默认,然后选择创建数据集“minidata”。  —下图是03初始数据集—    下面是数据集创建的过程,因为我们的文件很小很少,所以速度很快;如果是几千万张高清大图速度就会很慢,甚至要搭建分布式系统把IO分散到多台机器上。  —下图是04初始数据集中—  这是创建完成数据集的柱形统计图,鼠标恰好停在第二个柱形上,显示当前标记为“9”的图片有466个。  —下图是05创建完成数据集—  开始创建模型  有了数据集以后我们就可以创建模型了,我们选择创建一个图像分类模型(ImageClassificationModel),数据集选之前创建的“minidata”,训练圈数输30次,其他选项暂时保持默认。  —下图是06新建模型—  到了创建模型的下半段是选择网络构型,我们选择LeNet即可,将模型命名为TestA。  —下图是07选择LeNet—    这次Demo我们没做细节设置,但生产环境可能要经常修改配置文件。  —下图是08微调LeNet—    接下来就开始生成模型了,小数据集简单任务的速度还是很快的,而且验证正确率很高。但是如果是大任务大模型,可能会算上几天时间。  —下图是09开始生成模型—  模型生成完成,我们再看一下验证正确率很高了,如果生产环境正确率太低,可能你要微调创建模型的参数。  —下图是10训练完成后的accuracy–   调试模型  在模型页面往下拖就可以看到下载模型、测试模型等按钮,我们选择测试模型,将那个“白底红字2”提交做个测试。  —下图是11测试模型—    默认是测试Epoch#30,我们先跑10次试试。本来想省点服务器电费,结果只有20.3%的几率识别正确。  —下图是12TestA模型10圈结果—   我们提高测试圈数到25圈,结果准确率从20.3%提高到了21.9%。  —下图是13TestA模型25圈结果—   整个模型的上限是30圈,正确识别结果也才21.92%。到了这里我插一句,未正确识别可能是因为我的建模数据是28*28的黑白图,而我给测试图片大小和颜色都不对。  —下图是14TestA模型30圈结果—  更换模型继续调试  在TestA这个模型上可以点克隆任务,即制作一个同配置的模型再跑一次;这个按钮有意思啊,我们以前编译程序不通过的时候,retry十万次也是不通过啊,为什么克隆任务是个面板常用按钮?  —下图是15克隆模型TestA—    这时好玩的事情发生了,我做出的“TestA-Clone”,识别出数字2的几率是94.81%。  —下图是16克隆TestA结果—    我们再把老模型克隆一次,结果识别出数字2的几率是63.4%。  —下图是17再次克隆TestA结果—    我新建一个模型TestB,让它在TestA的基础上再次训练。  —下图是18新建TestB—    TestB的训练结果反而不如最早的那一版模型,正确率20.69%。  —下图是19TestB的训练结果—    没有最惨只有更惨,看我新训练的模型TestC。  —下图是20TestC训练失败—   从这次测试看,最好的模型是TestA-Clone,其次是Clone2。  —下图是21模型结果汇总—    但这就算找到合适模型了吗?我又手写了个数字2,还特地选的黑底白字28*28,结果这几个模型没一个识别准确的,全部识别失败。  —下图是22.新图识别失败—   第四.实战才能出模型  本次实验拿到正确率是94.81%的模型是意外惊喜,那个模型测其他图片失败倒是意料之中的。因为这次实验的初始样本才几千张,如果样本数量够多,过拟合(即噪音特征被纳入模型)的可能性就越小;我用的全部是默认调试选项,添加其他特征项调试模型可能会减少欠拟合(主特征没提取到)的几率;我并未明确定义该模型的使用场景,即没有明确训练数据、测试文件和生产文件是否相同。  我们看到完全相同配置的模型,只因为点击生成模型的时间不同,对同一个图片的识别结果确千差万别,再次强调这不是因果判断而是相关性计算。实验结论和我上文的主张相同,模型需要拿实战数据进行实际训练,且我们只能预估但不能预测模型生成结果。我做这个实验就是给大家解释,AI模型训练不是软件外包,不是谈拢了价格就能规划人日预估效果的。  一个AI技术供应商简单点就是卖现成的模型,比如说人脸识别模型、OCR识别模型等等。但如果客户有定制需求,比如说识别脸上有青春痘、识别是不是左撇子签名,那就需要先明确技术场景,再准备数据大干一场。至于练模型的时间是1天还是1个月不太确定,AI模型训练像做材料试验一样,可能半年也可能十年才能发现目标。  第五.IT工程师的新工作  前文我提到两个观点,第二个观点就是训练模型的工作并不难,IT工程师可以较为容易的学会训练模型的工作,然后我们就能继续扩展从业范围,在AI大浪潮中分一杯热羹了。  首先说技术不是门槛,我们举个IT工程师能听懂的例子:一个OracleDBA既没读过源码,也还没摸过新业务场景,甚至缺乏理论知识只能做常见操作;现在这个项目可以慢慢上线,让他离线调试SQL,拿到性能最佳值的点日志保存就完工了。做AI模型调试时,懂原理懂算法会让工作更有目的性,但更有目的性只能保证接近而不能保证命中目标。  根据上文的实验,我们可以看到有下列工作是需要人做的:  根据客户的要求,提出对原始数据的需求。这里要动业务方向的脑子,比如说想查一下什么人容易肥胖,天然能想到的是每个人的饮食和运动习惯,但专业医生会告诉你要调取转氨酶胆固醇一类的数据。  原始数据需要清洗整理和标注,没找到相关性的样本不是未标注的样本。前文试验中6000张图片可都是标注了0-9的数字的,我们测试模型是为了找到“2”这一组图片的相关性。清洗、整理和标注数据的工作可能是自动也可能是人工,自动做那就是我们写脚本或跑大数据,人工做就是提需求然后招1500个大妈给黄图打框,但工程师会对打框过程全程指导。这里还有取巧的方法,友商的模型太贵甚至不卖,那就是直接用友商的公有云API接口,或者买友商大客户的日志,让友商帮你完成数据筛检。  上文试验中仅仅是图片分类数据集,已经有很多可调整选项了;生产环境不仅有图片还有声音、文字、动作特征等数据集,数据集的设置是否合理,要不要重建数据集都需要多次调试和长期观察。  实验中生成模型没怎么调参数也只花了一分钟时间,但生产环境的模型生成参数要经常调整,而生成一个模型的时间可能是几小时甚至几天。  验证结果的准确性,如果是柔性需求可以目测几个测试结果就把模型上线了,但如果是刚性业务可能又要组织十万份以上样本进行测试验证。顺路说一句,用来训练模型的硬件未必是适用于来验证和跑生产环境的,如果是高可能还要换硬件部署。  模型也有日常维护,可能随着数据集的更新模型也要定期更新,也可能发现模型有个致命的误判会威胁到业务,这都需要及时处理。  第六.附赠的小观点  谈到最后再附赠一些个人观点,随机想的,只写论点不写论证过程了:  现在搭建和使用AI环境很难,但软件会进步和解决这个问题;三年前平台很难部署和维护,现在遍地都是一键部署和UI维护的云平台方案。  深度学习这个技术领域太吃数据和算力了,人脑不会像AI这么笨,可能以后会有新技术出现取代深度学习在AI领域的地位。  因为需要数据和算力,搞个AI公司比其他创业企业更难;现在有名的AI创业企业都是单一领域深耕三年以上,让用户提供数据他们只做单一典型模型。同样巨头企业搞AI也不容易,即使挖到人AI项目也要花时间冷起动,清洗数据不仅消耗体力同样消耗时间。  深度学习的计算过程不受控制,计算结果需要人来验证,所以它不能当做法务上的证据。当AI发现嫌疑人时警察会立刻采取行动,但它的创造者都无法描述AI下一步会如何下围棋。一个婴儿能尿出来世界地图,某人随手乱输能碰对银行卡的密码,AI会告诉你股市99.99%要暴涨,但这些都不能当做独立单责的证据。  搞AI需要准备大量数据,中国对美国有个特色优势,可以做数据标注的人很多而且价格便宜,但到模型实践这一步,中国的人力成本太低又限制了AI走向商用。  不要恐慌AI会消灭人类,对人类有威胁的AI肯定是有缺陷的AI,但人类一样也选出过希特勒这类有缺陷的领袖。也不要鼓吹AI会让人类失业社会动荡的,大家还是老老实实谈星座运势吧,我为什么就不担心自己失业?  有些事AI的确准率看起来很低实其很高,比如两人对话听能清楚80%的字就不错了,AI只听懂85%了的文字已经越超人类了。你看我打倒颠字序并不影响你读阅啊。
搜索风云榜
51Testing官方微信
51Testing官方微博
测试知识全知道这个我从eehome贴过来的。写的非常的好。我们用学单片机不要停在演示的基础上。只能让单片机完成局部事。这样我们永远不会走出流水灯地狱!!!
学习单片机也已经有几年了,藉此机会和大家聊一下我学习过程中的一些经历和想法吧。也感谢一线工人提供了这个机会。希望大家有什么好的想法和建议都直接跟帖说出来。毕竟只有交流才能够碰撞出火花来^_^。
几年前,和众多初学者一样,我接触到了单片机,立刻被其神奇的功能所吸引,从此不能自拔。很多个日夜就这样陪伴着它度过了。期间也遇到过非常多的问题,也
一度被这些问题所困惑……等到回过头来,看到自己曾经走过的路,唏嘘不已。经常混迹于论坛里,也看到了很多初学者发的求助帖子,看到他们走在自己曾走过的
弯路上,忽然想到了自己的那段日子,心里竟然莫名的冲动,凡此总总,我总是尽自己所能去回帖。很多时候,都想写一点什么东西出来,希望对广大的初学者有一
点点帮助。但总是不知从何处写起。今天借一线工人的台,唱一唱我的戏。“卖弄”也好,“吹嘘”也罢,我只是想认真的写写我这一路走来历经的总总,把其中
值得注意,以及经验的地方写出来,权当是我对自己的一个总结吧。而作为看官的你,如果看到了我的错误,还请一定指正,这样对我以及其它读者都有帮助,而至
于你如果从中能够收获到些许,那便是我最大的欣慰了。姑妄言之,姑妄听之。如果有啥好的想法和建议一定要说出来。
一路学习过来的过程中,帮助最大之一无疑来自于网络了。很多时候,通过网络,我们都可以获取到所需要的学习资料。但是,随着我们学习的深入,我们会慢慢发
现,网络提供的东西是有限度的,好像大部分的资料都差不多,或者说是适合大部分的初学者所需,而当我们想更进一步提高时,却发现能够获取到的资料越来越
少,相信各位也会有同感,铺天盖地的单片机资料中大部分不是流水灯就是LED,液晶,而且也只是仅仅作功能性的演示。于是有些人选择了放弃,或者是转移到
其他兴趣上面去了,而只有少部分人选择了继续摸索下去,结合市面上的书籍,然后在网络上锲而不舍的搜集资料,再从牛人的只言片语中去体会,不断动手实践,
慢慢的,也摸索出来了自己的一条路子。当然这个过程必然是艰辛的,而他学会了之后也不会在网络上轻易分享自己的学习成果。如此恶性循环下去,也就不难理解
为什么初级的学习资料满天飞,而深入一点的学习资料却很少的原因了。相较于其他领域,单片机技术的封锁更加容易。尽管已经问世了很多年了,有价值的资料还
是相当的欠缺,大部分的资料都是止于入门阶段或者是简单的演示实验。但是在实际工程应用中却是另外一回事。有能力的高手无暇或者是不愿公开自己的学习经
很多时候,我也很困惑,看到国外爱好者毫不保留的在网络上发布自己的作品,我忽然感觉到一丝丝的悲哀。也许,我们真的该转变一下思路了,帮助别人,其实也
是在帮助自己。啰啰嗦嗦的说了这么多,相信大家能够明白说的是什么意思。在接下来的一段日子里,我将会结合电子工程师之家举办的主题周活动写一点自己的想
法。尽可能从实用的角度去讲述。希望能够帮助更多的初学者更上一层楼。而关于这个主题周的最大主题我想了这样的一个名字“从单片机初学者迈向单片机工程
师”。名字挺大挺响亮,给我的压力也挺大的,但我会努力,争取使这样的一系列文章能够带给大家一点帮助,而不是看后大跌眼镜。这样的一系列文章主要的对象
是初学者,以及想从初学者更进一步提高的读者。而至于老手,以及那些牛XX的人,希望能够给我们这些初学者更多的一些指点哈~@_@
我们首先来看第一章节
从这一章开始,我们开始迈入单片机的世界。在我们开始这一章具体的学习之前,有必要给大家先说明一下。在以后的系列文章中,我们将以51内核的单片机为载
体,C语言为编程语言,开发环境为KEIL
uv3。至于为什么选用C语言开发,好处不言而喻,开发速度快,效率高,代码可复用率高,结构清晰,尤其是在大型的程序中,而且随着编译器的不断升级,其
编译后的代码大小与汇编语言的差距越来越小。而关于C语言和汇编之争,就像那个啥,每隔一段时间总会有人挑起这个话题,如果你感兴趣,可以到网上搜索相关
的帖子自行阅读。不是说汇编不重要,在很多对时序要求非常高的场合,需要利用汇编语言和C语言混合编程才能够满足系统的需求。在我们学习掌握C语言的同
时,也还需要利用闲余的时间去学习了解汇编语言。
1.从点亮LED(发光二极管)开始
在市面上众多的单片机学习资料中,最基础的实验无疑于点亮LED了,即控制单片机的I/O的电平的变化。
如同如下实例代码一般
void main(void)
& & LedInit() ;
& & While(1)
&&&LED = ON
&&&DelayMs(500)
&&&LED = OFF
&&&DelayMs(500)
程序很简单,从它的结构可以看出,LED先点亮500MS,然后熄灭500MS,如此循环下去,形成的效果就是LED以1HZ的频率进行闪烁。下面让我们分析上面的程序有没有什么问题。
看来看出,好像很正常的啊,能有什么问题呢?这个时候我们应该换一个思路去想了。试想,整个程序除了控制LED = ON ; LED =
这两条语句外,其余的时间,全消耗在了DelayMs(500)这两个函数上。而在实际应用系统中是没有哪个系统只闪烁一只LED就其它什么事情都不做了
的。因此,在这里我们要想办法,把CPU解放出来,让它不要白白浪费500MS的延时等待时间。宁可让它一遍又一遍的扫描看有哪些任务需要执行,也不要让
它停留在某个地方空转消耗CPU时间。
从上面我们可以总结出
无论什么时候我们都要以实际应用的角度去考虑程序的编写。
无论什么时候都不要让CPU白白浪费等待,尤其是延时(超过1MS)这样的地方。
下面让我们从另外一个角度来考虑如何点亮一颗LED。
先看看我们的硬件结构是什么样子的。
我手上的单片机板子是电子工程师之家的开发的学习板。就以它的实际硬件连接图来分析吧。如下图所示
&& (原文件名:led.jpg)
一般的LED的正常发光电流为10~20MA而低电流LED的工作电流在2mA以下(亮度与普通发光管相同)。在上图中我们可知,当Q1~Q8引脚上面的
电平为低电平时,LED发光。通过LED的电流约为(VCC - Vd)/ RA2
。其中Vd为LED导通后的压降,约为1.7V左右。这个导通压降根据LED颜色的不同,以及工作电流的大小的不同,会有一定的差别。下面一些参数是网上
有人测出来的,供大家参考。
红色的压降为1.82-1.88V,电流5-8mA,
绿色的压降为1.75-1.82V,电流3-5mA,
橙色的压降为1.7-1.8V,电流3-5mA
兰色的压降为3.1-3.3V,电流8-10mA,
白色的压降为3-3.2V,电流10-15mA,
(供电电压5V,LED直径为5mm)
74HC573真值表如下:
(原文件名:74hc573.jpg)
通过这个真值表我们可以看出。当OutputEnable引脚接低电平的时候,并且LatchEnable引脚为高电平的时候,Q端电平与D端电平相同。
结合我们的LED硬件连接图可以知道LED_CS端为高电平时候,P0口电平的变化即Q端的电平的变化,进而引起LED的亮灭变化。由于单片机的驱动能力
有限,在此,74HC573的主要作用就是起一个输出驱动的作用。需要注意的是,通过74HC573的最大电流是有限制的,否则可能会烧坏74HC573
这个芯片。
上面这个图是从74HC573的DATASHEET中截取出来的,从上可以看出,每个引脚允许通过的最大电流为35mA
整个芯片允许通过的最大电流为75mA。在我们设计相应的驱动电路时候,这些参数是相当重要的,而且是最容易被初学者所忽略的地方。同时在设计的时候,要
留出一定量的余量出来,不能说单个引脚允许通过的电流为35mA,你就设计为35mA,这个时候你应该把设计的上限值定在20mA左右才能保证能够稳定的
(设计相应驱动电路时候,应该仔细阅读芯片的数据手册,了解每个引脚的驱动能力,以及整个芯片的驱动能力)
& 了解了相应的硬件后,我们再来编写驱动程序。
& & 首先定义LED的接口
& & #define
&&&然后为亮灭常数定义一个宏,由硬件连接图可以,当P0输出为低电平时候LED亮,P0输出为高电平时,LED熄灭。
& & #define
LED_ON()& & LED =
0x00&&//所有LED亮
& & #define
LED_OFF()& & LED =
0xff&&//所有LED熄灭
下面到了重点了,究竟该如何释放CPU,避免其做延时空等待这样的事情呢。很简单,我们为系统产生一个1MS的时标。假定LED需要亮500MS,熄灭
500MS,那么我们可以对这个1MS的时标进行计数,当这个计数值达到500时候,清零该计数值,同时把LED的状态改变。
unsigned int g_u16LedTimeCount = 0 ;&
& //LED计数器
unsigned char g_u8LedState = 0 ;&
&& &//LED状态标志,
0表示亮,1表示熄灭
void LedProcess(void)
& & if(0 ==
g_u8LedState)&&//如果LED的状态为亮,则点亮LED
&&&LED_ON()
//否则熄灭LED
&&&LED_OFF()
void LedStateChange(void)
if(g_bSystemTime1Ms)&
&//系统1MS时标到
&&&g_bSystemTime1Ms
&&&g_u16LedTimeCount++
&//LED计数器加一
& & if(g_u16LedTimeCount
500)&&//计数达到500,即500MS到了,改变LED的状态。
&g_u16LedTimeCount = 0 ;
&g_u8LedState&&=
! g_u8LedS
上面有一个变量没有提到,就是g_bSystemTime1Ms
。这个变量可以定义为位变量或者是其它变量,在我们的定时器中断函数中对其置位,其它函数使用该变量后,应该对其复位(清0)
我们的主函数就可以写成如下形式(示意代码)
void main(void)
& & while(1)
&&&LedProcess()
&&&LedStateChange()
因为LED的亮或者灭依赖于LED状态变量(g_u8LedState)的改变,而状态变量的改变,又依赖于LED计数器的计数值
(g_u16LedTimeCount
,只有计数值达到一定后,状态变量才改变)所以,两个函数都没有堵塞CPU的地方。让我们来从头到尾分析一遍整个程序的流程。
程序首先执行LedProcess() ;函数
因为g_u8LedState 的初始值为0
(见定义,对于全局变量,在定义的时候最好给其一个确定的值)所以LED被点亮,然后退出LedStateChange()函数,执行下一个函数LedStateChange()
在函数LedStateChange()内部首先判断1MS的系统时标是否到了,如果没有到就直接退出函数,如果到了,就把时标清0以便下一个时标消息的
到来,同时对LED计数器加一,然后再判断LED计数器是否到达我们预先想要的值500,如果没有,则退出函数,如果有,对计数器清0,以便下次重新计
数,同时把LED状态变量取反,然后退出函数。
由上面整个流程可以知道,CPU所做的事情,就是对一些计数器加一,然后根据条件改变状态,再根据这个状态来决定是否点亮LED。这些函数执行所花的时间
都是相当短的,如果主程序中还有其它函数,则CPU会顺次往下执行下去。对于其它的函数(如果有的话)也要采取同样的措施,保证其不堵塞CPU,如果全部
基于这种方法设计,那么对于不是非常庞大的系统,我们的系统依旧可以保证多个任务(多个函数)同时执行。系统的实时性得到了一定的保证,从宏观上看来,就
是多个任务并发执行。
好了,这一章就到此为止,让我们总结一下,究竟有哪些需要注意的吧。
无论什么时候我们都要以实际应用的角度去考虑程序的编写。
无论什么时候都不要让CPU白白浪费等待,尤其是延时(超过1MS)这样的地方。
设计相应驱动电路时候,应该仔细阅读芯片的数据手册,了解每个引脚的驱动能力,
以及整个芯片的驱动能力
最重要的是,如何去释放CPU(参考本章的例子),这是写出合格程序的基础。
附完整程序代码(基于电子工程师之家的单片机开发板)
#include&reg52.h&
sbit LED_SEG&&=
P1^4;&&//数码管段选
sbit LED_DIG&&=
P1^5;&&//数码管位选
sbit LED_CS11 =
P1^6;&&//led控制位
sbit ir=P1^7;
#define LED P0&
&//定义LED接口
bit&&g_bSystemTime1Ms = 0
1MS系统时标
unsigned int&&g_u16LedTimeCount =
0 ; //LED计数器
unsigned char g_u8LedState = 0 ;&
&& &//LED状态标志,
0表示亮,1表示熄灭
#define LED_ON()&
&& &LED = 0x00
;&&//所有LED亮
#define LED_OFF()& & LED = 0xff
;&&//所有LED熄灭
void Timer0Init(void)
& & TMOD &= 0xf0
& & TMOD |= 0x01
&//定时器0工作方式1
&//定时器初始值
TL0&&=&&0x66
TR0&&= 1 ;
ET0&&= 1 ;
void LedProcess(void)
& & if(0 ==
g_u8LedState)&&//如果LED的状态为亮,则点亮LED
&&&LED_ON()
//否则熄灭LED
&&&LED_OFF()
void LedStateChange(void)
if(g_bSystemTime1Ms)&
&//系统1MS时标到
&g_bSystemTime1Ms = 0 ;
&g_u16LedTimeCount++ ;&
&//LED计数器加一
&if(g_u16LedTimeCount &=
500)&&//计数达到500,即500MS到了,改变LED的状态。
&g_u16LedTimeCount = 0 ;
&g_u8LedState&&=
! g_u8LedState& & ;
void main(void)
& & Timer0Init() ;
& & EA = 1 ;
&&&LED_CS11 = 1
; //74HC595输出允许
& & LED_SEG = 0
;&&//数码管段选和位选禁止(因为它们和LED共用P0口)
& & LED_DIG = 0 ;
& & while(1)
LedProcess() ;
LedStateChange() ;
void Time0Isr(void) interrupt 1
&//定时器重新赋初值
TL0&&=&&0x66
& & g_bSystemTime1Ms = 1
;& & //1MS时标标志位置位
“从单片机初学者迈向单片机工程师”
&第三章----模块化编程初识
&好的开始是成功的一半
&&通过上一章的学习,我想你已经掌握了如何在程序中释放CPU了。希望能够继续坚持下去。一个良好的开始是成功的一半。我们今天所做的一切都是为了在单片机编程上做的更好。
在谈论今天的主题之前,先说下我以前的一些经历。在刚开始接触到C语言程序的时候,由于学习内容所限,写的程序都不是很大,一般也就几百行而矣。所以所有
的程序都完成在一个源文件里面。记得那时候大一参加学校里的一个电子设计大赛,调试了一个多星期,所有程序加起来大概将近1000行,长长的一个文件,从
上浏览下来都要好半天。出了错误简单的语法错误还好定位,其它一些错误,往往找半天才找的到。那个时候开始知道了模块化编程这个东西,也尝试着开始把程序
分模块编写。最开始是把相同功能的一些函数(譬如1602液晶的驱动)全部写在一个头文件(.h)文件里面,然后需要调用的地方包含进去,但是很快发现这
种方法有其局限性,很容易犯重复包含的错误。
而且调用起来也很不方便。很快暑假的电子设计大赛来临了,学校对我们的单片机软件编程进行了一些培训。由于学校历年来参加国赛和省赛,因此积累了一定数量
的驱动模块,那些日子,老师每天都会布置一定量的任务,让我们用这些模块组合起来,完成一定功能。而正是那些日子模块化编程的培训,使我对于模块化编程有
了更进一步的认识。并且程序规范也开始慢慢注意起来。此后的日子,无论程序的大小,均采用模块化编程的方式去编写。很长一段时间以来,一直有单片机爱好者
在QQ上和我一起交流。有时候,他们会发过来一些有问题的程序源文件,让我帮忙修改一下。同样是长长的一个文件,而且命名极不规范,从头看下来,着实是痛
苦,说实话,还真不如我重新给他们写一个更快一些,此话到不假,因为手头积累了一定量的模块,在完成一个新的系统时候,只需要根据上层功能需求,在底层模
块的支持下,可以很快方便的完成。而不需要从头到尾再一砖一瓦的重新编写。藉此,也可以看出模块化编程的一个好处,就是可重复利用率高。下面让我们揭开模
块化神秘面纱,一窥其真面目。
& & C语言源文件 *.c
&&&提到C语言源文件,大家都不会陌生。因为我们平常写的程序代码几乎都在这个XX.C文件里面。编译器也是以此文件来进行编译并生成相应的目标文件。
作为模块化编程的组成基础,我们所要实现的所有功能的源代码均在这个文件里。理想的模块化应该可以看成是一个黑盒子。即我们只关心模块提供的功能,而不管
模块内部的实现细节。好比我们买了一部手机,我们只需要会用手机提供的功能即可,不需要知晓它是如何把短信发出去的,如何响应我们按键的输入,这些过程对
我们用户而言,就是是一个黑盒子。
在大规模程序开发中,一个程序由很多个模块组成,很可能,这些模块的编写任务被分配到不同的人。而你在编写这个模块的时候很可能就需要利用到别人写好的模
块的借口,这个时候我们关心的是,它的模块实现了什么样的接口,我该如何去调用,至于模块内部是如何组织的,对于我而言,无需过多关注。而追求接口的单一
性,把不需要的细节尽可能对外部屏蔽起来,正是我们所需要注意的地方。
& & C语言头文件 *.h
&&&谈及到模块化编程,必然会涉及到多文件编译,也就是工程编译。在这样的一个系统中,往往会有多个C文件,而且每个C文件的作用不尽相同。在我们的C文件中,由于需要对外提供接口,因此必须有一些函数或者是变量提供给外部其它文件进行调用。
假设我们有一个LCD.C文件,其提供最基本的LCD的驱动函数
& & LcdPutChar(char cNewValue)
;&&//在当前位置输出一个字符
而在我们的另外一个文件中需要调用此函数,那么我们该如何做呢?
头文件的作用正是在此。可以称其为一份接口描述文件。其文件内部不应该包含任何实质性的函数代码。我们可以把这个头文件理解成为一份说明书,说明的内容就
是我们的模块对外提供的接口函数或者是接口变量。同时该文件也包含了一些很重要的宏定义以及一些结构体的信息,离开了这些信息,很可能就无法正常使用接口
函数或者是接口变量。但是总的原则是:不该让外界知道的信息就不应该出现在头文件里,而外界调用模块内接口函数或者是接口变量所必须的信息就一定要出现在
头文件里,否则,外界就无法正确的调用我们提供的接口功能。因而为了让外部函数或者文件调用我们提供的接口功能,就必须包含我们提供的这个接口描述文件
----即头文件。同时,我们自身模块也需要包含这份模块头文件(因为其包含了模块源文件中所需要的宏定义或者是结构体),好比我们平常所用的文件都是一
式三份一样,模块本身也需要包含这个头文件。
下面我们来定义这个头文件,一般来说,头文件的名字应该与源文件的名字保持一致,这样我们便可以清晰的知道哪个头文件是哪个源文件的描述。
&&&于是便得到了LCD.C的头文件LCD.h
其内容如下。
&&&#ifndef&
&&&_LCD_H_
extern& &LcdPutChar(char
cNewValue) ;
& 这与我们在源文件中定义函数时有点类似。不同的是,在其前面添加了extern
修饰符表明其是一个外部函数,可以被外部其它模块进行调用。
&&&#ifndef&
&&&_LCD_H_
&&&#define&
&&&_LCD_H_
&&&这个几条条件编译和宏定义是为了防止重复包含。假如有两个不同源文件需要调用LcdPutChar(char
cNewValue)这个函数,他们分别都通过#include
“Lcd.h”把这个头文件包含了进去。在第一个源文件进行编译时候,由于没有定义过 _LCD_H_ 因此 #ifndef _LCD_H_
条件成立,于是定义_LCD_H_
并将下面的声明包含进去。在第二个文件编译时候,由于第一个文件包含时候,已经将_LCD_H_定义过了。因此#ifndef _LCD_H_
不成立,整个头文件内容就没有被包含。假设没有这样的条件编译语句,那么两个文件都包含了extern&&LcdPutChar(char
cNewValue) ; 就会引起重复包含的错误。
& & 不得不说的typedef
很多朋友似乎了习惯程序中利用如下语句来对数据类型进行定义
& & #define
uint&&unsigned int
&&&#define
uchar&&unsigned char
& & 然后在定义变量的时候 直接这样使用
&&uint&&g_nTimeCounter
不可否认,这样确实很方便,而且对于移植起来也有一定的方便性。但是考虑下面这种情况你还会 这么认为吗?
&&#define PINT unsigned int
*&&//定义unsigned int 指针类型
&&PINT&&g_npTimeCounter,
&那么你到底是定义了两个unsigned int
型的指针变量,还是一个指针变量,一个整形变量呢?而你的初衷又是什么呢,想定义两个unsigned int
型的指针变量吗?如果是这样,那么估计过不久就会到处抓狂找错误了。
& & 庆幸的是C语言已经为我们考虑到了这一点。typedef
正是为此而生。为了给变量起一个别名我们可以用如下的语句
typedef&&unsigned&&int&
& uint16 ;& &
//给指向无符号整形变量起一个别名 uint16
&typedef&&unsigned&&int&&*
puint16 ;&&//给指向无符号整形变量指针起一个别名
& & 在我们定义变量时候便可以这样定义了:
g_nTimeCounter&&=&&0
;&&//定义一个无符号的整形变量
&&puint16&&g_npTimeCounter&&;&
& //定义一个无符号的整形变量的指针
&&在我们使用51单片机的C语言编程的时候,整形变量的范围是16位,而在基于32的微处理下的整形变量是32位。倘若我们在8位单片机下编写的一些代
码想要移植到32位的处理器上,那么很可能我们就需要在源文件中到处修改变量的类型定义。这是一件庞大的工作,为了考虑程序的可移植性,在一开始,我们就
应该养成良好的习惯,用变量的别名进行定义。
如在8位单片机的平台下,有如下一个变量定义
& & uint16&
g_nTimeCounter&&=&&0
&&&如果移植32单片机的平台下,想要其的范围依旧为16位。
& & 可以直接修改uint16 的定义,即
typedef&&unsigned&&short&&int&
& uint16 ;
&&&这样就可以了,而不需要到源文件处处寻找并修改。
将常用的数据类型全部采用此种方法定义,形成一个头文件,便于我们以后编程直接调用。
文件名 MacroAndConst.h
其内容如下:
&_MACRO_AND_CONST_H_
&_MACRO_AND_CONST_H_
typedef& & unsigned
int& & uint16;
typedef& & unsigned
int& &UINT;
typedef& & unsigned
typedef& & unsigned
int& &UINT16;
typedef& & unsigned
int& &WORD;
typedef& & unsigned
typedef& &&
typedef& &&
typedef& & unsigned
long&&uint32;
typedef& & unsigned
&&&UINT32;
typedef& & unsigned
long& & DWORD;
typedef& & unsigned
typedef& & long&
&& &int32;
typedef& & long&
&& &INT32;
typedef& &
signed&&char&
typedef& &
signed&&char&
typedef& & unsigned
typedef& & unsigned
typedef& & unsigned
typedef& & unsigned
typedef& & unsigned
char& & uint8;
typedef& & unsigned
char& & BOOL;
至此,似乎我们对于源文件和头文件的分工以及模块化编程有那么一点概念了。那么让我们趁热打铁,将上一章的我们编写的LED闪烁函数进行模块划分并重新组织进行编译。
在上一章中我们主要完成的功能是P0口所驱动的LED以1Hz的频率闪烁。其中用到了定时器,以及LED驱动模块。因而我们可以简单的将整个工程分成三个模块,定时器模块,LED模块,以及主函数
对应的文件关系如下
Timer.c&&--Timer.h
&--Led.h
在开始重新编写我们的程序之前,先给大家讲一下如何在KEIL中建立工程模板吧,这个模板是我一直沿用至今。希望能够给大家一点启发。
下面的内容就主要以图片为主了。同时辅以少量文字说明。
我们以芯片AT89S52为例。
(原文件名:1.jpg)
(原文件名:2.jpg)
(原文件名:3.jpg)
(原文件名:4.jpg)
(原文件名:5.jpg)
(原文件名:6.jpg)
(原文件名:7.jpg)
(原文件名:8.jpg)
(原文件名:9.jpg)
(原文件名:10.jpg)
(原文件名:11.jpg)
(原文件名:12.jpg)
(原文件名:13.jpg)
(原文件名:14.jpg)
(原文件名:15.jpg)
(原文件名:16.jpg)
(原文件名:17.jpg)
(原文件名:18.jpg)
(原文件名:19.jpg)
(原文件名:20.jpg)
(原文件名:21.jpg)
(原文件名:22.jpg)
OK ,到此一个简单的工程模板就建立起来了,以后我们再新建源文件和头文件的时候,就可以直接保存到src文件目录下面了。
下面我们开始编写各个模块文件。
首先编写Timer.c 这个文件主要内容就是定时器初始化,以及定时器中断服务函数。其内容如下。
#include &reg52.h&
bit g_bSystemTime1Ms = 0 ;&
1MS系统时标
void Timer0Init(void)
& & TMOD &= 0xf0
& & TMOD |= 0x01
&//定时器0工作方式1
&//定时器初始值
TL0&&=&&0x66
TR0&&= 1 ;
ET0&&= 1 ;
void Time0Isr(void) interrupt 1
&//定时器重新赋初值
TL0&&=&&0x66
& & g_bSystemTime1Ms = 1
;& & //1MS时标标志位置位
由于在Led.c文件中需要调用我们的g_bSystemTime1Ms变量。同时主函数需要调用Timer0Init()初始化函数,所以应该对这个变量和函数在头文件里作外部声明。以方便其它函数调用。
Timer.h 内容如下。
#ifndef _TIMER_H_
#define _TIMER_H_
extern void Timer0Init(void) ;
extern bit g_bSystemTime1M
完成了定时器模块后,我们开始编写LED驱动模块。
Led.c 内容如下:
#include &reg52.h&
#include "MacroAndConst.h"
#include "Led.h"
#include "Timer.h"
static uint16&&g_u16LedTimeCount
= 0 ; //LED计数器
static uint8&&g_u8LedState = 0
&//LED状态标志, 0表示亮,1表示熄灭
#define LED P0&
&//定义LED接口
#define LED_ON()&
&& &LED = 0x00
;&&//所有LED亮
#define LED_OFF()& & LED = 0xff
;&&//所有LED熄灭
void LedProcess(void)
& & if(0 ==
g_u8LedState)&&//如果LED的状态为亮,则点亮LED
&&&LED_ON()
//否则熄灭LED
&&&LED_OFF()
void LedStateChange(void)
if(g_bSystemTime1Ms)&
&//系统1MS时标到
&&&g_bSystemTime1Ms
&&&g_u16LedTimeCount++
&//LED计数器加一
&&&if(g_u16LedTimeCount
&= 500) //计数达到500,即500MS到了,改变LED的状态。
&g_u16LedTimeCount = 0 ;
&g_u8LedState&&=
! g_u8LedState& & ;
这个模块对外的借口只有两个函数,因此在相应的Led.h 中需要作相应的声明。
Led.h 内容:
#ifndef _LED_H_
#define _LED_H_
extern void LedProcess(void) ;
extern void LedStateChange(void) ;
这两个模块完成后,我们将其C文件添加到工程中。然后开始编写主函数里的代码。
如下所示:
#include &reg52.h&
#include "MacroAndConst.h"
#include "Timer.h"
#include "Led.h"
sbit LED_SEG&&=
P1^4;&&//数码管段选
sbit LED_DIG&&=
P1^5;&&//数码管位选
sbit LED_CS11 =
P1^6;&&//led控制位
void main(void)
&&&LED_CS11 = 1
; //74HC595输出允许
& & LED_SEG = 0
;&&//数码管段选和位选禁止(因为它们和LED共用P0口)
&&&LED_DIG = 0
&&&Timer0Init()
&&&while(1)
& & LedProcess() ;
& & LedStateChange() ;
整个工程截图如下:
至此,第三章到此结束。
一起来总结一下我们需要注意的地方吧
C语言源文件(*.c)的作用是什么
2.& & C语言头文件(*.h)的作用是什么
3.& & typedef 的作用
4.& & 工程模板如何组织
5.& & 如何创建一个多模块(多文件)的工程
“从单片机初学者迈向单片机工程师”之KEY主题讨论
&&&按键程序编写的基础
&&从这一章开始,我们步入按键程序设计的殿堂。在基于单片机为核心构成的应用系统中,用户输入是必不可少的一部分。输入可以分很多种情况,譬如有的系统
支持PS2键盘的接口,有的系统输入是基于编码器,有的系统输入是基于串口或者USB或者其它输入通道等等。在各种输入途径中,更常见的是,基于单个按键
或者由单个键盘按照一定排列构成的矩阵键盘(行列键盘)。我们这一篇章主要讨论的对象就是基于单个按键的程序设计,以及矩阵键盘的程序编写。
◎按键检测的原理
常见的独立按键的外观如下,相信大家并不陌生,各种常见的开发板学习板上随处可以看到他们的身影。
(原文件名:1.jpg)
总共有四个引脚,一般情况下,处于同一边的两个引脚内部是连接在一起的,如何分辨两个引脚是否处在同一边呢?可以将按键翻转过来,处于同一边的两个引脚,
有一条突起的线将他们连接一起,以标示它们俩是相连的。如果无法观察得到,用数字万用表的二极管挡位检测一下即可。搞清楚这点非常重要,对于我们画PCB
的时候的封装很有益。
它们和我们的单片机系统的I/O口连接一般如下:
(原文件名:2.jpg)
对于单片机I/O内部有上拉电阻的微控制器而言,还可以省掉外部的那个上拉电阻。简单分析一下按键检测的原理。当按键没有按下的时候,单片机I/O通过上
拉电阻R接到VCC,我们在程序中读取该I/O的电平的时候,其值为1(高电平);
当按键S按下的时候,该I/O被短接到GND,在程序中读取该I/O的电平的时候,其值为0(低电平)
。这样,按键的按下与否,就和与该按键相连的I/O的电平的变化相对应起来。结论:我们在程序中通过检测到该I/O口电平的变化与否,即可以知道按键是否
被按下,从而做出相应的响应。一切看起来很美好,是这样的吗?
◎现实并非理想
在我们通过上面的按键检测原理得出上述的结论的时候,其实忽略了一个重要的问题,那就是现实中按键按下时候的电平变化状态。我们的结论是基于理想的情况得出来的,就如同下面这幅按键按下时候对应电平变化的波形图一样:
(原文件名:3.jpg)
而实际中,由于按键的弹片接触的时候,并不是一接触就紧紧的闭合,它还存在一定的抖动,尽管这个时间非常的短暂,但是对于我们执行时间以us为计算单位的微控制器来说,
它太漫长了。因而,实际的波形图应该如下面这幅示意图一样。
(原文件名:4.jpg)
这样便存在这样一个问题。假设我们的系统有这样功能需求:在检测到按键按下的时候,将某个I/O的状态取反。由于这种抖动的存在,使得我们的微控制器误以
为是多次按键的按下,从而将某个I/O的状态不断取反,这并不是我们想要的效果,假如该I/O控制着系统中某个重要的执行的部件,那结果更不是我们所期待
的。于是乎有人便提出了软件消除抖动的思想,道理很简单:抖动的时间长度是一定的,只要我们避开这段抖动时期,检测稳定的时候的电平不久可以了吗?听起来
确实不错,而且实际应用起来效果也还可以。于是,各种各样的书籍中,在提到按键检测的时候,总也不忘说道软件消抖。就像下面的伪代码所描述的一样。(假设
按键按下时候,低电平有效)
If(0 == io_KeyEnter)&
&//如果有键按下了
& & Delayms(20)
&//先延时20ms避开抖动时期
& & If(0 ==
io_KeyEnter)& &&
&&&//然后再检测,如果还是检测到有键按下
//是真的按下了,返回键值
& & return
KEY_NULL& &&
&&&//是抖动,返回空的键值
& & while(0 == io_KeyEnter)
;& & //等待按键释放
乍看上去,确实挺不错,实际中呢?在实际的系统中,一般是不允许这么样做的。为什么呢?首先,这里的Delayms(20) ,
让微控制器在这里白白等待了20 ms 的时间,啥也没干,考虑我在《学会释放CPU》一章中所提及的几点,这是不可取的。其次while(0
== io_KeyEnter)
;更是程序设计中的大忌(极少的特殊情况例外)。任何非极端情况下,都不要使用这样语句来堵塞微控制器的执行进程。原本是等待按键释放,结果CPU就一直
死死的盯住该按键,其它事情都不管了,那其它事情不干了吗?你同意别人可不会同意所以合理的分配好微控制的处理时间,是编写按键程序的基础。
◎消除抖动有必要吗?
的确,软件上的消抖确实可以保证按键的有效检测。但是,这种消抖确实有必要吗?有人提出了这样的疑问。抖动是按键按下的过程中产生的,如果按键没有按下,
抖动会产生吗?如果没有按键按下,抖动也会在I/O上出现,我会立刻把这个微控制器锤了,永远不用这样一款微控制器。所以抖动的出现即意味着按键已经按
下,尽管这个电平还没有稳定。所以只要我们检测到按键按下,即可以返回键值,问题的关键是,在你执行完其它任务的时候,再次执行我们的按键任务的时候,抖
动过程还没有结束,这样便有可能造成重复检测。所以,如何在返回键值后,避免重复检测,或者在按键一按下就执行功能函数,当功能函数的执行时间小于抖动时
间时候,如何避免再次执行功能函数,就成为我们要考虑的问题了。这是一个仁者见仁,智者见智的问题,就留给大家去思考吧。所以消除抖动的目的是:防止按键
一次按下,多次响应。
& “从单片机初学者迈向单片机工程师”之KEY主题讨论
&&&基于状态转移的独立按键程序设计
本章所描述的按键程序要达到的目的:检测按键按下,短按,长按,释放。即通过按键的返回值我们可以获取到如下的信息:按键按下(短按),按键长按,按键连
_发,按键释放。不知道大家还记得小时候玩过的电子钟没有,就是外形类似于CALL 机(CALL
机,好像是很古老的东西了)的那种,有一个小液晶屏,还有四个按键,功能是时钟,闹钟以及秒表。在调整时间的时候,短按+键每次调整值加一,长按的时候
调整值连续增加。小的时候很好奇,这样的功能到底是如何实现的呢,今天就让我们来剖析它的原理吧。
状态在生活中随处可见。譬如早上的时候,闹钟把你叫醒了,这个时候,你便处于清醒的状态,马上你就穿衣起床洗漱吃早餐,这一系列事情就是你在这个状态做的
事情。做完这些后你会去等车或者开车去上班,这个时候你就处在上班途中的状态…..中午下班时间到了,你就处于中午下班的状态,诸如此类等等,在每一个状
态我们都会做一些不同的事情,而总会有外界条件促使我们转换到另外一种状态,譬如闹钟叫醒我们了,下班时间到了等等。对于状态的定义出发点不同,考虑的方
向不同,或者会有些许细节上面的差异,但是大的状态总是相同的。生活中的事物同样遵循同样的规律,譬如,用一个智能充电器给你的手机电池充电,刚开始,它
是处于快速充电状态,随着电量的增加,电压的升高,当达到规定的电压时候,它会转换到恒压充电。总而言之,细心观察,你会发现生活中的总总都可以归结为一
个个的状态,而状态的变换或者转移总是由某些条件引起同时伴随着一些动作的发生。我们的按键亦遵循同样的规律,下面让我们来简单的描绘一下它的状态流程转
(原文件名:1.jpg)
&&下面对上面的流程图进行简要的分析。
首先按键程序进入初始状态S1,在这个状态下,检测按键是否按下,如果有按下,则进入按键消抖状态2,在下一次执行按键程序时候,直接由按键消抖状态进入
按键按下状态3,在此状态下检测按键是否按下,如果没有按键按下,则返回初始状态S1,如果有则可以返回键值,同时进入长按状态S4,在长按状态下每次进
入按键程序时候对按键时间计数,当计数值超过设定阈值时候,则表明长按事件发生,同时进入按键连_发状态S5。如果按键键值为空键,则返回按键释放状态
S6,否则继续停留在本状态。在按键连_发状态下,如果按键键值为空键则返回按键释放状态S6,如果按键时间计数超过连_发阈值,则返回连_发按键值,清
零时间计数后继续停留在本状态。
看了这么多,也许你已经有一个模糊的概念了,下面让我们趁热打铁,一起来动手编写按键驱动程序吧。
&&下面是我使用的硬件的连接图。
(原文件名:2.jpg)
&&硬件连接很简单,四个独立按键分别接在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&
#define KEY_VALUE_2&
#define KEY_VALUE_3&
#define KEY_VALUE_4&
#define KEY_NULL&
下面我们来编写按键的硬件驱动程序。
根据第一章所描述的按键检测原理,我们可以很容易的得出如下的代码:
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&
#define KEY_CONTINUE_PERIOD& &
//定义按键返回值状态(按下,长按,连_发,释放)
#define KEY_DOWN&
#define KEY_LONG&
#define KEY_CONTINUE&
#define KEY_UP&
//定义按键状态
#define KEY_STATE_INIT&
#define KEY_STATE_WOBBLE&
#define KEY_STATE_PRESS&
#define KEY_STATE_LONG&
#define KEY_STATE_CONTINUE&
#define KEY_STATE_RELEASE&
接着我们开始编写完整的上层按键扫描函数,按键的短按,长按,连按,释放等等状态的判断均是在此函数中完成。对照状态流程转移图,然后再看下面的函数代码,可以更容易的去理解函数的执行流程。完整的函数代码如下:
void GetKey(uint8 *pKeyValue)
& & static uint8 s_u8KeyState =
KEY_STATE_INIT ;
& & static uint8 s_u8KeyTimeCount
& & static uint8 s_u8LastKey =
KEY_NULL ;&&//保存按键释放时候的键值
& & uint8 KeyTemp = KEY_NULL
& & KeyTemp = KeyScan()
&&&//获取键值
& & switch(s_u8KeyState)
KEY_STATE_INIT :
&&&if(KEY_NULL
!= (KeyTemp))
&& &s_u8KeyState
= KEY_STATE_WOBBLE ;
KEY_STATE_WOBBLE :&
&& &//消抖
&&&s_u8KeyState
= KEY_STATE_PRESS ;& &
KEY_STATE_PRESS :
&&&if(KEY_NULL
!= (KeyTemp))
&& &s_u8LastKey
= KeyT //保存键值,以便在释放按键状态返回键值
&& &KeyTemp |=
KEY_DOWN ;&&//按键按下
&& &s_u8KeyState
= KEY_STATE_LONG ;
&& &s_u8KeyState
= KEY_STATE_INIT ;
KEY_STATE_LONG :
&&&if(KEY_NULL
!= (KeyTemp))
&if(++s_u8KeyTimeCount &
KEY_LONG_PERIOD)
s_u8KeyTimeCount = 0 ;
&& & KeyTemp |=
KEY_LONG ;&&//长按键事件发生
s_u8KeyState = KEY_STATE_CONTINUE ;
&& &s_u8KeyState
= KEY_STATE_RELEASE ;
KEY_STATE_CONTINUE :
&&&if(KEY_NULL
!= (KeyTemp))
&if(++s_u8KeyTimeCount &
KEY_CONTINUE_PERIOD)
s_u8KeyTimeCount = 0 ;
&& & KeyTemp |=
KEY_CONTINUE ;
&& &s_u8KeyState
= KEY_STATE_RELEASE ;
KEY_STATE_RELEASE :
&&&s_u8LastKey
|= KEY_UP ;
&&&KeyTemp =
&&&s_u8KeyState
= KEY_STATE_INIT ;
&&&default :
& & *pKeyValue = KeyT
//返回键值& &
关于这个函数内部的细节我并不打算花过多笔墨去讲解。对照着按键状态流程转移图,然后去看程序代码,你会发现其实思路非常清晰。最能让人理解透彻的,莫非
就是将整个程序自己看懂,然后想象为什么这个地方要这样写,抱着思考的态度去阅读程序,你会发现自己的程序水平会慢慢的提高。所以我更希望的是你能够认认
真真的看完,然后思考。也许你会收获更多。
不管怎么样,这样的一个程序已经完成了本章开始时候要求的功能:按下,长按,连按,释放。事实上,如果掌握了这种基于状态转移的思想,你会发现要求实现其它按键功能,譬如,多键按下,功能键等等,亦相当简单,在下一章,我们就去实现它。
在主程序中我编写了这样的一段代码,来演示我实现的按键功能。
void main(void)
& & uint8 KeyValue =
& & 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这条语句的功能。
继续更新。。。。。。。。。。“从单片机初学者迈向单片机工程师”之LED主题讨论周
&第五章--多任务环境下的数码管编程设计
[post]数码管在实际应用中非常广泛,尤其是在某些对成本有限制的场合。编写一个好用的LED程序并不是那么的简单。曾经有人这样说过,如果用数码管
和按键,做一个简易的可以调整的时钟出来,那么你的单片机就算入门了60%了。此话我深信不疑。我遇到过很多单片机的爱好者,他们问我说单片机我已经掌握
了,该如何进一步的学习下去呢?我并不急于回答他们的问题,而是问他们:会编写数码管的驱动程序了吧?“嗯”。会编写按键程序了吧?“嗯”。好,我给你出
一个小题目,你做一下。用按键和数码管以及单片机定时器实现一个简易的可以调整的时钟,要求如下:
8位数码管显示,显示格式如下
要求:系统有四个按键,功能分别是 调整,加,减,确定。在按下调整键时候,显示时的两位数码管以1 Hz
频率闪烁。如果再次按下调整键,则分开始闪烁,时恢复正常显示,依次循环,直到按下确定键,恢复正常的显示。在数码管闪烁的时候,按下加或者减键可以调整
相应的显示内容。按键支持短按,和长按,即短按时,修改的内容每次增加一或者减小一,长按时候以一定速率连续增加或者减少。
结果很多人,很多爱好者一下子都理不清楚思路。其实问题的根源在于没有以工程化的角度去思考程序的编写。很多人在学习数码管编程的时候,都是照着书上或者网上的例子来进行试验。殊不知,这些例子代码仅仅只是具有一个演示性的作用,拿到实际中是很难用的。举一个简单的例子。
下面这段程序是在网上随便搜索到的:
for(num=0;num&9;num++)&&
&P0=table[num];&&
&& &P2=code[num]
&& &delayms(2)
看出什么问题来了没有,如果没有看出来请仔细想一下,如果还没有想出来,请回过头去,认真再看一遍“学会释放CPU”这一章的内容。这个程序作为演示程序
是没有什么问题的,但是实际应用的时候,数码管显示的内容经常变化,而且还有很多其它任务需要执行,因此这样的程序在实际中是根本就无法用的,更何况,它
这里也调用了delayms(2)这个函数来延时2 ms这更是令我们深恶痛绝
本章的内容正是探讨如何解决多任务环境下(不带OS)的数码管程序设计的编写问题。理解了其中的思想,无论要求我们显示的形式怎么变化(如数码管闪烁,移位等),我们都可以很方便的解决问题。
数码管的显示分为动态显示和静态显示两种。静态显示是每一位数码管都用一片独立的驱动芯片进行驱动。比较常见的有74LS164,74HC595等。利用这类芯片的好处就是可以级联,留给单片机的接口只需要时钟线,数据线,因此比较节省I/O口。如下图所示:
利用74LS164级联驱动8个单独的数码管
静态显示的优点是程序编写简单。但是由于涉及到的驱动芯片数量比较多,同时考虑到PCB的布线等等因素,在低成本要求的开发环境下,单纯的静态驱动并不合适。这个时候就可以考虑到动态驱动了。
动态驱动的图如下所示(以EE21开发板为例)
由上图可以看出。8个数码管的段码由一个单独的74HC573驱动。同时每一个数码管的公共端连接在另外一个74HC573的输出上。当送出第一位数码管
的段码内容时候,同时选通第一位数码管的位选,此时,第一位数码管就显示出相应的内容了。一段时间之后,送出第二位数码管段码的内容,选通第二位数码管的
位选,这时显示的内容就变成第二位数码管的内容了……依次循环下去,就可以看到了所有数码管同时显示了。事实上,任意时刻,只有一位数码管是被点亮的。由
于人眼的视觉暂留效应以及数码管的余辉效应,当数码管扫描的频率非常快的时候,人眼已经无法分辨出数码管的变化了,看起来就是同时点亮的。我们假设数码管
的扫描频率为50 Hz, 则完成一轮扫描的时间就是1 / 50 = 20 ms
。我们的系统共有8位数码管,则每一位数码管在一轮扫描周期中点亮的时间为20 / 8 = 2.5 ms 。
动态扫描对时间要求有一点点严格,否则,就会有明显的闪烁。
假设我们程序 中所有任务如下:
& & LedDisplay()
;& & //数码管动态扫描
& & ADProcess()
//AD采集处理&&
& & TimerProcess()
;&&//时间相关处理&&
& & DataProcess()
;&&//数据处理&
LedDisplay() 这个任务的执行时间,如同我们刚才计算的那样,50 Hz频率扫描,则该函数执行的时间为20 ms 。
假设ADProcess()这个任务执行的的时间为2 ms ,TimerProcess()这个函数执行的时间为 1 ms
,DataProcess() 这个函数执行的时间为10 ms 。 那么整个主函数执行一遍的总时间为 20 + 2 + 1 + 10 =
33 ms 。即LedDisplay() 这个函数的扫描频率已经不为50 Hz 了,而是 1 / 33 = 30.3 Hz
。这个频率数码管已经可以感觉到闪烁了,因此不符合我们的要求。为什么会出现这种情况呢? 我们刚才计算的50 Hz
是系统只有LedDisplay()这一个任务的时候得出来的结果。当系统添加了其它任务后,当然系统循环执行一次的总时间就增加了。如何解决这种现象
了,还是离不开我们第二章所讲的那个思想。
系统产生一个2.5 ms 的时标消息。LedDisplay() , 每次接收到这个消息的时候,
扫描一位数码管。这样8个时标消息过后,所有的数码管就都被扫描一遍了。可能有朋友会有这样的疑问:ADProcess() 以及
DataProcess() 等函数执行的时间还是需要十几ms 啊,在这十几ms 的时间里,已经产生好几个2.5
ms的时标消息了,这样岂不是漏掉了扫描,显示起来还是会闪烁。能够想到这一点,很不错,这也就是为什么我们要学会释放CPU的原因。对于
ADProcess(),TimerProcess(),DataProcess(),等任务我们依旧要采取此方法对CPU进行释放,使其执行的时间尽可
能短暂,关于如何做到这一点,在以后的讲解如何设计多任务程序设计的时候会讲解到。
下面我们基于此思路开始编写具体的程序。
首先编写Timer.c文件。该文件中主要为系统提供时间相关的服务。必要的头文件包含。
#include &reg52.h&
#include "MacroAndConst.h"
为了方便计算,我们取数码管扫描一位的时间为2 ms。设置定时器0为2 ms中断一次。
同时声明一个位变量,作为2 ms时标消息的标志
bit g_bSystemTime2Ms = 0 ;&
2msLED动态扫描时标消息&&
初始化定时器0
void Timer0Init(void)
& & TMOD &= 0xf0
& & TMOD |= 0x01
&//定时器0工作方式1
&//定时器初始值
TL0&&=&&0xcc
在定时器0中断处理程序中,设置时标消息。
void Time0Isr(void) interrupt 1
&//定时器重新赋初值
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&
& & io_led_seg_cs = 1
&&&//开段码锁存,送段码数据
& & io_led_seg_cs = 0 ;
static void SendLedBitData(uint8 dat)
& & temp = (0x01
;&&//根据要选通的位计算出位码
& & 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
&&&SendLedBitData(8)
&&&//消隐,只需要设置位选不为0~7即可
&&&if(pBuffer[s_LedDisPos]
== '-')& &&
&//显示'-'号
&SendLedSegData(g_u8LedDisplayCode[16])
&SendLedSegData(g_u8LedDisplayCode[pBuffer[s_LedDisPos]])
&&&SendLedBitData(s_LedDisPos);
&&&if(++s_LedDisPos
&& &s_LedDisPos
函数内部定义一个静态的变量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秒。
#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]
&&&g_u8LedDisplayBuffer[1]
&&&g_u8LedDisplayBuffer[2]
&&&g_u8LedDisplayBuffer[3]
&&&g_u8LedDisplayBuffer[4]
&&&g_u8LedDisplayBuffer[5]
&&&g_u8LedDisplayBuffer[6]
&&&g_u8LedDisplayBuffer[7]
&&&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 ==
&&&s_u16ClockTickCount
&&&g_bTime1S = 1
从上面可以看出,s_u16ClockTickCount计数值达到500的时候,g_bTime1S时标消息产生。然后我们根据这个时标消息刷新数码管显示缓冲区:
void RunClock(void)
& & if(g_bTime1S )
&&&g_bTime1S = 0
&&&if(++g_u8LedDisplayBuffer[7]
&g_u8LedDisplayBuffer[7] = 0 ;
&if(++g_u8LedDisplayBuffer[6] == 6)
g_u8LedDisplayBuffer[6] = 0 ;
if(++g_u8LedDisplayBuffer[4] == 10)
&&&g_u8LedDisplayBuffer[4]&
&&&if(++g_u8LedDisplayBuffer[3]
&g_u8LedDisplayBuffer[3] = 0 ;
g_u8LedDisplayBuffer[0]&2)
if(++g_u8LedDisplayBuffer[1]==10)
&&&g_u8LedDisplayBuffer[1]
&&&g_u8LedDisplayBuffer[0]++;
if(++g_u8LedDisplayBuffer[1]==4)
&&&g_u8LedDisplayBuffer[1]
&&&g_u8LedDisplayBuffer[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篇就到此结束了,在以后的文章中,我们将开始学习如何编写实用的按键扫描程序。
“从单片机初学者迈向单片机工程师”之
&&&LED主题讨论周第四章----渐明渐暗的灯
看着学习板上的LED按照我们的意愿开始闪烁起来,你心里是否高兴了,我相信你会的。但是很快你就会感觉到太单调,总是同一个频率在闪烁,总是同一个亮度在闪烁。如果要是能够由暗逐渐变亮,然后再由亮变暗该多漂亮啊。嗯,想法不错,可以该从什么地方入手呢。
在开始我们的工程之前,首先来了解一个概念:PWM。
PWM(Pulse Width Modulation)是脉冲宽度调制的英文单词的缩写。下面这段话是通信百科中对其的定义:
脉冲宽度调制(PWM)是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术,广泛应用在从测量、通信到功率控制与变换的许多领域中。脉宽
调制是开关型稳压电源中的术语。这是按稳压的控制方式分类的,除了PWM型,还有PFM型和PWM、PFM混合型。脉宽调制式开关型稳压电路是在控制电路
输出频率不变的情况下,通过电压反馈调整其占空比,从而达到稳定输出电压的目的。
读起来有点晦涩难懂。其实简单的说来,PWM技术就是通过调整一个周期固定的方波的占空比,来调节输出电压的平均当电压,电流或者功率等被控量。我们可以
用一个水龙头来类比,把1S时间分成50等份,即每一个等份20MS。在这20MS时间里如果我们把水龙头水阀一直打开,那么在这20MS里流过的水肯定
是最多的,如果我们把水阀打开15MS,剩下的5MS关闭水阀,那么流出的水相比刚才20MS全开肯定要小的多。同样的道理,我们可以通过控制20MS时
间里水阀开启的时间的长短来控制流过的水的多少。那么在1S内平均流出的水流量也就可以被控制了。
当我们调整PWM的占空比时,就会引起电压或者电流的改变,LED的明暗状态就会随之发生相应的变化,听起来好像可以通过这种方法来实现我们想要的渐明渐暗的效果。让我们来试一下吧。
大家都知道人眼有一个临界频率,当LED的闪烁频率达到一定的时候,人眼就分辨不出LED是否在闪烁了。就像我们平常看电视一样,看起来画面是连续的,实
质不是这个样子,所有连续动作都是一帧帧静止的画面在1S的时间里快速播放出来,譬如每秒24帧的速度播放,由于人眼的视觉暂留效应,看起来画面就是连续
的了。同样的道理,为了让我们的LED在变化的过程中,我们感觉不到其在闪烁,可以将其闪烁的频率定在50Hz以上。同时为了看起来明暗过渡的效果更加明
显,我们在这里定义其变化范围为0~99(100等分).即最亮的时候其灰度等级为99,为0的时候最暗,也就是熄灭了。
于是乎我们定义PWM的占空比上限为99, 下限定义为0
#define&&LED_PWM_LIMIT_MAX&
#define&&LED_PWM_LIMIT_MIN&
假定我们LED的闪烁频率为50HZ,而亮度变化的范围为0~99共100等分。则每一等分所占用的时间为
1/(50*100)&&=&&200us
即我们在改变LED的亮灭状态时,应该是在200us整数倍时刻时。在这里我们用单片机的定时器产生200us的中断,同时每20MS调整一次LED的占
空比。这样在20MS * 100 = 2S的时间内LED可以从暗逐渐变亮,在下一个2S内可以从亮逐渐变暗,然后不断循环。
由于大部分的内容都可以在中断中完成,因此,我们的大部分代码都在Timer.c这个文件中编写,主函数中除了初始化之外,就是一个空的死循环。
Timer.c内容如下。
#include &reg52.h&
#include "MacroAndConst.h"
#define LED P0&
&//定义LED接口
#define LED_ON()&
&& &LED = 0x00
;&&//所有LED亮
#define LED_OFF()& & LED = 0xff
;&&//所有LED熄灭
#define&&LED_PWM_LIMIT_MAX&
#define&&LED_PWM_LIMIT_MIN&
static uint8 s_u8TimeCounter = 0 ; //中断计数
static uint8 s_u8LedDirection = 0 ; //LED方向控制 0 :渐亮 1 :渐灭
int8&&s_s8LedPWMCounter&&=
0 ; //LED占空比
void Timer0Init(void)
& & TMOD &= 0xf0
& & TMOD |= 0x01
&&&//定时器0工作方式1
&&&//定时器初始值(200us中断一次)
TL0&&=&&0x47
TR0&&= 1 ;
ET0&&= 1 ;
void Time0Isr(void) interrupt 1
& & static int8 s_s8PWMCounter =
&&&//定时器重新赋初值
TL0&&=&&0x47
& if(++s_u8TimeCounter &= 100)
//每20MS调整一下LED的占空比
&&&s_u8TimeCounter
&&&//如果是渐亮方向变化,则占空比递增
&if((s_s8LedPWMCounter &=
LED_PWM_LIMIT_MAX) &&(0 ==
s_u8LedDirection))
&s_s8LedPWMCounter++&&;
&if(s_s8LedPWMCounter &
LED_PWM_LIMIT_MAX)
s_u8LedDirection = 1 ;
s_s8LedPWMCounter
=&&LED_PWM_LIMIT_MAX
&&&//如果是渐暗方向变化,则占空比递渐
if((s_s8LedPWMCounter &= LED_PWM_LIMIT_MIN)
&&(1 == s_u8LedDirection))
&s_s8LedPWMCounter-- ;
&if(s_s8LedPWMCounter &
LED_PWM_LIMIT_MIN)
s_u8LedDirection = 0 ;
s_s8LedPWMCounter
=&&LED_PWM_LIMIT_MIN
&&&s_s8PWMCounter
= s_s8LedPWMCounter
;&&//获取LED的占空比
& if(s_s8PWMCounter & 0)
//占空比大于0,则点亮LED,否则熄灭LED
&&&LED_ON()
&&&s_s8PWMCounter--
&&&LED_OFF();&
其实PWM技术在我们实际生活中应用的非常多。比较典型的应用就是控制电机的转速,控制充电电流的大小,等等。而随着技术的发展,也出现了其他类型的PWM技术,如相电压PWM,线电压PWM,SPWM等等,如果有兴趣可以到网上去获取相应资料学习。
关于渐明渐暗的灯就简单的讲到这里。...
继续更新.........
今天讲个技巧方面的知识:& &&
一个有关0.;的运算想到的问题
碰到一哥们号称挺NB的嵌入软件工程师,看了他的代码后就欧拉,事情是在一个只有4K代码的单片机接2个DS18B20测温传感器,都知道DS18B20
输出数据只要乘以0.0625就是测量的温度值,这哥们说程序空间怎么也不够,实际上程序只有简单的采集两个DS18B20的数据转换成温度值,之后在
1602液晶上显示,挺简单个程序,怎么也想不通为什么程序空间不够。只读了一下代码发现程序就没动脑子,真的用浮点库把DS18B20数据直接乘以
0.0625了,那程序不超才怪呢,稍微动动脑子也会知道0.0625不就是1/16吗,把DS18B20的数据直接右移4位不就是了(当然要注意符
号),这右移程序可十分简单还省空间,问题很好解决,空间自然也就够了。
现在想来嵌入处理器确实是进步了,程序空间是越来越大,数据RAM空间也越来越大,导致很多人在写程序的时候真的是什么都不顾,借着C语言的灵活性真是纵
横驰骋,压根也不讲个程序效率和可靠性。正如前些日子见到一孩子用ARM
cortex-m3处理器给人接活写个便携表的1024点FFT算法,本身12位的AD系统,这小家伙直接到网上下载了浮点的FFT算法代码就给人加上
了,结果整个程序死慢死慢的,人家用户可不买单啊,这时要动动脑子把数据直接变成乘以某个数变成整数后用定点FFT处理,之后再把数据除一下不就行了。速
度自然也快了,而且也能省下空间。实际当中我们做嵌入软件很多时候犯懒都忽视程序执行效率问题,是都能实现功能,但有时候就是没法谈性能。我几次碰到这样
的工程师,直接把传感器的信号放大后进嵌入处理器的AD,也不看看AD数据是否稳定有效,直接就进行FFT运算,那FFT结果真是热闹,不难看出混叠很严
重,于是又机械地在FFT基础上再去衍生算法,系统程序越做越大,速度越做越慢。实际上也很简单的事,在传感器放大信号进AD之前来一级抗混叠滤波基本也
就解决了,大有所谓嵌入软件高手的概念是程序几乎是万能,实在解决不了就换大程序空间更高速的处理器,整个恶性循环。
经常听说现在流行低碳族,我想出色的嵌入软件工程师最容易成为低碳一族,只要让代码高效那处理器频率自然可以灵活降下来,自然耗电也就少了,二氧化碳排放也就少了。想想目前到处都是嵌入处理器,代码条数看来也别有效果。&&
我会继续更新。。。。。。。。。。。。。。。。。。
&如何设计复杂的多任务程序
我们在入门阶段,一般面对的设计都是单一的简单的任务,流程图可以如图 1 所示,通
常会用踏步循环延时来满足任务需要。
面对多任务,稍微复杂的程序设计,沿用图 1 的思想,我们会做出如图 2 所示的程序,
在大循环体中不断增加任务,通常还要用延时来满足特定任务节拍,这种程序设计思想它有
明显的不足,主要是各个任务之间相互影响,增加新的任何之后,以前很好的运行的任务有
可能不正常,例如数码管动态扫描,本来显示效果很好的驱动函数,在增加新的任务后出现
闪烁,显示效果变差了。
(原文件名:1.JPG)
图1 单一任务简单流程图& &&
多任务简单流程图
很明显,初学者在设计程序时,需要从程序构架思想上下功夫,在做了大量基本模块练
习之后,需要总结提炼自己的程序设计思路(程序架构思想)。
首先我们来理解“任务”,所谓任务,就是需要 CPU 周期“关照”的事件,绝大多数任
务不需要 CPU 一直“关照” ,例如启动 ADC 的启动读取。甚至有些任务“害怕”CPU 一直
“关照”例如 LCD 的刷新,因为 LCD 是显示给人看的,并不需要高速刷新,即便是显示的
内容在高速变化,也不需要高速刷新,道理是一样的。这样看来,让CPU做简单任务一定很
浪费,事实也是如此,绝大多数简单任务,CPU都是在“空转” (循环踏步延时) 。对任务总
结还可以知道,很多任务需要 CPU 不断“关照” ,其实这种“不断”也是有极限的,比如数
码管动态扫描,能够做到40Hz 就可以了,又如键盘扫描,能够做到20Hz(经验值),基本上
也就不会丢有效按键键值了,再如LCD刷新,我觉得做到 10Hz 就可以了,等等。看来,绝
大多数任务都是工作在低速频度。而我们的CPU一旦运行起来,速度又很快,CPU本身就是
靠很快的速度执行很简单的指令来胜任复杂的任务(逻辑)的。如果有办法把“快”的 CPU
分成多个慢的CPU,然后给不同的任务分配不同速度的CPU,这种设想是不是很好呢!确实
很好,下面就看如何将“快”的CPU划分成多个“慢”的 CPU。
根据这种想法,我们需要合理分配CPU资源来“关照”不同的任务,最好能够根据任务
本身合理占用CPU资源,首先看如图 3 所示的流程图,各个任务流程独立,各任务通过全局
变量来交互信息,在流程中有一个重要的模块“任务切换”,就是任务切换模块实现 CPU 合
理分配,这个任务切换模块是怎么实现的呢?
(原文件名:2.JPG)
图3 多任务复杂流程图
首先需要理解,CPU 一旦运行起来,就无法停止(硬件支持时钟停止的不在这里讨论),
谁能够控制一批脱缰的马呢?对了,有中断,中断能够让CPU回到特定的位置,设想,能不
能用一个定时中断,周期性的将 CPU这匹运行着的脱缰的马召唤回来,重新给它安排特定的
任务,事实上,任务切换就是这样实现的。
(原文件名:3.JPG)
图 4&&定时中断实现任务切换
如图 4A 所示,CPU 在空闲任务循环等待,定时中断将 CPU 周期性唤回,根据任务设计
了不同的响应频度,满足条件的任务将获得CPU资源,CPU为不同任务“关照”完成后,再
次返回空闲任务,如此周而复始,对于各个任务而言,好像各自拥有一个独立的CPU,各自
独立运行。用这种思想构建的程序框架,最大的好处是任务很容易裁剪,系统能够做得很复
在充分考虑单片机中断特性(在哪里中断就返回到哪里)后,实际可行的任务切换如图
4B所示,定时中断可能发生在任务调度,随机任务执行的任何时候,图中最大的框框所示,
不管中断在何时发生,它都会正常返回,定时中断所产生的影响只在任务调度模块起作用,
即依次让不同的任务按不同的节拍就绪。任务调度会按一定的优先级执行就绪任务。
总结不同的任务需要CPU关照的频度,选择最快的那个频度来设定定时器中断的节拍,
一般选择 200Hz,或者 100Hz 都可以。另外再给每个任务设定一个节拍控制计数器 C,也就
是定时器每中断多少次后执行任务一次。例如取定时中断节拍为 200Hz,给任务设定的 C=10,
则任务执行频度为 200/10=20Hz,如果是数码管扫描,按 40Hz 不闪烁规律,则任务节拍控制
计数器 C=5 即可。在程序设计中,C 代表着任务运行的节拍控制参数,我们习惯用 delay 来
描述,不同的任务用task0,task1……来描述。
明天继续写如何用代码实现!
下面我们来用代码实现以上多任务程序设计思想。
首先是任务切换
if(task_delay[0]==0)&
&task0();&&//task0就绪,
if(task_delay[1]==0)&
&task1();&&//task1就绪,
很显然,执行任务的条件是任务延时量task_delay=0,那么任务延时量谁来控制呢?定时
器啊!定时器中断对任务延时量减一直到归零,标志任务就绪。当没有任务就绪时,任务切
换本身就是一个Idle 任务。
void timer0(void) interrupt 1
if(task_delay[0]) task_delay[0]--;
if(task_delay[1]) task_delay[1]--;
例如 timer0 的中断节拍为 200Hz,task0_delay 初值为 10,则 task0()执行频度为
200/10=20Hz。
有了以上基础,我们来设计一个简单多任务程序,进一步深入理解这种程序设计思想。
任务要求:用单片机不同 IO 脚输出 1Hz,5Hz,10Hz,20Hz 方波信号,这个程序很短,将
直接给出。
#include "reg51.h"
#define TIME_PER_SEC 200& &
//定义任务时钟频率,200Hz
#define CLOCK & &
//定义时钟晶振,单位Hz&&
#define MAX_TASK 4&
&//定义任务数量
extern void
task0(void);&&//任务声明
extern void task1(void);
extern void task2(void);
extern void task3(void);
sbit f1Hz&&=
P1^0;&&//端口定义
sbit f5Hz&&= P1^1;
sbit f10Hz = P1^2;
sbit f20Hz = P1^3;
unsigned char
task_delay[4];&&//任务延时变量定义
//定时器0初始化
void timer0_init(void)
&for(i=0;i&MAX_TASK;i++)
task_delay=0;&&//任务延时量清零
& & TMOD = (TMOD
& 0XF0) | 0X01;&
&&&//定时器 0工作在模式
1, 16Bit定时器模
255-CLOCK/TIME_PER_SEC/12/256;&
255-CLOCK/TIME_PER_SEC/12%6;&
ET0 =1;& &&
&//开启定时器和中断
// 系统 OS定时中断服务
void timer0(void) interrupt 1
255-CLOCK/TIME_PER_SEC/12/256;
255-CLOCK/TIME_PER_SEC/12%6;&
for(i=0;i&MAX_TASK;i++) if(task_delay)
task_delay--;&&
//每节拍对任务延时变量减1 ,减至
0&&后,任务就绪。&
void main(void)
& & timer0_init();
& & EA=1;//开总中断&
& &while(1)
&&&if(task_delay[0]==0)
{task0(); task_delay[0] = TIME_PER_SEC/ 2;}
&&//要产生 1hz 信号,翻转周期就是
2Hz,以下同
&&if(task_delay[1]==0) {task1();
task_delay[1] = TIME_PER_SEC/10;}
&&//要产生 5hz 信号,翻转周期就是
10Hz,以下同
&&if(task_delay[2]==0) {task2();
task_delay[2] = TIME_PER_SEC/20;}
&&if(task_delay[3]==0) {task3();
task_delay[3] = TIME_PER_SEC/40;}&
void task0(void)
& & f1Hz =
void task1(void)
& & f5Hz =
void task2(void)
& & f10Hz =
!f10Hz;& &
void task3(void)
& & f20Hz =
!f20Hz;& &
仿真效果如图5 所示。
(原文件名:4.JPG)
5&&仿真波形图&&
同样的程序,同学们可以考虑用图 2 所示的思想设计,看看容易不容易,如果你的程序
实现了相同的功能,如果我改变要求,改变信号的频率,你的程序容易修改吗?
要进一步完善这种程序设计思想,有几个问题还需要考虑:
对任务本身有什么要求?
不同任务之间有没有优先级?(不同的事情总有个轻重缓急吧!)
任务间如何延时?
为了回答这些问题,下面我们来分析 CPU的运行情况。
(原文件名:5.JPG)
图 6 CPU运行情况示意图
CPU运行情况如图 6 所示,黑色区域表示 CPU进程,系统启动后, CPU将无休止的运行,
CPU资源将如何分配呢?程序首先进入“任务切换”进程,如果当前没有任务就绪,就在任
务切换进程循环(也可以理解为空闲进程),定时中断将 CPU 当前进程打断,在定时中断进
程可能让某些任务就绪,中断返回任务切换进程,很快会进入就绪任务 0,CPU“关照”完
任务 0,再次回到任务切换进程,如果还有其它任务就绪,还会再次进入其它任务,没有任
务就循环等待,定时中断会不断让新的任务就绪,CPU 也会不断进入任务“关照” 。这样不
同的任务就会获得不同的CPU资源,每一个任务都像是拥有一个独立的CPU 为之服务。
从这种进程切换我们可以看出,在定时中断和任务切换过程中,额外的占用了一些 CPU
资源, 这就是定时中断频度不宜太快, 否则将大大降低CPU的有效资源率, 当然太慢也不行。
另外就是 CPU每次关照任务的时间不能太长,如果超过一个中断周期,就会影响到其它任务
的实时性。所谓的实时性就是按定时中断设定的节拍,准时得到CPU关照。这样,每一个子
任务就必须简单,每次“关照”时间最好不要超过定时中断节拍周期(5ms 或 10ms,初学者
要对 ms 有一个概念,机器周期为 us 级的单片机,1ms 可以执行上千条指令,对于像数码管
扫描,键盘扫描,LCD显示等常规任务都是绰绰有余的,只是遇到大型计算,数据排序就显
关于任务优先级的问题:一个复杂系统,多个任务之间总有“轻重缓急”之区别,那些
需要严格实

我要回帖

 

随机推荐