在写之前我先说一下为什么写这篇文章,其实主要是出于这么一些考虑:
1、一个人力量有限,资源有限,了解到的东西就更加有限,现代社会讲究团队协作,SO......希望以此为契机,号召大家参与其中,将这些东西不断完善
2、开源软件,开源硬件,开源教程,其实这方面早有先河,但是更多的是单人收集的,单人写的,而本教程希望是集大众力量写教程服务于大众
3、这里面主要从基础单片机开始,然后是模块化编程,编程规范,从零开始构造单片机操作系统这些方面跟大家一起分享,其实还有很多非常优秀的编程思想,像状态机,PID算法等等,大家都可以一一添加进去
4、基础篇主要参考《爱上单片机》--杜洋、《电子设计从零开始》; 模块化主要参考uCOS作者《嵌入式系统构件》,然后结合个人的一些经验; 编程规范参考华为的规范文档,单片机系统参考《51时间系统》、实时系统uCos及阿莫电子论坛、正点原子论坛
5、个人学习技术性东西的原则:
先入门,有一个整体感知(也就是学习一些基础性的东西)->实践,发现问题,解决问题->再次学习基础性东西->由易到难做一些有挑战性的工程
说明:由于本人了解的东西有限,这里面如果有什么错误的地方,误导到家的地方,希望大家在看了本教程之后,大家多多参与,提出宝贵的意见。本篇主要以思路,方法与编程思想为主,层层深入,将复杂的问题,深奥的东西进行剖析,以便于大家更好的吸收、理解这些东西,至于怎么去用编程软件,下载等我会给出已经讲得很详细教程。
时间仓促,内容难免措辞不当,恳请大家不吝赐教
第一篇
51单片机基础篇
我们先来看下下图的一个对比
首先说一说我个人对单片机的理解,如果把我们人类的大脑比作MCU里面的核心CPU的话,那么人的手脚就相当于外设中的基本输入输出端口(Input and Output),人的耳朵相当于MCU的通信输入,嘴巴相当于通信输出,这里只是做一个比较简单的比喻。单片机是一个人为的给它安排事情去做的这么一个单片型微型计算机,其实我们非常熟悉电脑,电脑有内存,显卡,声卡等。单片机也有类似的这些东西,比如说有些功能强一点单片机内部自带AD转换模块、USB控制器、CAN总线控制器,为什么单片机只有这么些低端的东西呢?我们我们可以成本和需求方面考虑下,如果说在单片机里面集成个声卡你用的上么,集成个USB控制器大部分情况你又用的上么,还有这么一个东西集成在里面需不需要额外的成本。其实讲到这里,大家可以去了解下你们常用的手机,里面那块主芯片,市面上现在主要是联发科与高通,其余的就是三星的,还有我们国家华为的,手机厂商在发布新产品时,经常提到什么GPS,蓝牙,什么全网通(GSM/TDSCDMA/WCDMA/LTE)等等,基本上是宣传芯片的功能,并不是手机的功能,这也就是想告诉大家,手机上的那块主芯片也是单片机,只不过它的功能更强大一点,集成的东西更多一点;把这些问题想清楚了,然后我们就得到描述单片机常用的一句话:
其实我们从自面上也可以理解一下,单片机:就是单独的一个芯片的机器,大家试想一下,一个芯片的机器,那么这个芯片里面应该需要些什么呢??都可以大胆的去想。还有一个就是大家经常聊起的嵌入式,首先发表一下个人的看法,在我看来只要是上面带了单片机的,不论大小,不论功能强弱,都可以称为嵌入式,但是往往我们谈的更多的就是功能强大型的,能跑LINUX的.....个人觉得这是一个误区。
说到这里说点题外话,经常有人说232,485,CAN总线接口,但是跟很多人交流发现很多人没有把通信协议与这些物理接口区分清楚,我在这里用一个简单的比喻来跟大家说明下,上面我们所说的232,485,CAN总线都是物理接口,也就是我们每个人都有一个嘴巴、鼻子、耳朵一样,中国人有这些,美国人也有这些,同样非洲人也有这些东西(肢体残缺的不算),但是我们中国人主要用汉语交流,美国人主要用英语交流,德国用德语等等,我们要想顺利的与外国人沟通,那我们只能说他们的母语,这就是协议,像典型的协议在单片机中有很多,工业控制里面的modbus,openbus,工程车上面有SAEJ1939,然后再互联网领域里面我们众所周知的TCP/IP协议等等,modbus可以存在于232上,同样也可以通过485接口通信,CAN总线接口也行,是独立于物理接口的,是为了解决特定问题而诞生的。大家有空可以去看下计算机整个诞生的历史,关于这方面的书籍非常多,包括很多优秀的算法的诞生,软件的诞生,都是为了解决特定问题而出现的。学习单片机我们始终要明白,单片机的诞生也是为了方便人类生活的,说道这里我又不得不提一下计算机的语言,其实我们学过一点计算机的人都晓得,最初的计算机编程直接是二进制,但是慢慢的这些计算机科学家发现他们每天做的这些事情很多是重复的,并且二进制对于其它人来说非常难懂,然后就出现了汇编语言,也就是我们熟悉的A语言,A语言是接近计算机底层的,我们要用计算机的思考方式去编程,这时期的计算机也就只能少部分人能玩,总有一些“不安分”的计算机科学家在想办法解决这些问题,然后就出现了Basic语言,也就是B语言,B语言就有一点接近人的思考方式了,本人只是了解了一些基本的汇编语言,Basic语言不是太了解,我就不多说了,以免误导大家。后面有出现了C语言,接本上接近人的思考方式,我们也称这些语言是高级语言,之所以高级,是因为符合人类的思考方式,人类是高级动物嘛......这两年学习C++与JAVA语言的人非常多,也非常热,我只学习了一点点的C++语言,我用的最多是C语言,为什么没去学习C++ OR JAVA语言,如果认真去了解这些语言我们会发现,C++与JAVA乃至后面的D、E、F等语言都是越来越接近人的思考方式的,语言本身只是一个工具,真正核心的东西是思想,编程思想,就像我们解决一个问题有不同的方法,我们肯定会选择简单易行的方法,我给大家举个例子:我们从小都玩过俄罗斯方块,俄罗斯方块用汇编语言可以实现,用C语言也可以实现,同样用C++ JAVA也可以实现,但是它的核心算法却都是一样的,这一点大家可以去证实。C++的核心思想是面向对象,什么是面向对象我没有深入的去学习,只是了解了一些。但是我可以告诉大家一些东西,或许大家对语言本身会有一些感悟。假设我们去驾驶一辆汽车,我想大家所关心的应该是怎么去把它开动,以自动档为例,我们只要晓得怎么挂档,松手刹,踩刹车,打方向盘的基本操作就行了;大家可以想象一下如果我们去驾驶一辆汽车,在驾驶的过程中,我们关心这些问题,发动机怎么运行,里面的电路怎么工作,我踩刹车,哪个刹车钳在工作,我打方向盘的时候关注电子助力转向在怎么工作,在细化一点,发动的几个缸在工作,曲轴在转动等等。我想大家开车一定会非常辛苦。其实在这里汽车本身就是一个对象,对象里面的东西我们就不需要关注太多,我们只要关心我们要使用到的东西就可以了,对于汽车本身,发动机是一个对象,电子设备也是一个对象,我们驾驶汽车,操作这些东西,只需关注我们要用到的接口就行了。再近一点,我们看下我么学校的管理模式,整个学校是属于校长管理,但是校长并不是一对一的对我们进行管理,而是一级一级的往下通知,至于下面的细节他不会太多的去关心。就像我们要找别人帮忙一样,如果我们还关心别人用什么方法给我解决问题,我想别人一定非常反感,明白这一点我们对于我们以后学习这方面选择什么语言非常有帮助。其实不单单语言方面有这种面向对象、模块化思想,在电路设计当中这种模块化思想也体现的淋漓尽致,在这里我们一典型的工业PLC为例:PLC里面基本上带有哪些功能模块呢?有电源模块、AD转换模块、DA转换模块、开关量检测模块、控制继电器输出模块,通信模块有CAN、RS232、RS485、以太网接口。明白电路这方面的功能,对于基本电路检修非常有利。
由于本人了解的东西有限,这里面如果有什么错误的地方,误导到家的地方,希望大家在看了本教程之后,大家多多参与,提出宝贵的意见,也可以通过黄颜色的字在不删除原来的基础上修改,以便于完善本教程,写出一份适合我们自己学习的教程。还有就是本教程可能更多的以思路,方法与编程思想为主,层层深入,将复杂的问题,深奥的东西进行剖析,以便于大家更好的吸收这些方式方法,至于怎么去用编程软件,下载等我会给出相关书籍。。
一、如何学习单片机
发表一下个人的观点:书要看,但是不要过多的看,把它作为一个知识的补充,我们读了这么多年的书,其实只有在自己静下心来看书的时候效率才最高
首先,我们不能像以往学习其他课程一样,又是背,又是拿笔计算,学什么寄存器,从那难懂的汇编语言入手....我直接告诉大家,这样是达不到效果的,反而会丧失学习单片机的兴趣。那该如何去学呢,我们要充分利用自己的兴趣去学,去“玩”单片机,而不是被它所“玩”,在此过程中切记浮躁,不要跟着自己的情绪走,想学就学一下,不想学就不学,要持之以恒。
①刚开始可以对照着别人的代码抄写,但是后期只能借鉴别人的,不要一味的CTRL+C,CTRL+V,我晓得大家这两个东西用的很熟,多借鉴别人的编程方法,用自己的思路去写
②不要蜻蜓点水,得过且过,细微之处体现实力
③把时髦的技术挂在嘴边,不如把过时的技术记在心里
④经典的书时不时的去重新看一遍,多看国外的书
⑤网上的资源要多利用,但是不要瞎逛,要有目的
⑥单片机十天是学不会的,只能入门,要打持久战
⑦思想很重要,方法很重要!!
二、学习的一个基本流程
1、了解单片机是个什么东西,主要出现在那些领域,弄清自己为什么要学?
2、基本开发软件安装,代码下载(Keil是最基本的)
3、跟着教程从零开始建立第一个工程,一次不会多试几次:跟着教程把代码一个一个的敲上去,然后编译,出现错误不要立马询问他人,先自行尝试去解决,实在搞不懂再去问别人,当控制了LED的亮灭工作后,就要去分析一下这个LED到底是怎么点亮的,里面相应的代码又是怎么回事,只要要去了解一遍。
4、到了这个阶段,就可以学习单片机的其他东西,对照着代码一个一个字母的抄,不要觉得繁琐,没抄完一个功能,正确无误后,先做好备份,然后再去修改,看是否与你的预期相符,抄着抄着你就有感觉了,有了感觉就可以尝试着不参考别人的程序自己从零去建立自己想要实现的功能,从最简单的开始。
5、当把单片机的基本功能都学完后,就可以以工程的形式进一步学习了;用单片机实现一个电子钟,用单片机控制一辆小车.....在做这种工程的过程中我们会发现很多问题,解决很多问题,这样我们就上升了一个层次了。
6、编程规范,本来想将编程规范放在最前面,考虑到可能会让大家丧失学习单片机的热情。这阶段就要看人家写的代码,为什么看上去那么舒服,并且很容易阅读,函数一看就知道它用来干嘛......从规范着手,编写能重复利用,便于维护的代码
7、工程也做了,规范也有了,是不是有点感觉单片机不怎么好玩了?其实还有大把的东西需要你了解,随着工程一个比一个大,我们就要从全局开始思考这些问题了。要对一个复杂的工程分成一个个的模块,比如说在电子钟工程里,我们可以尝试着把它分为按键模块,显示模块,时钟模块......
Keil:软件不要汉化,英语没有那么可怕,它本身也自带了说明书,遇到有些问题可以去看它
Pretous:个人建议只用来做一些功能性的检验,像算法类型的,切不可在实际硬件中用其作为真实参考
Notepade++:平时查看,编写代码非常方
三、了解51单片的的基本东西及C语言
单片机:硬件设施,躯壳首先要了解基本的51单片机知识,实验室一般以国产宏晶公司的STC89C52RC为例,要知道怎么给单片机供电,有哪些基本的I/O口,I/O口的基本内部结构,串口登(这些东西只做基本了解,不需要刻意去记住,在后续的练习过程中,大家会慢慢记住的)
C语言:灵魂对于学习单片机C语言,个人建议刚开始可以直接跟着抄写代码就行了,至于为什么是那样写的不必纠结太多,练习一段时间后再去看那些单片机的C语言书籍,这样大家更深刻些
四、Keil软件的安装以及怎么用ISP软件下载
①Keil软件怎么安装,以及怎么破解,基本的设置,怎么使用,大家网上去搜索,如果这一点都做不到,不要说你会用电脑(关于使用这一块了解就行,后面在写代码的过程中会反反复复的用到的,不必刻意去记)
②程序烧写ISP(ISP--In System Programming是在线编程的意思)软件可直接从宏晶公司官www.stcmcu.com下载STC-ISP,这里面也有很多值得参考的东西,有事没事可以去看看,也可以百度,怎么安装USB转串口的驱动(USB转串口常用芯片:CP2102,PL2303,CH341),它的功能也就是将我们编写好的代码下载到我们的单片机当中,怎么从ISP软件中找相应单片机的型号进行代码的下载
烧写步骤:
(1)、在烧写前先断开单片机的电源(注意)
(2)、首先选中单片机的型号,根据自己用的单片机选定,我这里是STC89C52RC
(3)、打开要烧写的文件:如LED.HEX
(4)、选择当前有效的串口
(5)、点击下载按钮。
(6)、接通单片机的电源
关于下载我总结了一下,如果要重复下载的话,最好装个那种自锁的开关,然后每次去点击下载,直接按开关
③将别人验证成功简单功能(etc:LED灯闪烁)HEX文件下载进单片机以便验证自己的思路是否正确
五、点亮一颗LED灯
LED灯其实我们平时到处都可以看到,不同的LED灯的驱动电压有区别,这个我就不多说了,百度上一大堆,大家只要记住一点LED灯的那个限流电阻的值是怎么来的就行了,比如说红色LED灯的驱动电压是3V,电源是5V,一般的驱动电流大约10mA就足够了 R = (5V-3V)/10mA*1000 = 200Ω,我这只是举个例子,其他的大家都可以根据数据手册,或者某宝卖家提供的参数进行电阻的计算。
硬件部分
如图所示D1连接在P0.0端口,我们为什么要采用这种方式连接呢?可不可以将LED灯反向呢?对于这个问题我给出的回答是,有些可以,有些不可以。为什么是这样呢?
这个我们就要了解STC89C52RC这款单片机的GPIO内部的基本结构了,在本实验中所使用的端口为P0口,内部为开漏输出,什么是开漏输出?度娘.....因为在这个教程中我只教大家学习单片机知识,其它的不过多的说,以免牵扯太多,大家消化不了。P1~P3口是准双向上拉,这款单片机(5V)I/O口的驱动能力的灌电流20mA;弱上拉时,拉电流能力是200uA,设置成强推挽时,拉电流能力也可达20mA。
灌电流:即MCU被动输入电流。
拉电流:即MCU主动输出电流。
那下图就不用多解释了.....
代码部分:
/************ (C) COPYRIGHT 2016 wllis **************
*
* 文件名: LED.C
* 描 述:点亮一颗LED
* 作 者: wllis
* 日 期: 2016/01/15
* ****************************************************/
#include
typedef unsigned char UINT8;
typedef unsigned int UINT16;
sbit LED = P0^0;
/*
* 函数名:void DelayMS( UINT16 n )
* 描 述:延时 * 输 入:UINT16
* 输 出:无
*/
void DelayMS( UINT16 n )
{
UINT8 a;
while(--n)
{
for( a=114; a>0; a--); }
}
/*
* 函数名:void main()
* 描 述:主函数
* 输 入:无
* 输 出:无
*/
void main()
{
P0 = 0XFF; // 将P0端口全部初始化为高电平
while(1)
{
LED = 1; // LED灭
DelayMS(500); // 延时500毫秒
LED = 0; // LED亮
DelayMS(500); // 延时500毫秒
}
}
/********************end of file ***********************/
代码分析:
首先我们看下我们要点亮的LED怎么实现,先在代码中定义[图片上传失败sbit LED0 = P0^0
;也就是P0口的最低位,这句代码大家可能是初次接触,不怎么好懂,sbit是Keil开发环境中的关键字,是直接用来进行位定义的,它的用法大家参考我后面给出的书籍。这么P0又是怎么来的呢?我们来看下REG52.H这个头文件
在里面是不是看到有关于P0的定义,再看下那个英文单词BYTE Registers字节寄存器,一个字节是多少位?八位。sfr又是怎么回事呢?它的全称是Special Function Register这个是Keil软件规定的用来定义特殊寄存器的,这是规定,大家也没必要了解为什么了,感兴趣的话自行去了解。然后再来看下P0后面的0X80是什么意思?,这个其实就是P0端口的地址,讲到这里大家可能又会问,地址又是怎么回事?这个问题我给大家打个比方,比如说你到宾馆去住宿,你上服务台那边花钱开了一间房,服务员告诉你的房间在112号,这个112号也就是宾馆房间的一个地址,有了这个地址你就可以在你的房间自由进出了,看电视,睡觉等等。如果说没有地址那岂不是乱了套了,大家到处乱走,很容易走错别人的房间。单片机也是如此,有了这个地址我们就可以对它里面的东西进行操作,变换高低电平,读取高低电平,这个地址是怎么来的,从单片机的数据手册上可以找到,这个就不要问为什么了,当初造这块单片机的时候就早已经固定好了。
大家看下最下面的80h是不是有个P0 ....._,功能越强大的单片机,它的寄存器就越多,以至于它们的数据手册写了厚厚的一本书,像STM32、S3C2410大家可以去找它们的数据手册看看。再来看看当中的延时函数,如果我们把它进行拆解,外面一个500次的循环,内部一个114次的循环,500*114我的天呐,500多万次的减法,不停的对其中的数进行减法运算,一直到运算条件不成立为止,也就是CPU每运行一个指令都要花费一点时间,将这些时间全部加起来达到我们需要时间。说道这里,大家是不是感觉太TM浪费了,一个小小的延时,这么占用MCU的资源,以至于如果还有其他的东西需要运行的话都有困难,那有没有别的方法可以将这种方式的延时资源占用释放出来呢?答案肯定是有的,只要大家坚持往下看就会学到自己想学的东西....
六、流水灯实验
硬件电路
代码1:
/************ (C) COPYRIGHT 2016 wllis **************
*
* 文件名: Flow_LED.C
* 描 述:LED流水灯实验
* 作 者: wllis
* 日 期: 2016/01/21
*
****************************************************/
#include
typedef unsigned char UINT8;
typedef unsigned int UINT16;
/*
* 函数名:void DelayMS( UINT16 n )
* 描 述:简单的延时
* 输 入:UINT16
* 输 出:无
*/
void DelayMS( UINT16 n )
{
UINT8 a;
while(--n)
{
for( a=114; a>0; a--);
}
}
/*
* 函数名:void main()
* 描 述:主函数
* 输 入:无
* 输 出:无
*/
void main()
{
UINT8 i = 0;
P0 = 0XFF; // 将P0端口全部初始化为高电平 0XFF化为二进制为 1111 1111
while(1)
{
for( i=0; i<8; i++ )
{
P0 = 0XFE; // 1111 1110
DelayMS(100); // 延时100毫秒
P0 = 0XFD; // 1111 1101
DelayMS(100); // 延时100毫秒
P0 = 0XFB; // 1111 1011
DelayMS(100); // 延时100毫秒
P0 = 0XF7; // 1111 0111
DelayMS(100); // 延时100毫秒
P0 = 0XEF; // 1110 1111
DelayMS(100); // 延时100毫秒
P0 = 0XDF; // 1101 1111
DelayMS(100); // 延时100毫秒
P0 = 0XBF; // 1011 1111
DelayMS(100); // 延时100毫秒
P0 = 0X7F; // 0111 1111
DelayMS(100); // 延时100毫秒
}
}
}
代码2:
/************ (C) COPYRIGHT 2016 wllis **************
*
* 文件名: Flow_LED.C
* 描 述:LED流水灯实验
* 作 者: wllis
* 日 期: 2016/01/21
*
****************************************************/
#include
typedef unsigned char UINT8;
typedef unsigned int UINT16;
/*
* 函数名:void DelayMS( UINT16 n )
* 描 述:简单的延时
* 输 入:UINT16
* 输 出:无
*/
void DelayMS( UINT16 n )
{
UINT8 a;
while(--n)
{
for( a=114; a>0; a--);
}
}
/*
* 函数名:void main()
* 描 述:主函数
* 输 入:无
* 输 出:无
*/
void main()
{
UINT8 i = 0;
P0 = 0XFF; // 将P0端口全部初始化为高电平 0XFF化为二进制为 1111 1111
while(1)
{
for( i=0; i<8; i++ )
{
P0 = ~(0X01<
比较一下代码1及代码二,实现的功能是一样的,但是代码2精简了很多,到后面讲到定时器时我还将给大家介绍另外一种实现该功能的代码
代码分析:
首先将P0全部初始化为高电平,这么做的目的主要是为了方便后面的操作,打个简答的比方,如果我们起跑没有起跑线的话,那么我们在100米赛跑中怎么确定终点线呢,同样在这里我们只有初始化了,后面我们才晓得哪个位为高电平,怎么去操作,以免导致误触发,影响我们想要的结果。这个东西这么做大家现在可能不理解,但是慢慢的随着大家经历的多了,就能理解了。
流水的核心思想就是每个端口开启一段时间的低电平,然后把它关掉,在下一个位开启低电平这么循环操作,从而达到我们想要的效果,我给大家画个框图解释下。代码二用的相当简洁,我们首先来分解下(0X01<(0000 0001< P0端口 P0.7 P0.6 P0.5 P0.4 P0.3 P0.2 P0.1 P0.0
~(0X01)<<0 1 1 1 1 1 1 1 0
~(0X01)<<1 1 1 1 1 1 1 0 1
~(0X01)<<2 1 1 1 1 1 0 1 1
~(0X01)<<3 1 1 1 1 0 1 1 1
~(0X01)<<4 1 1 1 0 1 1 1 1
~(0X01)<<5 1 1 0 1 1 1 1 1
~(0X01)<<6 1 0 1 1 1 1 1 1
~(0X01)<<7 0 1 1 1 1 1 1 1
七、数码管
对于数码管其实我们可以这么理解,它就是几个LED灯拼凑起来的,我们通过控制其相应LED灯的亮灭就可以实现我们想要显示的数字或字母,至于它的分类,共阴、共阳大家自行去了解
在本实验中我只是演示了一个基本的显示0~9的一个功能
硬件原理图
从原理图我们可以了解到这是一个共阳的数码管,相应的每一段连接在P2端口,如下图所示
代码部分:
/************ (C) COPYRIGHT 2016 wllis **************
*
* 文件名: SEG.C
* 描 述:数码管从‘0’至‘9’循环显示
* 作 者: wllis
* 日 期: 2016/01/21
*
****************************************************/
#include
typedef unsigned char UINT8;
typedef unsigned int UINT16;
/**************************************************
硬件连接 a
xxxxxxxx a->P2.0
x x b->P2.1
f x g x b c->P2.2
xxxxxxxx d->P2.3
x x e->P2.4
e x x c f->P2.5
xxxxxxxx g->P2.6
d
**************************************************/
// 对数码管进行编码
code UINT8 Seg_Code[15] = { ~0X3F,// '0'
~0X06, // '1'
~0X5B, // '2'
~0X4F, // '3'
~0X66, // '4'
~0X6D, // '5'
~0X7D, // '6'
~0X07, // '7'
~0X7F, // '8'
~0X6F // '9'
};
/*
* 函数名:void DelayMS( UINT16 n )
* 描 述:简单的延时
* 输 入:UINT16
* 输 出:无
*/
void DelayMS( UINT16 n )
{
UINT8 a;
while(--n)
{
for( a=114; a>0; a--);
}
}
/*
* 函数名:void main()
* 描 述:主函数
* 输 入:无
* 输 出:无
*/
void main()
{
UINT8 i = 0;
P2 = 0XFF; // 将P2端口全部初始化为高电平 0XFF化为二进制为 1111 1111
while(1)
{
for( i=0; i<10; i++ )
{
P2 = Seg_Code[i]; // 数码管显示0~9
DelayMS(1000); // 延1000ms
}
}
}
/********************end of file ***********************/
代码分析:
首先我们看这个数码管编码比如0X3F我们来分解为二进制码(0011 1111),最后得到1100 0000,然后通过这段
P2 = Seg_Code[i];
将值赋给P2端口,最高位可以不用管,我们可以看到如果对应P2口的话P2.7,P2.6为高电平,其余为低电平,对应到数码管上是abcdef亮,也就是中间的那段g不亮,其余的六段都亮,显示一个’0’;再来看下(0X06),二进制为(0000 0110),再进一步变化得到1111 1001,对应到数码管上为bc亮,显示一个’1’,其他的都按照这个方法去分析就能明白数码管到底是怎么显示你想要的数字或字母。
八、按键程序
这是我们常用的按钮式开关,从图中可以看出我们真正想要的是中间那平整的线,假如我们实在开始时的回弹和结束时的回弹那中间去检测开关,大家想像一下会出现什么效果,大家尽管动手去验证,我反正是屡试不爽。
代码:
/************ (C) COPYRIGHT 2016 wllis **************
*
* 文件名: Button.C
* 描 述:用按键控制LED灯的亮灭
* 作 者: wllis
* 日 期: 2016/01/30
*
* 按键->P1.0 LED->P0.0
*
****************************************************/
#include
typedef unsigned char UINT8;
typedef unsigned int UINT16;
// 定义按键与LED灯
sbit LED = P0^0;
sbit SW = P1^0;
/*
* 函数名:void DelayMS( UINT16 n )
* 描 述:简单的ms延时
* 输 入:UINT16
* 输 出:无
*/
void DelayMS( UINT16 n )
{
UINT8 a;
while(--n)
{
for( a=114; a>0; a--);
}
}
/*
* 函数名:void main()
* 描 述:主函数
* 输 入:无
* 输 出:无
*/
void main()
{
// 初始化按键与LED灯
LED = 1;
SW = 1; //这个地方为什么这么做?
while(1)
{
if( 0==SW )
{
DelayMS(15);
if( 0==SW )
{
while(!SW);
LED = ~LED;
}
}
}
}
/********************end of file ***********************/
代码分析:我们首先来看下这段代码SW = 1;,这段代码为什么置1,自行去看书,看什么书我会给大家介绍的,我只告诉大家这个地方置为1是方便后面的检测,检测的整个过程是这样进行的首先检测按键月有没有按下去也就是有没有接地if(0==SW),然后对抖动部分进行短暂的延时,一般10~15ms,然后再次检测是否是误触发,如果不是就接着检测按键是否弹上来,弹上来之后就算是一个完整的按键过程了,接着执行按键要做的工作。
思考一下:
大家学到这里有没有发现什么问题?我们在LED闪烁,数码管0~9循环计数,以及按键控制LED灯亮灭的程序中都有用到延时函数,
也就是这个函数,我们来分析一下这个函数,是不是就是在里面循环做着一些我们不需要的事情,循环的减减减,一直减到零才继续执行下一步程序:是不是有点浪费,如果说要让几个LED灯实现不同频率的闪烁,而且都用次函数延时,大家可以思考一下,能不能实现??如果再加上按键呢,好不好实现?大家可以尽情的去试一下,我在这里可以提前告诉大家,用这种方法几乎不可能实现,那么我们有没有别的办法去实现呢?答案肯定是有的,并且在延时的过程中我们还可以用来做其他的事情,大家接着往下学。。。很多很多的惊喜_
九、定时器
定时器无论是在单片机还是操作系统当中是非常的重要,用途也非常广泛,废话我就不多说,怎么个有用法,我们用实验来证明。
本实验我将用定时器给大家实现很多不同的功能
实验一:用T/C0中断精确的实现数码管0~9计数
硬件原理图:
代码:
/************ (C) COPYRIGHT 2016 wllis **************
*
* 文件名: Timer0_SegCount.C
* 描 述:用T/C0实现数码管0~9的精确秒计数
* 作 者: wllis
* 日 期: 2016/01/30
*
* ****************************************************/
#include
typedef unsigned char UINT8;
typedef unsigned int UINT16;
/**************************************************
硬件连接 a
xxxxxxxx a->P2.0
x x b->P2.1
f x g x b c->P2.2
xxxxxxxx d->P2.3
x x e->P2.4
e x x c f->P2.5
xxxxxxxx g->P2.6
d
**************************************************/
// 对数码管进行编码
code UINT8 Seg_Code[15] = { ~0X3F, // '0'
~0X06, // '1'
~0X5B, // '2'
~0X4F, // '3'
~0X66, // '4'
~0X6D, // '5'
~0X7D, // '6'
~0X07, // '7'
~0X7F, // '8'
~0X6F // '9'
};
// 秒计数器
UINT8 Sec_Count;
UINT8 i;
/*
* 函数名: void Timer0_Init( void )
* 描 述:T/C0初始化函数
* 输 入:无
* 输 出:无
*/
void Timer0_Init( void )
{
TH0 = (65536-50000)/256; // 计数寄存器高8位
TL0 = (65536-50000)%256; // 计数寄存器低8位
TMOD = TMOD&0xf0; // 清除T/C0有关的位,其它的位不变
TMOD = TMOD|0X01; // 设置T/C0的位,16位定时器方式工作
ET0 = 1; // 允许T/C0中断
EA = 1; // 开总中断
TR0 = 1; // 启动定时器T/C0
}
/*
* 函数名:void main()
* 描 述:主函数
* 输 入:无
* 输 出:无
*/
void main()
{
Sec_Count = 0; // 清零
i = 0;
Timer0_Init(); // T/C0初始化
while(1);
}
/*
* 函数名:void Timer0IRQ(void)
* 描 述:定时器T/C0中断函数
* 输 入:无
* 输 出:无
*/
void Timer0IRQ(void) interrupt 1
{
// 重新载入定时器的初值
TH0 = (65536-50000)/256; // 计数寄存器高8位
TL0 = (65536-50000)%256; // 计数寄存器低8位
i++;
if( 20==i )
{
i = 0;
Sec_Count++;
if( 10==Sec_Count )
{
Sec_Count = 0;
}
}
P2 = Seg_Code[Sec_Count];
}
/********************end of file ***********************/
代码分析:
我们还是按照以往的思路,从main函数开始看,先对全局变量进行初始化,接下来执行T/C0的初始化,我们来分析一下
这段代码,首先对T/C0的高八位与第八位寄存器赋初值,这个有什么用呢?我们在这个实验当中是实现每秒的计数,在后面的代码当中我们只进行了20次的累加达到一秒的效果,说明我们的定时器中断时50ms一次,我们再看下
一个50000,方便计算用的,也就是我们需要的50ms,还有就是东西在中断中也出现了,在这里我给大家解释下,我们的定时器是进行加1的计数,在这个例程当中我们用的是16位定时器模式,也就是一个16位的二进制,它相当于一个容器,装满了之后就产生中断,为了方便下次再进行装载,我们必须将里面的东西进行重置,以实现我们需要的时间中断;再看这两句
TMOD = TMOD&0xf0这句实现的也就是一个清楚T/C0相应位的工作,后面这句TMOD = TMOD|0x01是对T/C0的模式进行设置,为什么是这样呢,我们来看下图:
这就是TMOD的全部高4位是T/C1的,低4位是T/C0的,在我们的设置当中相当于T/C0的GATE位,C/T位为零,然后对应到数据手册上可以找到
我就不多解释;后面两位是模式位,我也给大家附张数据手册上的截图,大家一看就懂
接着看
这几句是干什么用的,我们再来分析一下这个控制寄存器
两组一模一样的,TR0是用来启动定时器T/C0的,其它的我不多解释,你们对照着数据手册去看去实验,然后还有个ET0,EA,下图是中断控制寄存器
EA是开总中断用的,ET0就是允许T/C0中断。然后大家还有一个疑惑就是50000怎么就等于50ms了,我给大家附上下图就明白了
实验二:用定时器实现4个LED不同频率的闪烁
硬件原理图:
代码:
/************ (C) COPYRIGHT 2016 wllis **************
*
* 文件名: LED_Flash.C
* 描 述:通过定时器0实现4个LED的频率的闪烁
* 作 者: wllis
* 日 期: 2016/02/3
*
* LED0->P0.0
* LED1->P0.1
* LED2->P0.2
* LED3->P0.3
*
****************************************************/
#include
/*************** TYPEDEFINES ***********************/
typedef unsigned char UINT8;
typedef unsigned int UINT16;
/************** LED DEFINES ***********************/
sbit LED0 = P0^0;
sbit LED1 = P0^1;
sbit LED2 = P0^2;
sbit LED3 = P0^3;
// 全局变量定义
UINT8 LED0_Delay;
UINT8 LED1_Delay;
UINT8 LED2_Delay;
UINT8 LED3_Delay;
/*
* 函数名: void Timer0_Init( void )
* 描 述:T/C0初始化函数
* 输 入:无
* 输 出:无
*/
void Timer0_Init( void )
{
TH0 = (65536-50000)/256; // 计数寄存器高8位
TL0 = (65536-50000)%256; // 计数寄存器低8位
TMOD = TMOD&0xf0; // 清除T/C0有关的位,其它的位不变
TMOD = TMOD|0X01; // 设置T/C0的位,16位定时器方式工作
ET0 = 1; // 允许T/C0中断
EA = 1; // 开总中断
TR0 = 1; // 启动定时器T/C0
}
/*
* 函数名:void main()
* 描 述:主函数
* 输 入:无
* 输 出:无
*/
void main()
{
LED0 = 1;
LED1 = 1;
LED2 = 1;
LED3 = 1;
LED0_Delay = 0;
LED1_Delay = 0;
LED2_Delay = 0;
LED3_Delay = 0;
Timer0_Init();
while(1);
}
/*
* 函数名:void Timer0IRQ(void)
* 描 述:定时器T/C0中断函数
* 输 入:无
* 输 出:无
*/
void Timer0IRQ(void) interrupt 1
{
// 重新载入定时器的初值
TH0 = (65536-50000)/256; // 计数寄存器高8位
TL0 = (65536-50000)%256; // 计数寄存器低8位
LED0_Delay++;
LED1_Delay++;
LED2_Delay++;
LED3_Delay++;
// LED0 100ms
if( 2==LED0_Delay )
{
LED0_Delay=0;
LED0 = ~LED0;
}
// LED1 500ms
if( 10==LED1_Delay )
{
LED1_Delay=0;
LED1 = ~LED1;
}
// LED2 1000ms
if( 20==LED2_Delay )
{
LED2_Delay=0;
LED2 = ~LED2;
}
// LED3 1500ms
if( 30==LED3_Delay )
{
LED3_Delay=0;
LED3 = ~LED3;
}
}
/*************** end of file *************************/
上面两个例程中我们都没看到任何的延时函数,但是实现了我们想要的同样的效果,此时我们又在想,这么做有什么好处,比如说在性能上有没有什么提升,或者功耗有没有降低。。。等等。我们上面实现的功能都比较单一,要是多亮几个LED灯,还有其它的还需要处理那处理起来就感觉很复杂,我们有没有通用一点的方法呢?答案肯定是有的,还是用上面的四个LED灯,我们用一种更加巧妙的方法来解决延时的问题,在解决这个问题的过程中,我们将看到我们一直感觉很深奥的系统原形....
用系统模型实现四个LED灯不同频率的闪烁
在讲解这个模型之前我们先来了解一些系统的基本知识
我们在写51系列单片机程序时肯定有很多人跟我一样,只是把基础模块一个一个的实现,而不会很深入的去写,或许你能将它们组合,一般都是一个大循环while, 加一些延时Delay,但是你想过没,在你用while和Delay时你很犯了一个很大的错误,因为我们让MCU在处理Delay时白白的浪费了很多时间,因为MCU在等待Delay,也许你会说只是延时几s,或者几ms,但是你要知道我们的MCU是以us为单位工作的,在MCU等待的这段时间它可以处理很多事情了,所以如果在处理这些事情时加入一个实时操作系统,让RTOS帮我们合理分配MCU的时间。如果我们把这些事情当做RTOS的几个任务,并合理设置分配
RTOS的节拍和设置这些任务的执行频度,这就可以大大的提升了系统的速度和性能,这是以后写实用程序的最好开始。好了废话少说,进入主题~
在进入主题之前先说说我理解的几个概念:
系统任务:所谓任务,就是需要CPU 周期“关照”的事件,就是我们需要MCU做的事情。
实时操作系统:就是当外部产生事件时能及时快速处理,且根据处理结果在规定的时间之内作出相应的控制和响应,并控制所有实时任务协调一致运行的操作系统。
系统原理:单片机定时器延时中断来产生系统任务调度节拍,设置各个任务的执行频度,
来调度各任务。以实现系统多线 实时操作。
代码:
/************ (C) COPYRIGHT 2016 wllis **************
*
* 文件名: RTOS_LED_Flash.c
* 描 述:通过简单的系统实现4个LED的不同频率的闪烁
* 作 者: wllis
* 日 期: 2016/02/04
*
* LED0->P0.0
* LED1->P0.1
* LED2->P0.2
* LED3->P0.3
*
****************************************************/
#include
/*************** TYPEDEFINES ***********************/
typedef unsigned char UINT8;
typedef unsigned int UINT16;
// 系统全局变量
#define OS_CLOCK 12000000 // 系统晶振频率,单位Hz
#define TASK_CLOCK 200 // 任务中断节拍,单位Hz
#define TASK_MAX 4 // 任务数目
// 设置系统任务执行频度
#define TASK_DELAY0 TASK_CLOCK/1 // 每秒1次
#define TASK_DELAY1 TASK_CLOCK/2 // 每秒2次
#define TASK_DELAY2 TASK_CLOCK/4 // 每秒4次
#define TASK_DELAY3 TASK_CLOCK/8 // 每秒8次
/************** LED DEFINES ***********************/
sbit LED0 = P0^0;
sbit LED1 = P0^1;
sbit LED2 = P0^2;
sbit LED3 = P0^3;
/****************** Function defines **************/
void OS_Timer0_Init( void );
void OS_Task_Run( void (*ptask)() );
void OS_Run( void );
void task0( void );
void task1( void );
void task2( void );
void task3( void );
// 任务指针
void ( *const task[] )() = { task0,task1,task2,task3 };
// 系统任务执行频度参数
UINT8 Task_Delay[TASK_MAX];
/*
* 函数名:void main()
* 描 述:主函数
* 输 入:无
* 输 出:无
*/
void main()
{
// 端口初始化
LED0 = 1;
LED1 = 1;
LED2 = 1;
LED3 = 1;
// 开总中断
EA = 1;
OS_Timer0_Init(); // 系统定时器初始化
while(1)
{
OS_Run();
}
}
/*
* 函数名: void OS_Timer0_Init( void )
* 描 述:T/C0初始化函数
* 输 入:无
* 输 出:无
*/
void OS_Timer0_Init( void )
{
UINT8 i;
for( i=0;i
我们从这个例程中可以看到,如果在执行LED灯闪烁的任务当中是其他的东西,比如说按键处理、数码管显示OR 显示屏刷新、串口通信等等。。。那样我们可以在不耽误时间的同时还可以多做很多的事情,我在这里仅仅是起到抛砖引玉的作用,具体更深一层的运用需要大家自己学习。我会在本教程的后面给大家介绍几本个人觉得写的不错的书籍,并附上PDF给大家。
同时,在上面的例程当中我们可以看到代码量也随之增多了很多,如果项目在大一点的话,是不是感觉维护起来非常的困难,我会在后面跟大家一起分享模块化的编程思想,这里不做过多的介绍。
十、串口发送
在学习单片机的过程中,串口我们必须掌握,为什么一定要掌握?我给大家稍微说一下,首先每一款单片机里面都带有串口,二其他的接口不一定有;其二串口在工业控制里面是用的最多的通信接口,像PLC,HMI(人机界面,也就是PLC的显示屏),另外还有厂家专门做有非常实用的,能快速的进行二次开发的串口屏幕;另外一个非常实用的功能就是我们在调试程序的过程中,经常需要用到串口打出一些调试信息,比如说我现在有个采集电压的数据不对,但是我没有额外的显示模块,那么这时我就可以简单的配置下串口就可以将我需要的信息打印出来;还有串口结合稳定的通信协议,那将是你的得力助手。
在工业控制里面我们常用上图左所示的接口,相应的实验室电路上图右所示,也就是我们常说的RS232接口。
代码:
/************ (C) COPYRIGHT 2016 wllis **************
*
* 文件名: USART_TX.C
* 描 述:通过串口每隔一秒发送字母“A”
* 作 者: wllis
* 日 期: 2016/02/05
*
****************************************************/
#include
/******************** DATA TYPES *******************/
typedef unsigned char UINT8;
typedef unsigned int UINT16;
/*
* 函数名:void DelayMS( UINT16 n )
* 描 述:简单的延时
* 输 入:UINT16
* 输 出:无
*/
void DelayMS( UINT16 n )
{
UINT8 a;
while(--n)
{
for( a=114; a>0; a--);
}
}
/*
* 函数名:void USART_Init( void )
* 描 述:串口初始化函数
* 输 入:无
* 输 出:无
*/
void USART_Init( void )
{
SCON = 0x40; //serial mode 1, 8-bits
UART TMOD = 0x20; //timer 1, mode 2, 8-bits reload
PCON = 0x80; //SMOD = 1; double bps
TH1 = 0xfa; //Baund: 9600bps fosc=11.0592MHz
TL1 = 0xfa;
TR1 = 1; //timer1 run
}
/*
* 函数名:void main()
* 描 述:主函数
* 输 入:无
* 输 出:无
*/
void main()
{
// 串口初始化
USART_Init();
while(1)
{
SBUF = 'A'; // 发送字母‘A’
while(!TI); // 等待发送完成
DelayMS(1000);
}
}
/********************* end of file *********************/
代码分析:
USART全称:Universal Asynchronous Receiver -Transmitter,异步串行接口,
跟所有外设一样,串口也有相应的控制寄存器。
首先我们来看SCON,在程序中我们设置的位0x40,也就是0100 0000,对照下图
TI是发送中断标志位,当我们发送字符时,先将字符放到缓冲区SBUF,然后检测TI标志位是否为0以判断是否发送完毕
RI是接收中断标志位,当我们判断是否有字符发送过来时,通过检测RI位,然后读取缓冲区的数据。
从上图可以得到串口有四种工作模式,在我们的程序当中对应模式1:
该模式下,USART作为异步通信口,每一帧发送或接收10位数据,这10个位分别是1一个起始位0,8个数据位和一个停止为1;单片机的TXD为发送管脚,RXD为接收管脚,该模式下通信的波特率是可变的,一般由Timer1工作在模式2下,通过载入TH1和TL1的计数初值来设置波特率。Timer工作在模式2下,是一个8位自动重新装载的定时器,需要向TH1、TL1同时装载相同的计数器初始值。单片机会根据Timer1的设置情况是UART工作在特定的波特率下。模式1下波特率TH1(TL1)中载入计数初始值之间的关系如下图所示:
第二篇
编程思想
在思想篇之前,先说几句废话,其实我们随遍在学习那个学科之前,我们经常听老师说基础很重要,“基础不牢,地动山摇”;我想说的是这些话都没错,但是对于我们刚开始接触这些东西的人来说,是感受不到的。如果我们在学这些东西的过程中,遇到过一些问题,解决过一些问题,再回过头来看这些基础性的东西,我们就能体会老师之前的良苦用心了。就像我们刚开始学习C语言,老是想着学一些很炫,很酷的东西,想用C语言来做个游戏,做个能真真实实玩的图形界面小游戏,这些想法非常好,个人推荐在有一点C语言基础之后去学习WIN32编程,它能实现你的一些想法。在这个过程中我们就会慢慢体会到,语言本身只是个工具,我们而是借助于这个工具加上我们的想法来实现我们想要的东西,工具我们只要学会如何使用就行了,比如说一把斧头我们可以拿它来劈柴,木工拿它来做家具,艺术家拿它来做艺术品,在这过程中,斧头始终是斧头,只是不同思想的人使用它而已;在回过头来看下C语言,我们可以用它在我们的桌面操作系统上编写程序,也可以编写单片机程序,无论是计算机程序还是单片机程序,这里面主要是我们的思想在里面。我们再来看下硬件设计,这部分个人没有太多经验,只是分享个人的一些看法,
单片机的基础学习这方面的资料非常多,大部分人非常熟悉的《郭天祥10天学会单片机》,我了解的杜洋老师的《爱上单片机》,还有很多的论坛都有这方面的基础教程,淘宝网上随遍花个几块钱就可以买一大堆的学习资料等等。
很多人学单片机学了基础篇后,在很长的一段时间里一直停留在初级阶段,个人也有这么一段经历,有一段时间感觉没什么提升,还有一个就是编写的程序非常混乱,可重复性利用非常低。其实无论学习电子设计还是单片机的程序,一开始我们就可以积累一些我们以后会用到的东西。比如说我们用51单片机模拟了一个IIC程序,经过自己的实验验证非常稳定实用,但是当我们再次用到IIC时,大部分人是又得重新阅读好几遍代码,并且还不一定懂,有的甚至重新开始复制,粘贴,调试,大家有没有感觉浪费很多时间,这就是一个重复利用的问题。硬件设计也是一样,成熟稳定的功能电路我们就可以自己保留存档,必要时可以加上注释,以便于让自己再次用到时能短时间看懂。
在这里面我会主要以模块化、可重复性利用、程序规范为主,其实这方面的书籍也非常多,我在这里仅仅给大家起到抛砖引玉的作用,更深层次的学习需要大家自主的去看这些书,将书里面的实验认真去做,不断的去体会,总结。
第一章模块化
我们前面学习的都是一些实现一些简单功能的程序,代码量不大,百把多行,我们基本上能兼顾,能很好的维护好。后面我在用一个精简的系统实现四个LED灯不同频率的闪烁时,代码量就大了差不多有200多行,维护起来就有一点点困难了。如果我们做一个系统的工程,代码量肯定不止这么点,如果还用一个xxx.c的文件来写的话,估计以后维护起来非常困难,这个时候如何组织一个工程的代码就显得尤为重要了。计算机的发展史就是这样,我们能遇到的问题,那些科学家们也曾经经历过。所以模块化思想早就有,并且我们也有接触到,只是我们没发现而已,感觉不到而已。学过C语言的人都知道我们如果要处理数字性的东西时会包含math.h文件,字符串则包含string.h,但是很少有人去深层次的去了解这些东西。
我们首先来了解一下.H与.C文件的一些基本构造,在.H文件最开始的#ifndef _xx_xx #define _xx_xx,#endif是用来防止重复包含,里面的内容主要有#define的宏定义,结构体struct{},函数的定义void xxx_xxx( void ;而.C文件中主要包含在改文件中用到的变量,数组定义,其它模块要用到的全局变量尽量少定义,如果有定义,在.H文件中要进行extern声明。
我们先来改写一下之前实现的LED灯亮灭的程序,将原来的一个.C文件拆分成四个小文件,分别是main.h、main.c、LED.c、LED.h,考虑到在LED.C中没有函数,临时加了一个改变LED灯状态的函数,我们来看下内部是如何实现的:
main.c源码
/************ (C) COPYRIGHT 2016 wllis **************
*
* 文件名: main.c
* 描 述:main.c模块文件
* 作 者: wllis
* 日 期: 2016/02/11
*
****************************************************/
#include "LED.H"
/*
* 函数名:void main()
* 描 述:主函数
* 输 入:无
* 输 出:无
*/
void main()
{
LED = 1; // 初始化LED灯
while(1)
{
LED_State( ); // 改变LED灯的状态
DelayMS(500); // 延时500毫秒
}
}
/*
* 函数名:void DelayMS( INT16U n )
* 描 述:延时
* 输 入:INT16U
* 输 出:无
*/
void DelayMS( INT16U n )
{
INT8U a;
while(--n)
{
for( a=114; a>0; a--);
}
}
/********************end of file ***********************/
main.h源码
/************ (C) COPYRIGHT 2016 wllis **************
*
* 文件名: mian.h
* 描 述:点亮一颗LED
* 作 者: wllis
* 日 期: 2016/02/11
*
****************************************************/
#ifndef _MAIN_H
#define _MAIN_H
#include
/***************** TYPEDEF DATA *******************/
typedef unsigned char INT8U;
typedef unsigned int INT16U;
/******************* 函数原型 *********************/
void DelayMS( INT16U n );
#endif
/****************** end of file ********************/
LED.C源码
/************ (C) COPYRIGHT 2016 wllis **************
*
* 文件名: LED.C
* 描 述:LED.C模块文件
* 作 者: wllis
* 日 期: 2016/02/11
*
****************************************************/
#include "LED.H"
/*
* 函数名:void LED_State( void )
* 描 述:LED状态改变函数
* 输 入:无
* 输 出:无
*/
void LED_State( void )
{
if(LED)
LED = 0;
else
LED = 1;
}
/****************** eSnd of file *******************/
LED.H源码
/************ (C) COPYRIGHT 2016 wllis **************
*
* 文件名: LED.H
* 描 述:点亮一颗LED
* 作 者: wllis
* 日 期: 2016/02/11
*
****************************************************/
#ifndef _LED_H
#define _LED_H
#include "main.h"
/****************** LED定义 ************************/
sbit LED = P0^0;
void LED_State( void );
#endif
/**************** end of file *********************/
实现这么一个简单的LED灯亮灭这么做是有点繁琐,但是当你的工程越来越大时,这种模式就非常便于维护,如果彼此模块之间只存在被调用功能,全局变量比较少的或者没有的话,那么移植到别的单片机系统中是非常方便的;要想用好这种模式,我们要学会拆分,这种拆分思想,大家只有在实践中才能慢慢体会到,到了论坛上多逛逛,多看看别人的工程代码,自己动手去搭建一个简单的工程;下面给大家看一下专门写这个东西的一本书上的资料:
这是书本附带的源代码,这里面主要包括数模转换模块(AIO),串口通信(COMM),开关量输入输出(DIO),液晶显示模块(LCD),LED灯等等,
里面的文件是这样的一个XXX.C与配套的XXX.H的文件
我么再来进一步打开里面的文件看下:
首先来看下AIO.H、AIO.C源文件
再来看下DIO.C、DIO.H源文件
这些源文件主要是针对uCOSII在PC机上模拟移植的,源代码我会一起分享给大家,现在看这些代码可能有点困难,但是大家只要持之以恒的学习,到时候一起交流移植这些模块代码或者编写类似的代码不是什么问题。本人在空暇的时候在STM32上移植了AIO、KEY相关的两个模块,结合《嵌入式系统构件这本书》,你会发现这些模块代码写的非常优秀,里面有我们很多值得学习的编程思想,在KEY代码处理中主要用到的思想是状态机,这个思想我会单独写一个程序分析。给大家看代码的主要目的就是让大家感受一下这种规范、模块化代码的美。
我们再以之前用到的一个小型系统实现一个完整的数码时钟为例,来进一步了解这种模块化思想:
代码:
来看下代码组织结构
main.c
/************ (C) COPYRIGHT 2016 JZHG1992 **************
*
* 文件名: Main.C
* 描 述:按键处理模块
* 作 者: JZHG1992
* 整理日期: 2016/02/08 by wllis
*
*****************************************************/
#include "OS.h"
/*
* 函数名:void main( void )
* 描 述:主函数
* 输 入:无
* 输 出:无
*/
void main( void )
{
OS_Init( ); // 系统初始化
while(1)
{
OS_Run( ); // RTOS系统运行函数
}
}
/***************** end of file *********************/
os.c
/************ (C) COPYRIGHT 2016 JZHG1992 **************
*
* 文件名: OS.C
* 描 述:按键处理模块
* 作 者: JZHG1992
* 整理日期: 2016/02/08 by wllis
*
*****************************************************/
#include "OS.H"
/************************************
* 系统任务执行频度参数
*************************************/
INT8U Task_Delay[TASK_MAX];
/*
* 函数名:void OS_Timer0_Init( void )
* 描 述:T/C0初始化函数
* 输 入:无
* 输 出:无
*/
void OS_Timer0_Init( void )
{
INT8U i;
for ( i=0; i
/************************************
* 配置系统参数
*这里你可以根据你的需要修改
*************************************/
#define OS_CLOCK 12000000 // 系统晶振频率,单位Hz
#define TASK_CLOCK 200 // 任务中断节拍,单位Hz
#define TASK_MAX 3 // 任务数目
/************************************
* 定义变量类型
*************************************/
typedef unsigned char INT8U; // 宏定义INT8U
typedef unsigned int INT16U; // 宏定义INT16U
/************************************
* 系统任务外调函数与参数
*************************************/
extern INT8U Task_Delay[TASK_MAX]; // 系统任务执行频度参数
extern void OS_Timer0_Init( void ); // 系统定时器时钟初始化
extern void OS_Task_Run( void(*ptask)()); // 系统任务调度函数
extern void ( *const task[] )(); // 获得任务指针
extern void OS_Init( void ); // RTOS系统初始化
extern void OS_Run( void ); // RTOS系统运行函数
#endif
/**************** end of file ***********************/
OS_TASK.C
/************ (C) COPYRIGHT 2016 JZHG1992 **************
*
* 文件名: OS_TASK.C
* 描 述:按键处理模块
* 作 者: JZHG1992
* 整理日期: 2016/02/08 by wllis
*
*****************************************************/
#include "OS.H"
#include "SMG.h"
#include "key.h"
/************************************
* 设置系统任务执行频度
*************************************/
#define TASK_DELAY0 TASK_CLOCK/200 // 任务1的执行频度
#define TASK_DELAY1 TASK_CLOCK/100 // 任务2的执行频度
#define TASK_DELAY2 TASK_CLOCK/200 // 任务3的执行频度
/*
* 函数名:void task1( void )
* 描 述:数码管显示
* 输 入:无
* 输 出:无
*/
void task0( void )
{
Task_Delay[0] = TASK_DELAY0; // 设置任务执行度
/* 你的任务 */
SMG_Display( SMG_Dcode_Buff ); // 数码管显示函数
}
/*
* 函数名:void task2( void )
* 描 述:数码管时间函数
* 输 入:无
* 输 出:无
*/
void task1( void )
{
Task_Delay[1] = TASK_DELAY1; // 设置任务执行度
SMGW_EN =0;
SMGD_EN =0;
/* 你的任务 */
SMG_Run_Time(); // 数码管时间函数
}
/*
* 函数名:void task3( void )
* 描 述:按键调时
* 输 入:无
* 输 出:无
*/
void task2( void )
{
Task_Delay[2] = TASK_DELAY2; // 设置任务执行度
/* 你的任务 */
KEY_AdjustTime( ); // 按键调时
}
/************************************
* 获得任务指针
* 添加你的任务指针
*************************************/
void ( *const task[] )() = { task0, task1, task2 };
/*
* 函数名:void OS_Init( void )
* 描 述: RTOS初始化函数
* 输 入:无
* 输 出:无
*/
void OS_Init( void )
{
OS_Timer0_Init(); // 系统定时器时钟初始化
KEY_Init(); // 独立按键端口初始化
Clock_Init();
}
/*
* 函数名:void OS_Run( void )
* 描 述:RTOS运行函数
* 输 入:无
* 输 出:无
*/
void OS_Run( void )
{
INT8U i;
for ( i=0; iKEY_LONG_PERIOD)
{
key_count =0;
temp &= KEY_NULL;
temp |= KEY_LONG;
state = KEY_STATE_CONTINUE;
}
}
else
state = KEY_STATE_RELEASE;
}
break;
case KEY_STATE_CONTINUE: // 连击状态
{
if ( temp != KEY_NULL )
{
if ( ++key_count >KEY_CONTINUE_PERIOD)
{
key_count =0;
temp &= KEY_NULL;
temp |= KEY_CONTINUE;
}
}
else
state = KEY_STATE_RELEASE;
}
break;
case KEY_STATE_RELEASE: // 释放状态
{
key_r |= KEY_UP;
temp = key_r;
state = KEY_STATE_INIT;
}
break;
default:
break;
}
return *pBuff = temp;
}
/*
* 函数名:void KEY_AdjustTime( void )
* 描 述:按键调时
* 输 入:无
* 输 出:无
*/
INT8U time_flag=0;
void KEY_AdjustTime( void )
{
INT8U key=KEY_NULL;
static INT8U Num=0;
KEY_Way( &key );
switch ( key )
{
case (KEY_VALUE0|KEY_DOWN): // 如果是按下设定键
{
if ( ++Num >3 )
Num=0;
}
break;
case (KEY_VALUE1|KEY_DOWN): // 如果是按下加值键
{
if ( Num==1 )
{
CurrentTime.Hour++;
if( 24==CurrentTime.Hour ) CurrentTime.Hour = 0;
}
if ( Num==2 )
{
CurrentTime.Minute++;
if( 60==CurrentTime.Minute )CurrentTime.Minute = 0;
}
if ( Num==3 )
{
CurrentTime.Second++;
if( 60==CurrentTime.Second )CurrentTime.Second = 0;
}
}
break;
case ( KEY_VALUE2|KEY_DOWN): // 如果是按下减值键
{
if ( Num==1 )
{
CurrentTime.Hour--;
if( 0==CurrentTime.Hour )CurrentTime.Hour = 23;
}
if ( Num==2 )
{
CurrentTime.Minute--;
if( 0==CurrentTime.Minute )CurrentTime.Minute = 59;
}
if ( Num==3 )
{
CurrentTime.Second--;
if( 0==CurrentTime.Second )CurrentTime.Second = 59;
}
}
break;
case ( KEY_VALUE3|KEY_DOWN): // 如果是按下退出键
{
Num=0;
}
break;
case ( KEY_VALUE3|KEY_LONG): // 如果是长按退出键
{
time_flag=!time_flag; // 标志取反
}
break;
default:
break;
}
}
/******************* end of file *******************************/
KEY.H
/************ (C) COPYRIGHT 2016 JZHG1992 **************
*
* 文件名: KEY.H
* 描 述:按键处理模块
* 作 者: JZHG1992
* 整理日期: 2016/02/08 by wllis
*
*****************************************************/
#ifndef _KEY_H_
#define _KEY_H_
#include"OS.h"
/*********************************
* 独立按键端口定义
**********************************/
sbit key0 = P3^0;
sbit key1 = P3^1;
sbit key2 = P3^2;
sbit key3 = P3^3;
/*********************************
* 独立按键键值
**********************************/
#define KEY_VALUE0 0x0e
#define KEY_VALUE1 0x0d
#define KEY_VALUE2 0x0b
#define KEY_VALUE3 0x07
#define KEY_NULL 0x0f
/*********************************
* 独立按键方式
* 短按、长按、连击、释放
**********************************/
#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_DEBOUNCE 1
#define KEY_STATE_PRESS 2
#define KEY_STATE_LONG 3
#define KEY_STATE_CONTINUE 4
#define KEY_STATE_RELEASE 5
/*********************************
* 独立按键API函数
**********************************/
extern INT8U time_flag;
void KEY_Init( void ); // 独立按键端口初始化
INT8U KEY_Way( INT8U *pBuff );// 独立按键方式扫描
void KEY_AdjustTime( void ); // 按键调时
#endif
/***************** end of file **********************/
SMG.C
/************ (C) COPYRIGHT 2016 JZHG1992 **************
*
* 文件名: SMG.C
* 描 述:按键处理模块
* 作 者: JZHG1992
* 整理日期: 2016/02/08 by wllis
*
*****************************************************/
#include"SMG.H"
#include"key.h"
/************************************
* 数码管段码值和缓冲区
*************************************/
code INT8U SMG_Dcode[]= // 数码管段码
{ 0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,
0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8E,
0xbf, //'-'号代码
};
INT8U SMG_Dcode_Buff[8]={1,2,'-',4,0,'-',3,0}; // 显示缓冲区
TIME_TYPE CurrentTime; // 定义一个时间结构体
/*
* 函数名:static void Clock_Init( void )
* 描 述:时间初始化函数
* 输 入:无
* 输 出:无
*/
void Clock_Init( void )
{
CurrentTime.Hour = SMG_Dcode_Buff[0]*10+SMG_Dcode_Buff[1];
CurrentTime.Minute = SMG_Dcode_Buff[3]*10+SMG_Dcode_Buff[4];
CurrentTime.Second = SMG_Dcode_Buff[6]*10+SMG_Dcode_Buff[7];
}
/*
* 函数名:static void SMG_Dcode_Send( INT8U dat )
* 描 述:数码管段码发送函数
* 输 入:INT8U dat
* 输 出:无
*/
static void SMG_Dcode_Send( INT8U dat )
{
SMG_PORT = dat;
SMGD_EN = 1; // 使能位锁存段码
SMGD_EN = 0;
}
/*
* 函数名:static void SMG_Wcode_Send( INT8U dat )
* 描 述:数码管位码发送函数
* 输 入:INT8U dat
* 输 出:无
*/
static void SMG_Wcode_Send( INT8U dat )
{
INT8U temp;
temp = (0x01<7 ) Num = 0;
}
/*
* 函数名:void SMG_Run_Time( void )
* 描 述:数码管时间函数
* 输 入:无
* 输 出:无
*/
INT8U count = 0;
void SMG_Run_Time( void )
{
SMG_Dcode_Buff[0] = CurrentTime.Hour/10;
SMG_Dcode_Buff[1] = CurrentTime.Hour%10;
SMG_Dcode_Buff[3] = CurrentTime.Minute/10;
SMG_Dcode_Buff[4] = CurrentTime.Minute%10;
SMG_Dcode_Buff[6] = CurrentTime.Second/10;
SMG_Dcode_Buff[7] = CurrentTime.Second%10;
count++;
if( 100==count )
{
count = 0;
CurrentTime.Second++;
if( CurrentTime.Second>=60 )
{
CurrentTime.Second = 0;
CurrentTime.Minute++;
if( CurrentTime.Minute>=60 )
{
CurrentTime.Minute = 0;
CurrentTime.Hour++;
if( CurrentTime.Hour>=24 )
{
CurrentTime.Hour = 0;
}
}
}
}
}
/******************** end of file *****************************/
SMG.H
/************ (C) COPYRIGHT 2016 JZHG1992 **************
*
* 文件名: SMG.H
* 描 述:按键处理模块
* 作 者: JZHG1992
* 整理日期: 2016/02/08 by wllis
*
*****************************************************/
#ifndef _SMG_H_
#define _SMG_H_
#include"OS.H"
/************************************
* 数码管端口参数
*************************************/
#define SMG_PORT P1 // 数码管端口
sbit SMGD_EN = P0^1; // 数码管段码使能端
sbit SMGW_EN = P0^2; // 数码管位码使能端
/************************************
* 保存时间数据的结构体
*************************************/
typedef struct
{
INT8U Second;
INT8U Minute;
INT8U Hour ;
}TIME_TYPE;
extern TIME_TYPE CurrentTime; // 定义一个时间结构体 /************************************
* 数码管API函数申明
*************************************/
extern INT8U SMG_Dcode_Buff[8]; // 显示缓冲区
void Clock_Init( void ); // 时间初始化函数
void SMG_Display( INT8U *pBuff ); // 数码管显示函数
void SMG_Run_Time( void ); // 数码管时间函数
#endif
/******************** end of file ********************/
这个工程主要是在正点原子论坛上找到的,实现的是一个电子钟的功能,可以用按键进行调时间,按键用到的是状态机思想,硬件方面还用到两片锁存器74HC573,关于这个芯片本来想结合数据手册跟大家分享一下怎么用,但是苦于家里没有网络,希望大家去完善一下。
第二章程序规范
为什么要说规范,俗话说“没有规矩,不成方圆”,在任何行业都是一样的,如果我们都按照自己的思想去做事,那么行业沟通起来就非常困难。就像电子行业用的是电子行业符号,建筑行业用的是建筑行业的符号,互联网通信遵循TCP/IP协议一样,我们都按照大家约定俗成的规范做事,才使得我们能进行有效的沟通,可见规范的重要性。
华为的编程规范原文从排版、注释、标识符命名、可读性、变量、结构、函数、过程、可测性、程序效率、质量保证、代码编辑、编译、审查、代码测试、维护、宏这些方面阐述代码规范,原文有50多页,我这里主要介绍一些我们经常用到的东西
注释:
文件注释:在.C文件以及.h文件开头
/************ (C) COPYRIGHT 2016 wllis **************
*
* 文件名: USART_TX.C
* 描 述:通过串口每隔一秒发送字母“A”
* 作 者: wllis
* 日 期: 2016/02/05
*
****************************************************/
主要是用来描述这个文件是干什么用的,编写的日期,版本,还有编写者的大名,有的还会有修改记录等等,根据自己的需要可以去添加一些东西。
函数注释:
/*
* 函数名:void DelayMS( UINT16 n )
* 描 述:简单的延时
* 输 入:UINT16
* 输 出:无
*/
在有些参数非常多的函数当中,还专门有对每个参数的说明,具体举例应用等等。
它主要用来描述这个函数用来干什么,以及输入的参数,返回值,基本功能描述等,我学过一段时间的Win32,里面的函数这方面就做的非常好,有个时候这方面的注释比函数本身还长。微软有个专门的MSDN专门做这方面的工作,你能在里面查到每个函数的用法,有些还有具体的实例,是不是非常方便。可能你们当中有一些学过STM32的,它里面的库函数就是意法半导体(STM)公司专门开发的,以方便开发人员快速的使用,另外我们所使用的STC系列单片机也就是宏晶公司的51单片机,它的官网也在编写类似STM32库函数的东西,大家可以去它的官网看一看。
必要的地方给予注释,有些难以看出的变量,语句;注释的地方尽量保持对齐,注释风格尽量保持一致,比如我这里全部用 “// “ 模式,如果用”/**/”这种模式就尽量用这种模式:
TMOD = TMOD&0XF0; // 将与定时器0有关的部分清零
TMOD = TMOD|0X01; // 定时器0工作在16位定时器模式
P0 = ~(0X01<
对于简单的东西可以多使用typedef、#define定义,宏定义使用大写:
示例:
typedef unsigned char INT8U;
typedef unsigned int INT16U;
这种定义在uCosII里面用的非常好,也为大部分开发人员所使用
#define MAX_TASK 4
其实#define 还有很多用途:
示例:
#define ON 0
#define OFF 1
#define TRUE 1
#define FALSE 0
这样做会使得我们在阅读程序的过程中非常方便
函数、变量命名:
变量的命名我们一般遵循易于识别的原则比如INT8U,我们大概得知它是一个8位无符号的这么一个东西,Count是计数的意思,这方面可以参考微软的匈牙利命名法,微软在这方面是做的非常优秀的。
函数:函数命名应准确描述函数的功能,延时函数DelayMs()我们一看就知道是毫秒机延时函数。
示例:
void print_record( unsigned int rec_ind );
int input_record( void );
unsigned char get_current_color( void );
缩进、对齐:
一般我们在每个大括号里面的东西缩进四个英文字符的间隔,大括号一般按照如下方式对其
void main()
{
xxxxwhile(1)
{
xxxx// your code here
}
}
说明:里面的x代表空格
其实在华为编程规范里面要求缩进只能用空格,这是为什么呢?因为我们用不同的代码编辑器打开时它里面的tab对应的都是不同的
无论语句长短,单独成行:
示例:
不规范的写法
Flag1 = 0; Flag2 = 0;
规范的写法
Flag1 = 0;
Flag2 = 0;
善于运用括号,特别是我们不太清楚运算符的优先级时:
示例:
word = (high << 8) | low
if((a | b) && (a & c))
if((a | b) < (c & d))
说明:主要是防止在阅读程序时产生误解,另外一个我们可以不必话太多时间去了解每一个运算符的优先级
if、for、do、while、case、switch、default等语句鸽子独占一行,且if、for、do、while等语句的执行语句无论多少都要加括号{}。
不规范
if(xxx) return;
规范
if(xxx)
{
return;
}
程序的分界符(C语言的大括号‘{’和‘}’)应各自独占一行且位于同一列,同时与引用他们的语句对齐。在函数体的开始、类的定义、结构的定义、枚举的定义以及if、for、do、while、swithc、case语句中的程序都要采用如上的缩进方式。
不规范:
for(...){
// code...
}
if(...)
{
// code...
}
void example_fun( void )
{
// code...
}
规范:
for(...)
{
// code...
}
if(...)
{
// code...
}
void example_fun( void )
{
// code...
}
在两个以上的关键字、变量、常量进行对等操作时,他们之间的操作符之前、之后或者前后要加空格;进行非对等操作时,果果是关系密切的立即操作符(如->),后不应加空格。
示例:
(1)逗号、分号只在后面加空格
int a, b, c;
(2)比较操作符,赋值操作符“=”、“+=”,算术操作符“+”、“%”,逻辑操作符“&&”、
“&”,位域操作符“<<”,“^”等双目操作符的前后加空格。
if(current_time >= MAX_TIME_VALUE)
a = b + c;
a *= 2;
a = b^2;
(3)“->”、“.”前后不加空格。
P->id = pid;
空格用的要在一定程度可以让你写出的代码美观,整洁,让人一看上去不感觉凌乱。
函数的规模尽量控制在可维护范围,如果代码量过大,应对其进行拆分,华为给出的建议是低于200行,但是对于我们大部分单片机开发这来说,建议代码量控制在几十行,连同注释建议200行左右。
编写高内敛低耦合的函数,这方面个人是这么理解的,其实在我们模块化编程里面也有这方面的思想,也就是尽量编写函数时不要过多的用其他模块的变量以及函数调用,那样只会是日后的维护困难,另外一个就是不便于移植。
第三章
从零玩转单片机系统
讲到系统我们了解最多的可能莫过于windows操作系统,开源的就是linux,完整的来说linux只是一个系统的内核,在工业领域里包括书本上最多的是uCosII,但是对于初学51单片机的朋友,这款系统并不是很适合大家,一方面代码量有点大,另一方面里面有太多我们目前暂时不需要用到的东西(比如说:队列,消息...),还有一些让我们望而生畏的数据结构,综合以上几点,他并不适合初学者。所以本章主要参考《时间触发嵌入式系统设计模式》,这本书是一本非常适合从零开始学习系统的书,里面的一些概念很容易让以前没有接触过嵌入式RTOS的人在51单片机上实践,它的代码量很少,并且直接可以运行在51单片机上,另外这本书里面没有太多复杂的东西,只是一些很实用的东西。由于本人时间有限,另外就是在系统方面很多问题理解的不是太深刻,我就简单的讲一讲我对单片机系统的理解,具体的东西请大家参考我给出的几个教程《51系统调度》、《如何设计复杂的多任务程序》、以及《时间触发嵌入式系统设计模式》。
我们大部分时间设计的程序就是围绕着while(1)大循环工作的,可能在简单的工程里面并不能发现什么问题,但是随着我们的工程越来越复杂,这种方法就相当不适用了。比如说我们一个工程里面有用到LED灯,液晶显示屏,按键,串口通信;LED灯每秒闪烁一次,液晶屏每秒刷新两次,按键用状态机没15毫秒轮循一次,串口每秒发送一次给上位机。如果里面的延时还采用之前的消耗MCU资源的模式,那这个工程将很难实现,系统的出现就是为了解决这些问题而生的。它很巧妙的把时间延时利用起来,每个系统都有一个心脏(定时器),有些单片机(像STM32)还专门给它造了个心脏(系统定时器),它在里面起到非常重要的监督作用。它不断的检查每个任务的延时是否到了,如果到了就再次运行该任务。包括大名鼎鼎的uCosII,里面
实验中用到的小型系统定时器0中断服务函数所做的事情,是不是做的就是对要进行的延时不断的做减法运算,因为定时器是每秒执行200次,也就是5ms一次,所以相对使用直接延时来说节约了大量的资源。其实我们也可以想到,我们最终要做的就是解决延时的过程中的资源浪费问题,所以大部分系统用定时器以一定频率去处理延时问题,解决了延时问题就解决了系统资源浪费问题。当然了,系统的奥妙不仅仅是这么点东西,但是对于我们刚学习系统来说,关注的太多,只会分散我们的精力,感觉系统复杂,让我们望而止步。所以我们要从看的见的着手。
后记:
纵观电子技术,互联网,开源的硬件、软件,无一不是从国外引进的。我想说的是这是个资源的时代,要善于掌握资源,利用资源,注重团队合作,多与你周围的人沟通、在沟通中产生思想,在沟通中进步。不要再虚幻的世界里寻找成就感与自信,多在现实生活中找回你的自信,下一个辉煌定将属于我们。
注意:以上所有要求光看不练假把式,需要每个人一个一个的落实到位,身体力行。