独闷闷网
标题: 从单片机基础到程序框架(连载) [打印本页]
作者: jianhong_wu 时间: 2016-1-17 10:16
标题: 从单片机基础到程序框架(连载)
本帖最后由 jianhong_wu 于 2020-5-22 13:37 编辑
从单片机基础到程序框架(群友“飞扬”整理的带目录的全集2019wps版).rar
(2.85 MB, 下载次数: 1340)
从单片机基础到程序框架(群友“飞扬”整理的带目录的全集2019pdf版).rar
(3.61 MB, 下载次数: 1715)
该教程免费授权给所有的出版社和做单片机学习板的厂家和各大培训机构以及全国各大院校,我本人不从中赢利也不收取任何版权费用,我本人也不卖书也不卖学习板也不搞线下培训。该教程的版权无偿捐给全社会。
第一节:我的价值观。
第二节:初学者的疑惑。
第三节:单片机最重要的一个特性。
第四节:平台软件和编译器软件的简介。
第五节:用keil2软件关闭,新建,打开一个工程的操作流程。
第六节:把.c源代码编译成.hex机器码的操作流程。
第七节:本节预留。
第八节:把.hex机器码程序烧录到单片机的操作流程。
第九节:本节预留。
第十节:程序从哪里开始,要到哪里去?
第十一节:一个在单片机上练习C语言的模板程序。
第十二节:变量的定义和赋值。
第十三节:赋值语句的覆盖性。
第十四节:二进制与字节单位,以及常用三种变量的取值范围。
第十五节:二进制与十六进制。
第十六节:十进制与十六进制。
第十七节:加法运算的5种常用格式。
第十八节:连加、自加、自加简写、自加1。
第十九节:加法运算的溢出。
第二十节:隐藏中间变量为何物?
第二十一节:减法运算的5种常见格式。
第二十二节:连减、自减、自减简写、自减1。
第二十三节:减法溢出与假想借位。
第二十四节:借用unsigned long类型的中间变量可以减少溢出现象。
第二十五节:乘法运算中的5种常用组合。
第二十六节:连乘、自乘、自乘简写,溢出。
第二十七节:整除求商。
第二十八节:整除求余。
第二十九节:“先余后商”和“先商后余”提取数据某位,哪家强?
第三十节:逻辑运算符的“与”运算。
第三十一节:逻辑运算符的“或”运算。
第三十二节:逻辑运算符的“异或”运算。
第三十三节:逻辑运算符的“按位取反”和“非”运算。
第三十四节:移位运算的左移。
第三十五节:移位运算的右移。
第三十六节:括号的强制功能---改变运算优先级。
第三十七节:单字节变量赋值给多字节变量的疑惑。
第三十八节:第二种解决“运算过程中意外溢出”的便捷方法。
第三十九节:if判断语句以及常量变量的真假判断。
第四十节:关系符的等于“==”和不等于“!=”。
第四十一节:关系符的大于“>”和大于等于“>=”。
第四十二节:关系符的小于“<”和小于等于“<=”。
第四十三节:关系符中的关系符:与“&&”,或“||”。
第四十四节:小括号改变判断优先级。
第四十五节: 组合判断if...else if...else。
第四十六节: 一维数组。
第四十七节: 二维数组。
第四十八节: while循环语句。
第四十九节: 循环语句do while和for。
第五十节: 循环体内的continue和break语句。
第五十一节: for和while的循环嵌套。
第五十二节: 支撑程序框架的switch语句。
第五十三节: 使用函数的三要素和执行顺序。
第五十四节: 从全局变量和局部变量中感悟“栈”为何物。
第五十五节: 函数的作用和四种常见书写类型。
第五十六节: return在函数中的作用以及四个容易被忽略的功能。
第五十七节: static的重要作用。
第五十八节: const(或code)在定义数据时的作用。
第五十九节: 全局“一键替换”功能的#define。
第六十节: 指针在变量(或常量)中的基础知识。
第六十一节: 指针的中转站作用,地址自加法,地址偏移法。
第六十二节: 指针,大小端,化整为零,化零为整。
第六十三节: 指针“化整为零”和“化零为整”的“灵活”应用。
第六十四节: 指针让函数具备了多个相当于return的输出口。
第六十五节: 指针作为数组在函数中的入口作用。
第六十六节: 指针作为数组在函数中的出口作用。
第六十七节: 指针作为数组在函数中既“入口”又“出口”的作用。
第六十八节: 为函数接口指针“定向”的const关键词。
第六十九节: 宏函数sizeof()。
第七十节: “万能数组”的结构体。
第七十一节: 结构体的内存和赋值。
第七十二节: 结构体的指针。
第七十三节: 结构体数据的传输存储和还原。
第七十四节: 结构体指针在函数接口处的频繁应用。
第七十五节: 指针的名义(例:一维指针操作二维数组)。
第七十六节: 二维数组的指针。
第七十七节: 指针唯一的“单向”输出通道return。
第七十八节: typedef和#define和enum。
第七十九节: 各种变量常量的命名规范。
第八十节: 单片机IO口驱动LED。
第八十一节: 时间和速度的起源(指令周期和晶振频率)。
第八十二节: Delay阻塞延时控制LED闪烁。
第八十三节: 累计主循环的“非阻塞”延时控制LED闪烁。
第八十四节: 中断与中断函数。
第八十五节: 定时中断的寄存器配置。
第八十六节: 定时中断的“非阻塞”延时控制LED闪烁。
第八十七节: 一个定时中断产生N个软件定时器。
第八十八节: 两大核心框架理论(四区一线,switch外加定时中断)。
第八十九节: 跑马灯的三种境界。
第九十节: 多任务并行处理两路跑马灯。
第九十一节: 蜂鸣器的“非阻塞”驱动。
第九十二节: 独立按键的四大要素(自锁,消抖,非阻塞,清零式滤波)。
第九十三节: 独立按键鼠标式的单击与双击。
第九十四节: 两个独立按键构成的组合按键。
第九十五节: 两个独立按键的“电脑键盘式”组合按键。
第九十六节: 独立按键“一键两用”的短按与长按。
第九十七节: 独立按键按住不松手的连续均匀触发。
第九十八节: 独立按键按住不松手的“先加速后匀速”的触发。
第九十九节: “行列扫描式”矩阵按键的单个触发(原始版)。
第一百节: “行列扫描式”矩阵按键的单个触发(优化版)。
第一百零一节: 矩阵按键鼠标式的单击与双击。
第一百零二节: 两个“任意行输入”矩阵按键的“有序”组合触发。
第一百零三节: 两个“任意行输入”矩阵按键的“无序”组合触发。
第一百零四节: 矩阵按键“一键两用”的短按与长按。
第一百零五节: 矩阵按键按住不松手的连续均匀触发。
第一百零六节: 矩阵按键按住不松手的“先加速后匀速”触发。
第一百零七节: 开关感应器的识别与软件滤波。
第一百零八节: 按键控制跑马灯的启动和暂停和停止。
第一百零九节: 按键控制跑马灯的方向。
第一百一十节: 按键控制跑马灯的速度。
第一百一十一节: 工业自动化设备的开关信号的运动控制。
第一百一十二节: 数码管显示的基础知识。
第一百一十三节: 动态扫描的数码管显示数字。
第一百一十四节: 动态扫描的数码管显示小数点。
第一百一十五节: 按键控制数码管的秒表。
第一百一十六节: 按键控制数码管的倒计时。
第一百一十七节: 按键切换数码管窗口来设置参数。
第一百一十八节: 按键让某位数码管闪烁跳动来设置参数。
第一百一十九节: 一个完整的人机界面的程序框架的脉络。
第一百二十节: 按键切换窗口切换局部来设置参数。
第一百二十一节: 可调参数的数码管倒计时。
第一百二十二节: 利用定时中断做的“时分秒”数显时钟。
第一百二十三节: 一种能省去一个lock自锁变量的按键驱动程序。
第一百二十四节: 数显仪表盘显示“速度、方向、计数器”的跑马灯。
第一百二十五节: “双线”的肢体接触通信。
第一百二十六节: “单线”的肢体接触通信。
第一百二十七节: 单片机串口接收数据的机制。
第一百二十八节: 接收“固定协议”的串口程序框架。
第一百二十九节: 接收带“动态密匙”与“累加和”校验数据的串口程序框架。
第一百三十节: 接收带“动态密匙”与“异或”校验数据的串口程序框架。
第一百三十一节: 灵活切换各种不同大小“接收内存”的串口程序框架。
第一百三十二节:“转发、透传、多种协议并存”的双缓存串口程序框架。
第一百三十三节:常用的三种串口发送函数。
第一百三十四节:“应用层半双工”双机串口通讯的程序框架。
作者: waphaoyun 时间: 2016-1-17 10:40
我抢个沙发鸿哥不会生气吧?
作者: jianhong_wu 时间: 2016-1-18 11:08
本帖最后由 jianhong_wu 于 2016-8-1 14:52 编辑
第一节:我的价值观。
第一节_pdf文件.pdf
(62.92 KB, 下载次数: 3163)
我2006年毕业,2009年就出来做自由职业者在深圳以接单片机项目谋生,到现在我有自己的机器人技术有限公司,做数控机器人系统,运动卡,三轴五轴联动之类的产品。目前公司的ARM单片机编程,DSP编程,FPGA编程,电路板设计,上位机软件都是由我带领的研发团队在做。我只专心做技术,而市场,生产,行政,资金,财务,采购都不用我分心去管,有我另外的合伙人雷总去打理,所以我常感恩能过上研发创作的日子,是因为有雷总的关照。
光有经济保障还是不够的,人最重要的是要找到自己的归宿自己的位置。我最爱看老子,庄子,孔子,王阳明的圣人书,王阳明说人人皆可成为圣人,所以我一直在追求圣人之道,我渴望成为圣人,圣人之道有真三不朽之说,立功,立言,立德。在立功层面,我这一辈子的定位就是做技术,我想做超级宇宙技术大牛,特别牛的那种牛,然后以我的技术协助雷总把我们的机器人公司做大做强。在立言层面,在不涉及我公司商业机密的前提下,我的天命和归宿就是做单片机技术分享,写一辈子源源不断的技术分享连载帖,然后写一本《从单片机基础到程序框架》的书,帮助更多单片机初学者,出书可以满足我在立言方面的追求。在立德层面,我平时信因果,在生活中多传播正能量。
我有自知之明,我的天命就是传播单片机技术。人最宝贵的东西是生命,生命属于人只有一次,人的一生应当这样度过:当他回首往事的时候,他不因虚度年华而悔恨,也不应碌碌无为而羞愧。在他临死的时候,他能够这样说:我的整个生命和全部精力,都献给了世界上最壮丽的事业——为传播单片机技术而奋斗。
作者: szdzjs 时间: 2016-1-23 19:05
本帖最后由 szdzjs 于 2016-1-23 19:08 编辑
来欣赏鸿哥的佳作,就算没了沙发,已无板凳,坐个地板也别无遗憾!
作者: jianhong_wu 时间: 2016-1-26 13:32
本帖最后由 jianhong_wu 于 2016-1-31 15:54 编辑
第二节:初学者的疑惑。
【2.1 单片机应用的核心技术是什么?】
单片机应用的核心技术是什么?是按键,数码管,流水灯,串口。是它们的程序框架。按键和数码管是输入是人机界面,把它们的程序框架研究透了,以后做彩屏或者更花销的显示界面,程序框架也可以通用。流水灯是应用程序是APP,把它的程序框架研究透了,以后控制飞机大炮的程序框架也是一样。串口是通讯是接口,把它的程序框架研究透了,以后搞SPI,CAN,USB等通讯项目时,上层的程序框架也可以通用。如果某天你突然腰酸背痛可能是缺钙了,如果某天你第一次做项目时突然发现无从下手,你缺的可能是程序框架。
【2.2 跟我学单片机到底是学什么?】
跟我学单片机到底是学什么?我的回答是像驾驶汽车一样驾驭单片机。我教给大家的是驾驶汽车的技术而不是研发汽车的技术。因此每当别人问我学51单片机,PIC,AVR,stm32哪个更加有前途,应该先学哪个再学哪个时,我的回答是既然你是学驾驶技术,那么用桑塔纳车来学还是用宝马车来学有差别吗?差别很小的,它们只是不同的厂家而已,只要会一种其它的就触类旁通了。把学单片机当作考驾照这是我常用的一个比喻。
【2.3 单片机神奇的工作原理是什么?】
单片机神奇的工作原理是什么?初学者不用纠结这个问题,这不是我们学习的方向。考驾照只要大概知道汽车是由四个轮,发动机,制动系统,离合器,方向盘等部分构成就够了,再深入的细节不用纠结。学单片机只要大概知道单片机内部由运算器,寄存器,IO口,复位电路,晶振电路等部分组成就够了,再深入的不用纠结。说实话,我本人做单片机开发有很多年了,但是我对单片机的工作原理也很模糊,就像人为什么能通过大脑来灵活控制双手,对于我仍然是一个迷。有这样的疑惑时咋办?我建议用“游戏规则”这个概念去应付它。游戏规则是不需要解释的,只要遵守就可以了。在应用的技术领域,把暂时不解的东西当作游戏规则来解读和遵守是我常用的思维方式。
【2.4 很难记住繁杂的寄存器?】
很难记住繁杂的寄存器?寄存器不用死记硬背,只要知道它大概的操作流程,有哪几类就够了。配置寄存器时,可参考别人已经配置好的代码,这些代码都很容易通过网络或者书本获得。也可以查找芯片数据手册,有很多单片机厂家会给出各个功能的代码范例。
【2.5 C语言很难学?】
C语言很难学?暂时不用学指针,暂时不用学结构体,暂时不用学多文件编程,暂时不用学链表,暂时不用学宏定义,暂时不用学文件操作,暂时不用学所有的数据类型。只要会:
5条指令语句switch语句,if else语句,while语句,for语句,=赋值语句。
7个运算符+,-,*,/,|,&,!。
4个逻辑关系符||,&&,!=,==.
3个数据类型unsigned char, unsigned int, unsigned long。
3个进制相互转化,二进制,十六进制,十进制。
1个void函数。
1个一维数组code(或const) unsigned char array[]。
只要会上述一些知识点,应付任何一种逻辑功能的单片机软件都足够了。
我在学校时只学了汇编程序,出来工作后才知道用C语言也能开发单片机,一开始只用常见的几条语句就把项目做出来了,没有用到printf这类繁琐格式的语句,第一次感觉C语言原来是那么容易那么简单,我是到后来才陆陆续续用到C语言其他的高级功能。
【2.6 汇编语言很难学怎么办?】
汇编语言很难学怎么办?我提个建议,对于初学者,一开始就学汇编语言确实难学,不如先学C语言,学会了C语言再学汇编,这样理解起来就容易多了。也可以把C语言列入必修课,汇编语言列入选修课,因为实际工作中也是C语言为主。
【2.7 很难记住各种繁杂的通信协议?】
很难记住繁杂的各种通信协议?IIC,SPI,232,CAN,USB等等。不用记那么多,你只要理解串行和并行通讯方式的基本原理就可以了,剩下的只是不同的协议而已,工作时再根据需要去看看相关资料就可以上手。不管世上有多少种通讯协议,物理世界上只有这两种通讯方式。
【2.8 很难写出短小精悍的程序?】
很难写出短小精悍的程序?初学者不用纠结于此。很多项目开发,程序容量不是刻意追求的目标,多一点少一点没关系,不会是寸土寸金的事情,现在大容量的单片机品种也非常多,反而更值得关注的是程序的运行效率,可读性和可修改性。当然,一些成本敏感的消费类电子不在此讨论范围,这类项目往往对程序容量也要求很苛刻。
作者: jianhong_wu 时间: 2016-1-31 21:06
本帖最后由 jianhong_wu 于 2016-1-31 21:07 编辑
第三节:单片机最重要的一个特性。
【3.1 单片机的“一”。】
图3.1
“道生一,一生二,二生三,三生万物。”《道德经》认为,世间万物,缤纷多彩,都源自一个东西,这个“一”的东西就是“道”。电子世界也存在“一”,这个“一”繁衍出手机,电脑,电视机,机器人等丰富多彩的电子世界,这个“一”就是单片机最重要的一个特性:“程序下载进内存后,单片机既可以通过管脚识别外部输入的高低电平信号,也可以通过管脚对外部输出不同时间长度的高低电平。”这句话有5个关键词“程序,内存,管脚,电平,时间。”下面我详细解读每个关键词的含义,涉及到某些专用名词如果不理解也没关系,主要是让大家有个感性的认识就足矣。
【3.2 程序。】
单片机程序有3种:C程序,汇编程序,机器程序。能下载进单片机的只有机器程序,C程序和汇编程序都不能直接下载进单片机,所以C程序和汇编程序都要经过编译软件翻译成机器程序后,才能下载进单片机。程序是语言,语言是用来交流,交流就必须存在两个对象,对象分别是程序员和单片机。程序员是人所以用C或者汇编语言,单片机是机器所以用机器语言,人和机是不同的世界,两者交流就需要一个翻译家,翻译家就是编译软件,俗称编译器,它能把C语言或者汇编语言翻译成单片机能识别的机器语言。机器语言就是0101的代码,一般以十六进制的形式呈现。
理论上,程序员也可以抛开C和汇编语言,直接用机器语言做项目。我读书时老师让我们做实验就是这么整。那时我还不知有烧录器,老师让我们先用汇编语言写好程序,然后自己充当编译器,对照汇编语言和机器语言的指令表,人工把汇编语言翻译成十六进制的机器语言,最后把机器语言按字节一个一个的输入到特定的实验设备来观察现象。现在回想起来,老师当时的初衷是让我们了解编程语言的本质。
既然可以直接用机器语言做项目,为什么还要C语言或者汇编语言?在C语言或者汇编语言没有诞生前,程序员就是通过在纸带上打孔来代表01的机器语言,此时相当于结绳记事的原始阶段。后来人类发明了汇编语言,通过英语单词来表示01机器语言特定的指令语句,此时开始诞生了汇编语言的编译器相当于进入象形文字的阶段。再到后来人类又发明了C语言,通过数学符号和英语单词来表达自己的逻辑,诞生了C语言的编译器相当于进入了汉字白话文阶段,从此程序员写出来的C程序就非常方便移植,编辑,阅读,传播,继承。现在单片机开发的主流是C语言,我本人出来工作后就没有用过汇编做项目开发。C语言是必修课,汇编语言是选修课;C语言是白话文简单易懂,汇编语言是文言文繁琐难读。当然汇编也有它的应用场合,汇编的翻译效率高,往往是一句汇编语言对应一句机器语言,而一句C语言有可能对应几句机器语言,这样C程序的代码效率在很大程度上取决于编译器的水平,编译器能不能帮你翻译出高效的机器语言对于我们来说往往像黑盒子,不像汇编语言那么可控制。所以很多嵌入式系统某段要求简洁高效的源代码往往用汇编来写,也有少数一些很便宜的单片机不提供C编译器,只能用汇编语言开发。要不要学汇编,最好根据个人的工作需求来决定。
【3.3 内存。】
单片机就像MP3,程序代码就像歌曲,把不同的歌曲下载到MP3里就可以听到不同的音乐,把不同的程序下载到单片机里,单片机就能做不同的事。能装程序的单片机必然有内存,内存由ROM和RAM组成,ROM和RAM都能装东西,但各有不同。
ROM的优点是存储的东西断电后不会丢失,缺点是存储的东西上电后不能更改,想要改变ROM的内容除非重新下载程序,而且下载次数有限制,FLASH的ROM最大次数通常是10万次,而OTP的ROM只能下载1次,所以平时上电工作时ROM存储的东西是不能更改的,某些具有IAP功能的高级单片机不在此讨论范围内。而RAM恰好反过来,RAM的优点是存储的东西上电后可以随时被单片机更改,更改次数没有限制,缺点是存储的东西断电后会丢失,没有记忆功能。
ROM和RAM各有特点,单片机从中各取所长。ROM用来存储不可更改的指令代码和常量数据,ROM的容量往往相当于代码的容量。RAM用来存储可以被更改的变量数据,RAM的容量往往相当于全局变量和局部变量的容量。不管是用C语言还是汇编,所写的程序代码就自然包含了指令代码、常量数据、全局变量、局部变量,那么谁在幕后帮我们进行分类存储,谁把一个程序代码的一分为二让它们在ROM和RAM里各就各位?是编译器软件和下载器(烧录器),编译器除了把C语言翻译成机器语言之外,还帮我们分好了类,分配好了存储的地址和位置,下载器(烧录器)再根据这些信息把程序存储到内存中,这些工作一般不用程序员干预,它们自动完成。
【3.4 管脚。】
管脚是单片机与外部电路进行能量和信息交互的桥梁。有电源,复位,晶振和IO口这4类管脚。
第一类电源管脚。是给单片机内部电路供电的接口。单片机有两种常用的供电电压,一般是3.3V或者5V,有的单片机两种电压都兼容。
第二类复位管脚。单片机上电后需要外部电路给它一个瞬间高电平或者低电平的复位信号,才能启动工作。复位电路通常是由电容和电阻组成的充电电路来实现,也有一些系统是用专门的复位芯片来实现。
第三类晶振管脚。任何单片机想要工作必须要有晶振。单片机执行程序指令是按一个节拍一个节拍来执行的。这个节拍的时间就是由晶振产生,所以把晶振比喻成单片机的心脏是非常恰当的。现在也有很多单片机直接把晶振集成到内部,这类单片机不用外接晶振也可以。
第四类IO口管脚。这是跟我们编写程序关联最密切的管脚。前面提到的电源,复位,晶振这3种管脚是为了让单片机能工作,俗称单片机工作的三要素。而单片机工作的具体内容就是通过IO口管脚来体现的。比如,IO口能识别按健的输入,也能输出驱动继电器工作的开关信号,也能跟外围器件进行通信。
【3.5 电平。】
电平就是电压的两种状态,低或者高,低相当于程序里的0,高相当于程序里的1。单片机IO口管脚检测到的电压低于或等于0.8V时是低电平,程序里读取到的是0数字。单片机IO口管脚检测到的电压高于或等于2.4V时是高电平,程序里读取到的是1数字。必须注意的是,IO口输入的最大电压不能超过单片机的供电电压。单片机输出的低电平是0V,单片机输出的高电平等于它的供电电压值,往往是3.3V或者5V。
【3.6 时间。】
时间是单片机程序必不可少的元素。跟外围芯片通信的时序节拍需要时间,驱动发光二极管闪烁需要时间,工控自动化的某些延时需要时间。单片机的时间来源自两方面。第一方面源自指令的周期时间。单片机是根据节拍来执行程序指令的,每执行一条指令都要消耗一点时间,只要让程序执行的指令数量越多,产生的时间就越长,通过调整所执行指令的数量就可以得到所需的时间长度。第二方面源自单片机内部自带的定时器。假如设置定时器每20毫秒产生一次中断,现在要获取10秒钟的时间,只需在程序里统记500次定时中断就可以了,因为1秒等于1000毫秒。指令和定时器这两者的时间最后都来源于晶振。
作者: jianhong_wu 时间: 2016-2-11 10:46
本帖最后由 jianhong_wu 于 2016-2-11 10:47 编辑
第四节:平台软件和编译器软件的简介。
【4.1 平台软件和编译器软件的各自分工。】
C语言写在哪?谁来把C语言翻译成单片机可以识别的机器语言?这就是平台软件和编译器软件的分工。平台软件负责编辑C语言,编译软件负责把C语言翻译成单片机可以识别的机器语言。
【4.2 每一种单片机的平台软件和编译器软件不一定是唯一的。】
C语言在单片机的应用也是最近这些年发展起来的,早期做单片机的原厂更关注芯片硬件本身,配套的C语言开发软件方面涉入不深,他们往往只管把单片机芯片生产出来后,给大伙提供一个汇编语言的编译器软件就草率了事,所以给了很多第三方商家做平台软件和C编译器的机会,后来单片机原厂也乐意支持和配合这些第三方开发软件的厂商,也有一些单片机原厂直接收购这类第三方软件公司。因此,不同厂家的单片机,它所用的平台和编译器软件可能都不一样。即使是同样一个厂家的单片机,它也有可能存在多种不同的第三方平台软件和编译器软件,每一种单片机所用的平台软件和编译器不一定是唯一的。比如stm8单片机可以用 STVD软件平台,也可以IAR平台。stm32单片机可以用keil平台,也可以用IAR平台。
【4.3 平台软件和编译器软件的宿主与寄生关系。】
平台软件选定了之后,所用的编译器软件也可能存在多种选择,并不是一种平台软件就绑定一种编译器软件。生物学的比喻,平台软件是宿主,编译器软件是寄生在平台软件里的。一个平台软件可以嵌入多种不同的编译器软件,平台软件和编译器软件存在一对多的关系。比如,PIC单片机的平台软件是MPLAB,8位PIC单片机是PICC编译器,12位PIC单片机是PIC18编译器,16位PIC单片机是C30编译器。而且MPLAB平台软件与上述各种编译器软件都要单独一个一个分开来安装,最后运行MPLAB平台软件,在里面操作某个菜单设置选项,把各种C编译器软件跟MPLAB平台软件关联起来。
【4.4 51单片机的平台软件和编译器软件。】
我后面的讲解,51单片机的平台软件用keil2,编译器软件用C51。单片机程序开发需要用到这两种软件,但在项目开发的时候,只要跟平台软件打交道就可以了,因为编译器软件是当做一种独立配件嵌入到平台软件里,统一接受平台软件的控制。我在用PIC的8位单片机时,需要安装一次MPLAB平台软件,也需要独立再安装一次PICC编译器软件,然后运行MPLAB平台软件,在里面操作某个菜单设置选项,把PICC编译器跟MPLAB平台软件关联起来,也就是我所说的把PICC编译器嵌入到MPLAB平台软件里,统一接受平台软件的控制,但我平常写代码时只需要跟MPLAB平台软件打交道就可以了。我早期在做51单片机开发时,也是需要把keil平台软件和C51软件分开安装,然后再把它们关联起来,但是现在从keil2版本开始,在安装keil平台软件时就已经默认把C51安装好了,并且自动把C51嵌入到了keil平台软件。我现在用keil2这个版本的平台软件,只需要安装一次keil2平台软件就可以了,不需要像早期那样再单独安装C51编译器。
作者: jianhong_wu 时间: 2016-2-19 15:04
本帖最后由 jianhong_wu 于 2016-3-6 06:42 编辑
第五节:用keil2软件关闭,新建,打开一个工程的操作流程。
【5.1 本教程选择keil2软件版本的原因。】
Keil软件目前有Keil2,Keil4,Keil5等版本。本教程之所以选用keil2版本,是因为keil2版本比较单纯,它本身内置了C51编译器,并且只适用于51单片机不能适用于stm32这类单片机。而Keil4,Keil5等版本不仅可以适用于51单片机的,还可以适用于ARM类的单片机,它们有C51编译器和MDK-ARM编译器两种选择,在同一个keil4或者keil5版本里,C51和MDK-ARM两者往往只能二选一,MDK-ARM编译器是针对stm32这类单片机,如果你电脑上用了MDK-ARM编译器想再切换到C51编译器就很麻烦了往往不兼容,为了电脑上既能用C51编译器,又能兼容MDK-ARM编译器,我的电脑上是同时安装了C51编译器的keil2和MDK-ARM编译器的keil4,一台电脑同时安装keil2和keil4不会冲突,能兼容的。
【5.2 如何在不用关闭keil2软件的前提下又能关闭当前被打开的工程?】
要关闭当前工程,最简单的方法是直接点击keil2软件右上角的“X”直接把keil2软件也一起关了,这种方法不在讨论范围,现在要介绍的是如何在不关闭keil2软件的前提下又能关闭当前被打开的工程。
图5.2.1
第一步:启动keil2软件。
双击桌面”keil uVision2”的图标启动keil2软件。
----------------------------------步骤之间的分割线----------------------------------------
图5.2.2
第二步:关闭被打开的已有工程。
启动keil2软件后,假设发现此软件默认打开了一个之前已经存在的工程。关闭已有工程的操作是这样子的:点击上面”Project”选项,在弹出的下拉菜单中选择“Close Project”即可。这时keil2软件处于“空”的状态,没有打开任何工程了。
【5.3 keil2如何新建一个工程?】
图5.3.1
第一步:新建一个工程文件夹。
在电脑D盘目录下新建一个文件夹,取名为“stc89c52rc”。
- 补充说明:
- (1)文件夹的命名以及后面涉及到的工程文件名统统都不要用中文,请全部用英文,数字,或者下划线这些字符。即使keil软件支持中文名,我建议也不要用中文名,因为在单片机这个行业,有一些单片机厂家提供的平台软件,某些版本是不支持中文名的,所以大家从一开始就养成这个习惯,以后可以避免遇到一些不必要的麻烦。
- (2)新建的文件夹请直接放在某盘的根目录下,而不要放到某个已有文件夹的目录下。一方面是因为已有的文件名目录往往带有中文单词,另外一方面是有一些单片机厂家的平台软件不支持嵌入层次太深的文件目录,所以大家从一开始就养成这个习惯,以后可以避免遇到一些不必要的麻烦。
复制代码
----------------------------------步骤之间的分割线----------------------------------------
第二步:启动keil2软件。
双击桌面”keil uVision2”的图标启动keil2软件。
----------------------------------步骤之间的分割线----------------------------------------
第三步:关闭默认被打开的已有工程。
启动keil2软件后,如果发现此软件默认打开了一个之前已经存在的工程,请先关闭此工程让keil2软件处于“空”的状态,如果没有发现此软件默认打开已有工程,这一步可以忽略跳过。关闭已有工程的操作是这样子的:点击上面”Project”选项,在弹出的下拉菜单中选择“Close Project”即可。这时keil2软件处于“空”的状态,没有打开任何工程了。
----------------------------------步骤之间的分割线----------------------------------------
图5.3.4.1
图5.3.4.2
图5.3.4.3
图5.3.4.4
第四步:利用工具向导新建一个工程。
点击上面”Project”选项,在弹出的下拉菜单中选择“New Project...”,在弹出的对话框中,选择保存的目录是刚才第一步新建的文件夹“stc89c52rc”,同时输入跟文件夹名称一样的工程文件名“stc89c52rc”,然后单击“保存”按键(一个新工程模板就建成了),单击“保存”按键后此时会弹出一个选择单片机型号的对话框,单击”Atmel”这个厂家前面的“+”号,在展开的下拉选项中选中“AT89C52”这个型号,然后点击“确定”,此时会弹出一个英文询问框,大概意思是“是否要复制STARTUP.A51这个文件到工程里?”我们单击“否”即可。
- 补充说明:
- (1)以上新建的保存文件名应该跟我们第一步在D盘新建的文件夹名称一致,确保都是“stc89c52rc”,因为有一些单片机厂家的平台软件是有这个要求的,所以大家养成这个习惯,以后可以避免遇到一些不必要的麻烦。
- (2)上面之所以选择Atmel厂家的AT89C52单片机,是因为本教程选用的单片机STC89C52RC跟AT89C52是兼容的。
- (3)在弹出的英文询问框,大致意思是“是否要复制STARTUP.A51这个文件到工程里?”,那么STARTUP.A51这个文件有什么含义?STARTUP.A51是一个启动程序文件,在单片机进入.c程序执行main函数之前,先去执行这个启动程序,这个启动程序是专门用来初始化RAM和设置堆栈等,如果我们选“否”不添加这个启动程序,编译器也会自动加入一段我们不能更改的默认启动程序。如果选“是”,那么这个文件就会出现在我们工程里,我们可以根据需要对它进行更改。但是大多数的情况下,我们都不会去更改此文件,所以无论你选“是”还是“否”,只要你不更改START.A51文件,对我们来说都是一样的。因此我本人一般情况下都是选“否”。
复制代码
----------------------------------步骤之间的分割线----------------------------------------
图5.3.5.1
图5.3.5.2
图5.3.5.3
第五步:新建一个.c源文件。
点击上面”File”选项,在弹出的下拉菜单中选择“New...”,会看到弹出来一个名字为”Text1”的文件。再一次点击上面”File”选项,在弹出的下拉菜单中选择“Save”,会弹出一个保存的对话框,此时还是选择保存在第一步新建的文件夹目录下,并且把“Text1”文件名更改为“stc89c52rc.c”(注意后缀是.c扩展名),单击“保存”。
- 补充说明:
- (1)此时你如果打开D目录下“stc89c52rc”的文件夹,你会发现此文件夹有一个“stc89c52rc.c”的文件,这个文件就是在这一步被新建添加进来的,但是此文件“stc89c52rc.c”目前跟整个工程还没有关联,还需要在接下来的第六步那里进行关联操作。
- (2)上面新建添加的文件,它的文件名必须是带.c这个扩展名,表示此文件是C文件格式,这一个很重要不要搞错了。往后我们所写的C语言程序代码就是写在此C格式的文件里。此文件也俗称C源文件。
复制代码
----------------------------------步骤之间的分割线----------------------------------------
图5.3.6.1
图5.3.6.2
图5.3.6.3
第六步:把刚才新建的.c源文件添加到工程里,跟当前工程关联起来。
点击左边竖着的选项框里面的”Target 1”前面的“+”号,在展开的下拉菜单下看到“Source Group 1”。右键单击“Source Group 1”选项,在下拉菜单中选择“Add Files to Group ‘Source Group 1’”选项,弹出一个文件选择对话框,单击选中刚才新建的.c源文件,然后单击一次“Add”按钮,此时虽然对话框没有关闭,但是已经悄悄地把.c源文件添加到工程里了(这个地方keil的用户体验设计得不够好,容易让人误解还没有把文件添加进来),这时再点击一次“Close”按钮先把此对话框关闭,然后发现左边的“Source Group 1”前面多了一个”+”号,单击此”+”号展开,发现下面的文件恰好是刚才新添加进去的.c源文件“stc89c52rc.c”。
- 补充说明:
- (1)在刚才的操作中,我本人觉得keil软件有一个地方的用户体验做得不够好,容易引起误解。就是在弹出一个文件选择对话框时,先单击选中刚才新建的.c源文件,此时单击一次“Add”按钮,已经相当于把.c文件添加进工程了,但是此时keil软件并没有自动关闭对话框,这样很容易让初学者误以为.c源文件还没有被添加进去。
复制代码
----------------------------------步骤之间的分割线----------------------------------------
图5.3.7
第七步:至此,可以正常的编辑C语言代码了。
双击打开左边Target1里面Source Group1下刚刚被添加进工程的“stc89c52rc.c”源文件,就可以在此“stc89c52rc.c”文件下输入C语言代码了,请把以下范例代码复制进去,然后再一次点击”File”选项,在弹出的下拉菜单中选择“Save”保存。此时,新建一个工程的步骤已经完成。供复制的范例代码如下:
- #include "REG52.H"
- void delay_long(unsigned int uiDelayLong); //延时函数
- sbit led_dr=P1^6;
- void main()
- {
- while(1)
- {
- led_dr=1; //LED亮
- delay_long(100); //延时50000个空指令的时间
- led_dr=0; //LED灭
- delay_long(100); //延时50000个空指令的时间
- }
- }
- void delay_long(unsigned int uiDelayLong) //延时函数
- {
- unsigned int i;
- unsigned int j;
- for(i=0;i<uiDelayLong;i++)
- {
- for(j=0;j<500;j++); //内嵌循环的空指令数量
- }
- }
复制代码
------------------此处为分割线,上面的是代码的结束,下面的是补充说明的开始------------------
- 补充说明:
- (1)可能有些朋友不是用keil2版本,如果他们是用keil4的版本,当把代码复制到keil4时,如果中文注释出现乱码怎么办?解决办法是这样的:点击keil4软件的左上角"Edit",在下拉菜单中选最后一项“Configuration”,在弹出的对话框中把Encoding的选项改成“Chinese GB2312(Simplified)”.然后删除所有C代码,重新复制一次代码进去就恢复正常了。当然,我们用keil2版本不会遇到这个问题,况且keil2版本的"Edit"下拉菜单也没有“Configuration”这个选项,所以keil2和keil4还是有一些差别的。
复制代码
【5.4 keil2如何打开一个现有的工程?】
第一步:启动keil2软件。
双击桌面”keil uVision2”的图标启动keil2软件。
----------------------------------步骤之间的分割线----------------------------------------
第二步:关闭默认被打开的已有工程。
启动keil2软件后,如果发现此软件默认打开了一个之前已经存在的工程,请先关闭此工程让keil2软件处于“空”的状态,如果没有发现此软件默认打开已有工程,这一步可以忽略跳过。关闭已有工程的操作是这样子的:点击上面”Project”选项,在弹出的下拉菜单中选择“Close Project”即可。这时keil2软件处于“空”的状态,没有打开任何工程了。
----------------------------------步骤之间的分割线----------------------------------------
图5.4.3.1
图5.4.3.2
第三步:打开一个现成的工程。
点击上面”Project”选项,在弹出的下拉菜单中选择“Open Project”,在弹出的文件对话框中,找到需要被打开工程文件夹(本例程是D盘下的“stc89c52rc”文件夹),在此文件夹目录下单击选中“stc89c52rc.Uv2”这个工程文件名,然后点击“打开”,就可以打开一个现有的工程文件了。
作者: jianhong_wu 时间: 2016-2-28 11:16
本帖最后由 jianhong_wu 于 2016-3-25 00:06 编辑
第六节:把.c源代码编译成.hex机器码的操作流程。
第六节_pdf文件.pdf
(406.18 KB, 下载次数: 4285)
【6.1 详细步骤。】
第一步:启动keil2软件。
双击桌面”keil uVision2”的图标启动keil2软件。
----------------------------------步骤之间的分割线----------------------------------------
第二步:关闭默认被打开的已有工程。
启动keil2软件后,如果发现此软件默认打开了一个之前已经存在的工程,请先关闭此工程让keil2软件处于“空”的状态,如果没有发现此软件默认打开已有工程,这一步可以忽略跳过。关闭已有工程的操作是这样子的:点击上面”Project”选项,在弹出的下拉菜单中选择“Close Project”即可。这时keil2软件处于“空”的状态,没有打开任何工程了。
----------------------------------步骤之间的分割线----------------------------------------
第三步:打开一个现成的工程。
点击上面”Project”选项,在弹出的下拉菜单中选择“Open Project”,在弹出的文件对话框中,找到需要被打开工程文件夹(本例程是上一节在D盘下建的“stc89c52rc”文件夹),在此文件夹目录下单击选中“stc89c52rc.Uv2”这个工程文件名,然后点击“打开”,就可以打开一个现有的工程文件了。
----------------------------------步骤之间的分割线----------------------------------------
图6.1.4.1
图6.1.4.2
第四步:设置编译环境让keil2软件允许产生.hex格式的机器码文件。
鼠标右键点击选中左边选项框里面的”Target 1”选项,在右键下拉菜单中选择“ Options for Target‘Target 1’ ”选项,弹出一个编译环境设置对话框,左键单击上面子菜单切换到“Output”窗口下,把“Create Hex File”勾选上。点击“确定”。
- 补充说明:
- (1)这个选项很重要,必须把“Create Hex File”选项勾上,否则后续的操作不能在工程文件夹的目录里生成.Hex的机器码文件。对于一个文件夹的工程模板,只需要设置一次就可以保存起来了,下次开电脑重新打开此工程模板时不需要再设置,这些被设置的参数都是能掉电保存起来的。
复制代码
----------------------------------步骤之间的分割线----------------------------------------
图6.1.5.1
第五步:启动编译。
在确保stc89c52rc.c源文件里面有C语言源代码的情况下(如果没有,请先复制上一节的例程源代码),点击上面”Project”选项,在弹出的下拉菜单中点击“Rebuild all target files”编译命令,编译器开始编译工作。
----------------------------------步骤之间的分割线----------------------------------------
图6.1.6.1
第六步:在”Output Window”窗口下观察编译结果。
可以在最下方的”Output Window”窗口下观察到编译的过程提示。”Output Window”窗口默认出现在源代码区的最下方,如果没有发现”Output Window”窗口,请把鼠标的光标移动到最下方的滑动条下边,当它呈现移动光标的形状时,按住左键往上拖动就可以看到“Output Window”窗口了。当“Output Window”窗口最后一行显示“"stc89c52rc" - 0 Error(s), 0 Warning(s).”等信息时,表示翻译工程结束了。其中0 Error(s)代表编译成功,没有任何错误。0 Warning(s)代表没有任何警告。
- 补充说明:
- (1)只要有一个错误Error产生,就说明编译不通过。如果没有任何错误Error产生,但是有几个警告Warning产生,在这种情况下很多时候都不影响程序的正常运行,只有少数情况下是会影响代码的正常运行的,因此我本人建议哪怕是一个警告,大家也不要放过它,也要找到产生这个警告的原因。
复制代码
图6.1.6.2
- (2)查找错误的时候,只需要双击错误提示error那行内容,光标就会自动跳到源代码错误的附近,方便大家寻找语法错误。
复制代码- (3)还有一种很实用的方法,就是直接把提醒出错那一整行英文复制粘贴到网上去搜索,往往能搜索到所需的正确答案或者重要提示。
复制代码
----------------------------------步骤之间的分割线----------------------------------------
图6.1.7.1
第七步:编译后生成.hex机器码文件的目录位置。
以上编译成功后,只要打开电脑D盘的stc89c52rc文件夹,就可以找到.hex扩展名的机器码文件,这个文件就是我们要下载到单片机的机器码文件。
【6.2 注意!最后,还有一个非常重要的keil编译环境需要设置。】
STC89C52单片机与AT89C52单片机是兼容的,它们程序容量ROM都是8K字节,而它们的数据容量RAM容量是不一样的,STC89C52的RAM是512字节,而AT89C52的RAM是256字节,尽管两者的RAM容量有一些小差异,但是对于我们用来入门学习来说,这些都是无所谓的,所以本教程硬件平台虽然是用STC89C52单片机,但是keil的编译环境其实是用AT89C52的芯片环境,因此本教程就以AT89C52为准。 刚才提到AT89C52的程序容量ROM是8K字节,数据容量RAM是256字节,那么问题来了,很多初学者经常遇到,有一些程序代码ROM明明没有超过8K,或者数据容量明明还没超过256字节,编译器居然报错提醒容量不够! 什么原因?怎么解决?
什么原因?是单片机的内存分配模式问题引起的,具体原因暂时不深入讲解。
怎么解决?最后还要有一个非常重要的keil编译环境需要要设置,设置步骤是:
图6.2.1
如上图所示,在一个已经打开的工程里,鼠标右键点击选中左边选项框里面的”Target 1”选项,在右键下拉菜单中选择“ Options for Target‘Target 1’ ”选项,弹出一个编译环境设置对话框,第一步单击上面子菜单切换到“Target”窗口下,第二步在Memory Model选项的下拉菜单中选中“Compact: variables in PDATA”,第三步在Code Ram Size选项的下拉菜单中选中“Large: 64K program”,第四步点击“确定”。
作者: jianhong_wu 时间: 2016-3-6 13:10
本帖最后由 jianhong_wu 于 2016-8-1 10:28 编辑
作者: jianhong_wu 时间: 2016-3-12 16:57
本帖最后由 jianhong_wu 于 2016-3-12 18:31 编辑
第八节:把.hex机器码程序烧录到单片机的操作流程。
第八节_pdf文件.pdf
(721.09 KB, 下载次数: 5020)
USB转串口的驱动程序CH340.zip
(132.42 KB, 下载次数: 2596)
【8.1 烧录程序的本质。】
“烧录”是比较专业的说法,很多初学者第一次听这词还以为跟火有关,莫名其妙的“烧录”是啥意思?烧录其实就是下载,烧录程序就是下载程序。下载好理解了吧,下载电影,下载歌曲,让播放器去播放。此处的下载程序跟下载歌曲的“下载”完全是一回事。有人会问,下载歌曲到手机,手机是成品,下载程序到单片机,单片机也是成品?新买回来的单片机不是一张白纸的电子元件吗?其实,新买回来的单片机就是一个成品,它不是白纸,它内部已经嵌入了一段系统程序,这个系统程序就像你刚买回来的手机就帮你预装了安卓系统一样,只是它的用户存储区是空白的。比如手机,你往这个存储区里存电影就可以看电影,存音乐就可以听音乐。比如单片机,你往这个存储区存不同的程序就可以让单片机做不同的事。而预装在新单片机内部的系统程序就是专门负责跟外部接口通讯,同时负责把hex格式的程序代码存放在单片机内部正确的位置,这个就是烧录程序(下载程序)的本质。这样一比喻,所以.hex格式的烧录文件跟.MP3格式的音乐文件在存储本质上是一样的。
再回顾总结一下,烧录程序的本质是:把单片机当做一个存储器,每一条程序指令都对应一个唯一的存储地址,把这些指令以字节为单位一条条存储到指定的存储地址中,这就是烧录程序的本质。对于STC89C52RC单片机,在下载程序时需要上位机软件和一根USB转串口线。上位机软件负责把.hex格式的机器码文件打开,机器码文件里面记录着每条程序指令所对应的地址信息,下载过程时,上位机软件根据.hex文件记录的指令内容和对应的地址信息,经过USB转串口线,跟单片机的预置系统程序进行串口通讯,从而把.hex记录的信息传输到单片机内部的flash存储器中,实现了程序的烧录下载。
【8.2 烧录程序所需的工具和软件。】
(1)装有XP或者WIN7系统的电脑一台。
其它更高系统的我还没测试过,应该问题也不大。
图8.2.2
(2)带9针串口、1颗LED灯、电源拨动开关、能5V电源供电的stc89c52rc单片机核心板一块。
单片机的学习离不开硬件平台的编程练习,本教程用的是坚鸿51学习板,此学习板的原理图在第七节已经分享到附件资料里。大家也可以根据原理图自己焊接一块学习板来学习,或者用其它厂家带有串口的单片机学习板来学习。
串口是用来单片机跟电脑通讯的接口,是STC89C52RC单片机下载程序的通道。LED灯用来观察单片机是否正常运行程序。电源拨动开关方便烧录程序时提供所需的断电和上电的操作。本单片机系统是5V供电。
图8.2.3
(3)主控芯片是CH340的USB转RS232串口线一条。
我之所以推荐主控芯片是CH340的USB转RS232串口线,因为CH340的下载线在烧录程序时很稳定可靠。这款USB转串口线可以在淘宝购买到。
图8.2.4
(4)5V供电的USB电源线一条。
此USB线可以从电脑的USB口取电,也可以从输出5V的手机充电器处取电。但是我建议大家用输出5V的手机充电器来供电,因为很多电脑的USB口供电干扰比较大,会影响程序烧录。
图8.2.5
(5)主控芯片是CH340的USB转RS232串口线驱动安装程序。
此驱动程序USB转RS232串口线的厂家通常都会提供,但是建议用我在附件资料里推荐给大家的驱动程序,毕竟这个程序经过我本人验证测试过。
图8.2.6
(6)烧录程序和串口助手功能都具备的“stc-isp-15xx-v6.85I”上位机软件。
这是宏晶单片机官方免费提供的上位机软件,可以在宏晶单片机的官网上下载获取。这款软件有很多功能,除了有下载程序和串口助手的功能外,还可以用来配置自动生成所需的初始化代码。当然,本教程后面主要是用到此软件的下载程序和串口助手这两个功能。所以大家所选的软件版本必须是v6.85I版本或者以上的版本,因为早些年有一些版本只有烧录功能但是没有串口助手的功能。
【8.3 把.hex文件烧录到单片机的操作流程。】
前面第6节内容已经教大家把一个驱动LED灯闪烁的C源代码编译成.hex文件的操作流程,同时在D盘的“stc89c52rc”文件夹里已经生成了一个“stc89c52rc.hex”的机器码文件,现在就要教大家如何烧录此文件到单片机内。此程序的功能是让单片机驱动一颗LED灯闪烁。
图8.3.1.1
图8.3.1.2
图8.3.1.3
第一步:安装USB转串口驱动程序。
上位机“stc-isp-15xx-v6.85I”烧录软件就是安装在电脑端的用户软件,电脑跟单片机进行通讯,需要一根USB转串口线,欲使USB转串口线正常工作,必须预先安装USB转串口的驱动程序。具体的操作是这样的:在本连载贴附件资料处下载“USB转串口的驱动程序CH340.zip”文件压缩包,解压后打开此文件夹,找到“SETUP.EXE”这个安装应用程序,双击启动,在弹出的界面中,单击“安装”按钮即可完成驱动程序的安装。
----------------------------------步骤之间的分割线----------------------------------------
图8.3.2.1
图8.3.2.2
图8.3.2.3
图8.3.2.4
第二步:硬件线路连接,同时记录串口号。
把USB转串口线插入电脑USB口,此时USB转串口线的另外一端连接51学习板的9针串口。同时,电源线一端用输出的5V手机充电器USB端口供电,电源线另一端连接51学习板的USB供电端口,此时可以通过51学习板的电源拨动开关来控制断电和上电。然后是查找串口号,方法是:以电脑WIN7系统为例,右击桌面“计算机”,单击选择下拉菜单的“管理”选项,在弹出的窗口中,点击“设备管理器”选项切换到对应的设置窗口,双击“端口(COM和LPT)”选项,在展开的下拉选项中,会看到“USB-SERTAL CH340(COM3)”,这个COM3就是我们要记住的串口号,记住此串口号,后面的步骤要用到。你们的串口号不一定是COM3,请以你们电脑显示的串口号为准。
----------------------------------步骤之间的分割线----------------------------------------
图8.3.3
第三步:打开上位机用户软件“stc-isp-15xx-v6.85I.exe”。
这个软件可以在宏晶单片机的官网下载获取,获取到的软件压缩包只需解压后就可以使用,不用安装,
直接双击打开“stc-isp-15xx-v6.85I.exe”,此时会弹出“温馨提示”的窗口,我们按“确定”就可以进入到真正的工作界面了。
----------------------------------步骤之间的分割线----------------------------------------
图8.3.4
第四步:选择单片机型号。
在“单片机型号”的下拉菜单中选择“STC89C/LE52RC”这个型号。如果中途弹出推荐选用其它型号的窗口,可以忽略它,我们用来学习只要认准“STC89C/LE52RC”这个型号就可以了。
----------------------------------步骤之间的分割线----------------------------------------
第五步:设置串口号。
点击“串口号”右侧的选项,在“串口号”的下拉菜单中,选择跟前面第二步所记录一样的串口号COM3(你们的串口号不一定是COM3,请根据你们的电脑实际显示情况来选择)。
----------------------------------步骤之间的分割线----------------------------------------
第六步:设置最低波特率和最高波特率。
“最低波特率”设置为2400,,“最高波特率”设置为9600。波特率如果设置太高,可能会导致烧录(下载)不成功。
----------------------------------步骤之间的分割线----------------------------------------
第七步:导入.hex格式的机器码文件。
点击“打开程序文件”的按钮,在弹出的对话框中,选择D盘下“stc89c52rc”文件夹目录下的“stc89c52rc.hex”,双击把“stc89c52rc.hex”导入到上位机用户软件。
----------------------------------步骤之间的分割线----------------------------------------
图8.3.8
第八步:启动下载。
点击“下载/编程”的按钮,发现“正在检测目标单片机..”的提示信息,此时需要通过电源波动开关把51学习板重新断电然后再上电才能正常下载,很多人也把这个重新上电的过程称为“冷启动”。之所以要重新断电再上电,是因为单片机内部预置的系统程序只在上电短暂的瞬间才会检查一下是否接收到需要重新烧录程序的指令,如果没有接收到烧录指令,单片机整个话语权就由原来的系统程序转交给我们的用户程序来接管,所以此串口后面的时间就给我们用户程序来使用。因此每次烧录程序时,先启动上位机的下载命令,此时上位机不断发送请求下载的命令给单片机,但是此时单片机并不理会这些指令,因为此时单片机的话语权已经交给了我们的用户程序,此时并不是预置系统程序在掌控,所以除非重新断电然后再上电那一瞬间才会让系统内置程序去检测并且响应此下载命令。另外多说一句,其实不是所有厂家的单片机在烧录程序时都需要“冷启动”,也不是所有单片机都支持串口烧录,各厂家的单片机烧录程序方式会有一些差异,但基本原理是一样的,大同小异。
----------------------------------步骤之间的分割线----------------------------------------
图8.3.9
第九步:“冷启动”后观察是否操作成功的信息。
执行完前面第九步的“冷启动”后,如果发现有“...操作成功!”的提示信息,就说明程序下载成功了。此时会发现51学习板上面的一颗LED灯不断闪烁,是因为我们的LED灯程序驱动它才开始闪烁的,说明我们的程序在单片机上正常工作了。
补充说明:
(1)以后只要每次重新编译了C源代码后,都会生成最新版本的.hex格式文件,所以每次烧录程序时,必须重新返回第七步,重新执行一次导入最新版本.hex格式文件的操作,确保被烧录的程序是最新版本的.hex烧录文件。
【8.4 51学习板下载程序失败时的解决办法。】
(1)可以先松一下单片机卡座,稍微挪动一下单片机,然后再卡紧单片机。卡座必须卡紧单片机,避免接触不良。
(2)改变供电电源,很多电脑的USB口供电电源干扰非常大,严重影响下载程序,请把USB电源线插入到手机充电器5V的USB接口,效果显著,明显提高了下载的成功率。
(3)检查确保所选择的单片机型号是STC89C/LE52RC,如果软件弹出推荐其它型号的单片机窗口,不用管它,我们就选STC89C/LE52RC。
(4)检查STC-ISP烧写软件是否选择匹配的COM口。
(5)单片机是靠串口烧录程序进去的,单片机的串口是P3.0,P3.1两根线经过232转换芯片,然后才与USB转串口线连接的。因此,在烧录程序时,请确保P3.0,P3.1两个IO口不能跳线连接到其它外围元器件上。
(6)点击“下载/编程”后,记得再断电并且重新上电一次。看看是否烧录成功。
(7)确保最低波特率一直设置为2400,最高波特率为9600。如果还不行再把最高波特率也改成2400试试。
(8)如果还不行,就退出软件,拔掉USB转串口线,同时断电(必须把整根电源线拔出!),重新插入USB串口线,重新插入电源线开电,重新打开软件。
(9)如果还不行,学习板先断电(必须把整根电源线拔出!),然后重启一次电脑。
(10)总之:如果还不行,就按上述步骤多折腾几次。
(11)最后实在不行,就尝试更换到其它USB口,或者尝试更换到其它电脑上试试。
作者: jianhong_wu 时间: 2016-3-17 07:37
本帖最后由 jianhong_wu 于 2016-8-1 10:29 编辑
第九节:本节预留。
本节预留。
作者: jianhong_wu 时间: 2016-3-19 10:56
本帖最后由 jianhong_wu 于 2016-3-19 11:22 编辑
第十节:程序从哪里开始,要到哪里去?
第十节_pdf文件.pdf
(85.68 KB, 下载次数: 4347)
程序从哪里开始,要到哪里去?为了让初学者了解C语言程序的执行顺序,我把程序分成三个区域:进入主程序前的区域,主程序的初始化区域,主程序的循环区域。当然,这里三个区的分类暂时没有把中断程序的情况考虑进去,中断程序的内容我会在后面相关的章节中再详细介绍,这里暂时不考虑中断。
进入主程序前的区域。这是上电后,在单片机执行主程序代码之前就已经完成了的工作。包括头文件的包含,宏定义,内存分配这些工作。这部分的内容可以暂时不用去了解,我会在后面的一些章节中陆续深入讲解。
主程序的初始化区域。这是上电后,单片机进入主程序后马上就要执行的程序代码,这部分区域的代码有一个特点,大家也必须记住的,就是单片机只执行一次。只要单片机不重启,不复位,那么上电后这部分的代码只被执行一次。
主程序的循环区域。单片机在主程序中执行完了初始化区域的代码,紧接着就进入这片循环区域的代码。单片机一直在逐行循环执行这些代码,执行到末尾时又返回到循环区域的开始处继续开始新一轮的执行,周而复始,往复循环,这就是上电后单片机的最终归宿,一直处在循环的状态。
下面我跟大家分析一个程序源代码的三个区域和执行顺序,大家先看中文解释部分的内容,暂时不用理解每行指令的语法,有个整体的认识就可以了。此源代码实现的功能是:上电后,蜂鸣器鸣叫一声就停止(初始化区域),然后看到一个LED灯一直在不停的闪烁(循环区域)。
图10.1
源代码如下:
- #include "REG52.H" //进入主程序前的区域:头文件包含
- sbit beep_dr=P3^4; //进入主程序前的区域:宏定义
- sbit led_dr=P1^6; //进入主程序前的区域:宏定义
- unsigned long i; //进入主程序前的区域:内存分配
- void main() //主程序入口,即将进入初始化区域
- {
- beep_dr=0; //第一步:初始化区域:蜂鸣器开始鸣叫。
- for(i=0;i<6250;i++); //第二步:初始化区域:延时0.5秒左右。也就是蜂鸣器鸣叫的持续时间。
- beep_dr=1; //第三步:初始化区域:蜂鸣器停止鸣叫。
- while(1) //执行完上面的初始化区域,即将进入循环区域
- {
- led_dr=0; //第四步:循环区域:LED开始点亮。
- for(i=0;i<6250;i++); //第五步:循环区域:延时0.5秒左右。也就是LED点亮的持续时间。
- led_dr=1; //第六步:循环区域:LED开始熄灭。
- for(i=0;i<6250;i++); //第七步:循环区域:延时0.5秒左右。也就是LED熄灭的持续时间。
- } //执行完上面第七步后,单片机又马上返回到上面第四步继续往下执行。
- }
复制代码
上述代码执行顺序分析:
单片机进入主程序后,从第一步到第三步是属于初始化区域,只被执行一次。然后进入循环区域,从第四步执行到第七步,执行完第七步之后,马上又返回上面第四步继续循环往下执行,单片机一直处于第四步到第七步的往复循环中。可以很清晰的看到,上面的main和while(1)关键词就是三个区域的边界分割线。
经过以上的分析,可以看出这三个区域的大概分布如下:
- //...进入主程序前的区域
- void main()
- {
- //...初始化区域
- while(1)
- {
- //...循环区域
- }
- }
复制代码
作者: jianhong_wu 时间: 2016-3-27 22:55
本帖最后由 jianhong_wu 于 2018-12-16 12:42 编辑
第十一节:一个在单片机上练习C语言的模板程序。
第十一节可复制的模板源程序.zip
(1.42 KB, 下载次数: 753)
第十一节_pdf文件.pdf
(294.23 KB, 下载次数: 2064)
【11.1 一套完整的模板源代码。】
先给大家附上一套完整的模板源代码,后面章节练习C语言的模板程序就直接复制此完整的源代码,此源代码适合的单片机型号是STC89C52RC,晶振是11.0592MHz,串口波特率是9600,初学者只需修改代码里从“C语言学习区域的开始”到“C语言学习区域的结束”的区域,其它部分不要更改。可复制的源代码请到网上论坛原贴处复制或者下载,搜索本教程名字就可以找到原贴出处。一套完整的模板源代码如下:
- #include "REG52.H"
- void View(unsigned long u32ViewData);
- void to_BufferData(unsigned long u32Data,unsigned char *pu8Buffer,unsigned char u8Type);
- void SendString(unsigned char *pu8String);
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a; //定义一个变量a。
- unsigned int b; //定义一个变量b。
- unsigned long c; //定义一个变量c。
- a=100; //给变量a赋值。
- b=10000; //给变量b赋值。
- c=1000000000; //给变量c赋值。
- View(a); //在电脑串口端查看第1个数a。
- View(b); //在电脑串口端查看第2个数b。
- View(c); //在电脑串口端查看第3个数c。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
- void View(unsigned long u32ViewData)
- {
- static unsigned char Su8ViewBuffer[43];
- code unsigned char Cu8_0D_0A[]={0x0d,0x0a,0x00};
- code unsigned char Cu8Start[]={"开始..."};
- static unsigned char Su8FirstFlag=0;
- static unsigned int Su16FirstDelay;
- if(0==Su8FirstFlag)
- {
- Su8FirstFlag=1;
- for(Su16FirstDelay=0;Su16FirstDelay<10000;Su16FirstDelay++);
- SendString(Cu8Start);
- SendString(Cu8_0D_0A);
- SendString(Cu8_0D_0A);
- }
- to_BufferData(u32ViewData,Su8ViewBuffer,1);
- SendString(Su8ViewBuffer);
- to_BufferData(u32ViewData,Su8ViewBuffer,2);
- SendString(Su8ViewBuffer);
- to_BufferData(u32ViewData,Su8ViewBuffer,3);
- SendString(Su8ViewBuffer);
- to_BufferData(u32ViewData,Su8ViewBuffer,4);
- SendString(Su8ViewBuffer);
- SendString(Cu8_0D_0A);
- }
- void to_BufferData(unsigned long u32Data,unsigned char *pu8Buffer,unsigned char u8Type)
- {
- code unsigned char Cu8Array1[]={0xB5,0xDA,0x4E,0xB8,0xF6,0xCA,0xFD,0x00};
- code unsigned char Cu8Array2[]="十进制:";
- code unsigned char Cu8Array3[]="十六进制:";
- code unsigned char Cu8Array4[]="二进制:";
- static unsigned char Su8SerialNumber=1;
- static unsigned int Su16BufferCnt;
- static unsigned int Su16TempCnt;
- static unsigned int Su16TempSet;
- static unsigned long Su32Temp1;
- static unsigned long Su32Temp2;
- static unsigned long Su32Temp3;
- static unsigned char Su8ViewFlag;
- if(1==u8Type)
- {
- for(Su16BufferCnt=0;Su16BufferCnt<7;Su16BufferCnt++)
- {
- pu8Buffer[Su16BufferCnt]=Cu8Array1[Su16BufferCnt];
- }
- pu8Buffer[2]=Su8SerialNumber+'0';
- pu8Buffer[Su16BufferCnt]=0x0d;
- pu8Buffer[Su16BufferCnt+1]=0x0a;
- pu8Buffer[Su16BufferCnt+2]=0;
- Su8SerialNumber++;
- return;
- }
- else if(2==u8Type)
- {
- for(Su16BufferCnt=0;Su16BufferCnt<7;Su16BufferCnt++)
- {
- pu8Buffer[Su16BufferCnt]=Cu8Array2[Su16BufferCnt];
- }
- Su32Temp1=1000000000;
- Su32Temp2=10;
- Su16TempSet=10;
- }
- else if(3==u8Type)
- {
- for(Su16BufferCnt=0;Su16BufferCnt<9;Su16BufferCnt++)
- {
- pu8Buffer[Su16BufferCnt]=Cu8Array3[Su16BufferCnt];
- }
- Su32Temp1=0x10000000;
- Su32Temp2=0x00000010;
- Su16TempSet=8;
- }
- else
- {
- for(Su16BufferCnt=0;Su16BufferCnt<7;Su16BufferCnt++)
- {
- pu8Buffer[Su16BufferCnt]=Cu8Array4[Su16BufferCnt];
- }
- Su32Temp1=0x80000000;
- Su32Temp2=0x00000002;
- Su16TempSet=32;
- }
- Su8ViewFlag=0;
- for(Su16TempCnt=0;Su16TempCnt<Su16TempSet;Su16TempCnt++)
- {
- Su32Temp3=u32Data/Su32Temp1%Su32Temp2;
- if(Su32Temp3<10)
- {
- pu8Buffer[Su16BufferCnt]=Su32Temp3+'0';
- }
- else
- {
- pu8Buffer[Su16BufferCnt]=Su32Temp3-10+'A';
- }
- if(0==u32Data)
- {
- Su16BufferCnt++;
- break;
- }
- else if(0==Su8ViewFlag)
- {
- if('0'!=pu8Buffer[Su16BufferCnt])
- {
- Su8ViewFlag=1;
- Su16BufferCnt++;
- }
- }
- else
- {
- Su16BufferCnt++;
- }
- Su32Temp1=Su32Temp1/Su32Temp2;
- }
- pu8Buffer[Su16BufferCnt]=0x0d;
- pu8Buffer[Su16BufferCnt+1]=0x0a;
- pu8Buffer[Su16BufferCnt+2]=0;
- }
- void SendString(unsigned char *pu8String)
- {
- static unsigned int Su16SendCnt;
- static unsigned int Su16Delay;
- SCON=0x50;
- TMOD=0X21;
- TH1=TL1=256-(11059200L/12/32/9600);
- TR1=1;
- ES = 0;
- TI = 0;
- for(Su16SendCnt=0;Su16SendCnt<43;Su16SendCnt++)
- {
- if(0==pu8String[Su16SendCnt])
- {
- break;
- }
- else
- {
- SBUF =pu8String[Su16SendCnt];
- for(Su16Delay=0;Su16Delay<800;Su16Delay++);
- TI = 0;
- }
- }
- }
复制代码
【11.2 模板程序的使用说明。】
图11.2.1
大多数初学者在学习C语言的时候,往往是在电脑端安装上VC平台软件来练习C语言,这种方法只要在代码里调用printf语句,编译后就可以看到被printf语句调用的变量,挺方便的。本教程没有用这种方法,既然本教程的C语言主要针对单片机,所以我想出了另外一种方法,这种方法就是直接在单片机上练习C语言,这样会让初学者体验更深刻。这种方法对硬件平台要求不高,只要51学习板上有一个9针的串口就可以,这个串口既可以用来烧录程序,也可以用来观察代码里的某个变量,只要在代码里调用View函数就可以达到类似VC平台软件下printf语句的效果,View函数可以向串口输出某个变量的十进制,十六进制和二进制,大家只要在电脑端的串口助手软件就可以看到某个变量的这些信息,View函数能查看的变量最大数值范围是4个字节的unsigned long变量,十进制的范围是从0到4294967295,也可以查看unsigned int 和unsigned char的类型变量(数据的进制以及long,int,char等知识点大家目前还没接触到,因此不懂也没关系,当前只要有个大概的认识就可以,暂时不用深入理解,后面章节还会详细介绍)。View函数是我整个模板程序的其中一部分,所以要用这种方法就必须先复制我整个模板程序,初学者练习代码的活动范围仅仅局限于模板程序里的“C语言学习区域”,在此区域里有一个main主函数,main主函数内有一个初始化区域,初学者往往在这个初始化区域里练习C语言就够了,初学者最大的活动范围不能超过从“C语言学习区域的开始”到“C语言学习区域的结束”这个范围,这个范围之外其它部分的代码主要用来实现数据处理和串口发送的功能,大家暂时不用读懂它,直接复制过来就可以了。比如:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- //...初始化区域,也就是主要用来给初学者学习C语言的区域。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
上述例子中,初学者练习代码只能在从“C语言学习区域的开始”到“C语言学习区域的结束”这个范围,此范围外的代码直接复制过来不要更改。我们再来分析分析下面节选的main函数源代码:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a; //定义一个变量a。
- unsigned int b; //定义一个变量b。
- unsigned long c; //定义一个变量c。
- a=100; //给变量a赋值。
- b=10000; //给变量b赋值。
- c=1000000000; //给变量c赋值。
- View(a); //在电脑串口端查看第1个数a。
- View(b); //在电脑串口端查看第2个数b。
- View(c); //在电脑串口端查看第3个数c。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
上述节选的main函数代码里,比如“a=100; //给变量a赋值。”这行代码,所谓的“赋值”就是“=”这个语句,它表面上像我们平时用的等于号,实际上不是等于号,而是代表“给”的意思,把“=”符号右边的数复制一份给左边的变量,比如“a=100;”就是代表把100这个数值复制一份给变量a,执行这条指令后,a就等于100了。这里的分号“;”代表一条程序指令的结束。 而双斜线“//”是注释语句,双斜线“//”这行后面的文字或字符都是用来注释用的,编译器会忽略双斜线“//”这一行后面的文字或字符,编译器不把注释文字或字符列入源代码,也就是“//”这一行中后面的文字或字符是不占单片机内存的。当然“//”仅仅局限于当前一行代码。上面除了“//”是注释语句外,上面的“/*”和“*/”之间也是注释语句,跟”//”的作用一样,只不过“/*”是注释开始,“*/”是注释结束,它们的范围不局限于一行,而是从“/*”到“*/”的范围,因此可以用于注释连着的多行文字或者字符。
接着在分析上述代码中最重要的函数,也是本节最核心最重要的函数View(某个变量)。比如“ View(a); ”这行代码,View(a)就是要把变量a的十进制,十六进制和二进制的数值都发送到串口,我们通过USB转串口线让学习板连接上电脑,在电脑串口助手软件上就能看到被View函数调用的变量a的信息。
【11.3 如何在电脑上使用串口助手软件查看被View函数调用的变量?】
前面章节在讲烧录程序时提到一个叫“stc-isp-15xx-v6.85I”的上位机软件,这个软件除了用来烧录程序,还集成了串口助手软件的功能。所以本节直接共用烧录程序时的USB转串口线和“stc-isp-15xx-v6.85I”软件就可以了,无需额外再购买新的USB转串口线和下载其它串口助手软件,但是如何设置这个“stc-isp-15xx-v6.85I”上位机软件,还是有一些需要特别注意的地方的,现在把这个详细的步骤介绍给大家。
第一步:设置烧录软件的选项。
按前面章节介绍烧录程序时所需的步骤,用USB转串口线连接51学习板和电脑,记录COM号,打开“stc-isp-15xx-v6.85I”软件,选择单片机型号,选择对应的串口号(COM号),设置最低波特率和最高波特率,这部分的内容跟烧录程序时的配置步骤是一样的,唯一必须要特别注意的是最高波特率必须选择9600!最低波特率建议选择2400。否则在烧录完程序后,当上位机集成软件自动切换到串口助手软件窗口时,接收区域显示的一些汉字信息可能会出现乱码。
----------------------------------步骤之间的分割线----------------------------------------
图11.3.2
第二步:设置串口助手软件的选项。
先点击右上方选中“串口助手”选项切换到串口助手的窗口,接收缓冲区选择“文本模式”,串口选择匹配的COM号(跟烧录软件一致的COM号),波特率必须选择9600,勾选上“编程完成后自动打开串口”选项,最后点击“打开串口”按钮使之切换到显示“关闭串口”的文字状态,至此串口助手软件的设置完毕。接下来就是按烧录程序的流程,打开新的HEX程序文件,程序烧录完成后上位机软件会自动切换到串口助手的串口,就可以观察到View函数从单片机上发送过来的某个变量的十进制,十六进制,二进制的信息了。接收缓冲区的窗口比较小,如果收到的信息比较多,只要在上下方向拖动窗口右边的滑块就可以依次看到全部的信息。如果想让单片机重新发送数据,只要让51学习板断电重启就可以重发一次数据,当串口助手的接收区接收的信息太多影响观察时,大家可以点击“清空接收区”的按钮来清屏,然后断电重启让它再重发一次数据。在电脑的串口助手软件里观察到的数据格式大概是什么样子的呢?比如编译完本章节上述完整的模板源代码程序后,会在串口助手软件里看到a,b,c三个变量的信息如下:
- 开始...
- 第1个数
- 十进制:100
- 十六进制:64
- 二进制:1100100
- 第2个数
- 十进制:10000
- 十六进制:2710
- 二进制:10011100010000
- 第3个数
- 十进制:1000000000
- 十六进制:3B9ACA00
- 二进制:111011100110101100101000000000
复制代码
多说一句,烧录程序后,当软件自动切换到串口助手软件选项的窗口时,串口助手窗口显示单片机返回的信息,这时有可能第一行的文字“开始...”会丢失或者显示不出来,但是后面其它的关键信息不受影响,我猜测可能是串口助手软件本身的某个环节存在的小bug,跟我们没关系,我们不用深究原因,因为不会影响我们的使用。此时如果让单片机断电重启就可以看到第一行的文字“开始...”。
【11.4 如何利用现有的工程编辑编译新的源代码?】
本教程后面有很多章节的源代码,是不是每个章节都要重新建一个工程?其实不用。我们只要用一个工程就可以编译编辑本教程所有章节的源代码。方法很简单,就是打开一个现有的工程,用快捷组合键“Ctrl+A”把原工程里面的C源代码全部选中,再按“Backspace”清空原来的代码,然后再复制本教程相关章节的代码粘贴到工程的C文档里,重新编译一次就可以得到对应的Hex格式的烧录文件。用这种方法的时候,建议大家做好每个程序代码的备份。每完成一个项目的小进度,都要及时把源代码存储到电脑硬盘里,电脑硬盘里每个项目对应一个项目文件夹,每个项目文件夹里包含很多不同版本编号的源代码文件,每个源代码文件名都有流水编号,方便识别最新版本的程序,每天下班前都要把最新版本的源代码文件上传到自己的网盘里备份,在互联网时代,把源代码存到自己的网盘,可以随时异地存取,即使遇到电脑故障损坏也不担心数据永久丢失。
【11.5 编辑源代码的5个常用快捷键。】
介绍一下常用的快捷键,好好利用这5个快捷键,会让你在编辑源代码时效率明显提高。
(1)选中整篇所有的内容:组合键Ctrl+A。
(2)把选中的内容复制到临时剪贴板:组合键Ctrl+C。
(3)把临时剪贴板的内容粘贴到光标开始处:组合键Ctrl+V。
(4)把选中的一行或者几行内容整体往右边移动:单键Tab。每按一次就移动几个空格,很实用。
(5)把选中的一行或者几行内容整体往左边移动:组合键Shift+Tab。每按一次就移动几个空格,很实用。
作者: jianhong_wu 时间: 2016-4-3 10:21
本帖最后由 jianhong_wu 于 2016-4-3 10:52 编辑
第十二节:变量的定义和赋值。
第十二节_pdf文件.pdf
(105.69 KB, 下载次数: 4166)
【12.1 学习C语言的建议和方法。】
先提一些学C语言的建议和方法,帮大家删繁就简,去掉一些初学者常见的思想包袱。现阶段我们的学习是使用单片机,把单片机当做一个成品,把单片机当做一个忠诚的士兵,学习C语言就是学习如何使用单片机,如何命令单片机,如何让单片机听懂我们的话并且听我们指挥。单片机内部太细节的构造原理暂时不用过多去关注,只要知道跟我们使用相关的几个特征就可以,这样初学者的学习包袱就没有那么重,就可以把重点放在使用上的,而不是好奇于根本原理的死磕到底。学C语言跟学习英语的性质是一样的,都是在学习一门外语,只是C语言比英语的语法要简单很多,非常容易上手,词汇量也没有英语那么多,C语言常用单词才几十个而已。学习任何一门语言的秘诀在于练习,学习C语言的秘诀是多在单片机上练习编程。本教程后面几乎每个章节都有例程,这个例程很重要,初学者即使看懂了,我也强烈建议要把“C语言学习区域”的那部分代码亲自上机敲键盘练习一遍,并且看看实验现象是否如你所愿。
【12.2 变量定义和赋值的感性认识。】
这些年我用过很多单片机,比如51,PIC,LPC17系列,STM8,STM32等单片机。尽管各类单片机有一些差异,但是在选单片机时有3个参数我们一定会关注的,它们分别是:工作频率,数据存储器RAM,程序存储器ROM。工作频率跟晶振和倍频有关,决定了每条指令所要损耗的时间,从而决定了运算速度。RAM跟代码里所定义变量的数量有关。ROM跟程序代码量的大小有关。程序是什么?程序就是由对象和行为两者构成的。对象就是变量,就是变量的定义,就是RAM,RAM的大小决定了一个程序允许的对象数量。行为就是赋值,判断,跳转,运算等语法,就是ROM,ROM的大小决定了一个程序允许的行为程度。本节的标题是“变量的定义和赋值”,其中“定义”就是对象,“赋值”就是行为。
【12.3 变量的定义。】
变量的定义。一个程序最大允许有多少个对象,是由RAM的字节数决定的(字节是一种单位,后面章节会讲到)。本教程的编译环境是以AT89C52芯片为准,AT89C52这个单片机有256个字节的RAM,但是并不意味着程序就一定要全部占用这些RAM。程序需要占用多少RAM,完全是根据程序的实际情况来决定,需要多少就申请多少。这里的“对象”就是变量,这里的“申请”就是变量的定义。
定义变量的关键字。常用有3种容量的变量,每种变量的取值范围不一样。第一种是”unsigned char”变量,取值范围从0到255,占用RAM一个字节,比喻成一房一厅。第二种是”unsigned int”变量,取值范围从0到65535,占用RAM两个字节,比喻成两房一厅。第三种是“unsigned long”变量,取值范围从0到4294967295,占用RAM四个字节,比喻成四房一厅。unsigned char,unsigned int和unsigned long都是定义变量的关键字,所谓关键字也可以看成是某门外语的单词,需要大家记忆的,当然不用死记硬背,只要多上机练习就自然熟记于心,出口成章。多说一句,上述的变量范围是针对本教程所用的单片机,当针对不同的单片机时上述变量的范围可能会有一些小差异,比如在stm32单片机中,unsigned int的字节数就不是两个字节,而是四个字节,这些都是由所选的编译器决定的,大家暂时有个初步了解就可以。
定义变量的语法格式。定义变量的语法格式由3部分组成:关键字,变量名,分号。比如:
其中unsigned char就是关键字,a就是变量名,分号”;”就是一条语句的结束符号。
变量名的命名规则。变量名的第一个字符不能是数字,必须是字母或者下划线,字母或者下划线后面可以带数字,一个变量名之间的字符不能带空格,两个独立变量名之间也不能用空格隔开(但是两个独立变量名之间可以用逗号隔开)。变量名不能跟编译器已征用的关键字重名,不能跟函数名重名,这个现象跟古代要求臣民避讳皇帝的名字有点像。哪些名字是合法的,哪些名字是不合法的?现在举一些例子说明:
- unsigned char 3a; //不合法,第一个字符不能是数字。
- unsigned char char; //不合法,char是编译器已征用的关键字。
- unsigned char a b; //不合法,ab是一个变量名,a与b的中间不能有空格。
- unsigned char a,b; //合法,a和b分别是一个独立的变量名,a与b的中间可以用逗号隔开。
- unsigned char a; //合法。
- unsigned char abc; //合法。
- unsigned char _ab; //合法。
- unsigned char _3ab; //合法。
- unsigned char a123; //合法。
- unsigned char a12ced; //合法。
复制代码
定义变量与RAM的内在关系。当我们定义一个变量时,相当于向单片机申请了一个RAM空间。C编译器会自动为这个变量名分配一个RAM空间,每个字节的RAM空间都有一个固定唯一的地址。把每个字节的RAM空间比喻成房间,这个地址就是房号。地址是纯数字编号,不利于我们记忆,C语言编译器为了降低我们的工作难度,不用我们记每个变量的地址,只需要记住这个变量的名称就可以了。操作某个变量名,就相当于操作某个对应地址的RAM空间。变量名与对应地址RAM空间的映射关系是C编译器暗中悄悄帮我们分配好的。比如:
- unsigned char a; //a占用一个字节的RAM空间,这个空间的地址由C编译自动分配。
- unsigned char b; //b占用一个字节的RAM空间,这个空间的地址由C编译自动分配。
- unsigned char c; //c占用一个字节的RAM空间,这个空间的地址由C编译自动分配。
复制代码
上述a,b,c三个变量各自占用一个字节的RAM空间,同时被C编译器分配了3个不同的RAM空间地址。
变量定义的初始化。变量定义之后,等于被C编译器分配了一个RAM空间,那么这个空间里面存储的数据是什么?如果没有刻意给它初始化,RAM空间里面存储的数据是不太确定的,是默认的。有些场合,需要在给变量分配RAM空间时就给它一个固定的初始值,这就是变量定义的初始化。变量初始化的语法格式由3部分组成:关键字,变量名赋值,分号。比如:
其中unsigned char就是关键字。
其中a=9就是变量名赋值。a从被C编译器分配RAM空间那一刻起,就默认是预存了一个9的数据。
分号“;”就是一条语句的结束符号。
【12.4 变量的赋值。】
赋值语句的含义。把右边对象的内容复制一份给左边对象。赋值语句有一个很重要的特性,就是覆盖性,左边对象原来的内容会被右边对象复制过来的新内容所覆盖。比如,左边对象是变量a,假设原来a里面存的数据是3,右边对象是数据6,执行赋值语句后,会把右边的6赋值给了对象a,那么a原来的数据3就被覆盖丢失了,变成了6。
赋值语句的格式。赋值语句的语法格式由4部分组成:左边对象,关键字,右边对象,分号。比如:
其中a就是左边对象。
其中“=”就是关键字。写法跟我们平时用的等于号是一样,但是在C语言里不是等于的意思,而是代表赋值的意思,它是代表中文含义的“给”,而不是用于判断的“等于”,跟等于号是两码事(C语言的等于号是“==”,这个后面章节会讲到)。
其中b就是右边对象。
其中分号“;”代表一条语句的结束符。
赋值语句与ROM的关系。赋值语句是行为的一种,所以编译会把赋值这个行为翻译成对应的指令,这些指令在下载程序时最终也是以数据的形式存储在ROM里,指令也是以字节为单位(字节是一种单位,后面章节会讲到)。本教程的编译环境是以AT89C52芯片为准,AT89C52这个单片机有8K的ROM容量,也就是有8192个字节的ROM(8乘以1024等于8192),但是并不意味着程序就一定要全部占用这些ROM。程序需要占用多少ROM,完全是根据程序的行为程度决定,也就是通常所说的你的程序容量有多大,有多少行代码。多说一句,在单片机或者我们常说的计算机领域里,存储容量是以字节为单位,而每K之间的进制不是我们日常所用的1000,而是1024,所以刚才所说的8K不是8000,而是8192,这个是初学者很容易迷惑的地方。刚才提到,赋值语句是行为,凡是程序的行为指令都存储在单片机的ROM区。C编译器会把一条赋值语句翻译成对应的一条或者几条机器码,机器码指令也是以字节为单位的。下载程序的时候,这些机器码就会被下载进单片机的ROM区。比如以下这行赋值语句:
- unsigned char a;
- unsigned char b=3;
- a=b;
复制代码
经过C编译器编译后会生成以字节为单位的机器码。这些机器码记录着这些信息:变量a的RAM地址,变量b的RAM地址和初始化时的预存数据3,以及把b变量的内容赋值给a变量的这个行为。所有这些信息,不管是“数据”还是“行为”,本质都是以“数据”(或称数字,数码都可以)的形式存储记录的,单位是字节。
【12.5 例程的分析和练习。】
接下来练习一个程序实例。直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。本章节在“C语言学习区域”练习的代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a; //定义的变量a被分配了一个字节的RAM空间,保存的数据是不确定的默认值。
- unsigned char b; //定义的变量b被分配了一个字节的RAM空间,保存的数据是不确定的默认值。
- unsigned char c; //定义的变量c被分配了一个字节的RAM空间,保存的数据是不确定的默认值。
- unsigned char d=9; //定义的变量d被分配了一个字节的RAM空间,保存的数据被初始化成9.
- b=3; //把3赋值给变量b,b由原来不确定的默认数据变成了3。
- c=b; //把变量b的内容复制一份赋值给左边的变量c,c从不确定的默认值变成了3。
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
开始...
- 第1个数
- 十进制:255
- 十六进制:FF
- 二进制:11111111
- 第2个数
- 十进制:3
- 十六进制:3
- 二进制:11
- 第3个数
- 十进制:3
- 十六进制:3
- 二进制:11
- 第4个数
- 十进制:9
- 十六进制:9
- 二进制:1001
复制代码
分析:
第1个数a居然是255,这个255从哪来?因为a我们一直没有给它初始值,也没有给它赋值,所以它是不确定的默认值,这个255就是所谓的不确定的默认值,是编译器在定义变量a时分配的,带有不确定的随机性,不同的编译器可能分配的默认值都会存在差异。根据我的经验,unsigned char类型定义的默认值往往是0或者255(255是十六进制的0xff,十六进制的内容后续章节会讲到)。
作者: jianhong_wu 时间: 2016-4-10 10:21
本帖最后由 jianhong_wu 于 2016-4-10 10:45 编辑
第十三节:赋值语句的覆盖性。
第十三节_pdf文件.pdf
(69.44 KB, 下载次数: 3969)
【13.1 什么是赋值语句的覆盖性?】
上述代码,执行完这条赋值语句后,会把右边变量b的数值复制一份给左边变量a,a获得了跟b一样的数值,但是a原来自己的数值却丢失了,为什么会丢失?就是因为被b复制过来的新数据给覆盖了,这就是赋值语句的覆盖性。
【13.2 例程的分析和练习。】
既然赋值语句有覆盖性的特点,那么如何让两个变量相互交换数值?假设a原来的数据是1,b原来的数据是5,交换数据后,a的数据应该变为5,b的数据应该变为1,怎么做?很多初学者刚看到这么简单的题目,会马上根据日常生活的思路,你把你的东西给我,我把我的东西给你,就两个步骤而已,看似很简单,现在按这个思路编写一段程序看看会出什么问题,代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a=1; //定义的变量a被分配了1个字节的RAM空间,保存的数据被初始化成1。
- unsigned char b=5; //定义的变量b被分配了1个字节的RAM空间,保存的数据被初始化成5。
- b=a; //第一步:为了交换,先把a的数赋值给b。
- a=b; //第二步:为了交换,再把b的数赋值给a。
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第2个数
- 十进制:1
- 十六进制:1
- 二进制:1
复制代码
分析:
第1个数a和第2个数b居然都是1!这不是我们想要的结果。我们要的交换结果是:交换后,a变为5,b变为1。在哪个环节出了问题?把镜头切换到上述代码的“第一步”和“第二步”,由于b的数据在执行完“第一步”后,b自己原来的数据5被覆盖丢失了变成新的数据1,接着执行“第二步”后,此时相当于把 b的新数据1赋值给a,并没有5!所以a和b的数据都是1,不能达到交换后“a为5,b为1”的目的。其实就是赋值语句的覆盖性在作祟。
上述交换数据的程序宣告失败!怎么办?既然赋值语句具有覆盖性,那么两变量想交换数据,就必须借助第三方变量来寄存,此时只需要多定义一个第三方变量t。正确的代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a=1; //定义的变量a被分配了1个字节的RAM空间,保存的数据被初始化成1。
- unsigned char b=5; //定义的变量b被分配了1个字节的RAM空间,保存的数据被初始化成5。
- unsigned char t; //定义一个第三方变量t,用来临时寄存数值。
- t=b; //第一步:为了避免b的数据在赋值后被覆盖丢失,先寄存一份在第三方变量t那里。
- b=a; //第二步:把a的数赋值给b,b原来的数据虽然丢失,但是b在t变量那里有备份。
- a=t; //第三步:再把b在t变量里的备份赋值给a。注意,这里不能用b,因b原数据已被覆盖。
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:5
- 十六进制:5
- 二进制:101
- 第2个数
- 十进制:1
- 十六进制:1
- 二进制:1
复制代码
分析:
实验结果显示,两变量的数值交换成功。
【13.3 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-4-17 09:19
本帖最后由 jianhong_wu 于 2016-4-17 09:43 编辑
第十四节:二进制与字节单位,以及常用三种变量的取值范围。
第十四节_pdf文件.pdf
(81.21 KB, 下载次数: 3804)
【14.1 为什么要二进制?】
为什么要二进制?我们日常生活明明是十进制的,为何数字电子领域偏要选择二进制?这是由数字硬件电路决定的。人有十个手指头,人可以直接发出十种不同声音来命名0,1,2,3...9这些数字,人可以直接用眼睛识别出十种不同状态的信息,但是数字底层基础硬件电路要直接处理和识别十种状态却很难,相对来说,处理和识别两种状态就轻松多了,所以选择二进制。比如,一颗LED灯的亮或灭,一个IO口的输出高电平或低电平,识别某一个点的电压是高电平或低电平,只需要三极管等基础元器件就可把硬件处理电路搭建起来,二进制广泛应用在数字电路的存储,通讯和运算等领域,想学好单片机就必须掌握它。
【14.2 二进制如何表示成千上万的大数值?】
二进制如何表示成千上万的数值?现在用LED灯的亮和灭来跟大家讲解。
(1)1个LED灯:
合计:共2种状态。
(2)2个LED灯挨着:
- 灭灭 第0种状态
- 灭亮 第1种状态
- 亮灭 第2种状态
- 亮亮 第3种状态
复制代码
合计:共4种状态。
(3)3个LED灯挨着:
- 灭灭灭 第0种状态
- 灭灭亮 第1种状态
- 灭亮灭 第2种状态
- 灭亮亮 第3种状态
- 亮灭灭 第4种状态
- 亮灭亮 第5种状态
- 亮亮灭 第6种状态
- 亮亮亮 第7种状态
复制代码
合计:共8种状态。
(4)8个LED灯挨着:
- 灭灭灭灭灭灭灭灭 第0种状态
- 灭灭灭灭灭灭灭亮 第1种状态
- ...... 第N种状态
- 亮亮亮亮亮亮亮灭 第254种状态
- 亮亮亮亮亮亮亮亮 第255种状态
复制代码
合计:共256种状态。
(5)16个LED灯挨着:
- 灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭 第0种状态
- 灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭亮 第1种状态
- ...... 第N种状态
- 亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮灭 第65534种状态
- 亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮 第65535种状态
复制代码
合计:共65536种状态。
(6)32个LED灯挨着:
- 灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭 第0种状态
- 灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭灭亮 第1种状态
- ...... 第N种状态
- 亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮灭 第4294967294种状态
- 亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮亮 第4294967295种状态
复制代码
合计:共4294967296种状态。
结论:
连续挨着的LED灯越多,能表达的数值范围就越大。
【14.3 什么是位?】
什么是位?以上一个LED灯就代表1位,8个LED灯就代表8位。位的英文名是用bit来表示。一个变量的位数越大就意味着这个变量的取值范围越大。一个单片机的位数越大,就说明这个单片机一次处理的数据范围就越大,意味着运算和处理速度就越快。我们日常所说的8位单片机,32位单片机,就是这个位的概念。为什么32位的单片机比8位单片机的处理和运算能力强,就是这个原因。
【14.4 什么是字节?】
什么是字节?字节是计算机很重要的一个基本单位,一个字节有8位。8个LED灯挨着能代表多少种状态,就意味着一个字节的数据范围有多大。从上面举的例子中,我们知道8个LED灯挨着,能表示从0到255种状态,所以一个字节的取值范围就是从0到255。
【14.5 三种常用变量的取值范围是什么?】
前面章节曾提到三种常用的变量:unsigned char,unsigned int ,unsigned long。现在有了二进制和字节的基础知识,就可以跟大家讲讲这三种变量的取值范围,而且很重要,这是我们写单片机程序必备的概念。
unsigned char的变量占用1个字节RAM,共8位,根据前面LED灯的例子,取值范围是从0到255。
unsigned int的变量占用2个字节RAM,共16位,根据前面LED灯的例子,取值范围是从0到65535。多说一句,对于51内核的单片机,unsigned int的变量是占用2个字节。如果是在32位的stm32单片机,unsigned int的变量是占用4个字节的,所以不同的单片机不同的编译器是会有一些差异的。
unsigned long的变量占用4个字节RAM,共32位,根据前面LED灯的例子,取值范围是从0到4294967295。
【14.6 例程练习和分析。】
现在我们编写一个程序来验证unsigned char,unsigned int,unsigned long的取值范围。
定义两个unsigned char变量a和b,a赋值255,b赋值256,255和256恰好处于unsigned char的取值边界。
再定义两个unsigned int变量c和d,c赋值65535,d赋值65536,65535和65536恰好处于unsigned int的取值边界。
最后定义两个unsigned long变量e和f,e赋值4294967295,f赋值4294967296,4294967295和4294967296恰好处于unsigned long的取值边界。
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a; //定义一个变量a,并且分配了1个字节的RAM空间。
- unsigned char b; //定义一个变量b,并且分配了1个字节的RAM空间。
- unsigned int c; //定义一个变量c,并且分配了2个字节的RAM空间。
- unsigned int d; //定义一个变量d,并且分配了2个字节的RAM空间。
- unsigned long e; //定义一个变量e,并且分配了4个字节的RAM空间。
- unsigned long f; //定义一个变量f,并且分配了4个字节的RAM空间。
- a=255; //把255赋值给变量a,a此时会是什么数?会超范围溢出吗?
- b=256; //把256赋值给变量b,b此时会是什么数?会超范围溢出吗?
- c=65535; //把65535赋值给变量c,c此时会是什么数?会超范围溢出吗?
- d=65536; //把65536赋值给变量d,d此时会是什么数?会超范围溢出吗?
- e=4294967295; //把4294967295赋值给变量e,e此时会是什么数?会超范围溢出吗?
- f=4294967296; //把4294967296赋值给变量f,f此时会是什么数?会超范围溢出吗?
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- View(e); //把第5个数e发送到电脑端的串口助手软件上观察。
- View(f); //把第6个数f发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:255
- 十六进制:FF
- 二进制:11111111
- 第2个数
- 十进制:0
- 十六进制:0
- 二进制:0
- 第3个数
- 十进制:65535
- 十六进制:FFFF
- 二进制:1111111111111111
- 第4个数
- 十进制:0
- 十六进制:0
- 二进制:0
- 第5个数
- 十进制:4294967295
- 十六进制:FFFFFFFF
- 二进制:11111111111111111111111111111111
- 第6个数
- 十进制:0
- 十六进制:0
- 二进制:0
复制代码
分析:
通过实验结果,我们知道unsigned char变量最大能取值到255,如果非要赋值256就会超出范围溢出后变成了0。unsigned int变量最大能取值到65535,如果非要赋值65536就会超出范围溢出后变成了0。unsigned long变量最大能取值到4294967295,如果非要赋值4294967296就会超出范围溢出后变成了0。
【14.7 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: 转眼十年未谋面 时间: 2016-4-20 16:26
鸿哥你好,我是当初在群里跟你因为一些技术问题有过争执的“转眼十年未谋面”。。。今日偶然看见你这个帖子,对你的奉献精神感到十分钦佩和仰慕。。。。分享和开源也一直是我的技术信条,愿与君共勉。。
作者: jianhong_wu 时间: 2016-4-23 10:31
本帖最后由 jianhong_wu 于 2016-4-23 10:54 编辑
第十五节:二进制与十六进制。
第十五节_pdf文件.pdf
(75.46 KB, 下载次数: 3616)
【15.1 十六进制是二进制的缩写。】
在我的印象中,C51编译器好像并不支持二进制的书写格式,即使它能支持二进制的书写格式,二进制的书写还是有个弊端,就是数字太多太长了,写起来非常费劲不方便,怎么办?解决办法就是用十六进制。十六进制是二进制的缩写,之所以称它为二进制的缩写,是因为它们的转换关系非常简单直观,不需要借助计算器即可相互转换。
【15.2 何谓十六进制?】
何谓十六进制?欲搞清楚这个问题,还得先从十进制说起。所谓十进制,就是用一位字符可以表示从0到9这十个数字。所谓二进制,就是用一位字符可以表示从0到1这二个数字。所谓十六进制,当然也就是用一位字符可以表示从0到15这十六个数字。但是十六进制马上就会面临一个问题,十六进制的10到15这6个数其实是有两位字符组成的,并不是一位呀?于是C语言用这些字符A,B,C,D,E,F分别替代10,11,12,13,14,15这6个数,10前面的0到9还是跟十进制的字符一致。A,B,C,D,E,F也可以用小写a,b,c,d,e,f来替代,在数值上不区分大小写,比如十六进制的a与A都是表示十进制的10。
【15.3 二进制与十六进制是如何转换的?】
前面提到了十六进制是二进制的缩写,它们的转换关系非常简单直观,每1位十六进制的字符,对应4位二进制的字符。关系如下:
十进制 二进制 十六进制
0 0000 0
1 0001 1
2 0010 2
3 0011 3
4 0100 4
5 0101 5
6 0110 6
7 0111 7
8 1000 8
9 1001 9
10 1010 A
11 1011 B
12 1100 C
13 1101 D
14 1110 E
15 1111 F
二进制转换成十六进制的时候,如果不是4位的倍数,则最左边高位默认补上0凑合成4位的倍数。比如一个二进制的数101001,可以在左边补上2个0变成00101001,然后把每4位字符转成1个十六进制的字符。左边高4位0010对应十六进制的2,右边低4位1001对应十六进制的9,所以二进制的101001合起来最终转换成十六进制的数是29(实际上正确的写法是0x29,为什么?请继续往下看。)。
【15.4 十六进制数的标准书写格式是什么样子的?】
十六进制的标准书写格式是什么样子的?实际上,十六进制29并不能直接写成29,否则就跟十进制的写法混淆了。为了把十六进制和十进制的书写格式进行区分,C语言规定凡是十六进制必须加一个数字0和一个字母x作为前缀,也就是十六进制必须以0x作为前缀,所以刚才的十六进制29就应该写成0x29,否则,如果直接写29编译器会认为是十进制的29,而十进制的29转换成十六进制是0x1D(十进制与十六进制之间如何转换在后面章节会讲到),0x29与0x1D可见差别很大的,凡是不加前缀的都会被默认为十进制。 多说一句,在C语言程序里,对于同样一个数值,既可以用十六进制,也可以用十进制,比如:d=0x2C与d=44的含义是一样的,因为十六进制的0x2C和十进制的44最终都会被C51编译器翻译成二进制00101100,是表示同样大小的数值。
【15.5 例程练习和分析。】
现在我们编写一个程序来观察十六进制和二进制的关系。
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a; //定义一个变量a,并且分配了1个字节的RAM空间。
- unsigned char b; //定义一个变量b,并且分配了1个字节的RAM空间。
- unsigned char c; //定义一个变量c,并且分配了1个字节的RAM空间。
- unsigned char d; //定义一个变量d,并且分配了1个字节的RAM空间。
- a=0x06; //十六进制前记得加0x前缀,超过9部分的字母不分大小写。
- b=0x0A; //十六进制前记得加0x前缀,超过9部分的字母不分大小写。
- c=0x0e; //十六进制前记得加0x前缀,超过9部分的字母不分大小写。
- d=0x2C; //十六进制前记得加0x前缀,超过9部分的字母不分大小写。
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:6
- 十六进制:6
- 二进制:110
- 第2个数
- 十进制:10
- 十六进制:A
- 二进制:1010
- 第3个数
- 十进制:14
- 十六进制:E
- 二进制:1110
- 第4个数
- 十进制:44
- 十六进制:2C
- 二进制:101100
复制代码
分析:
通过实验结果,我们知道二进制与十六进制的转换关系确实非常清晰简单,所以十六进制也可以看作是二进制的缩写。
【15.6 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-5-2 10:21
本帖最后由 jianhong_wu 于 2016-5-2 10:59 编辑
第十六节:十进制与十六进制。
第十六节_pdf文件.pdf
(329 KB, 下载次数: 3580)
【16.1 十进制与十六进制各自的应用场合。】
C语言程序里只用了十进制和十六进制这两种书写格式,有的初学者会问,为什么没有用二进制?我的回答是:不是没有用二进制,而是十六进制已经代表了二进制,因为十六进制就是二进制的缩写形式,所以可以把十六进制和二进制看作是同一个东西。
十进制和十六进制各自有什么应用场合?十六进制方便人们理解机器,通常应用在配置寄存器,底层通讯驱动,底层IO口驱动,以及数据的移位、转换、合并等场合,在底层驱动程序方面经常要用到。而十进制则方便人们直观理解数值的大小,在程序应用层要经常用到。总之,进制只是数据的表现形式而已,不管是什么进制的数,最终经过编译后都可以看做是二进制的数据。
【16.2 十进制与十六进制相互转换的方法。】
十进制与十六进制如何相互转换?其实很多教科书上都有介绍它们之间如何通过手工计算进行转换的方法,这种方法当然是有助于我们深入理解数据的含义和转换关系,有兴趣的朋友可以自己找相关书籍来看看,但是在实际应用中,我本人是从来没有用过这种手工计算方法,而我用的方法是最简单直接的,就是借助电脑自带的计算器进行数制转换即可。现在把这种方法介绍给大家,以WIND7系统的电脑为例来讲解详细的操作步骤。
图16.2.1.1 点击“所有程序”选项切换到系统自带程序的窗口
图16.2.1.2 在“附件”子菜单下点击“计算器”启动此软件
图16.2.1.3 已启动的“计算器”软件界面
第一步:打开电脑自带的计算器。
点击电脑左下角“开始” 菜单,在菜单中点击“所有程序”选项切换到自带程序的窗口,在此窗口下,再点击“附件”的文件夹图标,在“附件”子菜单下点击“计算器”启动此软件。
----------------------------------步骤之间的分割线----------------------------------------
图16.2.2.1 把“计算器”的主界面切换到“程序员”界面
图16.2.2.2 已打开的“程序员”界面
第二步:把“计算器”的主界面切换到“程序员”界面。
点击打开左上角“查看”的下拉菜单,在下拉菜单中选择“程序员”选项。
----------------------------------步骤之间的分割线----------------------------------------
图16.2.3.1 在十进制的选项下输入十进制的数据
图16.2.3.2 把十进制的数据转换成十六进制的数据
第三步:十进制转换成十六进制的方法。
点击勾选中“十进制”选项,在此选项下输入十进制的数据,输入数据后,再切换点击勾选“十六进制”,即可完成从十进制到十六进制的数据转换。比如输入十进制的“230”,切换到十六进制后就变成了“E6”。
----------------------------------步骤之间的分割线----------------------------------------
图16.2.4.1 在十六进制的选项下输入十六进制的数据
图16.2.4.2 把十六进制的数据转换成十进制的数据
第四步:十六进制转换成十进制的方法。
点击勾选中“十六进制”选项,在此选项下输入十六进制的数据,输入数据后,再切换点击勾选“十进制”,即可完成从十六进制到十进制的数据转换。比如输入十六进制的“AC”,切换到十进制后就变成了“172”。
----------------------------------步骤之间的分割线----------------------------------------
第五步:十六进制,十进制,八进制,二进制它们四者之间相互转换的方法。
我们看到“计算器”软件里已经包含了十六进制,十进制,八进制,二进制这四个选项,所以它们之间相互转换的方法跟上面介绍的步骤是一样的。
----------------------------------步骤之间的分割线----------------------------------------
【16.3 例程练习和分析。】
现在我们编写一个程序来验证上面讲到的两个例子:
(1)输入十进制的230,看看它的十六进制是什么样的。
(2)输入十六进制的AC,看看它的十进制是什么样的。
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a; //定义一个变量a,并且分配了1个字节的RAM空间。
- unsigned char b; //定义一个变量b,并且分配了1个字节的RAM空间。
- a=230; //把十进制的230赋值给变量a,在串口助手上观察一下它的十六进制是不是E6。
- b=0xAC; //把十六进制的AC赋值给变量b,在串口助手上观察一下它的十进制是不是172。
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:230
- 十六进制:E6
- 二进制:11100110
- 第2个数
- 十进制:172
- 十六进制:AC
- 二进制:10101100
复制代码
分析:
通过实验结果,发现在单片机上转换的结果和在电脑自带“计算器”上转换的结果是一样的。
【16.4 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-5-9 14:08
本帖最后由 jianhong_wu 于 2016-5-9 14:37 编辑
第十七节:加法运算的5种常用格式。
第十七节_pdf文件.pdf
(75.82 KB, 下载次数: 5004)
【17.1 单片机本身具备基础的数学算术能力。】
单片机本身是一个成品,本身就具备了基础的加减乘除能力,把单片机当做一个大人,我们需要做的只是沟通而已,叫他做加法他就做加法,叫他做减法就他就做减法,至于他是如何计算出来的不用管,“他”本身内部的电路结构就具备了这种基础运算的能力。人机沟通依然是用C语言,本节讲的加法运算,用的C语言符号跟我们日常用的数学加法符号是一样的,都是符号“+”。多说一句,单片机这种内置的基础运算能力并不是无限大的,而是数值不能超过某个范围,如果在加数或者运算结果的数值范围超过4294967295的情况下,要继续实现这类加法运算,这个就需要我们在单片机本身基础的运算能力上专门去编写一套大数据算法的程序才能实现,这个大家暂时不用深入理解,先学好当前基础再说。
【17.2 加法语法格式。】
加法语法格式:
“保存变量”=“加数1”+“加数2”+...+“加数N”;
含义:右边的“加数”与“加数”相加(这里统一把平时所说的被加数也归类为加数),并且把最终的运算结果赋值给左边的“保存变量”。注意,这里的符号“=”不是等于号的意思,而是赋值的意思。左边的“保存变量”必须是变量,不能是常量,否则编译时会报错。而右边的“加数”既可以是变量,也可以是常量,也可以是“保存变量”本身自己。多说一句,什么是变量和什么是常量?变量就是可以在程序中被更改的,是分配的一个RAM空间。而常量往往就是常数值,或者是被分配在ROM空间的一个具体数值。下面根据右边“加数”与“加数”的不同组合,列出了加法运算的5种常用格式。
第1种:“加数1”是常量,“加数2”是常量。比如:
分析:数字“3”和“15”都是常量。执行上述语句后,保存变量a变成了18。
第2种:“加数1”是变量,“加数2”是常量。比如:
- unsigned char b;
- unsigned char x=10;
- b=x+15;
复制代码
分析:x是变量,“15”是常量。由于原来x变量里面的数值是10,执行上述语句后,保存变量b变成 了25。而变量x则保持不变,执行完所有语句后x还是10。
第3种:“加数1”是变量,“加数2”是变量。比如:
- unsigned char c;
- unsigned char x=10;
- unsigned char y=6;
- c=x+y;
复制代码
分析:x是变量,y也是变量。由于原来x变量里面的数值是10,y变量里面的数值是6,执行上述语句后,保存变量c变成了16。而变量x和y则保持不变,x还是10,y还是6。
第4种:“加数1”是保存变量本身,“加数2”是常量。比如:
- unsigned char d=2;
- d=d+18;
- d=d+7;
复制代码
分析:d是保存变量本身,“18”是常量。这类语句有一个特点,具备了自加功能,可以更改自己本身的数值。比如原来保存变量d的数值是2,执行“d=d+18;”语句后,d变成了20,接着再执行完“d=d+7;”语句后,d最后变成了27。
第5种:“加数1”是保存变量本身,“加数2”是变量。比如:
- unsigned char e=2;
- unsigned char x=10;
- unsigned char y=6;
- e=e+x;
- e=e+y;
复制代码
分析:e是保存变量,x与y都是变量。这类语句有一个特点,具备了自加功能,可以更改自己本身的数值。比如原来保存变量e的数值是2,x的数值是10,执行“e=e+x;”语句后,e变成了12。由于y的数值是6,接着再执行完“e=e+y;”语句后,所以e最后变成了18。
【17.3 例程练习和分析。】
现在我们编写一个程序来验证上面讲到的5个加法例子:
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a; //定义一个变量a,并且分配了1个字节的RAM空间。
- unsigned char b; //定义一个变量b,并且分配了1个字节的RAM空间。
- unsigned char c; //定义一个变量c,并且分配了1个字节的RAM空间。
- unsigned char d=2; //定义一个变量d,并且分配了1个字节的RAM空间。初始化默认为2.
- unsigned char e=2; //定义一个变量e,并且分配了1个字节的RAM空间。初始化默认为2.
- unsigned char x=10; //定义一个变量x,并且分配了1个字节的RAM空间。初始化默认为10.
- unsigned char y=6; //定义一个变量y,并且分配了1个字节的RAM空间。初始化默认为6.
- //第1种:“加数1”是常量,“加数2”是常量。
- a=3+15;
- //第2种:“加数1”是变量,“加数2”是常量。
- b=x+15;
- //第3种:“加数1”是变量,“加数2”是变量。
- c=x+y;
- //第4种:“加数1”是保存变量本身,“加数2”是常量。
- d=d+18;
- d=d+7;
- //第5种:“加数1”是保存变量本身,“加数2”是变量。
- e=e+x;
- e=e+y;
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- View(e); //把第5个数e发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:18
- 十六进制:12
- 二进制:10010
- 第2个数
- 十进制:25
- 十六进制:19
- 二进制:11001
- 第3个数
- 十进制:16
- 十六进制:10
- 二进制:10000
- 第4个数
- 十进制:27
- 十六进制:1B
- 二进制:11011
- 第5个数
- 十进制:18
- 十六进制:12
- 二进制:10010
复制代码
分析:
通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。
【17.4 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-5-15 12:02
本帖最后由 jianhong_wu 于 2016-5-15 12:29 编辑
第十八节:连加、自加、自加简写、自加1。
第十八节_pdf文件.pdf
(63.83 KB, 下载次数: 5043)
【18.1 连加。】
上一节的加法例子中,右边的加数只有两个。实际上,C语言规则没有限制加数的个数,它的通用格式如下:
“保存变量”=“加数1”+“加数2”+...+“加数N”;
当右边的加数个数超过两个的时候,这种情况就是我所说的“连加”,每个加数的属性没有限定,可以是常量,也可以是变量。比如:
- a=1+69+102; //加数全部是常量。
- b=q+x+y+k+r; //加数全部是变量。
- c=3+x+y+5+k; //加数有的是常量,有的是变量。
复制代码
连加的运行顺序是,赋值符号“=”右边的加数挨个相加,把每一次的运算结果放在一个临时的隐蔽变量里,这个隐蔽的变量我们看不到,是单片机系统内部参与运算时的专用寄存器,等右边所有的加数连加的计算结果出来后,再把这个隐蔽变量所保存的计算结果赋值给左边的“保存变量”。
【18.2 自加、自加简写、自加1。】
什么是自加?当赋值符号“=”右边的加数只要其中有一个是“保存变量”本身时,这种情况就是“自加”,自加在程序里有一个特点,只要加数不为0,那么每执行一次这行代码,“保存变量”本身就会增大一次,不断执行这行代码,“保存变量”本身就会不断增大,而每次的增大量就取决于赋值符号“=”右边所有加数之和。自加的常见格式如下:
“保存变量”=“保存变量”+“加数1”;
“保存变量”=“保存变量”+“加数1”+“加数2”+...+“加数N”;
在这类自加计算式中,当右边的加数有且仅有一个是“保存变量”本身时,那么上述自加计算式可以简写成如下格式:
“保存变量”+=“加数1”;
“保存变量”+=“加数1”+“加数2”+...+“加数N”;
这种格式就是“自加简写”。现在举几个例子如下:
- d+=6; //相当于d=d+6;
- e+=x; //相当于e=e+x;
- f+=18+y+k; //相当于f=f+18+y+k;
复制代码
这些例子都是很常规的自加简写,再跟大家讲一种很常用的特殊简写。当右边只有两个加数,当一个加数是“保存变量”,另一个加数是常数1时,格式如下:
“保存变量”=“保存变量”+1;
这时候,可以把上述格式简写成如下两种格式:
“保存变量”++;
++“保存变量”;
这两种格式也是俗称的“自加1”操作。比如:
- g++; //相当于g=g+1或者g+=1;
- ++h; //相当于h=h+1或者h+=1;
复制代码
也就是说自加1符号“++”可以在变量的左边,也可以在变量的右边,它们在这里本质是一样的,没有差别,但是,如果是在某些特定情况下,这时自加1符号“++”在左边还是在右边是有差别的,有什么差别呢?这个内容以后再讲。
【18.3 例程练习和分析。】
现在我们编写一个程序来验证上面讲到的例子:
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a; //定义一个变量a,并且分配了1个字节的RAM空间。
- unsigned char b; //定义一个变量b,并且分配了1个字节的RAM空间。
- unsigned char c; //定义一个变量c,并且分配了1个字节的RAM空间。
- unsigned char d=5; //定义一个变量d,并且分配了1个字节的RAM空间。初始化默认为5.
- unsigned char e=5; //定义一个变量e,并且分配了1个字节的RAM空间。初始化默认为5.
- unsigned char f=5; //定义一个变量f,并且分配了1个字节的RAM空间。初始化默认为5.
- unsigned char g=5; //定义一个变量g,并且分配了1个字节的RAM空间。初始化默认为5.
- unsigned char h=5; //定义一个变量h,并且分配了1个字节的RAM空间。初始化默认为5.
- unsigned char q=1; //定义一个变量q,并且分配了1个字节的RAM空间。初始化默认为1.
- unsigned char x=3; //定义一个变量x,并且分配了1个字节的RAM空间。初始化默认为3.
- unsigned char y=6; //定义一个变量y,并且分配了1个字节的RAM空间。初始化默认为6.
- unsigned char k=2; //定义一个变量k,并且分配了1个字节的RAM空间。初始化默认为2.
- unsigned char r=8; //定义一个变量r,并且分配了1个字节的RAM空间。初始化默认为8.
- //第1个知识点:连加。
- a=1+69+102; //加数全部是常量。a的结果为:172。
- b=q+x+y+k+r; //加数全部是变量。b的结果为:20。
- c=3+x+y+5+k; //加数有的是常量,有的是变量。c的结果为:19。
- //第2个知识点:自加。
- d+=6; //相当于d=d+6; d的结果为:11。
- e+=x; //相当于e=e+x; e的结果为:8。
- f+=18+y+k; //相当于f=f+18+y+k; f的结果为:31。
- //第3个知识点:自加1。
- g++; //相当于g=g+1或者g+=1; g的结果为:6。
- ++h; //相当于h=h+1或者h+=1; h的结果为:6。
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- View(e); //把第5个数e发送到电脑端的串口助手软件上观察。
- View(f); //把第6个数f发送到电脑端的串口助手软件上观察。
- View(g); //把第7个数g发送到电脑端的串口助手软件上观察。
- View(h); //把第8个数h发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:172
- 十六进制:AC
- 二进制:10101100
- 第2个数
- 十进制:20
- 十六进制:14
- 二进制:10100
- 第3个数
- 十进制:19
- 十六进制:13
- 二进制:10011
- 第4个数
- 十进制:11
- 十六进制:B
- 二进制:1011
- 第5个数
- 十进制:8
- 十六进制:8
- 二进制:1000
- 第6个数
- 十进制:31
- 十六进制:1F
- 二进制:11111
- 第7个数
- 十进制:6
- 十六进制:6
- 二进制:110
- 第8个数
- 十进制:6
- 十六进制:6
- 二进制:110
复制代码
分析:
通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。
【18.4 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-5-22 11:15
本帖最后由 jianhong_wu 于 2016-5-22 11:34 编辑
第十九节:加法运算的溢出。
第十九节_pdf文件.pdf
(51.85 KB, 下载次数: 5006)
【19.1 什么是加法运算的溢出?】
前面章节介绍的三种数据类型unsigned char ,unsigned int ,unsigned long,它们的数值都是有最大范围的,分别是255,65535,4294967295。如果运算结果超过了变量本身的最大范围,会出现什么结果、有什么规律,这就是本节要讲的溢出。
(1)什么是溢出?先看一个例子如下:
- unsigned char a;
- a=0x8536;
复制代码
分析:
因为a是unsigned char变量,位数是8位,也就是一个字节,而0x8536是16位,两个字节,这种情况下,把两字节的0x8536强行赋值给单字节变量a,变量a只能接收到最低8位的一个字节0x36,而高8位的一个字节0x85就被丢失了,这个就是本节所说的溢出。
(2)再看一个例子如下:
- unsigned char b=0xff;
- b=b+1;
复制代码
分析:
b默认值是0xff,再加1后,变成了0x0100保存在一个隐藏的中间变量,然后再把这个中间变量赋值给单字节变量b,b只能接收到低8位的一个字节0x00,所以运算后b的数值由于溢出变成了0x00。
(3)再看一个例子如下:
- unsigned char c=0xff;
- c=c+2;
复制代码
分析:
c默认值是0xff,再加2后,变成了0x0101保存在一个隐藏中间变量,然后再把这个中间变量赋值给单字节变量c,c只能接收到低8位的一个字节0x01,所以运算后c的数值由于溢出变成了0x01。
(4)再看一个例子如下:
- unsigned int d=0xfffe;
- d=d+5;
复制代码
分析:
d默认值是0xfffe,再加5后,变成了0x10003保存在一个隐藏中间变量,由于这个隐藏的中间变量是unsigned int类型,只能保存两个字节的数据,所以在中间变量这个环节就溢出了,实际上隐藏的中间变量只保存了0x0003,然后再把这个中间变量赋值给16位的两字节变量d,d理所当然就是0x0003。
(5)再看一个例子如下:
- unsigned long e=0xfffffffe;
- e=e+5;
复制代码
分析:
e默认值是0xfffffffe,再加5后,变成了0x100000003保存在一个隐藏中间变量,由于这个隐藏的中间变量是unsigned long类型,只能保存四个字节的数据,所以在中间变量这个环节就溢出了,实际上隐藏的中间变量只保存了0x00000003,然后再把这个中间变量赋值给32位的四字节变量e,e理所当然也是0x00000003。
【19.2 例程练习和分析。】
现在我们编写一个程序来验证上面讲到的例子:
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a; //一个字节
- unsigned char b=0xff; //一个字节
- unsigned char c=0xff; //一个字节
- unsigned int d=0xfffe; //两个字节
- unsigned long e=0xfffffffe; //四个字节
- a=0x8536;
- b=b+1;
- c=c+2;
- d=d+5;
- e=e+5;
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- View(e); //把第5个数e发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:54
- 十六进制:36
- 二进制:110110
- 第2个数
- 十进制:0
- 十六进制:0
- 二进制:0
- 第3个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第4个数
- 十进制:3
- 十六进制:3
- 二进制:11
- 第5个数
- 十进制:3
- 十六进制:3
- 二进制:11
复制代码
分析:
通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。
【19.3 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-5-29 11:38
本帖最后由 jianhong_wu 于 2016-5-29 11:57 编辑
第二十节:隐藏中间变量为何物?
第二十节_pdf文件.pdf
(88.58 KB, 下载次数: 4536)
【20.1 隐藏中间变量为何物?】
“隐藏中间变量”虽然视之不见摸之不着,但是像空气一样无处不在。它有什么规律,是什么类型,数值范围是多大,研究它有什么实用价值?这就是本节要解开之谜。
前面章节提到,两个加数相加,其结果暂时先保存在一个“隐藏中间变量”里,运算结束后才把这个“隐藏中间变量”赋值给左边的“保存变量”。这里的“隐藏中间变量”到底是unsigned int类型还是unsigned long类型?为了研究它的规律,我在keil自带的C51编译环境下,专门编写了几个测试程序来观察实际运行的结果。
“保存变量”=“加数1”+“加数2”;
下面分别变换“保存变量”、“加数1”、“加数2”这三个元素的数据类型,来观察“隐藏中间变量”背后的秘密。
(1)“unsigned int”=“unsigned char”+“unsigned char”;
- unsigned int a;
- unsigned char x1=0x12;
- unsigned char y1=0xfe;
- a=x1+y1;
复制代码
运算结果:a等于0x0110。
分析过程:两个char类型的数相加其运算结果暂时保存在“隐藏中间变量”,当运算结果大于两个“加数”unsigned char本身时,并没有发生溢出现象,unsigned int类型的“保存变量”a最终得到了完整的结果0x0110。
初步结论:这种情况,“隐藏中间变量”估计为unsigned int 类型。
(2)“unsigned long”=“unsigned int”+“unsigned char”;
- unsigned long b;
- unsigned int x2=0xfffe;
- unsigned char y2=0x12;
- b=x2+y2;
复制代码
运算结果:b等于十六进制的0x0010。
分析过程:一个unsigned int类型的数与一个unsigned char类型的数相加,当运算结果大于其中最大加数unsigned int类型本身时,因为左边的“保存变量”本来就是unsigned long类型,所以我本来以为运算结果应该是unsigned long类型的0x00010010,但是实际结果出乎我的意料,最终结果是unsigned int类型的0x0010,显然发生了溢出现象。
初步结论:这种情况,“隐藏中间变量”估计为unsigned int 类型。
(3)“unsigned long”=“常量”+“常量”;
- unsigned long c;
- c=50000+50000;
复制代码
运算结果:c等于100000。
分析过程:unsigned int的最大数据范围是65535,而两个常量相加,其结果超过了65535却还能完整保存下来。
初步结论:这种右边加数都是常量的情况下,“隐藏中间变量”估计等于左边的“保存变量”类型。
(4)“unsigned long”=“unsigned int”+“常量”;
- unsigned long d;
- unsigned long e;
- unsigned int x3=50000;
- d=x3+30000;
- e=x3+50000;
复制代码
运算结果:d等于14464,e等于100000。
分析过程:本来以为d应该等于80000的,结果却是14464显然发生了溢出。而同样的条件下,e是100000却没有发生溢出。
个人结论:这个现象让我突然崩溃,实在研究不下去了。这是一种很怪异的现象,为什么同样的类型,因为常量的不同,一个发生了溢出,另外一个没有发生溢出?这时的“隐藏中间变量”到底是unsigned int类型还是unsigned long类型?我无法下结论。经过上述简单的测试,我发现规律是模糊的,模糊的规律就不能成为规律。如果真要按这种思路研究下去,那真是没完没了,因为还有很多情况要研究,当超过3个以上加数相加,同时存在unsigned long,unsigned int,unsigned char,以及“常量”这4种类型时又是什么规律?在不同的C编译器里又会是什么现象?即使把所有情况的规律摸清楚了又能怎么样,因为那么繁杂很容易忘记导致出错。有什么解决的办法吗?
【20.2 解决办法。】
“当遇到有争议的问题时,与其参与争议越陷越深,还不如想办法及时抽身绕开争议。”根据这个指导思想,我提出一种解决思路“为了避免出现意想不到的溢出,在实际项目中,所有参与运算的变量都预先转化为unsigned long变量,再参与运算。”
当然,也可能有人会问,如果计算结果超过了unsigned long最大范围时怎么办?我的回答是:首先,大多数项目的计算量都比较简单,一般情况下都不会超过unsigned long最大范围,但是,如果真遇到有可
能超过unsigned long最大范围的运算项目时,那么就要用另外一种BCD码数组的运算算法来解决,而这个方法本节暂时不介绍,等以后再讲。 继续回到刚才的话题,“为了避免出现意想不到的溢出,在实际项目中,所有参与运算的变量都预先转化为unsigned long变量,再参与运算。”如何把所有的运算变量都转化为unsigned long变量?现在介绍一下这个方法。
第一个例子:比如上述第(2)个例子,其转换方法如下:
- unsigned long f;
- unsigned int x2=0xfffe;
- unsigned char y2=0x12;
- unsigned long t; //多增加一个long类型的变量,用来变换类型。
- unsigned long r; //多增加一个long类型的变量,用来变换类型。
- t=0; //把变量的高位和低位全部清零。
- t=x2; //把x2的数值先放到一个long类型的变量里,让”加数”跟”保存变量”类型一致。
- r=0; //把变量的高位和低位全部清零。
- r=y2; //把y2的数值先放到一个long类型的变量里,让”加数”跟”保存变量”类型一致。
- f=t+r;
复制代码
运算结果:f等于十六进制的0x00010010,没有发生溢出现象。
第二个例子:比如上述第(4)个例子,其转换方法如下:
- unsigned long g;
- unsigned long h;
- unsigned int x3=50000;
- unsigned long t; //多增加一个long类型的变量,用来变换类型
- t=0; //把变量的高位和低位全部清零。
- t=x3; //把x3的数值先放到一个long类型的变量里,让”加数”跟”保存变量”类型一致。
- g=t+30000;
- h=t+50000;
复制代码
运算结果:g等于80000,h等于100000。都没有发生溢出。
【20.3 例程练习和分析。】
现在我们编写一个程序来验证上面讲到的例子:
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned int a; //第(1)个例子
- unsigned char x1=0x12;
- unsigned char y1=0xfe;
- unsigned long b; //第(2)个例子
- unsigned int x2=0xfffe;
- unsigned char y2=0x12;
- unsigned long c; //第(3)个例子
- unsigned long d; //第(4)个例子
- unsigned long e;
- unsigned int x3=50000;
- unsigned long f; //第(2)个例子改进之后
- unsigned long g; //第(4)个例子改进之后
- unsigned long h;
- unsigned long t; //多增加一个long类型的变量,用来变换类型。
- unsigned long r; //多增加一个long类型的变量,用来变换类型。
- //第(1)个例子
- a=x1+y1;
- //第(2)个例子
- b=x2+y2;
- //第(3)个例子
- c=50000+50000;
- //第(4)个例子
- d=x3+30000;
- e=x3+50000;
- //第(2)个例子改进之后
- t=0; //把变量的高位和低位全部清零。
- t=x2; //把x2的数值先放到一个long类型的变量里,让”加数”跟”保存变量”类型一致。
- r=0; //把变量的高位和低位全部清零。
- r=y2; //把y2的数值先放到一个long类型的变量里,让”加数”跟”保存变量”类型一致。
- f=t+r;
- //第(4)个例子改进之后
- t=0; //把变量的高位和低位全部清零。
- t=x3; //把x3的数值先放到一个long类型的变量里,让”加数”跟”保存变量”类型一致。
- g=t+30000;
- h=t+50000;
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- View(e); //把第5个数e发送到电脑端的串口助手软件上观察。
- View(f); //把第6个数f发送到电脑端的串口助手软件上观察。
- View(g); //把第7个数g发送到电脑端的串口助手软件上观察。
- View(h); //把第8个数h发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:272
- 十六进制:110
- 二进制:100010000
- 第2个数
- 十进制:16
- 十六进制:10
- 二进制:10000
- 第3个数
- 十进制:100000
- 十六进制:186A0
- 二进制:11000011010100000
- 第4个数
- 十进制:14464
- 十六进制:3880
- 二进制:11100010000000
- 第5个数
- 十进制:100000
- 十六进制:186A0
- 二进制:11000011010100000
- 第6个数
- 十进制:65552
- 十六进制:10010
- 二进制:10000000000010000
- 第7个数
- 十进制:80000
- 十六进制:13880
- 二进制:10011100010000000
- 第8个数
- 十进制:100000
- 十六进制:186A0
- 二进制:11000011010100000
复制代码
分析:
通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。
【20.4 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-6-5 16:00
本帖最后由 jianhong_wu 于 2016-6-5 16:24 编辑
第二十一节:减法运算的5种常见格式。
第二十一节_pdf文件.pdf
(67.93 KB, 下载次数: 4352)
【21.1 减法语法格式。】
减法语法格式:
“保存变量”=“减数1”-“减数2”-...-“减数N”;
含义:右边的“减数”与“减数”相减(这里暂时把平时所说的被减数也归类为减数),并且把最终的运算结果赋值给左边的“保存变量”。注意,这里的符号“=”不是等于号的意思,而是赋值的意思。左边的“保存变量”必须是变量,不能是常量,否则编译时会报错。右边的“减数”既可以是变量,也可以是常量,也可以是“保存变量”本身自己。多说一句,什么是变量和常量?变量是可以在程序中被更改的,被分配的一个RAM空间。常量往往是数字,或者被分配在ROM空间的一个具体数值。下面根据右边“减数”与“减数”的不同组合,列出了减法运算的5种常见格式。
第1种:“减数1”是常量,“减数2”是常量。比如:
分析:数字“15”和“3”都是常量。执行上述语句后,保存变量a变成了12。
第2种:“减数1”是变量,“减数2”是常量。比如:
- unsigned char b;
- unsigned char x=15;
- b=x-10;
复制代码
分析:x是变量,“10”是常量。由于原来x变量里面的数值是15,执行上述语句后,保存变量b变成了5。而变量x则保持不变,x还是15。
第3种:“减数1”是变量,“减数2”是变量。比如:
- unsigned char c;
- unsigned char x=15;
- unsigned char y=6;
- c=x-y;
复制代码
分析:x是变量,y也是变量。由于原来x变量里面的数值是15,y变量里面的数值是6,执行上述语句后,保存变量c变成了9。而变量x和y则保持不变,x还是15,y还是6。
第4种:“减数1”是保存变量本身,“减数2”是常量。比如:
- unsigned char d=18;
- d=d-2;
- d=d-7;
复制代码
分析:d是保存变量,“2”和“7”都是常量。这类语句有一个特点,具备了自减功能,可以更改自己本身的数值。比如原来保存变量d的数值是18,执行“d=d-2;”语句后,d变成了16,接着再执行完“d=d-7;”语句后,d最后变成了9。
第5种:“减数1”是保存变量本身,“减数2”是变量。比如:
- unsigned char e=28;
- unsigned char x=15;
- unsigned char y=6;
- e=e-x;
- e=e-y;
复制代码
分析:e是保存变量,x与y都是变量。这类语句有一个特点,具备了自减功能,可以更改自己本身的数值。比如原来保存变量e的数值是28,执行“e=e-x;”语句后,e变成了13,接着再执行完“e=e-y;”语句后,e最后变成了7。
【21.2 例程练习和分析。】
现在我们编写一个程序来验证上面讲到的5个减法例子:
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a; //定义一个变量a,并且分配了1个字节的RAM空间。
- unsigned char b; //定义一个变量b,并且分配了1个字节的RAM空间。
- unsigned char c; //定义一个变量c,并且分配了1个字节的RAM空间。
- unsigned char d=18; //定义一个变量d,并且分配了1个字节的RAM空间。初始化默认为18.
- unsigned char e=28; //定义一个变量e,并且分配了1个字节的RAM空间。初始化默认为28.
- unsigned char x=15; //定义一个变量x,并且分配了1个字节的RAM空间。初始化默认为15.
- unsigned char y=6; //定义一个变量y,并且分配了1个字节的RAM空间。初始化默认为6.
- //第1种:“减数1”是常量,“减数2”是常量。
- a=15-3;
- //第2种:“减数1”是变量,“减数2”是常量。
- b=x-10;
- //第3种:“减数1”是变量,“减数2”是变量。
- c=x-y;
- //第4种:“减数1”是保存变量本身,“减数2”是常量。
- d=d-2;
- d=d-7;
- //第5种:“减数1”是保存变量本身,“减数2”是变量。
- e=e-x;
- e=e-y;
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- View(e); //把第5个数e发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:12
- 十六进制:C
- 二进制:1100
- 第2个数
- 十进制:5
- 十六进制:5
- 二进制:101
- 第3个数
- 十进制:9
- 十六进制:9
- 二进制:1001
- 第4个数
- 十进制:9
- 十六进制:9
- 二进制:1001
- 第5个数
- 十进制:7
- 十六进制:7
- 二进制:111
复制代码
分析:
通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。
【21.3 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-6-12 10:03
本帖最后由 jianhong_wu 于 2016-6-12 16:05 编辑
第二十二节:连减、自减、自减简写、自减1。
第二十二节_pdf文件.pdf
(74.81 KB, 下载次数: 4089)
【22.1 连减。】
上一节的减法例子中,右边的减数只有两个。实际上,C语言规则没有限制减数的个数,它的通用格式如下:
- “保存变量”=“减数1”-“减数2”-“减数3”-...-“减数N”;
复制代码
当右边的减数个数超过两个的时候(这里暂时把平时所说的被减数也归类为减数),这种情况就是“连减”。每个减数的属性没有限定,可以是常量,也可以是变量。比如:
- a=68-3-15; //减数全部是常量。
- b=q-x-y-k; //减数全部是变量。
- c=63-x-5-k; //减数有的是常量,有的是变量。
复制代码
连减的运行顺序是,赋值符号“=”右边的减数挨个相减,把每一次的运算结果放在一个临时的隐藏中间变量里,这个隐藏的变量我们看不到,是单片机系统内部参与运算时的专用寄存器,等右边所有减数连
减的计算结果出来后,再把隐藏变量所保存的计算结果赋值给左边的“保存变量”。
【22.2 自减、自减简写、自减1。】
什么是自减?当赋值符号“=”右边的第1个减数是“保存变量”本身时(这里暂时把平时所说的被减数也归类为减数),这种情况就是“自减”。自减在程序里有一个特点,只要第2个减数不为0,那么每执行一次这行代码,“保存变量”本身就会减小一次,不断执行这行代码,“保存变量”本身就会不断减小,而每次的减小量就取决于赋值符号“=”右边从第2个减数开始后面所有减数之和。自减的常见格式如下:
- “保存变量”=“保存变量”-“减数2”;
- “保存变量”=“保存变量”-“减数2”-“减数3”-...-“减数N”;
复制代码
在这类自减计算式中,当只有右边的第1个减数是“保存变量”本身时,那么上述自减计算式可以简写成如下格式:
- “保存变量”-=“减数2”;
- “保存变量”-=(“减数2”+“减数3”+...+“减数N”);
复制代码
这种格式就是“自减简写”。现在举几个例子如下:
- d-=6; //相当于d=d-6;
- e-=x; //相当于e=e-x;
- f-=18-y-k; //相当于f=f-(18-y-k);
复制代码
这些例子都是很常规的自减简写,再跟大家讲一种很常用的特殊简写。当右边只有两个减数,而第1个减数是“保存变量”,第2个减数是常数1时,格式如下:
这时候,可以把上述格式简写成如下两种格式:
这两种格式也是俗称的“自减1”操作。比如:
- g--; //相当于g=g-1或者g-=1;
- --h; //相当于h=h-1或者h-=1;
复制代码
自减1符号“--”可以在变量的左边,也可以在变量的右边,它们在这里本质是一样的,没有差别。当然,如果是在循环条件语句中,这时自减1符号“--”在左边还是在右边是有一点点微弱的差别,这方面的内容以后再讲。
【22.3 例程练习和分析。】
现在我们编写一个程序来验证上面讲到的例子:
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a; //定义一个变量a,并且分配了1个字节的RAM空间。
- unsigned char b; //定义一个变量b,并且分配了1个字节的RAM空间。
- unsigned char c; //定义一个变量c,并且分配了1个字节的RAM空间。
- unsigned char d=65; //定义一个变量d,并且分配了1个字节的RAM空间。初始化默认为65.
- unsigned char e=38; //定义一个变量e,并且分配了1个字节的RAM空间。初始化默认为38.
- unsigned char f=29; //定义一个变量f,并且分配了1个字节的RAM空间。初始化默认为29.
- unsigned char g=5; //定义一个变量g,并且分配了1个字节的RAM空间。初始化默认为5.
- unsigned char h=5; //定义一个变量h,并且分配了1个字节的RAM空间。初始化默认为5.
- unsigned char q=50; //定义一个变量q,并且分配了1个字节的RAM空间。初始化默认为50.
- unsigned char x=3; //定义一个变量x,并且分配了1个字节的RAM空间。初始化默认为3.
- unsigned char y=6; //定义一个变量y,并且分配了1个字节的RAM空间。初始化默认为6.
- unsigned char k=2; //定义一个变量k,并且分配了1个字节的RAM空间。初始化默认为2.
- //第1个知识点:连减。
- a=68-3-15; //减数全部是常量。a的结果为:50。
- b=q-x-y-k; //减数全部是变量。b的结果为:39。
- c=63-x-5-k; //减数有的是常量,有的是变量。c的结果为:53。
- //第2个知识点:自减简写。
- d-=6; //相当于d=d-6; d的结果为:59。
- e-=x; //相当于e=e-x; e的结果为:35。
- f-=18-y-k; //相当于f=f-(18-y-k); f的结果为:19。
- //第3个知识点:自减1。
- g--; //相当于g=g-1或者g-=1; g的结果为:4。
- --h; //相当于h=h-1或者h-=1; d的结果为:4。
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- View(e); //把第5个数e发送到电脑端的串口助手软件上观察。
- View(f); //把第6个数f发送到电脑端的串口助手软件上观察。
- View(g); //把第7个数g发送到电脑端的串口助手软件上观察。
- View(h); //把第8个数h发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:50
- 十六进制:32
- 二进制:110010
- 第2个数
- 十进制:39
- 十六进制:27
- 二进制:100111
- 第3个数
- 十进制:53
- 十六进制:35
- 二进制:110101
- 第4个数
- 十进制:59
- 十六进制:3B
- 二进制:111011
- 第5个数
- 十进制:35
- 十六进制:23
- 二进制:100011
- 第6个数
- 十进制:19
- 十六进制:13
- 二进制:10011
- 第7个数
- 十进制:4
- 十六进制:4
- 二进制:100
- 第8个数
- 十进制:4
- 十六进制:4
- 二进制:100
复制代码
分析:
通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。
【22.4 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-6-19 12:37
本帖最后由 jianhong_wu 于 2016-6-19 12:54 编辑
第二十三节:减法溢出与假想借位。
第二十三节_pdf文件.pdf
(74.93 KB, 下载次数: 3849)
【23.1 减法溢出与假想借位。】
英文“unsigned”的中文意思就是”无符号的”,延伸含义是“无负号无负数”的意思,所以unsigned char ,unsigned int ,unsigned long这三种类型数据都是无负号无负数的,取值只能是0和正数,那么问题来了,当被减数小于减数的时候,运算结果会是什么样子,有什么规律?这就是本节要研究的减法溢出。
第一个例子:
分析:
左边的“保存变量”a的数据长度是1个字节8位,a=0-1可以看成是十六进制的a=0x00-0x01。由于0x00比0x01小,所以假想一下需要向高位借位,借位后成了a=0x100-0x01。所以a的最终结果是0xff(十进制是255),这个“假想一下需要向高位借位”的过程就是本节制造的新概念“假想借位”。根据“假想借位”这个规律,如果是b也是unsigned char 类型,那么b=2-5自然就相当于b=0x102-0x05,运算结果b等于0xfd(十进制是253)。
第二个例子:
分析:
左边的“保存变量”c的数据长度是2个字节16位,c=0-1可以看成是十六进制的c=0x0000-0x0001。由于0x0000比0x0001小,所以假想一下需要向高位借位,借位后成了c=0x10000-0x0001。所以c的最终结果是0xffff(十进制是65535)。根据“假想借位”这个规律,如果是d也是unsigned int 类型,那么d=2-5自然就相当于d=0x10002-0x0005,运算结果d等于0xfffd(十进制是65533)。
综合分析:
为什么上述例子中会出现数据越减越大的奇葩现象?是因为减法溢出,是因为“假想借位”中的“借”是“光借不还”。一句话,根本问题就是溢出问题。
【23.2 因为减法溢出,所以加减顺序......】
第三个例子:请分析下面例子中e和f因加减运算顺序不同而引发什么问题。
- unsigned char e;
- unsigned char f;
- e=1-6+7;
- f=1+7-6;
复制代码
用两种思路分析:
第一种思路:只看过程不看结果。加减法的运算优先级是从左到右,e先减法后加法,1减去6就有溢出了,所以过程有问题。而f先加法后减法,整个过程没有问题。
第二种思路:先看结果再分析过程。e的运算结果居然是2,f的运算结果也是2。好奇怪,既然e的过程有问题,为什么运算结果却没有问题?其实e发生两次溢出,第一次是减法溢出,第二次是加法溢出,所以“溢溢得正”(这句话是开玩笑的)。1-6“假想借位”后相当于0x101-0x06,运算结果等于0xfb(十进制是251),然后0xfb再加上0x07等于0x102,因为e是unsigned char 类型只有1个字节,根据加法溢出的规律,最后只保留了低8位的一个字节0x02,所以运算结果就是十进制的2。
结论:
虽然e的运算结果侥幸是对的,但是其运算过程发生了溢出是有问题的,当运算式子更复杂一些,比如有不同类型的变量时,就有可能导致运算结果也出错。所以得出的结论是:在加减法运中,为了减少出现减法溢出的现象,建议先加法后减法。在后续章节讲到的乘除法运算中,为了减小运算带来的误差也建议大家先乘法后除法。
【23.3 例程练习和分析。】
现在我们编写一个程序来验证上面讲到的例子:
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a; //定义一个变量a,并且分配了1个字节的RAM空间。
- unsigned char b; //定义一个变量b,并且分配了1个字节的RAM空间。
- unsigned int c; //定义一个变量c,并且分配了2个字节的RAM空间。
- unsigned int d; //定义一个变量d,并且分配了2个字节的RAM空间。
- unsigned char e; //定义一个变量e,并且分配了1个字节的RAM空间。
- unsigned char f; //定义一个变量f,并且分配了1个字节的RAM空间。
- //第一个例子,针对a与b都是unsigned char类型数据。
- a=0-1;
- b=2-5;
- //第二个例子,针对c与d都是unsigned int类型的数据。
- c=0-1;
- d=2-5;
- //第三个例子,e与f的加减顺序不一样。
- e=1-6+7;
- f=1+7-6;
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- View(e); //把第5个数e发送到电脑端的串口助手软件上观察。
- View(f); //把第6个数f发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:255
- 十六进制:FF
- 二进制:11111111
- 第2个数
- 十进制:253
- 十六进制:FD
- 二进制:11111101
- 第3个数
- 十进制:65535
- 十六进制:FFFF
- 二进制:1111111111111111
- 第4个数
- 十进制:65533
- 十六进制:FFFD
- 二进制:1111111111111101
- 第5个数
- 十进制:2
- 十六进制:2
- 二进制:10
- 第6个数
- 十进制:2
- 十六进制:2
- 二进制:10
复制代码
分析:
通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。
【23.4 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-6-27 11:04
本帖最后由 jianhong_wu 于 2016-6-27 11:17 编辑
第二十四节:借用unsigned long类型的中间变量可以减少溢出现象。
第二十四节_pdf文件.pdf
(62.79 KB, 下载次数: 3263)
【24.1 为什么要借用unsigned long类型的中间变量?】
为什么要借用unsigned long类型的中间变量进行算术运算?其实就是为了减少溢出的问题。溢出是因为数据超过了它的最大范围,unsigned char ,unsigned int ,unsigned long三种数据类型中,unsigned long的取值是最大的。当参与运算变量中存在非unsigned long类型的时候,在运算前,先让每个非unsigned long类型的变量借用一个unsigned long类型的中间变量,然后才开始运算,可以大大减少运算中的溢出问题。 unsigned long的取值是从0到4294967295,万一数据超过了4294967295怎么办?可用BCD码的数组方式进行运算,这种数组运算的方法我以后会跟大家介绍,初学者现在暂时不用深入了解它。
【24.2 如何借用unsigned long类型的中间变量?】
借用中间变量的方法是引入中间变量,有多少个非unsigned long类型变量就引入多少个unsigned long中间变量,再借这个“壳”进行运算,最后再把中间变量的计算结果返回给实际变量。请看下面例子。
转换之前:
- unsigned int a;
- unsigned char x=195;
- unsigned long y=101;
- a=x-y; //进行算术减法运算
复制代码
分析:
上述公式用到3个变量,其中a和x都不是unsigned long变量,因此需要为它们分别引入两个unsigned long类型的中间变量t和s,于是乎,继续往下看......
转换之后:
- unsigned int a;
- unsigned char x=195;
- unsigned long y=101;
- unsigned long t; //引入的中间变量t,用来给a借用。
- unsigned long s; //引入的中间变量s,用来给x借用。
- //第一步:使用之前先清零
- t=0; //t在用之前,先把t的32位全部清零。
- s=0; //s在用之前,先把s的32位全部清零。
- s=x; //s接收x原数据,等效于x借用unsigned long中间变量s这个壳。
- t=s-y; //此处unsigned long类型的t就默认代表了unsigned int类型的变量a。
- //第二步:因为其它的变量都是临时的,所以运算结束后再返回计算结果给原来的变量。
- a=t; //运算结束后再把计算结果返回给原来的变量a。
复制代码
分析:
第一步:unsigned long类型的中间变量在转换之前为什么要先赋值0进行清零,比如上述代码的“s=0;”?因为它是32位的数据类型,它也是一个随机数,如果不清零,后续的其它类型的变量可能是16位或者8位的类型变量,这些宽度不一的变量在给32位的变量赋值的时候,只能覆盖到32位变量的低16位或者低8位,无法等效于实际借用者变量的数值,所以有可能会出错。
第二步:因为其它的变量都是临时的,所以运算结束后应该再返回计算结果给原来的实际变量。在这里要多说一句,实际项目中,最后接收运算结果的变量应该根据项目所需去选择它的类型,建议尽量选择unsigned long类型吧,否则,如果中间变量的计算结果大于接收变量本身的类型范围,也会发生溢出。比如,上述最后一行代码a=t,如果此时t的数值大于65535,a也会发生溢出的现象。 但是如果a本身是unsigned long 类型,就不会发生这种现象。
加法,乘法,除法在借用中间变量的时候,跟本节减法例子中的思路也大同小异。
【24.3 建议在算术运算中确保所有的变量都是unsigned long类型。】
不管是以前讲的加法,现在讲的减法,还是未来讲的乘法和除法,我都会建议“在加减乘除四则运算中,凡是非unsigned long类型的变量,都应该借用unsigned long类型的中间变量进行运算,最后再返回计算结果给实际的变量。”unsigned long变量是三种数据类型中取值范围最大的数,借用此类型的中间变量,可以减少在简单运算中可能出现的溢出问题。
作者: jianhong_wu 时间: 2016-7-4 09:04
本帖最后由 jianhong_wu 于 2016-7-4 09:42 编辑
第二十五节:乘法运算中的5种常用组合。
第二十五节_pdf文件.pdf
(72.44 KB, 下载次数: 2827)
【25.1 乘法语法格式。】
乘法语法格式:
“保存变量”=“乘数1”*“乘数2”*..*“乘数N”;
含义:为什么C语言的乘法符号并不是我们熟悉的“X”而是“*”?我猜测是因为“X”跟键盘的大写字母“X”重复有冲突了,而“*”轮廓跟“X”很相似,并且也可以在键盘上通过“Shift+8”的组合键直接键入“*”,所以用“*”作为乘法符号。上述乘法格式中,右边的“乘数”与“乘数”相乘(这里暂时把平时所说的被乘数也归类为乘数),并且把最终的运算结果赋值给左边的“保存变量”。注意,这里的符号“=”不是等于号的意思,而是赋值的意思。左边的“保存变量”必须是变量,不能是常量,否则编译时会报错。右边的“乘数”既可以是变量,也可以是常量,也可以是“保存变量”本身自己。多说一句,什么是变量和常量?变量是可以在程序中被更改的,被分配的一个RAM空间。常量往往是数字,或者被分配在ROM空间的一个具体数值。下面根据右边“乘数”与“乘数”的不同组合,列出了乘法运算的5种常用组合。
第1种:“乘数1”是常量,“乘数2”是常量。比如:
分析:数字“15”和“3”都是常量。执行上述语句后,保存变量a变成了45。
第2种:“乘数1”是变量,“乘数2”是常量。比如:
- unsigned char b;
- unsigned char x=15;
- b=x*10;
复制代码
分析:x是变量,“10”是常量。由于原来x变量里面的数值是15,执行上述语句后,保存变量b变成了150。而变量x则保持不变,x还是15。
第3种:“乘数1”是变量,“乘数2”是变量。比如:
- unsigned char c;
- unsigned char x=15;
- unsigned char y=6;
- c=x*y;
复制代码
分析:x是变量,y也是变量。由于原来x变量里面的数值是15,y变量里面的数值是6,执行上述语句后,保存变量c变成了90。而变量x和y则保持不变,x还是15,y还是6。
第4种:“乘数1”是保存变量本身,“乘数2”是常量。比如:
- unsigned char d=18;
- d=d*2;
- d=d*7;
复制代码
分析:d是保存变量,“2”和“7”都是常量。这类语句有一个特点,具备了自乘功能,可以更改自己本身的数值。 比如原来保存变量d的数值是18,执行“d=d*2;”语句后,d变成了36,接着再执行完“d=d*7;”语句后,d最后变成了252。
第5种:“乘数1”是保存变量本身,“乘数2”是变量。比如:
- unsigned char e=2;
- unsigned char x=15;
- unsigned char y=6;
- e=e*x;
- e=e*y;
复制代码
分析:e是保存变量,x与y都是变量。这类语句有一个特点,具备了自乘功能,可以更改自己本身的数值。比如原来保存变量e的数值是2,执行“e=e*x;”语句后,e变成了30,接着再执行完“e=e*y;”语句后,e最后变成了180。
【25.2 例程练习和分析。】
现在我们编写一个程序来验证上面讲到的5个乘法例子:
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a; //定义一个变量a,并且分配了1个字节的RAM空间。
- unsigned char b; //定义一个变量b,并且分配了1个字节的RAM空间。
- unsigned char c; //定义一个变量c,并且分配了1个字节的RAM空间。
- unsigned char d=18; //定义一个变量d,并且分配了1个字节的RAM空间。初始化默认为18.
- unsigned char e=2; //定义一个变量e,并且分配了1个字节的RAM空间。初始化默认为2.
- unsigned char x=15; //定义一个变量x,并且分配了1个字节的RAM空间。初始化默认为15.
- unsigned char y=6; //定义一个变量y,并且分配了1个字节的RAM空间。初始化默认为6.
- //第1种:“乘数1”是常量,“乘数2”是常量。
- a=15*3;
- //第2种:“乘数1”是变量,“乘数2”是常量。
- b=x*10;
- //第3种:“乘数1”是变量,“乘数2”是变量。
- c=x*y;
- //第4种:“乘数1”是保存变量本身,“乘数2”是常量。
- d=d*2;
- d=d*7;
- //第5种:“乘数1”是保存变量本身,“乘数2”是变量。
- e=e*x;
- e=e*y;
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- View(e); //把第5个数e发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:45
- 十六进制:2D
- 二进制:101101
- 第2个数
- 十进制:150
- 十六进制:96
- 二进制:10010110
- 第3个数
- 十进制:90
- 十六进制:5A
- 二进制:1011010
- 第4个数
- 十进制:252
- 十六进制:FC
- 二进制:11111100
- 第5个数
- 十进制:180
- 十六进制:B4
- 二进制:10110100
复制代码
分析:
通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。
【25.3 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-7-12 10:02
本帖最后由 jianhong_wu 于 2016-7-12 10:23 编辑
第二十六节:连乘、自乘、自乘简写,溢出。
第二十六节_pdf文件.pdf
(79.58 KB, 下载次数: 2636)
【26.1 连乘。】
上一节的乘法例子中,右边的乘数只有两个。实际上,C语言规则没有限制乘数的个数,它的通用格式如下:
- “保存变量”=“乘数1”*“乘数2”...*“乘数N”;
复制代码
当右边的乘数个数超过两个的时候(这里暂时把平时所说的被乘数也归类为乘数),这种情况就是“连乘”。每个乘数的属性没有限定,可以是常量,也可以是变量。比如:
- unsigned char x=3; //定义一个变量x,初始化默认为3.
- unsigned char y=6; //定义一个变量y,初始化默认为6.
- unsigned char k=2; //定义一个变量k,初始化默认为2.
- a=2*5*3; //乘数全部是常量。a的结果为30。
- b=k*x*y; //乘全部是变量。b的结果为36。
- c=x*5*y; //乘数,有的是常量,有的是变量。c的结果为90。
复制代码
连乘的运行顺序是,赋值符号“=”右边的乘数挨个相乘,把每一次的运算结果放在一个临时的隐蔽中间变量里,这个隐蔽的变量我们看不到,是单片机系统内部参与运算时的专用寄存器,等右边所有乘数连乘的计算结果出来后,再把隐蔽变量所保存的计算结果赋值给左边的“保存变量”。
【26.2 自乘与自乘简写。】
什么是自乘?当赋值符号“=”右边的乘数只要其中有一个是“保存变量”本身时,这种情况就是“自乘”,常见格式如下:
- “保存变量”=“保存变量”*“乘数1”;
- “保存变量”=“保存变量”*(“乘数1”*“乘数2”...*“乘数N”);
复制代码
上述自乘计算式可以简写成如下格式:
- “保存变量”*=“乘数1”;
- “保存变量”*=“乘数1”*“乘数2”...*“乘数N”;
复制代码
这种格式就是“自乘简写”。现在举几个例子如下:
- unsigned char d=5; //定义一个变量d,初始化默认为5.
- unsigned char e=5; //定义一个变量e,初始化默认为5.
- unsigned char f=5; //定义一个变量f,初始化默认为5.
- unsigned char x=3; //定义一个变量x,初始化默认为3.
- unsigned char y=6; //定义一个变量y,初始化默认为6.
- unsigned char k=2; //定义一个变量k,初始化默认为2.
- d*=6; //相当于d=d*6;最后d的结果为30。
- e*=x; //相当于e=e*x;最后e的结果为15。
- f*=2*y*k; //相当于f=f*(2*y*k);最后f的结果为120。
复制代码
【26.3 有没有“自乘1”的特殊写法?】
之前在讲加法的自加和减法的自减运算时,还给大家介绍了它们另外一种特殊的简写方式。比如减法运算,当右边只有2减数,当一个减数是“保存变量”,另一个是常数1时,格式如下:
这时候,可以把上述格式简写成如下两种格式:
这两种格式也是俗称的“自减1”操作。比如:
- g--; //相当于g=g-1或者g-=1;
- --h; //相当于h=h-1或者h-=1;
复制代码
那么,本节所讲的自乘运算,有没有“g**”或者“**h”这种特殊的“自乘1”写法?答案很明显,C语言里没有“自乘1”这种特殊写法。因为任何一个数“自乘1”还是等于它本身,所以在乘法运算中这种特殊写法就没有存在的意义。多说一句,如果某天有朋友在某个地方看到“**h”这类语句,它的本意跟“自乘”没关系,而是跟C语言的另一块知识点“指针”有关。
【26.4 乘法的溢出。】
乘法的溢出规律跟加减法的溢出规律是一样的。举一个例子如下:
- unsigned char m=30;
- unsigned char n=10;
- unsigned char a;
- a=m*n;
复制代码
分析:m与n相乘,相当于30乘以10,运算结果是300(十六进制是0x012c)保存在一个隐藏中间变量,根据前面加减法运算的规律,我猜测这个隐藏中间变量可能是unsigned int类型,然后再把这个中间变量赋值给单字节变量a,a只能接收十六进制的低8位字节0x2c,所以运算后a的数值由于溢出变成了十六进制的0x2c(十进制是44)。由于乘法的溢出规律跟加减法的溢出规律是一样的,所以不再多举例子。在实际项目中,为了减少溢出的现象,我建议,不管加减乘除,凡是参与运算的变量全部都应该转化成unsigned long变量,转化的方法已经在前面章节讲过,不再重复讲解这方面的内容。
【26.5 例程练习和分析。】
现在编写一个程序来验证刚才讲到的连乘和自乘简写:
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a;
- unsigned char b;
- unsigned char c;
- unsigned char d=5; //定义一个变量d,初始化默认为5.
- unsigned char e=5; //定义一个变量e,初始化默认为5.
- unsigned char f=5; //定义一个变量f,初始化默认为5.
- unsigned char x=3; //定义一个变量x,初始化默认为3.
- unsigned char y=6; //定义一个变量y,初始化默认为6.
- unsigned char k=2; //定义一个变量k,初始化默认为2.
- //第1个知识点:连乘。
- a=2*5*3; //乘数全部是常量。a的结果为30。
- b=k*x*y; //乘数全部是变量。b的结果为36。
- c=x*5*y; //乘数,有的是常量,有的是变量。c的结果为90。
- //第2个知识点:自乘的简写。
- d*=6; //相当于d=d*6;最后d的结果为30。
- e*=x; //相当于e=e*x;最后e的结果为15。
- f*=2*y*k; //相当于f=f*(2*y*k);最后f的结果为120。
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- View(e); //把第5个数e发送到电脑端的串口助手软件上观察。
- View(f); //把第6个数f发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:30
- 十六进制:1E
- 二进制:11110
- 第2个数
- 十进制:36
- 十六进制:24
- 二进制:100100
- 第3个数
- 十进制:90
- 十六进制:5A
- 二进制:1011010
- 第4个数
- 十进制:30
- 十六进制:1E
- 二进制:11110
- 第5个数
- 十进制:15
- 十六进制:F
- 二进制:1111
- 第6个数
- 十进制:120
- 十六进制:78
- 二进制:1111000
复制代码
分析:
通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。
【26.6 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-7-17 13:53
本帖最后由 jianhong_wu 于 2016-7-17 14:15 编辑
第二十七节:整除求商。
第二十七节_pdf文件.pdf
(83.7 KB, 下载次数: 2540)
【27.1 什么叫整除?】
最小的细分单位是“1”的除法运算就是整除,“1”不能再往下细分成小数点的除法运算就是整除。比如:
10除以4,商等于2.5。------(带小数点,这个不是整除)
10除以4,商等于2,余数是2。------(这才是整除)
什么时候带小数点,什么时候是整除?取决于参与运算的变量类型。标准的C语言中,其实远远不止我前面所说的unsigned char ,unsigned int ,unsigned long这三种类型,比如还有一种叫浮点数类型的float,当参与运算的变量存在float类型时,就可能存在小数点。关于小数点的问题以后再讲,现在暂时不深入讲解,现在要知道的是,unsigned char ,unsigned int ,unsigned long这三种变量类型的除法都是属于整除运算,不带小数点的。
【27.2 整除的运算符号是什么样子的?】
10除以4,商等于2,余数是2,这个整除的过程诞生了两个结果,一个是商,一个是余数,与此对应,整除就诞生出两个运算符号,你如果想计算结果返回商就用“整除求商”的符号“/”,你如果想计算结果返回余数就用“整除求余”的符号“%”。咋一看,整除运算中用到的两个符号“/”和“%”都不是我们日常生活中熟悉的除号“÷”,我个人猜测是因为“÷”这个符号在电脑键盘上不方便直接输入,因此C语言的语法规则选用“/”和“%”作为整除的运算符号。
【27.3 整除求商“/”。】
整除求商的通用格式:
- “保存变量”=“被除数” / “除数1” / “除数2”... / “除数N”;
复制代码
跟之前讲的加减运算一样,赋值符号“=”左边的“保存变量”必须是变量,右边的可以是变量和常量的任意组合。如果右边只有两个参与运算的数据,就是整除求商的常见格式。
整除求商的常见格式:
现在深入分析一下整除求商的运算规律。
(1)当除数等于0时。
我们都知道,数学运算的除数是不允许等于0的,如果在51单片机中非要让除数为0,商会出现什么结果?我测试了一下,发现有一个规律:在unsigned char的变量类型下,如果“除数”是变量的0,商等于十进制的255(十六进制是0xff)。如果“除数”是常量的0,商等于十进制的1。比如:
- unsigned char a;
- unsigned char b;
- unsigned char y=0;
- a=23/y; //除数变量y里面是0,那么a的结果是255(十六进制的0xff)。
- b=23/0; //除数是常量0,那么b的结果是1。
复制代码
平时做项目要尽量避免“除数是0”的情况,离它越远越好,但是既然除数不能为0,为什么我非要做“除数为0”时的实验呢?意义何在?这个实验的意义是,虽然我知道除数为0时会出错,但是我不知道这个错到底严不严重,会不会导致整个程序崩溃,当我做了这个实验后,我心中的石头才放下了,万一除数为0时,最多只是运算出错,但是不至于整个程序会崩溃,这样我心里就有了一个底,当哪天我某个程序崩溃跑飞时,我至少可以排除了“除数为0”这种情况,引导我从其它方面去找bug。
(2)当被除数小于除数时。商等于0。比如:
- unsigned char c;
- c=7/10; //c的结果是0。
复制代码
(3)当被除数等于除数时。商等于1。比如:
- unsigned char d;
- d=10/10; //d的结果是1。
复制代码
(4)当被除数大于除数时。商大于0。比如:
- unsigned char e;
- unsigned char f;
- e=10/4; //e的结果是2,大于0。
- f=10/3; //f的结果是3,大于0。
复制代码
【27.4 整除求商的自除简写。】
当被除数是“保存变量”时,存在自除运算的简写。
上述自除运算的简写如下:
比如:
- unsigned char e;
- g/=5; //相当于g=g/5;
复制代码
【27.5 整除求商有没有“自除1”的特殊写法?】
加减法有自加1“++g”和自减1“g--”的特殊写法,但是除法不存在这种自除1的特殊写法,因为一个数除以1还是等于它本身,所以自除1没有任何意义,因此C语言语法中没有这种写法。
【27.6 整除求商的溢出。】
除法的溢出规律跟加法的溢出规律是一样的,所以不再多举例子。在实际项目中,为了避免一不小心就溢出的问题,我建议,不管加减乘除,凡是参与运算的变量全部都应该转化成unsigned long变量,转化的方法已经在前面章节讲过,不再重复讲解这方面的内容。
【27.7 例程练习和分析。】
现在编写一个程序来验证刚才讲到的整除求商:
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a;
- unsigned char b;
- unsigned char c;
- unsigned char d;
- unsigned char e;
- unsigned char f;
- unsigned char g=10; //初始化为10
- unsigned char y=0; //除数变量初始化为0。
- //(1)当除数等于0时。
- a=23/y;
- b=23/0; //这行代码在编译时会引起一条警告“Warning”,暂时不用管它。
- //(2)当被除数小于除数时。
- c=7/10;
- //(3)当被除数等于除数时。
- d=10/10;
- //(4)当被除数大于除数时。
- e=10/4;
- f=10/3;
- //(5)整除求商的简写。
- g/=5; //相当于g=g/5;
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- View(e); //把第5个数e发送到电脑端的串口助手软件上观察。
- View(f); //把第6个数f发送到电脑端的串口助手软件上观察。
- View(g); //把第7个数g发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:255
- 十六进制:FF
- 二进制:11111111
- 第2个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第3个数
- 十进制:0
- 十六进制:0
- 二进制:0
- 第4个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第5个数
- 十进制:2
- 十六进制:2
- 二进制:10
- 第6个数
- 十进制:3
- 十六进制:3
- 二进制:11
- 第7个数
- 十进制:2
- 十六进制:2
- 二进制:10
复制代码
分析:
通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。
【27.8 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-7-24 10:40
本帖最后由 jianhong_wu 于 2016-7-24 11:39 编辑
第二十八节:整除求余。
第二十八节_pdf文件.pdf
(79.55 KB, 下载次数: 2407)
【28.1 整除求余“%”。】
上一节讲到,求商求余都是属于整除运算,区别是:求商返回商,求余返回余,求商是“/”,求余是“%”。求余的运算符号恰好就是我们平时常用的百分号“%”,之所以选择百分号作为求余的运算符号,我猜测是因为,在小于100%的数据中,如果我们仔细回味一下百分号的分子与分母的关系,其实就隐含了一层淡淡的求余的味道。
整除求余的通用格式:
- “保存变量”=“被除数”% “除数1” % “除数2”...% “除数N”;
复制代码
跟之前讲的加减运算一样,赋值符号“=”左边的“保存变量”必须是变量,右边的可以是变量和常量的任意组合。如果右边只有两个参与运算的数据,就是整除求余的常见格式。
整除求余的常见格式:
现在深入分析一下整除求余的运算规律。
(1)当除数等于0时。
我们都知道,数学运算除数是不允许等于0的,如果在单片机中非要让除数为0,余数会出现什么结果?我在keil的C51编译环境试过,发现有一个规律:如果除数是变量的0,那么余数等于被除数。如果除数是常量的0,那么余数等于1。还有一种特殊的情况是编译不通过的,这种情况是“当被除数是变量,而除数是常量的0”。比如:
- unsigned char a;
- unsigned char b;
- unsigned char k=10;
- unsigned char y=0; //除数初始化为0
- a=23%y; //除数变量y里面是0,a的结果等于被除数23。
- b=23%0; //除数是常量0,b的结果是1。
- b=k%0; //这种特殊情况编译不通过:被除数是变量,而除数是常量的0。
复制代码
平时做项目要尽量避免“除数是0”的情况,离它越远越好,但是既然除数不能为0,为什么我非要做“除数为0”时的实验呢?意义何在?这个实验的意义是,虽然我知道除数为0时会出错,但是我不知道这个错到底严不严重,会不会导致整个程序崩溃,当我做了这个实验后,我心中的石头才放下了,万一除数为0时,最多只是运算出错,但是不至于整个程序会崩溃,这样我心里就有了一个底,当哪天我某个程序崩溃跑飞时,我至少可以排除了“除数为0”这种情况,引导我从其它方面去找bug。
(2)当被除数小于除数时。余数等于被除数本身。比如:
- unsigned char c;
- c=7%10; //c的结果是7。
复制代码
(3)当被除数等于除数时。余数等于0。比如:
- unsigned char d;
- d=10%10; //d的结果是0。
复制代码
(4)当被除数大于除数时。余数必然小于除数。比如:
- unsigned char e;
- unsigned char f;
- e=10%4; //e的结果是2。
- f=10%3; //f的结果是1。
复制代码
(5)当除数等于1时。余数必然等于0。
- unsigned char g;
- g=7%1; //g的结果是0。
复制代码
【28.2 整除求余的自除简写。】
当被除数是“保存变量”时,存在自除求余的简写。
上述自除求余的简写如下:
比如:
- unsigned char h=9;
- h%=5; //相当于h=h%5; 最后余数的计算结果是4。
复制代码
【28.3 整除求余有没有“自除1”的特殊写法?】
加减法有自加1“++g”和自减1“g--”的特殊写法,但是求余的除法不存在这种自除1的特殊写法,因为任何一个数除以1的余数必然等于0,所以求余的自除1没有任何意义,因此C语言语法中没有这种特殊写法。
【28.4 整除求余的溢出。】
不管是求商还是求余,除法的溢出规律跟加法的溢出规律是一样的,所以不再多举例子。在实际项目中,为了避免一不小心就溢出的问题,我建议,不管加减乘除,凡是参与运算的变量全部都应该转化成unsigned long变量,转化的方法已经在前面章节讲过,不再重复讲解这方面的内容。
【28.5 例程练习和分析。】
现在编写一个程序来验证刚才讲到的整除求余:
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a;
- unsigned char b;
- unsigned char c;
- unsigned char d;
- unsigned char e;
- unsigned char f;
- unsigned char g;
- unsigned char h=9; //初始化为9。
- unsigned char k=10; //初始化为10。
- unsigned char y=0; //除数变量初始化为0。
- //(1)当除数等于0时。
- a=23%y;
- b=23%0;
- // b=k%0; //这种特殊情况编译不通过:“被除数”是变量,而“除数”是常量的0。
- //(2)当被除数小于除数时。
- c=7%10;
- //(3)当被除数等于除数时。
- d=10%10;
- //(4)当被除数大于除数时。
- e=10%4;
- f=10%3;
- //(5)当除数等于1时。
- g=7%1;
- //(6)自除求余的简写。
- h%=5; //相当于h=h%5;
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- View(e); //把第5个数e发送到电脑端的串口助手软件上观察。
- View(f); //把第6个数f发送到电脑端的串口助手软件上观察。
- View(g); //把第7个数g发送到电脑端的串口助手软件上观察。
- View(h); //把第8个数h发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:23
- 十六进制:17
- 二进制:10111
- 第2个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第3个数
- 十进制:7
- 十六进制:7
- 二进制:111
- 第4个数
- 十进制:0
- 十六进制:0
- 二进制:0
- 第5个数
- 十进制:2
- 十六进制:2
- 二进制:10
- 第6个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第7个数
- 十进制:0
- 十六进制:0
- 二进制:0
- 第8个数
- 十进制:4
- 十六进制:4
- 二进制:100
复制代码
分析:
通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。
【28.6 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-7-31 11:47
本帖最后由 jianhong_wu 于 2016-7-31 12:03 编辑
第二十九节:“先余后商”和“先商后余”提取数据某位,哪家强?
第二十九节_pdf文件.pdf
(79.95 KB, 下载次数: 2186)
【29.1 先余后商。】
求商求余除了数学运算外,在实际单片机项目中还有一个很常用的功能,就是提取某个数的个十百千位。提取这些位有什么用呢?用途可大了,几乎凡是涉及界面显示的项目都要用到,比如数码管的显示,液晶屏的显示。提取某个数的个十百千位是什么意思呢?比如8562这个数,提取处理后,就可以得到千位的8,百位的5,十位的6,个位的2。这里提到的“个,十,百,千”位只是一个虚数,具体是多少应该根据实际项目而定,也有可能是“个,十,百,千,万,十万,百万...”等位,总之,提取的思路和方法都是一致的。下面以8562这个数为例开始介绍提取的思路和方法。
第一步:先把8562拆分成8562,562,62,2这四个数。怎么拆分呢?用求余的算法。比如:
- 8562等于8562%10000;
- 562等于8562%1000;
- 62等于8562%100;
- 2等于8562%10;
复制代码
第二步:再从8562,562,62,2这四个数中分别提取8,5,6,2这四个数。怎么提取呢?用求商的算法。比如:
- 8等于8562/1000;
- 5等于562/100;
- 6等于62/10;
- 2等于2/1;
复制代码
第三步:最后,把第一步和第二步的处理思路连写在一起如下:
- 8等于8562%10000/1000;
- 5等于8562%1000/100;
- 6等于8562%100/10;
- 2等于8562%10/1;
复制代码
仔细观察,上述处理思路的规律感特别清晰,我们很容易发现其中的规律和原因,如果要提取“万,十万,百万...”的位数,也是用一样的思路。另外,多说一句,根据我的经验,有一些单片机的C编译器可能不支持long类型数据的求余求商连写在一起,那么就要分两步走“先求余,再求商”,分开来操作。比如:
- unsigned char a;
- a=8562%10000/1000; //提取千位。
复制代码
分成两步走之后如下:
- unsigned char a;
- a=8562%10000;
- a=a/1000; //提取千位。
复制代码
提取其它位分两步走的思路也是一样,不多说。
【29.2 先商后余。】
刚才讲到了“先余后商”的提取思路,其实也可以倒过来“先商后余”,也就是先求商再求余数。下面还是以8562这个数为例。
第一步:先把8562拆分成8,85,856,8562这四个数。怎么拆分呢?用求商的算法。比如:
- 8等于8562/1000;
- 85等于8562/100;
- 856等于8562/10;
- 8562等于8562/1;
复制代码
第二步:再从8,85,856,8562这四个数中分别提取8,5,6,2这四个数。怎么提取呢?用求余的算法。比如:
- 8等于8%10;
- 5等于85%10;
- 6等于856%10;
- 2等于8562%10;
复制代码
第三步:最后,把第一步和第二步的处理思路连写在一起如下:
- 8等于8562/1000%10;
- 5等于8562/100%10;
- 6等于8562/10%10;
- 2等于8562/1%10;
复制代码
上述的规律感也是特别清晰的。
【29.3 “先余后商”和“先商后余”哪家强?】
上面讲了“先余后商”和“先商后余”这两种思路,到底哪种思路在实际项目中更好呢?其实我个人倾向于后者的“先商后余”,为什么呢?请看这个例子,以3100000000这个数为例,要提取该数的“十亿”位3。
第一种:用“先余后商”的套路如下:
- 3等于3100000000%10000000000/1000000000;
复制代码
这里出现了一个问题,我们知道,unsigned long类型最大的数据是0xffffffff,转换成十进制后最大的数是4294967295,但是上面出现的10000000000这个数比unsigned long类型最大的数据4294967295还要大,这个就会引来我个人的担忧,C编译器到底会怎么处理,很有可能会出现意想不到的错误,至少会让我感到心里不踏实。当然,也许会有一些朋友说,这个是多虑的,最高位完全可以把求余这一步省略,这个说法也对,但是作为一种“套路”,我还是喜欢“套路”的对称感,“套路”之所以成为“套路”,是因为有一种对称感。下面再看看如果用“先商后余”的思路来处理,会不会出现这个担忧。
第二种:用“先商后余”的套路如下:
- 3等于3100000000/1000000000%10;
复制代码
这一次,上面出现的1000000000这个数比unsigned long类型最大的数据4294967295小,所以没有刚才那种担忧,也维护了“套路”的对称感。所以我在实际项目中喜欢用这种方法。
【29.4 例程练习和分析。】
现在编写一个程序来验证刚才讲到的两种思路:
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a; //千位
- unsigned char b; //百位
- unsigned char c; //十位
- unsigned char d; //个位
- unsigned char e; //千位
- unsigned char f; //百位
- unsigned char g; //十位
- unsigned char h; //个位
- //x初始化为8562,必须是unsignd int类型以上,不能是char类型,char最大范围是255。
- unsigned int x=8562; //被提取的数
- //第一种:先余后商。
- a=x%10000/1000; //提取千位
- b=x%1000/100; //提取百位
- c=x%100/10; //提取十位
- d=x%10/1; //提取个位
- //第二种:先商后余。
- e=x/1000%10; //提取千位
- f=x/100%10; //提取百位
- g=x/10%10; //提取十位
- h=x/1%10; //提取个位
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- View(e); //把第5个数e发送到电脑端的串口助手软件上观察。
- View(f); //把第6个数f发送到电脑端的串口助手软件上观察。
- View(g); //把第7个数g发送到电脑端的串口助手软件上观察。
- View(h); //把第8个数h发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
开始...
- 第1个数
- 十进制:8
- 十六进制:8
- 二进制:1000
- 第2个数
- 十进制:5
- 十六进制:5
- 二进制:101
- 第3个数
- 十进制:6
- 十六进制:6
- 二进制:110
- 第4个数
- 十进制:2
- 十六进制:2
- 二进制:10
- 第5个数
- 十进制:8
- 十六进制:8
- 二进制:1000
- 第6个数
- 十进制:5
- 十六进制:5
- 二进制:101
- 第7个数
- 十进制:6
- 十六进制:6
- 二进制:110
- 第8个数
- 十进制:2
- 十六进制:2
- 二进制:10
复制代码
分析:
通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。
【29.5 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-8-8 16:48
本帖最后由 jianhong_wu 于 2016-8-8 17:24 编辑
第三十节:逻辑运算符的“与”运算。
第三十节_pdf文件.pdf
(71.26 KB, 下载次数: 2063)
【30.1 “与”运算。】
不管是十进制还是十六进制,单片机底层的运算都是以二进制的形式进行的,包括前面章节的加减乘除运算,在单片机的底层处理也是以二进制形式进行。只不过加减乘除我们平时太熟悉了,以十进制的形式口算或者笔算也能得到正确的结果,所以不需要刻意把十进制的数据先转换成二进制,然后再模拟单片机底层的二进制运算。但是本节的逻辑“与”运算,在分析它的运算过程和规律的时候,必须把所有的数据都转化成二进制才能进行分析,因为它强调的是二进制的位与位之间的逻辑运算。我们知道,二进制中的每一位只能是0或者1,两个数的“与”运算就是两个数被展开成二进制后的位与位之间的逻辑“与”运算。
“与”运算的运算符号是“&”。运算规律是:两个位进行“与”运算,只有两个位都同时是1运算结果才能等于1,,否则,只要其中有一位是0,运算结果必是0.比如:
- 0&0等于0。
- 0&1等于0。
- 1&0等于0。
- 1&1等于1。
复制代码
注意,上述的0和1都是指二进制的0和1。
现在举一个完整的例子来分析“与”运算的规律。有两个unsigned char类型的十进制数分别是12和9,求12&9的结果是多少?分析步骤如下:
第一步:先把参与运算的两个数以二进制的格式展开。十进制转二进制的方法请参考前面第14,15,16节的内容。
- 十进制12的二进制格式是:00001100。
- 十进制9的二进制格式是: 00001001。
复制代码
第二步:二进制数右对齐,按上下每一位进行“与”运算。
- 十进制的12 -> 00001100
- 十进制的9 -> &00001001
- “与”运算结果是 -> 00001000
复制代码
第三步:把二进制的00001000转换成十六进制是:0x08。转换成十进制是8。所以12&9的结果是8。
上述举的例子只能分析“与”运算的规律,并没有看出“与”运算的意义所在。“与”运算有啥用途呢?其实用途很多,最常见的用途是可以指定一个变量二进制格式的某位清零,其它位保持不变。比如一个unsigned char类型的变量b,数据长度一共是8位,从右往左:
想让第0位清零,其它位保持不变,只需跟十六进制的0xfe相“与”:b=b&0xfe。
想让第1位清零,其它位保持不变,只需跟十六进制的0xfd相“与”:b=b&0xfd。
想让第2位清零,其它位保持不变,只需跟十六进制的0xfb相“与”:b=b&0xfb。
想让第3位清零,其它位保持不变,只需跟十六进制的0xf7相“与”:b=b&0xf7。
想让第4位清零,其它位保持不变,只需跟十六进制的0xef相“与”:b=b&0xef。
想让第5位清零,其它位保持不变,只需跟十六进制的0xdf相“与”:b=b&0xdf。
想让第6位清零,其它位保持不变,只需跟十六进制的0xbf相“与”:b=b&0xbf。
想让第7位清零,其它位保持不变,只需跟十六进制的0x7f相“与”:b=b&0x7f。
根据上述规律,假设b原来等于十进制的85(十六进制是0x55,二进制是01010101),要想把此数据的第0位清零,只需b=b&0xfe。最终b的运算结果是十进制是84(十六进制是0x54,二进制是01010100)。把它们展开成二进制格式的运算过程如下:
- 十进制的85 -> 01010101
- 十六进制的0xfe -> &11111110
- “与”运算结果是 -> 01010100
复制代码
【30.2 例程练习和分析。】
现在编写一个程序来验证刚才讲到的“与”运算:
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a;
- unsigned char b=85; //十六进制是0x55,二进制是01010101。
- a=12&9;
- b=b&0xfe;
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:8
- 十六进制:8
- 二进制:1000
- 第2个数
- 十进制:84
- 十六进制:54
- 二进制:1010100
复制代码
分析:
通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。
【30.3 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-8-14 13:34
本帖最后由 jianhong_wu 于 2016-8-14 14:04 编辑
第三十一节:逻辑运算符的“或”运算。
第三十一节_pdf文件.pdf
(64.15 KB, 下载次数: 1867)
【31.1 “或”运算。】
“或”运算也是以位为单位进行运算的。位是指二进制中的某一位,位只能是0或者1。两个数的“或”运算就是转换成二进制后每一位的“或”运算。
“或”运算的符号是“|”。运算规律是:两个位的“或”运算,如果两个位都是0,那么运算结果才是0,否则只要其中有一位是1,那么运算结果必定是1。比如:
- 0|0等于0。
- 0|1等于1。
- 1|0等于1。
- 1|1等于1。
复制代码
现在举一个完整的例子来分析“|”运算的规律。有两个unsigned char类型的十进制数分别是12和9,求12|9的结果是多少?分析步骤如下:
第一步:先把参与运算的两个数以二进制的格式展开。十进制转二进制的方法请参考前面第13,14,15节的内容。
- 十进制12的二进制格式是:00001100。
- 十进制9的二进制格式是: 00001001。
复制代码
第二步:二进制数右对齐,按上下每一位进行“或”运算。
- 十进制的12 -> 00001100
- 十进制的9 -> |00001001
- “或”运算结果是 -> 00001101
复制代码
第三步:把二进制的00001101转换成十六进制是:0x0D。转换成十进制是13。所以12|9的结果是13。
上一节讲的“与”运算最常见的用途是可以指定一个变量的某位清0,而本节的“或”运算刚好相反,“或”运算最常见的用途是可以指定一个变量的某位置1,其它位保持不变。比如一个unsigned char类型的变量b,数据长度一共是8位,从右往左:
想让第0位置1,其它位保持不变,只需跟十六进制的0x01相“或”:b=b|0x01。
想让第1位置1,其它位保持不变,只需跟十六进制的0x02相“或”:b=b|0x02。
想让第2位置1,其它位保持不变,只需跟十六进制的0x04相“或”:b=b|0x04。
想让第3位置1,其它位保持不变,只需跟十六进制的0x08相“或”:b=b|0x08。
想让第4位置1,其它位保持不变,只需跟十六进制的0x10相“或”:b=b|0x10。
想让第5位置1,其它位保持不变,只需跟十六进制的0x20相“或”:b=b|0x20。
想让第6位置1,其它位保持不变,只需跟十六进制的0x40相“或”:b=b|0x40。
想让第7位置1,其它位保持不变,只需跟十六进制的0x80相“或”:b=b|0x80。
根据上述规律,假设b原来等于十进制的84(十六进制是0x54,二进制是01010100),要想把此数据的第0位置1,只需b=b|0x01。最终b的运算结果是十进制是85(十六进制是0x55,二进制是01010101)。把它们展开成二进制格式的运算过程如下:
- 十进制的84 -> 01010100
- 十六进制的0x01 -> |00000001
- “或”运算结果是 -> 01010101
复制代码
【31.2 例程练习和分析。】
现在编写一个程序来验证刚才讲到的“或”运算:
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a;
- unsigned char b=84; //十六进制是0x54,二进制是01010100。
- a=12|9;
- b=b|0x01;
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:13
- 十六进制: D
- 二进制:1101
- 第2个数
- 十进制:85
- 十六进制:55
- 二进制:1010101
复制代码
分析:
通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。
【31.3 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-8-21 10:11
本帖最后由 jianhong_wu 于 2016-8-21 10:40 编辑
第三十二节:逻辑运算符的“异或”运算。
第三十二节_pdf文件.pdf
(67.98 KB, 下载次数: 1892)
【32.1 “异或”运算。】
“异或”运算也是以位为单位进行运算的。位是指二进制中的某一位,位只能是0或者1。两个数的“异或”运算就是转换成二进制后每一位的“异或”运算。
“异或”运算的符号是“^”。运算规律是:两个位的“异或”运算,如果两个位都相同,那么运算结果就是0;如果两个位不同(相异),则运算结果是1。比如:
- 0^0等于0。(两个位相同)
- 0^1等于1。(两个位相异)
- 1^0等于1。(两个位相异)
- 1^1等于0。(两个位相同)
复制代码
现在举一个完整的例子来分析“^”运算的规律。有两个unsigned char类型的十进制数分别是12和9,求12^9的结果是多少?分析步骤如下:
第一步:先把参与运算的两个数以二进制的格式展开。十进制转二进制的方法请参考前面第14,15,16节的内容。
- 十进制12的二进制格式是:00001100。
- 十进制9的二进制格式是: 00001001。
复制代码
第二步:二进制数右对齐,按上下每一位进行“异或”运算。
- 十进制的12 -> 00001100
- 十进制的9 -> ^00001001
- “异或”运算结果是 -> 00000101
复制代码
第三步:把二进制的 00000101转换成十六进制是:0x05。转换成十进制是5。所以12^9的结果是5。
【32.2 “异或”在项目中的应用。】
“异或”在哪些项目上经常应用?以我个人的项目经验,平时很少用“异或”,我本人在项目中用过两次“异或”,第一次是在某项目做串口通讯协议时,通过“异或”算法,增加一个校验字节,此校验字节是一串数据依次相“异或”的总结果,目的是为了增加数据传送时的抗干扰能力。第二次是把它用来对某变量的某个位进行取反运算,如何用“异或”来实现对某位进行取反的功能?要实现这个功能,首先要清楚“异或”运算有一个潜在的规律:任何一个位,凡是与0进行“异或”运算都保持不变,凡是与1进行“异或”运算都会达到取反的运算效果。因此,如果想某位实现取反的功能,只要把相关的位与“1”进行“异或”运算就可以实现取反的功能。二进制中的一个位要么是0,要么是1,不管是0还是1,只要与1进行“异或”运算,是会达到取反的运算目的,0的会变成1,1的会变成0。请看以下这个例子:
- 0^1等于1。(两个位相异)
- 1^1等于0。(两个位相同)
复制代码
以上的例子只是列举了一个位,如果把一个字节的8位展开来,只要某位与“1”进行“异或”运算,都可以实现某位取反的功能。比如,一个十六进制的0x55,如果要这个字节的低4位都取反,高4位不变,只需要把该数据与十六进制的0x0F进行“异或”运算就可以达到目的。请看以下这个例子:
- 十六进制的0x55 -> 01010101
- 十六进制的0x0F -> ^00001111
- “异或”运算结果是 -> 01011010
复制代码
上述运算结果二进制的01011010转换成十六进制是0x5A,转换成十进制是90。
【32.3 例程练习和分析。】
现在编写一个程序来验证刚才讲到的“异或”运算:
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a;
- unsigned char b;
- a=12^9;
- b=0x55^0x0F;
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:5
- 十六进制:5
- 二进制:101
- 第2个数
- 十进制:90
- 十六进制:5A
- 二进制:1011010
复制代码
分析:
通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。
【32.4 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-8-28 20:29
本帖最后由 jianhong_wu 于 2016-8-28 21:16 编辑
第三十三节:逻辑运算符的“按位取反”和“非”运算。
第三十三节_pdf文件.pdf
(73.59 KB, 下载次数: 1615)
【前面章节漏讲的补充。】
前面的章节中,漏讲了“与,或,异或”的简写格式,在这类运算中,当赋值语句左边的“保存变量”也是参与运算的变量本身时,存在简写的语法格式,比如:
- a&=0x01; //相当于a=a&0x01;
- a|=0x01; //相当于a=a|0x01;
- a^=0x01; //相当于a=a^0x01;
复制代码
【33.1 “按位取反”运算。】
“按位取反”运算也是以位为单位进行运算的。位是指二进制中的某一位,位只能是0或者1。跟前面“加、减、乘、除、与、或、异或”有点不一样的地方是,“按位取反”的运算只有1个对象,它不像加法运算那样可以与其它第2个对象产生关系,比如“a加b”这里有2个对象a和b,而“a按位取反”只有1个对象a。一个数的“按位取反”运算就是把该数转换成二进制后对每一位的“取反”运算。
“按位取反”运算的符号是波浪符号“~”。运算规律是:针对一个数的“按位取反”,先将其展开成二进制的格式,然后每个位取反,所谓取反就是1的变成0,0的变成1。现在举一个完整的例子来分析“~”运算的规律。有两个unsigned char类型的十进制数分别是5和0,求~5和~0的结果分别是多少?分析步骤如下:
第一步:先把参与运算的两个数以二进制的格式展开。十进制转二进制的方法请参考前面第14,15,16节的内容。
- 十进制5的二进制格式是: 00000101。
- 十进制0的二进制格式是: 00000000。
复制代码
第二步:将它们二进制格式的每一位取反,1的变成0,0的变成1。
(a)对5的按位取反。
- 十进制的5 -> ~00000101
- “按位取反”运算结果是 -> 11111010
复制代码
(b)对0的按位取反。
- 十进制的0 -> ~00000000
- “按位取反”运算结果是 -> 11111111
复制代码
第三步:
- (a)把二进制的11111010转换成十六进制是:0xFA。转换成十进制是250。所以~5的结果是250。
- (b)把二进制的11111111转换成十六进制是:0xFF。转换成十进制是255。所以~0的结果是255。
复制代码
【33.2 “非”运算。】
注意,“非”运算不是以位为单位进行运算的。“非”跟“按位取反”有点相似,但是区别也明显。“按位取反”是以位为单位进行运算的,侧重在局部。而“非”是针对一个数的整体,侧重在全局。“非”只有两种状态“假”和“真”。0代表假,大于0的数值代表真,也可以说“非”假即真,“非”真即假。不是假的就是真的,不是真的就是假的。强调的是两种状态的切换。在数值表示上,用0代表假的状态,用1代表真的状态。“非”的对象也只有1个,它不像加法运算那样可以与其它第2个对象产生关系,比如“a加b”这里有2个对象a和b,而“a的非”只有1个对象a。 “非”运算的符号是感叹号“!”,注意输入这类运算符号的时候不能用汉字输入法,而是要切换到英文字符的输入法下再输入,否则编译不通过(其它运算符也一样,都要求在字符输入法下输入)。“非”运算的规律是:针对某个数的“非”,不管此数有多大,只要它大于0,那么被“非”后就一定是0。也不管此数是什么变量类型,只要它数值等于0,那么被“非”后就一定是1,而不是0xff或者0xffff之类。
现在举一个完整的例子来分析“!”运算的规律。有两个unsigned char类型的十进制数分别是5和0,求!5和!0的结果分别是多少?分析思路如下:
(a)针对5的“非”运算。
(b)针对0的“非”运算。
【33.3 例程练习和分析。】
现在编写一个程序来验证刚才讲到的“按位取反”和“非”运算:
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a=5;
- unsigned char b=5;
- unsigned char c=0;
- unsigned char d=0;
- a=~a;
- b=!b;
- c=~c;
- d=!d;
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:250
- 十六进制:FA
- 二进制:11111010
- 第2个数
- 十进制:0
- 十六进制:0
- 二进制:0
- 第3个数
- 十进制:255
- 十六进制:FF
- 二进制:11111111
- 第4个数
- 十进制:1
- 十六进制:1
- 二进制:1
复制代码
分析:
通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。
【33.4 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-9-4 12:32
本帖最后由 jianhong_wu 于 2016-9-4 13:38 编辑
第三十四节:移位运算的左移。
第三十四节_pdf文件.pdf
(98.42 KB, 下载次数: 1753)
【34.1 “左移”运算。】
“左移”运算也是以位为单位进行运算的。位是指二进制中的某一位,位只能是0或者1。欲理解某个数“左移”运算的内部规律,必先把该数展开成二进制的格式,然后才好分析。“左移”运算的符号是“<<”,它的通用格式如下:
运算规律是:“被移数”先被复制一份放到某个隐蔽的临时变量(也称作寄存器),然后对此临时变量展开成二进制的格式,左边是高位,右边是低位,此二进制格式的临时变量被整体由右往左移动了n位,原来左边的高n位数据被直接覆盖,而右边由于数据位移动而新空出的低n位数据被直接填入0,最后再把移位运算的结果存入“保存变量”。多问一句,这行代码执行完毕后,“保存变量”和“被移数”到底哪个变量发生了变化,哪个变量维持不变?大家记住,只有赋值语句“=”左边的“保存变量”发生数值变化,而右边的“被移数”没有发生变化,因为“被移数”被操作的不是它自己本身,而是它的复制品替身(某个隐蔽的临时变量,也称寄存器)。这条规律对“加、减、乘、除、与、或、异或、非、取反”等运算都是适用的,重要的事情再重复一次,这条规律就是:只有赋值语句“=”左边的“保存变量”发生数值变化,而赋值语句“=”右边的“运算变量”本身不会发生变化,因为“运算变量”被操作的不是它自己本身,而是它的复制品替身(某个隐蔽的临时变量,也称寄存器)。
上述通用格式中的n代表被一次左移的位数,可以取0,当n等于0的时候,代表左移0位,其实就是数值维持原来的样子没有发生变化。
现在举一个完整的例子来分析“<<”运算的规律。有两个unsigned char类型的变量a和b,它们的数值都是十进制的5,求a=a<<1和b=b<<2的结果分别是多少?分析步骤如下:
第一步:先把a和b变量原来的数值以二进制的格式展开。十进制转二进制的方法请参考前面第14,15,16节的内容。
- a变量是十进制5,它的二进制格式是: 00000101。
- b变量是十进制5,它的二进制格式是: 00000101。
复制代码
第二步:将a左移1位,将b左移2位。
(1)a=a<<1,就是将a左移1位。
- a左移前是 -> 00000101
- a左移1位后是 -> 00001010
复制代码
结果分析:把二进制的00001010转换成十六进制是:0x0A。转换成十进制是10。所以a初始值是5,左移1位后的结果是10。
(2)b=b<<2,就是将b左移2位。
- b左移前是 -> 00000101
- b左移2位后是 -> 00010100
复制代码
结果分析:把二进制的00010100转换成十六进制是:0x14。转换成十进制是20。所以b初始值是5,左移2位后的结果是20。
【34.2 “左移”与乘法的关系。】
上面的例子,仔细观察,发现一个规律:5左移1位就变成了10(相当于5乘以2),5左移2位就变成了20(相当于5乘以2再乘以2)。这个现象背后的规律是:在左移运算中,只要最高位不发生溢出的现象,那么每左移1位就相当于乘以2,左移2位相当于乘以2再乘以2,左移3位相当于乘以2再乘以2再乘以2......以此类推。这个规律反过来从乘法的角度看,也是成立的:某个数乘以2,就相当于左移1位,某个数乘以2再乘以2相当于左移2位,某个数乘以2再乘以2再乘以2相当于左移3位......以此类推。那么问题来了,同样是达到乘以2的运算结果,从运算速度的角度对比,“左移”和“乘法”哪家强?答案是:一条左移语句的运算速度比一条乘法语句的运算速度要快很多倍。
【34.3 “左移”的常见应用之一:不同数据类型之间的合并。】
比如有两个unsigned char单字节的类型数据H和L,H的初始值是十六进制的0x12,L的初始值是十六进制的0x34,要将两个单字节的H和L合并成一个unsigned int双字节的数据c,其中H是高8位字节,L是低八位字节,合并成c后,c的值应该是十六进制的0x1234,此程序如何写?就需要用到左移。程序分析如下:
- unsigned char H=0x12; //单字节
- unsigned char L=0x34; //单字节
- unsigned int c; //双字节
- c=H; //c的低8位被H覆盖,也就是c的低8位得到了H的值。
- c=c<<8; //及时把c的低8位移动到高8位,同时c原来的低8位被填入0
- c=c+L; //此时c再加L,c的低8位就L的值。
复制代码
程序运行结果:c就等于十六进制的0x1234,十进制是4660。
【34.4 “左移”的常见应用之二:聚焦在某个变量的某个位。】
前面第31节讲到“或”运算,其中讲到可以对某个变量的某个位置1,当时是这样讲的,片段如下:
- “或”运算最常见的用途是可以指定一个变量的某位置1,其它位保持不变。比如一个unsigned char类型的变量b,数据长度一共是8位,从右往左:
- 想让第0位置1,其它位保持不变,只需跟十六进制的0x01相“或”:b=b|0x01。
- 想让第1位置1,其它位保持不变,只需跟十六进制的0x02相“或”:b=b|0x02。
- 想让第2位置1,其它位保持不变,只需跟十六进制的0x04相“或”:b=b|0x04。
- 想让第3位置1,其它位保持不变,只需跟十六进制的0x08相“或”:b=b|0x08。
- 想让第4位置1,其它位保持不变,只需跟十六进制的0x10相“或”:b=b|0x10。
- 想让第5位置1,其它位保持不变,只需跟十六进制的0x20相“或”:b=b|0x20。
- 想让第6位置1,其它位保持不变,只需跟十六进制的0x40相“或”:b=b|0x40。
- 想让第7位置1,其它位保持不变,只需跟十六进制的0x80相“或”:b=b|0x80。
复制代码
但是这样写很多程序员会嫌它不直观,哪里不直观?就是0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80这些数不直观,这些数只是代表了聚焦某个变量不同的位。如果把这些十六进制的数值换成左移的写法,在阅读上就非常清晰直观了。比如:0x01可以用1<<0替代,0x02可以用1<<1替代,0x04可以用1<<2替代......0x80可以用1<<7替代。左移的n位,n就恰好代表了某个变量的某个位。于是,我们把上面的片段更改成左移的写法后,如下:
- “或”运算最常见的用途是可以指定一个变量的某位置1,其它位保持不变。比如一个unsigned char类型的变量b,数据长度一共是8位,从右往左:
- 想让第0位置1,其它位保持不变,只需:b=b|(1<<0)。
- 想让第1位置1,其它位保持不变,只需:b=b|(1<<1)。
- 想让第2位置1,其它位保持不变,只需:b=b|(1<<2)。
- 想让第3位置1,其它位保持不变,只需:b=b|(1<<3)。
- 想让第4位置1,其它位保持不变,只需:b=b|(1<<4)。
- 想让第5位置1,其它位保持不变,只需:b=b|(1<<5)。
- 想让第6位置1,其它位保持不变,只需:b=b|(1<<6)。
- 想让第7位置1,其它位保持不变,只需:b=b|(1<<7)。
复制代码
分析:这样改进后,阅读就很清晰直观了,只是在程序代码的效率速度方面,因为多增加了一条左移指令,意味着要多消耗一条指令的时间,那么到底该选择哪种?其实各有利弊,应该根据个人的编程喜好和实际项目来取舍。很多32位的单片机在初始化寄存器的库函数里大量应用这种左移的方法来操作,目的就是为了增加代码可读性。
根据上述规律,假设d原来等于十进制的84(十六进制是0x54,二进制是01010100),要想把此数据的第0位置1,只需d=d|(1<<0)。最终d的运算结果是十进制是85(十六进制是0x55,二进制是01010101)。
刚才上面讲到第31节的“或”运算,其实在第30节的“与”运算中也是可以用这种左移的方法来聚焦,只是要多配合一条“取反”的指令才可以。“与”运算跟“或”运算刚刚相反,它是对某个变量的某个位清零,当时是这样讲的,片段如下:
- “与”运算最常见的用途是可以指定一个变量二进制格式的某位清零,其它位保持不变。比如一个unsigned char类型的变量b,数据长度一共是8位,从右往左:
- 想让第0位清零,其它位保持不变,只需跟十六进制的0xfe相“与”:b=b&0xfe。
- 想让第1位清零,其它位保持不变,只需跟十六进制的0xfd相“与”:b=b&0xfd。
- 想让第2位清零,其它位保持不变,只需跟十六进制的0xfb相“与”:b=b&0xfb。
- 想让第3位清零,其它位保持不变,只需跟十六进制的0xf7相“与”:b=b&0xf7。
- 想让第4位清零,其它位保持不变,只需跟十六进制的0xef相“与”:b=b&0xef。
- 想让第5位清零,其它位保持不变,只需跟十六进制的0xdf相“与”:b=b&0xdf。
- 想让第6位清零,其它位保持不变,只需跟十六进制的0xbf相“与”:b=b&0xbf。
- 想让第7位清零,其它位保持不变,只需跟十六进制的0x7f相“与”:b=b&0x7f。
复制代码
但是这样写很多程序员会嫌它不直观,哪里不直观?就是0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f这些数不直观,这些数只是代表了聚焦某个变量不同的位。如果把这些十六进制的数值换成左移的写法,在阅读上就非常清晰直观了,但是注意,这里左移之后还要配一条“取反”语句。比如:0xfe可以用~(1<<0)替代,0xfd可以用~(1<<1)替代,0xfb可以用~(1<<2)替代......0x7f可以用~(1<<7)替代。左移的n位后再取反,n就恰好代表了某个变量的某个位。于是,我们把上面的片段更改成左移的写法后,如下:
- “与”运算最常见的用途是可以指定一个变量二进制格式的某位清零,其它位保持不变。比如一个unsigned char类型的变量b,数据长度一共是8位,从右往左:
- 想让第0位清零,其它位保持不变,只需:b=b&(~(1<<0))。
- 想让第1位清零,其它位保持不变,只需:b=b&(~(1<<1))。
- 想让第2位清零,其它位保持不变,只需:b=b&(~(1<<2))。
- 想让第3位清零,其它位保持不变,只需:b=b&(~(1<<3))。
- 想让第4位清零,其它位保持不变,只需:b=b&(~(1<<4))。
- 想让第5位清零,其它位保持不变,只需:b=b&(~(1<<5))。
- 想让第6位清零,其它位保持不变,只需:b=b&(~(1<<6))。
- 想让第7位清零,其它位保持不变,只需:b=b&(~(1<<7))。
复制代码
分析:这样改进后,阅读就很清晰直观了,只是在程序代码的效率速度方面,因为多增加了一条左移指令和一条取反指令,意味着要多消耗两条指令的时间,那么到底该选择哪种?其实各有利弊,应该根据个人的编程喜好和实际项目来取舍。很多32位的单片机在初始化寄存器的库函数里大量应用这种左移的方法来操作,目的就是为了增加代码可读性。
根据上述规律,假设e原来等于十进制的85(十六进制是0x55,二进制是01010101),要想把此数据的第0位清零,只需e=e&(~(1<<0))。最终e的运算结果是十进制是84(十六进制是0x54,二进制是01010100)。
【34.5 左移运算的“左移简写”。】
当被移数是“保存变量”时,存在“左移简写”。
上述左移简写如下:
比如:
- unsigned char f=1;
- unsigned char g=1;
- f<<=1; //就相当于f=f<<1;
- g<<=2; //就相当于g=g<<2;
复制代码
【34.6 例程练习和分析。】
现在编写一个程序来验证刚才讲到的“左移”运算:
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a=5;
- unsigned char b=5;
- unsigned char H=0x12; //单字节
- unsigned char L=0x34; //单字节
- unsigned int c; //双字节
- unsigned char d=84;
- unsigned char e=85;
- unsigned char f=1;
- unsigned char g=1;
- //左移运算中蕴含着乘2的规律。
- a=a<<1; //a左移1位,相当于a=a*2,从原来的5变成了10。
- b=b<<2; //b左移2位,相当于b=b*2*2,从原来的5变成了20。
- //左移的应用之一:不同变量类型的合并。
- c=H; //c的低8位被H覆盖,也就是此时c的低8位得到了H的各位值。
- c=c<<8; //及时把c的低8位移动到高8位,同时c原来的低8位被填入0
- c=c+L; //此时c再加L,c的低8位就L的值。此时c得到了H和L合并而来的值。
- //左移的应用之二:聚焦在某个变量的某个位。
- d=d|(1<<0); //对第0位置1。
- e=e&(~(1<<0)); //对第0位清零。
- //左移简写。
- f<<=1; //就相当于f=f<<1;
- g<<=2; //就相当于g=g<<2;
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- View(e); //把第5个数e发送到电脑端的串口助手软件上观察。
- View(f); //把第6个数f发送到电脑端的串口助手软件上观察。
- View(g); //把第7个数g发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:10
- 十六进制:A
- 二进制:1010
- 第2个数
- 十进制:20
- 十六进制:14
- 二进制:10100
- 第3个数
- 十进制:4660
- 十六进制:1234
- 二进制:1001000110100
- 第4个数
- 十进制:85
- 十六进制:55
- 二进制:1010101
- 第5个数
- 十进制:84
- 十六进制:54
- 二进制:1010100
- 第6个数
- 十进制:2
- 十六进制:2
- 二进制:10
- 第7个数
- 十进制:4
- 十六进制:4
- 二进制:100
复制代码
分析:
通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。
【34.7 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-9-11 11:55
本帖最后由 jianhong_wu 于 2016-9-11 13:43 编辑
第三十五节:移位运算的右移。
第三十五节_pdf文件.pdf
(80.92 KB, 下载次数: 1761)
【35.1 “右移”运算。】
“右移”运算也是以位为单位进行运算的。位是指二进制中的某一位,位只能是0或者1。欲理解某个数“右移”运算的内部规律,必先把该数展开成二进制的格式,然后才好分析。“右移”运算的符号是“>>”,它的通用格式如下:
运算规律是:“被移数”先被复制一份放到某个隐蔽的临时变量(也称作寄存器),然后对此临时变量展开成二进制的格式,左边是高位,右边是低位,此二进制格式的临时变量被整体由左往右移动了n位,原来左边由于数据位移动而新空出的高n位数据被直接填入0,而右边由于数据位移动而导致低n位数据被直接覆盖,最后再把移位运算的结果存入“保存变量”。多问一句,这行代码执行完毕后,“保存变量”和“被移数”到底哪个变量发生了变化,哪个变量维持不变?大家记住,只有赋值语句“=”左边的“保存变量”发生数值变化,而右边的“被移数”没有发生变化,因为“被移数”被操作的不是它自己本身,而是它的复制品替身(某个隐蔽的临时变量,也称寄存器)。
上述通用格式中的n代表被一次右移的位数,可以取0,当n等于0的时候,代表右移0位,其实就是数值维持原来的样子没有发生变化。
现在举一个完整的例子来分析“>>”右移运算的规律。有两个unsigned char类型的变量a和b,它们的数值都是十进制的5,求a=a>>1和b=b>>2的结果分别是多少?分析步骤如下:
第一步:先把a和b变量原来的数值以二进制的格式展开。十进制转二进制的方法请参考前面第14,15,16节的内容。
- a变量是十进制5,它的二进制格式是: 00000101。
- b变量是十进制5,它的二进制格式是: 00000101。
复制代码
第二步:将a右移1位,将b右移2位。
(1)a=a>>1,就是将a右移1位。
- a右移前是 -> 00000101
- a右移1位后是 -> 00000010
复制代码
结果分析:把二进制的00000010转换成十六进制是:0x02。转换成十进制是2。所以a初始值是5,右移1位后的结果是2。
(2)b=b>>2,就是将b右移2位。
- b右移前是 -> 00000101
- b右移2位后是 -> 00000001
复制代码
结果分析:把二进制的00000001转换成十六进制是:0x01。转换成十进制是1。所以b初始值是5,右移2位后的结果是1。
【35.2 “右移”与除法的关系。】
左移一位相当于乘以2,而右移跟左移恰恰相反,右移一位相当于除以2,注意,这里的除法是整除,不带小数点的。比如上面例子,5右移1位就变成了2(相当于5整除2等于2),5右移2位就变成了1(相当于5整除2再整除2等于1)。这个现象背后的规律是:在右移运算中,每右移1位就相当于整除2,右移2位相当于整除2再整除2,右移3位相当于整除2再整除2再整除2......以此类推。这个规律反过来从除法的角度看,也是成立的:某个数整除2,就相当于右移1位,某个数整除2再整除2相当于右移2位,某个数整除2再整除2再整除2相当于右3位......以此类推。那么问题来了,同样是达到整除2的运算结果,从运算速度的角度对比,“右移”和“整除”哪家强?答案是:一条右移语句的运算速度比一条整除语句的运算速度要快很多倍。
【35.3 “右移”的常见应用:不同数据类型之间的分解。】
比如有一个双字节unsigned int类型的变量c,它的初始值是0x1234,要把它分解成两个unsigned char单字节的类型数据H和L,其中H是高8位字节,L是低8位字节,分解后H应该等于0x12,L应该等于0x34,此程序如何写?就需要用到右移。程序分析如下:
- unsigned char H; //单字节
- unsigned char L; //单字节
- unsigned int c=0x1234; //双字节
- L=c; //c的低8位直接赋值给单字节的L
- H=c>>8; //c先把高8位右移到低8位,然后再把这8位数据赋值给H
复制代码
程序运行结果:H就等于十六进制的0x12,十进制是18。L就等于十六进制的0x34,十进制是52.提一个问题,请问执行完上述最后一条语句H=c>>8后,此时c的值是多少?答案是c仍然等于0x1234,因为c本身没有发生变化,只要它没有赋值给它自己,执行完语句后就不会改变它自己本身,也就是本节开篇就提到的:“被移数”被操作的不是它自己本身,而是它的复制品替身(某个隐蔽的临时变量,也称寄存器)。
【35.4 右移运算的“右移简写”。】
当被移数是“保存变量”时,存在“右移简写”。
上述右移简写如下:
比如:
- unsigned char d=8;
- unsigned char e=8;
- d>>=1; //就相当于d=d>>1;
- e>>=2; //就相当于e=e>>2;
复制代码
【35.5 例程练习和分析。】
现在编写一个程序来验证刚才讲到的“右移”运算:
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a=5;
- unsigned char b=5;
- unsigned char H; //单字节
- unsigned char L; //单字节
- unsigned int c=0x1234; //双字节
- unsigned char d=8;
- unsigned char e=8;
- //右移运算中蕴含着整除2的规律。
- a=a>>1; //a右移1位,相当于a=a/2,从原来的5变成了2。
- b=b>>2; //b右移2位,相当于b=b/2/2,从原来的5变成了1。
- //右移的常见应用:不同变量类型的分解。
- L=c; //c的低8位直接赋值给单字节的L
- H=c>>8; //c先把高8位右移到低8位,然后再把这8位数据赋值给H
- //右移简写。
- d>>=1; //就相当于d=d>>1;
- e>>=2; //就相当于e=e>>2;
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(H); //把第3个数H发送到电脑端的串口助手软件上观察。
- View(L); //把第4个数L发送到电脑端的串口助手软件上观察。
- View(d); //把第5个数d发送到电脑端的串口助手软件上观察。
- View(e); //把第6个数e发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:2
- 十六进制:2
- 二进制:10
- 第2个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第3个数
- 十进制:18
- 十六进制:12
- 二进制:10010
- 第4个数
- 十进制:52
- 十六进制:34
- 二进制:110100
- 第5个数
- 十进制:4
- 十六进制:4
- 二进制:100
- 第6个数
- 十进制:2
- 十六进制:2
- 二进制:10
复制代码
分析:
通过实验结果,发现在单片机上的计算结果和我们的分析是一致的。
【35.6 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-9-18 10:20
本帖最后由 jianhong_wu 于 2016-9-18 11:32 编辑
第三十六节:括号的强制功能---改变运算优先级。
第三十六节_pdf文件.pdf
(74.14 KB, 下载次数: 1720)
【36.1 括号的强制功能。】
C语言中的括号有强制的功能,比如本节内容的强制改变优先级,以及以后将要讲到的数据变量类型的强制转换,指针类型的强制转换,都是要用到括号。括号就是强制,强制就是括号。
【36.2 括号强制改变运算优先级。】
C语言的“加、减、乘、除、与、或、取反、左移、右移”等运算符是有严格优先级顺序的,但是我本人记忆力有限,做项目哪能记住这么多优先级的前后顺序,只是大概明白乘除的优先级比加减的优先级高,其它方面真的记不住那么多,怎么办?为了确保万一,我用到了“括号强制改变优先级”的功能,只要用了括号,就可以不按C语言默认的优先级顺序来出牌,可以人为的改变运算优先级,达到“随心所欲而不逾矩”的美妙境界。
括号的用法跟我们日常的数据运算公式的用法一致,先运行括号里面的运算,再执行其它运算。比如:
这行代码到底是先把变量a左移2位后再加5,还是先2加5等于7再让变量a左移7位?对于像我这样不能熟记C语言运算优先级顺序的人,这条语句很容易让我搞混。但是加上括号就明了,添加括号后如下:
不用多说,加上括号后,上述两行代码传递了清晰的优先级顺序。同理,再看一个例子:
到底是1加3的结果再乘以变量c,还是3乘以变量c的结果再加1?因为我记得乘除法的优先级比加减法的优先级高,所以答案是3乘以变量c的结果再加1。但是对于初学者,为了避免出错,加上括号就显得更加清晰了,添加括号后如下:
加括号后,优先级顺序一目了然。
【36.3 括号会不会带来额外的内存开销?】
有人会问,括号虽好,但是添加括号会不会带来额外的内存开销?答案是:不会。比如:
- c=1+3*c; //运算顺序:默认先乘,再加。
- c=1+(3*c); //运算顺序:强制先乘,再加。实现同样的功能,这里的括号也可以省略。
复制代码
上面两行代码,它们的运算顺序一样的,第二行代码虽然添加了括号,但是不会带来额外的内存开销,这两行代码所占的内存大小是一样的。
括号不是鸡肋,括号应该是保健品,食之有味,又完全无副作用。用了括号可以使程序更加具有可读性,也可以让自己避开优先级顺序的大坑。
【36.4 例程练习和分析。】
现在编写一个程序来验证刚才讲到的主要内容:
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned char a=0x01;
- unsigned char b=0x01;
- unsigned char c=0x02;
- unsigned char d=0x02;
- a=(a<<2)+5; //a左移2位后变成4,再加5等于9
- b=b<<(2+5); //2加5等于7,b再左移动7位等于128
- c=(1+3)*c; //1加3等于4,再乘以变量c等于8
- d=1+(3*d); //3乘以d等于6,再加1等于7
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:9
- 十六进制:9
- 二进制:1001
- 第2个数
- 十进制:128
- 十六进制:80
- 二进制:10000000
- 第3个数
- 十进制:8
- 十六进制:8
- 二进制:1000
- 第4个数
- 十进制:7
- 十六进制:7
- 二进制:111
复制代码
分析:
通过实验结果,发现在单片机上的实验结果和我们的分析是一致的。
【36.5 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-9-25 11:56
本帖最后由 jianhong_wu 于 2016-9-25 12:31 编辑
第三十七节:单字节变量赋值给多字节变量的疑惑。
第三十七节_pdf文件.pdf
(69.66 KB, 下载次数: 1994)
【37.1 不同类型变量的赋值疑惑。】
之前讲过,多字节变量赋值给单字节变量时,多字节变量的低8位直接覆盖单字节变量,这个很容易理解,比如:
- unsigned long a=0x12345678; //多字节变量
- unsigned char t=0xab; //单字节变量
- t=a; //多字节赋值给单字节变量,t的结果由原来的0xab变成了0x78
复制代码
那么,问题来了,如果调换过来,单字节赋值给多字节变量,多字节变量除了低8位被单字节变量所直接覆盖之外,其它剩余的位会是什么状态?会被0覆盖吗?还是会保持原来的数值不变?这个就是本节将要解开的疑惑。比如:
- unsigned long a=0x12345678; //多字节变量
- unsigned char t=0xab; //单字节变量
- a=t; //单字节赋值给多字节变量,此时,a到底是0x123456ab?还是0x000000ab?疑惑中......
复制代码
想解开此疑惑,只要亲自上机测试一下就知道结果。经过在keil平台下的C51编译器测试后,发现结果是这样子的:a是0x000000ab!也就是说,多字节变量其余高位是默认被0覆盖的。但是,我还有一个疑惑,是不是所有的C编译器都是这样默认处理,会不会在不同的C编译器平台下,会有不同的结论?所以,下面我再介绍两种比较可靠的办法给大家。
【37.2 我以前用的办法。】
我以前做项目的时候,每逢遇到这个疑惑,在不同变量赋值之前,我都多插入一行清零的代码,这行代码就是先把多字节变量通过直接赋值0来清零,因为我确信常量赋值都是直接覆盖的(其余高位都直接用0填充)。比如:
- unsigned long a=0x12345678; //多字节变量
- unsigned char t=0xab; //单字节变量
- a=0; //赋值之前先清零,这是我以前用的办法。
- a=t; //单字节赋值给多字节变量
复制代码
现在反省了一下,这种办法虽然可靠实用,但是显得过于保守。
【37.3 我现在用的办法:C语言类型的强制转换。】
前面章节提到,括号在C语言中有强制的意思,可以强制改变优先级,也可以强制促进不同变量类型的匹配。比如:
- unsigned long a=0x12345678; //多字节变量
- unsigned char t=0xab; //单字节变量
- a=(unsigned long)t; //此处的括号就是强制把t先转变成unsigned long类型,然后再赋值。
复制代码
这是我现在所使用的办法,推荐大家用这种。
【37.4 例程练习和分析。】
现在编写一个程序来验证刚才讲到的主要内容:
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void main() //主函数
- {
- unsigned long a=0x12345678; //多字节变量
- unsigned long b=0x12345678;
- unsigned long c=0x12345678;
- unsigned char t=0xab; //单字节变量
- a=t; //a是0x000000ab,其余高位默认被0覆盖。
- b=0; //这是我以前用的办法,显得过于保守
- b=t;
- c=(unsigned long)t; //C语言的类型强制转换。现在推荐大家用这种。
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:171
- 十六进制:AB
- 二进制:10101011
- 第2个数
- 十进制:171
- 十六进制:AB
- 二进制:10101011
- 第3个数
- 十进制:171
- 十六进制:AB
- 二进制:10101011
复制代码
分析:
通过实验结果,发现在单片机上的实验结果和我们的分析是一致的。
【37.5 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-10-2 08:31
本帖最后由 jianhong_wu 于 2016-10-2 08:45 编辑
第三十八节:第二种解决“运算过程中意外溢出”的便捷方法。
第三十八节_pdf文件.pdf
(75.29 KB, 下载次数: 1919)
【38.1 意外溢出。】
运算过程中的意外溢出,稍不注意,就中招,不信,请看下面的例子:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- unsigned long a=0;
- unsigned int x=1000;
- unsigned int y=3000;
- void main() //主函数
- {
- a=x*y; //猜猜a是多大?
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
猜猜a是多大?很多人以为理所当然3000000,但是实际上是50880!中招了吧。莫名其妙的50880,就是因为意外溢出所致。怎么办呢?请看下面介绍的两种解决办法。
【38.2 第一种办法:引入中间变量。】
我在前面章节中曾多次说过“为了避免运算过程中的意外溢出,建议大家把所有参与运算的变量都用unsigned long类型的变量,如果不是unsigned long类型的变量,就引入unsigned long类型的中间变量。”这种老方法如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- unsigned long a=0;
- unsigned int x=1000;
- unsigned int y=3000;
- unsigned long s; //引入的unsigned long中间变量。
- unsigned long t; //引入的unsigned long中间变量。
- void main() //主函数
- {
- s=x; //先把变量的数值搬到unsigned long中间变量。
- t=y; //先把变量的数值搬到unsigned long中间变量。
- a=s*t; //中间变量代表原始变量进行运算。
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
这一次,运算结果是正确的3000000。
现在反省了一下,这种办法虽然可靠实用,但是显得有点罗嗦,而且引入的中间变量也无形中增加了一点内存。还有没有更好的办法?请看下面介绍的第二种办法。
【38.3 第二种办法:C语言的类型强制转换。】
前面章节提到,括号在C语言中有强制的意思,可以强制改变优先级,在本节也可以临时强制改变运算过程中的变量类型。在运算过程中临时强制改变类型变量,就可以省去额外引入的中间变量,这种方法相比上面第一种老办法确实更便捷灵活。
- /*---C语言学习区域的开始。-----------------------------------------------*/
- unsigned long a=0;
- unsigned int x=1000;
- unsigned int y=3000;
- void main() //主函数
- {
- a=(unsigned long)x*(unsigned long)y; //添加的两个括号就是类型的强制转换。
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
这一次,运算结果也是正确的3000000。
多说一句,除了上述的乘法运算之外,其它的加、减、除法运算适不适用呢?虽然我还没有逐个测试,但是我感觉应该是都适用的。因此,在“加、减、除”等运算中,在必要的时候,也要在相关的变量的前缀加上类型的强制转换。
【38.4 全局变量和局部变量。】
先插入一个知识点,细心的朋友会发现,我上面的例子中,定义的变量都放在了main函数之外的上面,这种把变量定义在函数外面的变量叫全局变量,以前例子中定义在函数内的变量叫局部变量。
- unsigned char a; //这个在函数之外,叫全局变量
- void main() //主函数
- {
- unsigned char b; //这个在函数之内,叫局部变量
- while(1)
- {
- }
- }
复制代码
上面例子中,a定义在函数之外是全局变量,b定义在函数之内是局部变量。全局变量与局部变量有什么不一样呢?以后的章节会仔细讲解这方面的知识,现在暂时不讲。之所以在这里提出这个知识点,是因为我今后的例子很多变量可能都会定义成全局变量,因此先在这里给大家打个招呼,知道C语言有这样一种语法就可以。
【38.5 例程练习和分析。】
现在编写一个程序来验证刚才讲到的主要内容:
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- unsigned long a=0;
- unsigned long b=0;
- unsigned long c=0;
- unsigned int x=1000;
- unsigned int y=3000;
- unsigned long s; //中间变量
- unsigned long t;
- void main() //主函数
- {
- a=x*y; //意外溢出
- s=x; //引入中间变量
- t=y;
- b=s*t;
- c=(unsigned long)x*(unsigned long)y; //类型的强制转换
-
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数a发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数a发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:50880
- 十六进制:C6C0
- 二进制:1100011011000000
- 第2个数
- 十进制:3000000
- 十六进制:2DC6C0
- 二进制:1011011100011011000000
- 第3个数
- 十进制:3000000
- 十六进制:2DC6C0
- 二进制:1011011100011011000000
复制代码
分析:
通过实验结果,发现在单片机上的实验结果和我们的分析是一致的。
【38.6 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-10-9 09:37
本帖最后由 jianhong_wu 于 2016-10-9 09:51 编辑
第三十九节:if判断语句以及常量变量的真假判断。
第三十九节_pdf文件.pdf
(67.75 KB, 下载次数: 1758)
【39.1 if语句常规的书写格式。】
“if”在英文里的含义是“如果”的意思,在C语言里也是这个意思,是判断语句的专用关键词,也是平时做项目时应用的频率最高的语句之一。
如果if小括号里面的条件满足,就执行条件后面大括号里的语句;如果条件不满足,则直接跳过条件后面大括号里的语句。“if”语句的常见格式如下:
- if(条件)
- {
- 语句1;
- 语句2;
- }
- 语句3;
- 语句4;
复制代码
上述分析:
如果(条件)满足,就从“语句 1”开始往下执行,直到把大括号里面所有的语句执行完之后,才跳出大括号,接着从大括号之外的“语句 3”开始往下执行。
如果(条件)不满足,就直接跳过大括号里所有的语句,直接从大括号之外的“语句 3”处开始往后执行。
【39.2 if语句省略大括号的用法。】
除了上述之外,还有一种省略大括号的书写格式,但是要注意,当if条件语句后面省略了大括号时,如果if小括号里面的条件满足,仅仅执行条件后面第一条语句,如果条件不满足,则跳过条件后面第一条语句。比如:
- if(条件)
- 语句1;
- 语句2;
- 语句3;
- 语句4;
复制代码
上述分析:
如果(条件)满足,就从语句1开始一直往下执行。
如果(条件)不满足,就直接跳过(条件)后的第一条语句“语句1”,直接从(条件)后的第二条语句“语句2”开始往后执行。
上述格式省略了大括号,实际上它等效于以下这种书写:
- if(条件)
- {
- 语句1;
- }
- 语句2;
- 语句3;
- 语句4;
复制代码
在实际项目中,为了阅读清晰,建议大家不要省略大括号。
【39.3 什么是真什么是假?】
刚才讲到,if语句后面必备(条件)。那么,这个(条件)如何裁定“满足”和“不满足”?专业术语,我们用“真”表示“满足”,用“假”表示“不满足”。(条件)的真假判断,有两种:第一种是数值判断,第二种是关系判断。本节先讲第一种,数值判断。格式如下:
- if(常量或者变量)
- {
- 语句1;
- 语句2;
- }
- 语句3;
- 语句4;
复制代码
当小括号里面的(常量或者变量)不等于0时,就代表小括号里面的条件“满足”,是“真”;当小括号里面的(常量或者变量)等于0时,就代表小括号里面的条件“不满足”,是“假”。举个例子:
- if(25)
- {
- 语句1;
- 语句2;
- }
- 语句3;
- 语句4;
复制代码
上述分析:
因为”if(条件)”的“条件”是常量“25”,25不等于0,所以是“真”。因此,条件满足,直接从第一条语句“语句1”处开始往下执行。
【39.4 例程练习和分析。】
现在编写一个程序,有5条if判断语句,如果条件为真,“统计变量a”就会自动加1,最后看看条件为真的语句有几条。
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- unsigned char x=2;
- unsigned char y=0;
- unsigned char a=0; //“统计变量a”,此变量统计有多少条语句是真的
- void main() //主函数
- {
- if(1) //常量不等于0,因此为真
- {
- a=a+1; //a由0自加1后变成1。
- }
- if(0) //常量等于0,因此为假
- {
- a=a+1; //由于条件为假,这条语句没有被执行,因此此时a仍然是1
- }
- if(15) //常量不等于0,因此为真
- {
- a=a+1; //a由1自加1后变成2。
- }
- if(x) //变量x为2,不等于0,因此为真
- {
- a=a+1; //a由,2自加1后变成3。
- }
- if(y) //变量y为0,等于0,因此为假
- {
- a=a+1; //由于条件为假,这条语句没有被执行,因此此时a仍然是3
- }
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:3
- 十六进制:3
- 二进制:11
复制代码
分析:
通过实验结果,发现在单片机上的实验结果和我们的分析是一致的。
【39.5 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-10-16 08:17
本帖最后由 jianhong_wu 于 2016-10-16 08:49 编辑
第四十节:关系符的等于“==”和不等于“!=”。
第四十节_pdf文件.pdf
(70.35 KB, 下载次数: 1808)
【40.1 关系符的等于“==”和不等于“!=”。】
C语言的“=”并不是等于号,而是赋值的意思,这点前面已讲过。为了跟赋值区分开来,C语言用“==”来表示等于号的关系符,用“!=”表示不等于的关系符,之所以用“!=”表示不等于的关系,是因为C语言中的“!”就是“取非”的运算符,有否定之意。
等于关系符“==”语句的常见格式如下:
- if(常量或变量==常量或变量)
- {
- 语句1;
- 语句2;
- }
- 语句3;
- 语句4;
复制代码
不等于关系符“!=”语句的常见格式如下:
- if(常量或变量!=常量或变量)
- {
- 语句1;
- 语句2;
- }
- 语句3;
- 语句4;
复制代码
上一节讲到,常量或变量在if语句中的真假判断,不等于0就是真,等于0就是假。而本节关系运算符的真假判断也很简单清晰,满足条件就是真,不满足条件就是假。例如:
- if(2==1) //2肯定不等于1,所以不满足条件“等于的关系”,因此为假,不会执行大括号内的语句。
- {
- 语句1;
- 语句2;
- }
复制代码
相反,请继续看下面不等于号“!=”这个例子:
- if(2!=1) //2肯定不等于1,所以满足条件“不等于的关系”,因此为真,会执行大括号内的语句。
- {
- 语句1;
- 语句2;
- }
复制代码
【40.2 建议把常量放在“==”或“!=”关系符的左边】
“if(a==1)”和“if(1==a)”在实现的功能上是相同的。但是,在实际做项目的时候,还是建议大家采用后面这种写法“if(1==a)”,把常量放在左边,这样写有什么好处?好处是,如果我们不小心把等于号“==”或者“!=”误写成赋值符号“=”时,C编译器在编译时,它能及时发现错误并且报错告知我们,因为常量在左边是无法赋值的,编译器能及时发现错误。但是如果常量在右边而变量在左边,因为变量是允许赋值的,所以有一些C语言编译器未必会报错,就会留下不易察觉的程序隐患。比如:
- if(a==5)
- {
- 语句1;
- }
- if(b!=2)
- {
- 语句2;
- }
复制代码
建议改成:
- if(5==a)
- {
- 语句1;
- }
- if(2!=b)
- {
- 语句2;
- }
复制代码
【40.3 例程练习和分析。】
现在编写一个实验程序,一共有8个给定的数,要统计其中数值“等于85”的数有几个,统计其中数值“不等于75”的数有几个。
程序代码如下:
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:2
- 十六进制:2
- 二进制:10
- 第2个数
- 十进制:7
- 十六进制:7
- 二进制:111
复制代码
分析:
变量a为2。(等于85的有x3,x8这2个)
变量b为7。(不等于75的有x1,x2,x3,x4,x5,x6,x8这7个)
通过实验结果,发现在单片机上的实验结果和我们的分析是一致的。
【40.4 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-10-23 08:53
本帖最后由 jianhong_wu 于 2016-10-23 09:05 编辑
第四十一节:关系符的大于“>”和大于等于“>=”。
第四十一节_pdf文件.pdf
(60.63 KB, 下载次数: 1720)
【41.1 大于“>”。】
大于关系符“>”语句的常见格式如下:
- if(常量或变量>常量或变量)
- {
- 语句1;
- 语句2;
- }
- 语句3;
- 语句4;
复制代码
上述if条件的真假判断规则是:如果左边的数大于右边的数,此条件为真(条件满足)。否则,为假(条件不满足)。例如:
- if(2>1) //2肯定大于1,所以满足条件“大于的关系”,因此为真,会执行大括号内的语句。
- {
- 语句1;
- 语句2;
- }
复制代码
【41.2 大于等于“>=”。】
大于关系符“>=”语句的常见格式如下:
- if(常量或变量>=常量或变量)
- {
- 语句1;
- 语句2;
- }
- 语句3;
- 语句4;
复制代码
上述if条件的真假判断规则是:如果左边的数大于或者等于右边的数,此条件为真(条件满足)。否则,为假(条件不满足)。例如:
- if(2>=2) //左边的2虽然不大于右边的2,但是左边的2等于右边的2,因此为真,满足条件。
- {
- 语句1;
- 语句2;
- }
复制代码
【41.3 例程练习和分析。】
现在编写一个实验程序,一共有8个给定的数,要统计其中数值大于79的数有几个,同时,也统计其中数值大于等于79的数又有几个。
程序代码如下:
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:4
- 十六进制:4
- 二进制:100
- 第2个数
- 十进制:5
- 十六进制:5
- 二进制:101
复制代码
分析:
变量a为4。(大于79的有x1,x3, x5,x8这4个)
变量b为5。(大于等于79的有x1,x3, x4, x5,x8这5个)
通过实验结果,发现在单片机上的实验结果和我们的分析是一致的。
【41.4 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-10-30 09:22
本帖最后由 jianhong_wu 于 2016-10-30 09:35 编辑
第四十二节:关系符的小于“<”和小于等于“<=”。
第四十二节_pdf文件.pdf
(60.83 KB, 下载次数: 1753)
【42.1 小于“<”。】
小于关系符“<”语句的常见格式如下:
- if(常量或变量<常量或变量)
- {
- 语句1;
- 语句2;
- }
- 语句3;
- 语句4;
复制代码
上述if条件的真假判断规则是:如果左边的数小于右边的数,此条件为真(条件满足)。否则,为假(条件不满足)。例如:
- if(2<1) //2肯定不小于1,所以不满足条件“小于的关系”,因此为假,不会执行大括号内的语句。
- {
- 语句1;
- 语句2;
- }
复制代码
【42.2 小于等于“<=”。】
小于关系符“<=”语句的常见格式如下:
- if(常量或变量<=常量或变量)
- {
- 语句1;
- 语句2;
- }
- 语句3;
- 语句4;
复制代码
上述if条件的真假判断规则是:如果左边的数小于或者等于右边的数,此条件为真(条件满足)。否则,为假(条件不满足)。例如:
- if(2<=2) //左边的2虽然不小于右边的2,但是左边的2等于右边的2,因此为真,满足条件。
- {
- 语句1;
- 语句2;
- }
复制代码
【42.3 例程练习和分析。】
现在编写一个实验程序,一共有8个给定的数,要统计其中数值小于79的数有几个,统计其中数值小于等于79的数有几个。
程序代码如下:
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:3
- 十六进制:3
- 二进制:11
- 第2个数
- 十进制:4
- 十六进制:4
- 二进制:100
复制代码
分析:
变量a为3。(小于79的有x2,x6, x7这3个)
变量b为4。(小于等于79的有x2, x4,x6, x7这4个)
通过实验结果,发现在单片机上的实验结果和我们的分析是一致的。
【42.4 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-11-6 10:48
本帖最后由 jianhong_wu 于 2016-11-6 11:07 编辑
第四十三节:关系符中的关系符:与“&&”,或“||”。
第四十三节_pdf文件.pdf
(87.09 KB, 下载次数: 1694)
【43.1 关系符中的与“&&”。】
前面在讲关系符的时候,讲了只存在1个(判断条件)的情况下,根据这个判断为真还是为假再执行对应的操作,那么,当同时存在2个(判断条件)以上的情况下,该如何描述(判断条件)与(判断条件)之间的关系,这就涉及本节所讲的“关系符中的关系符”:与“&&”,或“||”。
先讲“&&”语句,符号“&&”称为“与”,它的含义是:假如有两个以上的(条件判断),当所有的(条件判断)都满足的时候,才认为这个整体判断是真,否则,只要有1个(条件判断)不满足,那么整体判断就是假。这个规律,有点像很多开关在电路回路中的串联关系,只有所有串联在回路中的开关都是闭合的状态,这个回路才是畅通的,否则,只要有一个开关是断开的,整个回路就是断开的。
与语句“&&”的常见格式如下:
- if(第1个条件判断&&第2个条件判断…&&第N个条件判断)
- {
- 语句1;
- 语句2;
- }
- 语句3;
- 语句4;
复制代码
在上述格式中,只有if语句后面小括号内所有的(条件判断)都满足的时候,整体判断才为真,才会执行到大括号内的“语句1”和“语句2”,否则,只要有1个不满足,就直接跳到“语句3”处往下执行。
再举一个具体的例子,比如要取从70到80之间的所有数据,也就是说,既要大于等于70,同时又要小于等于80,程序代码可以这样书写:
- if(a>=70&&a<=80) //在70到80的区间范围(包括边界70和80)
- {
- 语句1;
- 语句2;
- ……
- 语句N;
- }
复制代码
【43.2 关系符中的或“||”。】
符号“||”称为“或”,它的含义是:假如有两个以上的(条件判断),只要有1个条件判断为真,则此整体判断裁定为真,否则,必须所有的(条件判断)都不满足,此整体判断才会裁定为假。这个规律,有点像很多开关在电路回路中的并联关系,并联在回路的多个开关,只要有1个开关是闭合的状态,那么这个回路肯定是畅通的,否则,必须全部开关都是断开的,整个回路才会是断开的。
或语句“||”的常见格式如下:
- if(第1个条件判断||第2个条件判断…||第N个条件判断)
- {
- 语句1;
- 语句2;
- }
- 语句3;
- 语句4;
复制代码
在上述格式中,只要if语句后面小括号内有1个(条件判断)是满足的时候,整体判断马上裁定为真,这时就会执行到大括号内的“语句1”和“语句2”,否则,必须全部的(条件判断)都不满足,整体判断才会裁定为假,这时就会直接跳到“语句3”处往下执行。
再举一个具体的例子,比如要取除了70到80之间以外的所有数据,也就是说,要么小于70,或者要么大于80,可以这样写:
- if(a<70||a>80) //在70到80的区间范围以外的数据(不包括边界70和80)
- {
- 语句1;
- 语句2;
- ……
- 语句N;
- }
复制代码
【43.3 “&”和“&&”,“|”和“||”的区别。】
前面章节讲过运算符的“&”和“|”,它们发音也是“与”和“或”,跟本节讲的关系符“&&”和“||”的发音是同音,因此很容易让初学者混淆。区别如下:
运算符的“&”和“|”,是属于运算符,是强调数与数,变量与变量,个体与个体之间的运算,而不是关系。它们之间的运算,是把一个数或一个变量转化成二进制后,进行二进制的0和1之间的“与”“或”运算。
关系符的“&&”和“||”,是属于关系符,是强调(条件判断)与(条件判断),关系与关系,整体与整体之间的关系判断,而不是运算。它们之间的关系,关键词是判断。
【43.4 “&&”和“||”的“短路”问题。】
关系符“&&”和“||”居然也有“短路”问题?大家不要惊异,这里所说的“短路”只是强调关系符内部判断的顺序和取舍。“短路”这个词在这里只是业内已经习惯了的一种称谓,虽然我个人感觉有一点怪怪的不自然,但是我自己也想不出其它更好的词来描述这种关系,因此就跟业内已习惯的称谓保持一致。
“&&”的“短路”,它内部判断的顺序和取舍是这个样子的:在两个以上的判断中,从左边到右边,依次逐个判断,先判断第1个(条件判断),再第2个(条件判断)...再第N个(条件判断),但是,在此期间,只要发现有1个条件是不满足,就马上退出判断,不再继续判断后面的(条件判断),因为,对于“与”的关系符,只要有1个条件判断是不满足(假),就可以马上裁定整体判断为假了,没必要继续判断后面的(条件判断)。
“||”的“短路”,它内部判断的顺序和取舍是这个样子的:在两个以上的判断中,从左边到右边,依次逐个判断,先判断第1个(条件判断),再第2个(条件判断)...再第N个(条件判断),但是,在此期间,只要发现有1个条件是满足,就马上退出判断,不再继续判断后面的(条件判断),因为,对于“或”的关系符,只要有1个条件判断是满足(真),就可以马上裁定整体判断为真了,没必要继续判断后面的(条件判断)。
上述文字中的“从左到右”就是“顺序”,“马上退出”就是“取舍”。这种关系之所以称谓为“短路”,我猜测可能是把“&&”和“||”比喻成在电路的回路中,只要有个1个地方短路了,就可以马上裁定这个回路是短路的,就不用再判断其它地方了。
【43.5 例程练习和分析。】
现在编写一个实验程序,一共有8个给定的数,要统计其中数值从70到80之间的数有几个,统计其中取除了70到80之间以外的数有几个。
程序代码如下:
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:2
- 十六进制:2
- 二进制:10
- 第2个数
- 十进制:6
- 十六进制:6
- 二进制:110
复制代码
分析:
变量a为2。(数值从70到80之间的有x4, x7这2个)
变量b为6。(除了70到80之间以外的有x1, x2,x3,x5, x6, x8这6个)
通过实验结果,发现在单片机上的实验结果和我们的分析是一致的。
【43.6 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-11-13 09:30
本帖最后由 jianhong_wu 于 2016-11-13 09:48 编辑
第四十四节:小括号改变判断优先级。
第四十四节_pdf文件.pdf
(77.52 KB, 下载次数: 1682)
【44.1 小括号的概述。】
小括号在C语言里一直有“强制、改变优先级、明确顺序”这层含义。C语言中,凡是在判断语句里插入了小括号,程序就会优先执行最里面小括号的判断语句,之后才会根据判断符的优先级执行其它相关语句。
此功能很实用,因为C语言的判断符号众多,非常不利于程序员记忆各种不同符号的优先级顺序,而小括号却解决了这个问题。只要在合适的地方插入恰当的小括号,就可以强制改变判断的优先级,有了此项功能就不用程序员再刻意去记忆繁杂的优先级,同时,也为实际项目带来两个好处,一个是明确判断顺序,另一个是改变判断顺序。多说一句,哪怕添加的小括号是多余的重复的啰嗦的,也不会对程序带来副作用,反而,只会给程序员内心带来更大的确定和安全感。比如:
两个if条件判断语句:
- if(a>=70&&a<=80)和if(a<70||a>80)
复制代码
有一些朋友喜欢插入两个小括号变成:
- if((a>=70)&&(a<=80))和if((a<70)||(a>80))
复制代码
在这里插入的小括号是多余的重复的啰嗦的,但是还好,不会对程序有副作用。上述的修改,在不知道 “>、>=、<、<=” 这类语句跟 “&&,||” 这类语句哪个优先级更高的前提下,插入了小括号,可以更加明确判断的顺序,这种做法也值得肯定。
【44.2 小括号的具体应用。】
我个人平时在面对同时存在“>、>=、<、<=”和 “&&、||” 这些语句时,由于我很清楚“>,>=,<,<=”比“&&,||” 这类语句的优先级更高,所以我不需要在此插入小括号来明确判断的顺序。但是遇到下面这种情况,我是一定会通过插入小括号的方式来明确判断的顺序。什么情况呢?如下:
if(“判断条件1”||“判断条件2 ”&&“判断条件3”)
这种情况下,就会很容易让我出现一个疑问,到底是先“判断条件1” 跟“判断条件2”相“或”,最后再跟“判断条件3”相“与”?还是先“判断条件2” 跟“判断条件3”相“与”,最后再跟“判断条件1”相“或”?如果此时果断插入小括号,就可以很容易明确它们的先后顺序,减少内心不必要的纠结。
插入小括号的第1种情况:
- if((“判断条件1”||“判断条件2 ”)&&“判断条件3”)
复制代码
插入小括号的第2种情况:
- if(“判断条件1”||(“判断条件2 ”&&“判断条件3”))
复制代码
上述两种情况,具体选择哪一种判断顺序要根据项目的需要来决定。同样的3个“判断条件”,如果插入的小括号的位置不一样,判断的顺序就不一样,那么结果也可能出现不一样,比如,上述判断条件:
假设“判断条件1”为“真”,
假设“判断条件2”为“真”,
假设“判断条件3”为“假”,
等效成如下:
插入小括号的第1种情况:
这种情况下,先判断最里面小括号的真假,(真||真)的结果是“真”,然后再把结果“真”和外面的“假”进行“与”判断,(真&&假)的结果是“假”,所以上述的最终判断是“假”,不能执行“语句1”。
插入小括号的第2种情况:
这种情况下,先判断最里面小括号的真假,(真&&假)的结果是“假”,然后再把结果“假”和外面的“真”进行“或”判断,(真||假)的结果是“真”,所以上述的最终判断是“真”,能执行“语句1”。
综合上述两种情况,对比之后,得出这样的结论:在同样的条件和关系下,如果插入不同位置的小括号,就可以得出不同的结果。也就是说,小括号可以让关系判断变得丰富起来,可以实现各种复杂的逻辑判断功能。
【44.3 例程练习和分析。】
现在编写一个实验程序验证上述两种判断顺序。
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- //x,y这三个变量作为条件判断的变量
- unsigned char x=5;
- unsigned char y=6;
- //a,b这两个变量作为输出判断结果的真假,0代表假,1代表真。
- unsigned char a=0; //默认为0,也就是默认为假
- unsigned char b=0; //默认为0,也就是默认为假
- void main() //主函数
- {
- if((x<y||y>x)&&x==y) //里面的条件是((真||真)&&假),最终结果判断是假
- {
- a=1;
- }
- if(x<y||(y>x&&x==y)) //里面的条件是(真||(真&&假)),最终结果判断是真
- {
- b=1;
- }
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:0
- 十六进制:0
- 二进制:0
- 第2个数
- 十进制:1
- 十六进制:1
- 二进制:1
复制代码
分析:
变量a为0。(0代表此条件判断结果为假)
变量b为1。(1代表此条件判断结果为真)
通过实验结果,发现在单片机上的实验结果和我们的分析是一致的。
【44.4 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-11-20 11:01
本帖最后由 jianhong_wu 于 2016-11-20 11:32 编辑
第四十五节: 组合判断if...else if...else。
第四十五节_pdf文件.pdf
(74.55 KB, 下载次数: 1738)
【45.1 三类组合判断语句的概述。】
if的英文含义是“如果”,else是“否则”,else if是“否则如果”。在C语言里,if,else if ,else 所表达的含义,跟英文也是一样的。
if,else if,else可以组成三种组合判断语句,第一种是“二选一”,第二种是“多选一”,第三种是“多选一或者什么都不选”。
这类组合判断语句可以这样解读:在众多条件判断中,先从第一个if条件开始判断,如果第一个if条件是真,那么不管后面的条件是否为真,都不再判断,直接执行第一个if条件后面大括号的语句,组合语句中其它剩余的条件不再判断直接跳过,否则,就挨个条件往下判断,只要其中一个条件满足,就不再判断剩余的条件,也就是我们日常所说的多选一,甚至在某些组合语句如果所有条件都不满足,那么什么也不选。总之,在如此众多的条件中,最多只能执行一个条件后面大括号的语句。组合语句还有一个规律:if语句只能出现在第一个条件判断,而且只能出现一次;else只能出现在最后,而且也只能出现一次;而else if语句总是出现在中间,绝对不能出现在第一个条件判断,如果没有else,也可以出现在最后的条件判断。多说一句,在上述所提到的“只能出现一次”的概念仅仅局限于在一个组合判断语句的范围内,而组合判断语句在整个程序的出现次数是不受限制的。
【45.2 二选一的组合判断。】
先讲第一种的“二选一”的书写格式,如下:
书写格式如下:
- if(条件1) //if只能出现第一个条件,并且只能出现一次
- {
- 语句1;
- }
- else //else只能出现最后,并且也只能出现一次。
- {
- 语句2;
- }
- 语句3;
复制代码
这类语句的书写特点是:第一个是if判断语句,最后一个是else语句,中间没有else if判断语句。
这类语句的执行顺序是:先判断第一个的if里面的(条件1),如果(条件1)满足而为真,就执行该(条件1)后面紧跟的大括号里面的“语句1”,执行完该大括号内的所有语句之后,就直接跳出整个组合判断的语句,不再判断也不再执行剩下来的else那部分的代码,直接跳到“语句3”处,从“语句3”处(包括“语句3”)继续往下执行。但是,如果第一个的if里面的(条件1)不满足而为假,那么就直接执行else后面大括号内的语句。也就是说,else是在if条件不满足时才执行的,所以叫“二选一”,在if和else之间二选一。
【45.3 多选一的组合判断。】
接着讲第二种书写格式的“多选一”,这种书写格式,跟第一种对比,是在if与else的中间多插入了N个else if的判断语句。书写格式如下:
- if(条件1) //if只能出现第一个条件,并且只能出现一次
- {
- 语句1;
- }
- else if(条件2) //else if只能出现中间或最后,可以出现多次
- {
- 语句2;
- }
- ...
- else if(条件N) //else if只能出现中间或最后,可以出现多次
- {
- 语句N;
- }
- else //else只能出现最后,并且也只能出现一次。
- {
- 语句N+1;
- }
- 语句N+2;
复制代码
这类语句的书写特点是:第一行是if开始,最后一行以else结束,中间是N个else if判断语句。
这类语句的执行顺序是:跟第一种“二选一”对比,判断顺序和规律大致也是一样的,也是从第一个if开始,往下逐个判断,然后到中间的else if,只要发现一个条件满足,就执行该条件后面的大括号内的代码,之后就马上结束整个组合判断语句,不再判断剩下的组合判断语句。但是,如果万一前面第一个if和中间所有的else if的条件都不满足而为假,就直接执行最后一个else大括号内的语句。所以叫“多选一”,在“第一个if、中间的else if、最后一个else”之间多选一。
【45.4 多选一或者什么都不选的组合判断。】
最后讲第三种书写格式的“多选一或者什么都不选”,这种书写格式,跟第二种对比,只有第一个if和其它的else if语句,没有最后那个else语句。书写格式如下:
- if(条件1) //if只能出现第一个条件,并且只能出现一次
- {
- 语句1;
- }
- else if(条件2) //else if只能出现中间或最后,可以出现多次
- {
- 语句2;
- }
- ...
- else if(条件N) //else if只能出现中间或最后,可以出现多次
- {
- 语句N;
- }
- 语句N+1;
复制代码
这类语句的书写特点是:第一行是if开始,中间是N个else if判断语句,没有最后一个else语句。
这类语句的执行顺序是:跟第二种“多选一”对比,判断顺序和规律大致也是一样的,也是从第一个if开始,往下逐个判断,然后到中间的else if,只要发现一个条件满足,就执行该条件后面的大括号内的代码,之后就马上结束整个组合判断语句,不再判断剩余的组合判断语句。但是,如果万一前面第一个if和中间所有的else if的条件都不满足而为假,因为此时没有else语句,就意味着整个组合判断语句都没有条件满足,因此就没有相关满足的代码被执行到。所以把这种情况称为“多选一或者什么都不选”。
【45.5 例程练习和分析。】
现在编写一个实验程序。
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- //x这个变量作为条件判断的变量
- unsigned char x=5;
- //a,b,c这3个变量作为输出判断结果,0代表什么语句都没执行,1代表执行了语句1,
- //2代表执行语句2,3代表执行语句3。
- unsigned char a=0;
- unsigned char b=0;
- unsigned char c=0;
- void main() //主函数
- {
- //第一种“二选一”
- if(x>6)
- {
- a=1; //1代表执行了“语句1”
- }
- else
- {
- a=2; //2代表执行了“语句2”
- }
- //第二种“多选一”
- if(x>6)
- {
- b=1; //1代表执行了“语句1”
- }
- else if(7==x)
- {
- b=2; //2代表执行了“语句2”
- }
- else
- {
- b=3; //3代表执行了“语句3”
- }
- //第三种“多选一或者什么都不选”
- if(x>6)
- {
- c=1; //1代表执行了“语句1”
- }
- else if(7==x)
- {
- c=2; //2代表执行了“语句2”
- }
- else if(8==x)
- {
- c=3; //3代表执行了“语句3”
- }
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:2
- 十六进制:2
- 二进制:10
- 第2个数
- 十进制:3
- 十六进制:3
- 二进制:11
- 第3个数
- 十进制:0
- 十六进制:0
- 二进制:0
复制代码
分析:
变量a为2。(2代表执行了语句2)
变量b为3。(3代表执行了语句3)
变量c为0。(0代表什么语句都没执行)
【45.6 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-11-27 11:29
本帖最后由 jianhong_wu 于 2016-12-4 08:55 编辑
第四十六节: 一维数组。
第四十六节_pdf文件.pdf
(90.96 KB, 下载次数: 1685)
【46.1 数组是什么?】
数组就是一堆变量或常量的集合。把一个数组里面某一个变量或者常量称为数组的元素,反过来也可以这么说,元素的集合就是数组。数组的最大特点就是内部所有的元素的地址都是挨家挨户相连的,同花顺似的,以第一个元素(下标是0的元素)为首地址,后来元素的地址挨个依次增大。首地址在RAM中的绝对地址往往是编译器自动分配的,我们不用管,可以看成是随机的。多说一句,在某些单片机,也可以通过特定的C语言关键词,强制要求编译器按我们的意愿,来分配到RAM中指定的某个绝对地址,这部分的内容这里暂时不讲。继续刚才的话题,首地址就像是一个坐标原点,一旦被编译器确定下来它在RAM中的地址,那么后面其它元素的地址都是在此基础上依次增大的,有规律的。正因为这个特点,数组在项目中往往起到缓存的作用。比如,在通信的项目中,用来作为一串数据的接收缓存。在界面显示的项目中,某个16x16点阵汉字的字模,需要一个内含32个元素的数组来作为缓存。在读写文件的项目中,也需要一个大数组来作为文件内容的缓存。在某些涉及复杂算法的项目,以数组作为缓存,并且通过配合循环语句或者指针,就可以快速批量的处理数据(循环语句和指针的相关知识后面章节会讲到)。总之,在项目应用中,数组无处不在。
数组分为一维数组,二维数组,三维数组。一维数组应用最广,二维数组其次,三维数组最少用。所以本教程只讲一维数组和二维数组,本节先讲一维数组。
【46.2 一维数组的书写格式和特点。】
一维数组不带初始化时候的定义格式如下:
数据类型是指unsigned char,unsigned int,unsigned long这类关键词;数组名就是由字母和数字组合而成的字符串,遵循常用变量的命名规则;N是数字,代表此数组内部有多少个元素。比如:
- unsigned char x[3]; //这里的3是数组内部元素的总数,但不是下标。
复制代码
上述这一行代码,就相当于一条语句定义了3个变量,这3个变量分别是x[0],x[1],x[2],但是不存在x[3]这个变量。这里,具体元素中括号内的“0,1,2”称为数组的下标,代表某个具体的元素。由此可见,数组有“批量定义”的特点。同时也要发现,此数组定义的N是3,代表内含3个元素变量,但是具体到某个元素的时候,下标不是从1开始,而是从0开始,最后一个也不是3而是2。可以这样描述,某个数组有N个元素,它具体元素的下标是从0开始,到N-1结束。那么问题来,如果一个数组明明最大只有N个元素,但是我在操作某个具体的元素时,非要用下标N或者N+1,也就是说,如果超过数组的范围的操作,会出现什么问题?后果严重吗?答案是:会导致数组越界出现异常或者编译不通过,可能会破坏其它数据,后果是严重的。因此大家使用数组的时候,要注意数组不能越界的问题。
刚刚讲了一维数组不带初始化的定义格式,现在接着讲带初始化的定义格式,如下:
- 数据类型 数组名[数组元素总数N]={元素0, 元素1,…元素N-1};
复制代码
比如:
- unsigned char y[3]={10,11,12};
复制代码
此数组一行代码定义了3个变量,分别是y[0], y[1], y[2]。而y[0]初始化为10,y[1]初始化为11,y[2]初始化为12。
在程序中,操作数组某个变量元素时,下标可以是常量,比如y[0],此时的0就是常量;下标也可以是变量,比如y的中括号内的i,此时的i就是变量。再强调一次,作为下标的常量或者变量i的数值必须小于数组定义时的元素个数,否则就会导致数组越界出现异常或者编译不通过。
中括号内的N什么时候是“数组的元素总数”,什么时候是“数组的元素下标”,这个问题对初学者很容易混淆。其实很简单,定义的时候是“数组的元素总数”,操作调用具体某个元素的时候是“数组的元素下标”。
【46.3 什么情况下可以省略定义的元素总数?】
一维数组在定义时,如果预先给它填写若干个初始化的数据,在语法上,也可以省略中括号[N]里面的元素总数N,这样编译器在编译时会根据初始化的总数来自动识别和定义此一维数组实际元素总数,分配对应数量的内存RAM。比如:
- unsigned char y[3]={10,11,12}; //没有省略元素总数的写法
复制代码
跟
- unsigned char y[]={10,11,12}; //在初始化的情况下,省略了元素总数的写法。
复制代码
的意义是一样的,都是合法的,都是C语言所允许的。注意,省略元素个数时必须要有初始化的数据,否则,编译器不知道此数组的长度,可能导致编译出错。
这个功能在实际应用中有什么作用呢?在实际应用中,此项功能一般会用在常量数组里,而不是变量的数组里。当在数组定义的前面加上“const”或者“code”(针对51单片机)的关键词时,原来“变量”的数组就会变成“常量”的数组,这时,如果把常量的数组用来作为某个转换表格,此功能就很实用。因为作为转换表格的常量数组,我们在编程程序的过程中,有可能随时往里面添加数组,这个时候,不用我们刻意去计算和调整数组的元素总数N,给我们写程序带来了便利。对于这个功能的应用,大家先有一个感性的认识即可,暂时不用深入去了解,因为后续的章节还会讲解这方面的内容。
【46.4 例程练习和分析。】
现在编写一个程序来熟悉一下一维数组的使用。
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- unsigned char x[3]; //此处的3不是下标,而是元素总数,里面的3个变量没有初始化
- unsigned char y[3]={10,11,12}; //里面三个元素变量y[0],y[1],y[2]分别初始化为10,11,12
- unsigned char i=0; //定义和初始化一个变量。用来做x数组的下标。
- void main() //主函数
- {
- x[i]=25; //此时下标i为0.相当于把25赋值给x[0]
- i=i+1; //i由0变成1.
- x[i]=26; //此时下标i为1.相当于把26赋值给x[1]
- i=i+1; //i由1变成2.
- x[i]=27; //此时下标i为2.相当于把27赋值给x[2]
- x[i]=x[i]+1; //此时x[2]自加1变成了28
- View(x[0]); //把第1个数x[0]发送到电脑端的串口助手软件上观察。
- View(x[1]); //把第2个数x[1]发送到电脑端的串口助手软件上观察。
- View(x[2]); //把第3个数x[2]发送到电脑端的串口助手软件上观察。
- View(y[0]); //把第4个数y[0]发送到电脑端的串口助手软件上观察。
- View(y[1]); //把第5个数y[1]发送到电脑端的串口助手软件上观察。
- View(y[2]); //把第6个数y[2]发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:25
- 十六进制:19
- 二进制:11001
- 第2个数
- 十进制:26
- 十六进制:1A
- 二进制:11010
- 第3个数
- 十进制:28
- 十六进制:1C
- 二进制:11100
- 第4个数
- 十进制:10
- 十六进制:A
- 二进制:1010
- 第5个数
- 十进制:11
- 十六进制:B
- 二进制:1011
- 第6个数
- 十进制:12
- 十六进制:C
- 二进制:1100
复制代码
分析:
变量元素x[0]为25。
变量元素x[1]为26。
变量元素x[2]为28。
变量元素y[0]为10。
变量元素y[1]为11。
变量元素y[2]为12。
【46.5 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-12-4 09:46
本帖最后由 jianhong_wu 于 2016-12-4 10:04 编辑
第四十七节: 二维数组。
第四十七节_pdf文件.pdf
(74.94 KB, 下载次数: 1708)
【47.1 二维数组的书写格式和特点。】
拿一维数组和二维数组来对比一下,一维数组只有一个下标,像由很多点连成的一条直线,整体给人的是一种“线”的观感。而二维数组有两个下标,这两个下标类似平面中的行与列,也类似平面中的X轴和Y轴的坐标,通过y轴和x轴坐标就可以找到所需的点,也就是二维数组的某个元素,因此,二维数组整体给人的是一种“面”的观感。
上述是对二维数组的感性描述,二维数组是由一维数组发展而来,所以继承了很多一维数组的特点。二维数组的所有“网点”元素的地址都是挨个相临的,先从第0行开始“扫描”当前行的列,第0行第0列,第0行第1列,第0行第2列......再第1行第0列,第1行第1列,第1行第2列......再第2行......再第N行,上一行“尾”元素跟下一行“头”元素的地址也是相临连续的。
二维数组未带初始化时的通用定义格式如下:
比如:
- unsigned char a[2][3]; //此处的2代表有2行,3代表有3列。
复制代码
分析:此二维数组定义了6个变量,跟一维数组一样,下标都是从0开始,到(N-1)时结束,此处的N代表行数或者列数。所以a[2][3]数组的元素挨个分别是a[0][0],a[0][1], a[0][2], a[1][0], a[1][1], a[1][2]这6个变量。这6个变量的地址是顺序挨个相连的。
二维数组有两种常用初始化格式,一种是逐行初始化,一种是整体初始化。
第一种逐行初始化:
- unsigned char a[2][3]=
- {
- {0,1,2},
- {3,4,5}
- };
复制代码
在逐行初始化定义二维数组时,只要有初始化的数据,也可以省略行下标,但是列下标不能省略,比如:
- unsigned char a[][3]=
- {
- {0,1,2},
- {3,4,5}
- };
复制代码
此时编译器会根据元素的个数来确定行数是多少。
第二种整体初始化,跟一维数组一样,内部数据元素不需要额外增加大括号来分行。
- unsigned char a[2][3]=
- {
- 0,1,2,3,4,5
- };
复制代码
或者
- unsigned char a[2][3]=
- {
- 0,1,2,
- 3,4,5
- };
复制代码
都行。
C语言是很丰富的语言,比如二维数组还允许不完全初始化的一些情况,这种情况我就不再深入讲解,我讲解的都是挑选一些针对以后单片机项目中可能会经常用到的语法。
二维数组我在很多项目上还是经常用到的,比如用在一些需要把所得的信息进行查表判断的项目,在每一行里放一条关键词字符串信息,利用循环语句进行逐行查找匹配信息。至于二维数组如何存放字符串的知识点以后再讲。这节的重点是让大家对二维数组有个初步的认识。
【47.2 例程练习和分析。】
现在编写一个程序来熟悉一下二维数组的书写和使用格式。
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- unsigned char a[2][3]= //定义和初始化一个二维数组
- {
- {0,1,2},
- {3,4,5}
- };
- void main() //主函数
- {
- a[0][0]=8; //故意把第0行第0列的这个变量赋值8,让大家看看如何直接操作二维数组某个元素。
- View(a[0][0]); //把第1个数a[0][0]发送到电脑端的串口助手软件上观察。
- View(a[0][1]); //把第2个数a[0][1]发送到电脑端的串口助手软件上观察。
- View(a[0][2]); //把第3个数a[0][2]发送到电脑端的串口助手软件上观察。
- View(a[1][0]); //把第4个数a[1][0]发送到电脑端的串口助手软件上观察。
- View(a[1][1]); //把第5个数a[1][1]发送到电脑端的串口助手软件上观察。
- View(a[1][2]); //把第6个数a[1][2]发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:8
- 十六进制:8
- 二进制:1000
- 第2个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第3个数
- 十进制:2
- 十六进制:2
- 二进制:10
- 第4个数
- 十进制:3
- 十六进制:3
- 二进制:11
- 第5个数
- 十进制:4
- 十六进制:4
- 二进制:100
- 第6个数
- 十进制:5
- 十六进制:5
- 二进制:101
复制代码
分析:
变量元素a[0][0]为8。从原来定义的0变成8,因为被main函数里的第1行代码赋值了8。
变量元素a[0][1]为1。
变量元素a[0][2]为2。
变量元素a[1][0]为3。
变量元素a[1][1]为4。
变量元素a[1][2]为5。
【47.3 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-12-11 10:48
本帖最后由 jianhong_wu 于 2016-12-11 11:11 编辑
第四十八节: while循环语句。
第四十八节_pdf文件.pdf
(94.91 KB, 下载次数: 1614)
【48.1 程序的“跑道”。】
经常听到这句话“程序跑起来了吗?”,程序在哪里跑?有跑道吗?有的。循环语句就像一条椭圆的跑道,程序在跑道上不停的跑,不知疲倦的跑,永无止境,一秒钟几百万圈的速度。单片机的main主函数内往往有一条while(1)语句,这就是单片机的“循环跑道”,称之为主循环,主循环内还可以继续嵌套多层while循环语句。
【48.2 while循环的常见格式。】
常见格式如下:
- while(条件)
- {
- 语句1;
- 语句2;
- ……
- 语句N;
- }
- 语句N+1;
复制代码
上面的“花括号内”称为“循环体内”,“花括号外”称为“循环体外”,现在来分析一下上述代码的执行规律,如下:
(1)像if语句一样,先判断while的(条件)是否为真。如果为“假”,就不执行循环体“内”的“语句1”至“语句N”,直接跳到循环体“外”的“语句N+1”处开始往下执行。如果为“真”,才执行循环体“内”的“语句1”至“语句N”,当执行完循环体“内”最后的“语句N“时,单片机会突然返回到第一行代码“while(条件)”处,继续判断循环的(条件)是否为真,如果为假就跳到循环体“外”的“语句N+1”,表示结束了当前循环。如果为真就继续从“语句1“执行到“语句N“,然后再返回while(条件)处,依次循环下去,直到条件为假时才罢休,否则一直循环下去。
(2)while(条件)语句中,条件判断真假的规则跟if语句一模一样,有两种类型:一种是纯常量或者变量类型的,只要此数值不等于0就认为是真,所以while(1)也称死循环语句,因为里面的条件永远不为0。对于死循环这样的语句,如果不遇到break,return,goto这些语句,那么就永远也别想跳出这个循环;另外一种是关系判断,以及关系语句之间的像“与或”关系这类的判断。这些条件判断的真假,跟if语句的规则是一样的,这里不再多讲。break,return,goto这些语句后面章节会讲到。
【48.3 while省略花括号,没带分号。】
- while(条件)
- 语句1;
- 语句2;
- ……
- 语句N;
- 语句N+1;
复制代码
上面的代码,居然没有了花括号,问题来了,此循环语句的“有效射程”究竟是多远,或者说,此循环语句的循环区域在哪里。现在跟大家解开这个谜团。第一行代码,while(条件)后面“没有分号”,接着第二行就是“语句1”,所以,这种情况跟if语句省略花括号的写法是一样的,此时循环体默认只包含离它最近的一条且仅仅一条的“语句1”,因此,上述的语句,等效于下面这种添加花括号的写法:
- while(条件)
- {
- 语句1;
- }
- 语句2;
- ……
- 语句N;
- 语句N+1;
复制代码
【48.4 while省略花括号,带分号。】
- while(条件);
- 语句1;
- 语句2;
- ……
- 语句N;
- 语句N+1;
复制代码
这次的代码跟刚才“48.3”的代码唯一的差别是,第一行代码,while(条件)后面“有分号”。所以它循环的有效范围就在第一行就结束了,不涉及“语句1”。此时,等效于下面这种添加花括号的写法:
- while(条件)
- {
- ; //这里的分号代表一条空语句
- }
- 语句1;
- 语句2;
- ……
- 语句N;
- 语句N+1;
复制代码
如果while的(条件)一直为“真”,单片机就一直在循环体内执行一条“无意义”的空语句,相当于“耗着”的状态,执行不到后面“语句1”的语句,除非,条件为“假”才罢休才会跳出循环体。
循环体内什么都没有,只写一条“空语句”,这种写法在实际项目中也是有用武之地的,比如,等待某件事是否满足条件,如果不满足,就一直死等死磕在这里,其它事情都干不了,这种“死等死磕”的做法,专业术语叫“阻塞”,与之反面相对应的是另外一个词叫“非阻塞”。对于循环的“阻塞”用法,老练的工程师通常会多加一个超时的判断,这些内容大家暂时不用深入了解,后续章节我会讲到。
【48.5 例程练习和分析。】
现在编写一个程序来熟悉一下while语句的书写和使用格式。
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- unsigned char a=0; //观察这个数最后的变化
- unsigned char b=0; //观察这个数最后的变化
- unsigned char i; //控制循环体的条件判断变量
- void main() //主函数
- {
- i=3;
- while(i) //i不断减小,直到变为0时才跳出此循环体
- {
- a=a+1; //当i从3减少到0的时候,这条语句被循环执行了3次。
- i=i-1; //循环的条件不断发生变化,不断减小
- }
- i=0;
- while(i<3) //i不断增大,当i大于或者等于3时才跳出此循环体
- {
- b=b+2; //当i从0增加到3的时候,这条语句被循环执行了3次。
- i=i+1; //循环的条件不断发生变化,不断增加
- }
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:3
- 十六进制:3
- 二进制:11
- 第2个数
- 十进制:6
- 十六进制:6
- 二进制:110
复制代码
分析:
变量a为3。a初始化为0,进入循环体内后,a每次加1,循环加3次,因此从0变成了3。
变量b为6。b初始化为0,进入循环体内后,b每次加2,循环加3次,因此从0变成了6。
【48.6 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-12-18 18:41
本帖最后由 jianhong_wu 于 2016-12-18 19:15 编辑
第四十九节: 循环语句do while和for。
第四十九节_pdf文件.pdf
(76.53 KB, 下载次数: 1556)
【49.1 do while语句的常见格式。】
格式如下:
- do
- {
- 语句1;
- 语句2;
- ……
- 语句N;
- } while(条件);
复制代码
上述代码,单片机从上往下执行语句,先从do那里无条件进来,从“语句1”开始往下执行,一直执行到“语句N”,才开始判断while(条件)的条件是否为真,如果为真继续返回到do的入口处,继续从“语句1”开始往下执行,依次循环。大家留意到了吗,do while和while语句有什么差别?差别是,do while是先无条件进来执行一次循环体(花括号里所有的程序代码),执行到循环体最底部才判断while(条件)的条件是否为真来决定是否继续循环,先上车再买票。而while语句是先判断条件是否为真再决定是否需要进入循环体,先买票再上车。
【49.2 for语句的简介。】
for语句也是循环语句,任何for语句能实现的功能都可以用while语句来实现同样的功能,for语句和while语句有什么差别呢?for语句把变量初始化,变量的条件判断,变量在执行循环体后的步进变化这三个常见要素集成在语句内部,以标准的格式书写出来。在很多场合下,for在书写和表达方面比while语句显得更加简洁和直观。
【49.3 for语句的自加格式。】
格式如下:
- for(变量的初始化语句; 变量的条件判断;变量在执行一次循环体后自加的步进变化)
- {
- 语句1;
- 语句2;
- ……
- 语句N;
- }
复制代码
在把上述变成更具体的代码例程如下:
- for(i=0; i<3;i++)
- {
- 语句1;
- 语句2;
- ……
- 语句N;
- }
复制代码
上述代码,单片机从上往下,在进入循环体前,先把变量i初始化赋值0(这行初始化代码在整个循环期间只被执行1次),然后判断i是否小于3这个条件,如果此条件为真,就开始正式进入循环体,从“语句1”往下执行到“语句N”,执行完一次循环体后,i就自加1(因为“i++”语句),此时i从原来初始化的0变成了1,接着再返回来到for语句的条件判断”i<3”那里,判断i是否继续满足“小于3”这个条件,如果此条件为真就继续往下执行,否则就跳过循环体结束当前循环。上述for语句实现的功能如果用while语句来写,等效于以下代码:
- i=0; //进入循环体之前先初始化给予初值
- while(i<3)
- {
- 语句1;
- 语句2;
- ……
- 语句N;
- i++; //执行一次循环体之后此变量自加发生变化
- }
复制代码
上述的while循环语句只执行了3次循环体。
【49.4 for语句的自减格式。】
刚才讲的for(i=0; i<3;i++)这种格式,它的变量i是不断自加的。还有一种比较常见的格式是i不断自减的,它的格式如下:
- for(i=3; i>0;i--)
- {
- 语句1;
- 语句2;
- ……
- 语句N;
- }
复制代码
上述自减的for语句功能如果用while语句来写,等效于以下代码:
- i=3; //进入循环体之前先初始化给予初值
- while(i>0)
- {
- 语句1;
- 语句2;
- ……
- 语句N;
- i--; //执行一次循环体之后此变量自减发生变化
- }
复制代码
上述的while循环语句只执行了3次循环体。
【49.5 for省略花括号,没带分号。】
前面讲的if和while语句中,都提到了省略花括号的情况,for语句也有这种写法,而且省略之后默认的有效范围都是一样的。请看例子如下:
- for(i=0; i<3;i++) //注意,这里没带分号。
- 语句1;
- 语句2;
- ……
- 语句N;
复制代码
分析:上述代码,跟if语句一样,此时循环体默认只包含“语句1”,等效于:
- for(i=0; i<3;i++) //注意,这里没带分号。
- {
- 语句1;
- }
- 语句2;
- ……
- 语句N;
复制代码
【49.6 for省略花括号,带分号。】
- for(i=0; i<3;i++); //注意,这里带分号。
- 语句1;
- 语句2;
- ……
- 语句N;
复制代码
分析:注意,此时循环体默认不包含“语句1”,而是等效于:
- for(i=0; i<3;i++)
- {
- ; //空语句。
- }
- 语句1;
- 语句2;
- ……
- 语句N;
复制代码
此时循环体内先循环执行三次空语句,然后才会结束for循环,接着才从“语句1”开始往下执行。
【49.7 for循环语句的条件判断。】
上面举的例子中,仅仅列出了for语句条件判断的小于号关系符“<”,其实,for语句条件判断的关系符跟if语句是一样通用的,凡是if语句能用的关系符都可以用在for语句上,比如“>”,“!=”,“==”,“<=”,“>=”等等。如下:
- for(i=0;i<=3;i++); //小于等于的情况。这种写法是合法的。
- for(i=0;i!=3;i++); //不等于的情况。这种写法是合法的。
- for(i=0;i==3;i++); //等于的情况。这种写法是合法的。
复制代码
【49.8 例程练习和分析。】
编写一个程序来熟悉一下do while和for语句的使用。
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- unsigned char a=0; //观察这个数最后的变化
- unsigned char b=0; //观察这个数最后的变化
- unsigned char c=0; //观察这个数最后的变化
- unsigned char i; //控制循环体的条件判断变量
- void main() //主函数
- {
- i=3;
- do
- {
- a=a+1; //每执行一次循环体a就增加1,此行代码被循环执行了3次
- i=i-1; //i不断变小
- }while(i); //i不断变小,当i变为0时才跳出此循环体
- for(i=0;i<3;i++)
- {
- b=b+2; //此行代码被循环执行了3次
- }
- for(i=3;i>0;i--)
- {
- c=c+3; //此行代码被循环执行了3次
- }
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:3
- 十六进制:3
- 二进制:11
- 第2个数
- 十进制:6
- 十六进制:6
- 二进制:110
- 第3个数
- 十进制:9
- 十六进制:9
- 二进制:1001
复制代码
分析:
变量a为3。a从0开始,循环加1,一共3次,因此等于3。
变量b为6。b从0开始,循环加2,一共3次,因此等于6。
变量c为9。c从0开始,循环加3,一共3次,因此等于9。
【49.9 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2016-12-25 10:36
本帖最后由 jianhong_wu 于 2016-12-25 10:49 编辑
第五十节: 循环体内的continue和break语句。
第五十节_pdf文件.pdf
(81.11 KB, 下载次数: 1536)
【50.1 continue语句。】
通常情况下,单片机在循环体里从第一行的“入口条件”开始往下执行,直至碰到循环体的边界“底部花括号”,才又折回到第一行的“入口条件”准备进行新一轮的循环。但是,若中途碰到continue语句,就会提前结束当前这一轮的循环,只要碰到continue语句,就立即折回到第一行的“入口条件”准备进行新一轮的循环。注意,continue语句“结束”的对象仅仅是“当前这一轮的循环”,并没有真正结束这个循环的生命周期。它好像拦路虎,遇到它,它说“你回去,第二天再来。”这台词里的“第二天再来”就强调这个循环体的生命周期还没有真正结束。举一个具体的例子,如下:
- while(…)或者for(…) //循环体的条件判断入口处
- { //循环体开始
- 语句1;
- 语句2;
- continue;
- 语句3;
- ……
- 语句N;
- } //循环体结束
复制代码
分析:上述语句中,单片机从“循环体的条件判断入口处”开始往下执行,碰到continue就马上折回到“循环体的条件判断入口处”,继续开始新一轮的循环,因此,这段代码,continue后面的“语句3”至“语句N”是永远也不会被执行到的。因为continue的拦截,上述语句等效于:
- while(…)或者for(…) //循环体的条件判断入口处
- { //循环体开始
- 语句1;
- 语句2;
- } //循环体结束
复制代码
问题来了,既然可以如此简化,还要continue干什么,不是多此一举?在实际应用中,continue肯定不会像上面这样单独使用,continue只有跟if语句结合,才有它存在的意义。例如:
- while(…)或者for(…) //循环体的条件判断入口处
- { //循环体开始
- 语句1;
- 语句2;
- if(某条件)
- {
- continue;
- }
- 语句3;
- ……
- 语句N;
- } //循环体结束
复制代码
【50.2 break语句。】
continue语句提前结束当前这一轮的循环,准备进入下一轮的新循环,强调“某次结束”,但不是真结束。break语句是直接跳出当前循环体,是真正的结束当前循环体,强调循环体的“生命结束”。举例如下:
- while(…)或者for(…) //循环体的条件判断入口处
- { //循环体开始
- 语句1;
- 语句2;
- break;
- 语句3;
- ……
- 语句N;
- } //循环体结束
- 语句(N+1); //循环体之外语句
复制代码
分析:上述语句中,单片机从“循环体的条件判断入口处”开始往下执行,突然碰到break语句,此时,立即无条件跳出当前循环体(无需判断while或者for的条件),直接执行到循环体之外的“语句(N+1)”,break后面的“语句3”至“语句N”也没有被执行到。实际项目中,break也往往会配合if一起使用,例如:
- while(…)或者for(…) //循环体的条件判断入口处
- { //循环体开始
- 语句1;
- 语句2;
- if(某条件)
- {
- break;
- }
- 语句3;
- ……
- 语句N;
- } //循环体结束
- 语句(N+1); //循环体之外语句
复制代码
【50.3 break语句能跳多远?】
break语句能跳多远?预知答案请先看以下例子:
- while(…)
- {
- 语句1;
- 语句2;
- while(…)
- {
- 语句3;
- break;
- 语句4;
- }
- 语句5;
- }
- 语句6;
复制代码
分析:上述例子中,在while循环里面有藏着第二个while循环,像这种循环之中还有循环的情况,通常称为循环嵌套。单片机从上往下执行,当遇到break后,它会跳到“语句5”那里呢,还是会跳到“语句6”那里?正确答案是“语句5”那里,这说明了break语句的“有效射程”仅仅刚好能跳出当前的循环体。也就是说,在上述循环嵌套的例子中,最内层的break只能跳出最内层的循环体,不能跳到最外层的“语句6”那里,如果需要继续跳出最外层的“语句6”那里,可以继续在外层的循环体内再增加一个break语句。
【50.4 还有哪些语句可以无条件跳出循环体?】
除了break以外,还有return和goto语句可以跳出循环体。这部分的内容大家只需大概了解一下即可。return语句比break语句还厉害,它不仅仅跳出当前循环体,还是跳出了当前函数,也就是提前结束了当前函数,这部分的内容后面章节会讲到,暂时不用管。而goto语句在C语言中大家都公认不建议用,因为它很容易扰乱大家常用的C语言编程结构,我本人也从来没有用过goto语句,因此不再深入讲解它。
【50.5 例程练习和分析。】
编写一个程序来熟悉一下continue和break语句的使用。
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- unsigned char a=0; //观察这个数最后的变化
- unsigned char b=0; //观察这个数最后的变化
- unsigned char c=0; //观察这个数最后的变化
- unsigned char d=0; //观察这个数最后的变化
- unsigned char i; //控制循环体的条件判断变量
- void main() //主函数
- {
- //i<6的条件判断是在进入循环体之前判断,而i的自加1是在执行完一次循环体之后才自加的。
- for(i=0;i<6;i++)
- {
- a=a+1; //被执行了6次,分别是第0,1,2,3,4,5次
- if(i>=3) //当i等于3的时候,开始“拦截”continue后面的代码。
- {
- continue; //提前结束本次循环,准备进入下一次循环
- }
- b=b+1; //被执行了3次,分别是第0,1,2次
- }
- //i<6的条件判断是在进入循环体之前判断,而i的自加1是在执行完一次循环体之后才自加的。
- for(i=0;i<6;i++)
- {
- c=c+1; //被执行了4次,分别是第0,1,2,3次
- if(i>=3) //当i等于3的时候,直接跳出当前循环体,结束此循环体的“生命周期”。
- {
- break; //马上跳出当前循环体
- }
- d=d+1; //被执行了3次,分别是第0,1,2次
- }
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:6
- 十六进制:6
- 二进制:110
- 第2个数
- 十进制:3
- 十六进制:3
- 二进制:11
- 第3个数
- 十进制:4
- 十六进制:4
- 二进制:100
- 第4个数
- 十进制:3
- 十六进制:3
- 二进制:11
复制代码
分析:
变量a为6。
变量b为3。
变量c为4。
变量d为3。
【50.6 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-1-4 10:22
本帖最后由 jianhong_wu 于 2017-1-4 10:44 编辑
第五十一节: for和while的循环嵌套。
第五十一节_pdf文件.pdf
(75.16 KB, 下载次数: 1510)
【51.1 循环的嵌套。】
大循环的内部又包含了小循环,称为循环嵌套。生活中,循环嵌套的现象很常见,一年12个月,假设每个月都是30天(仅仅假设而已),1月份30天,2月份30天......11月份30天,12月份30,这里的年就是大循环,年内部的月就是小循环。一年12个月,大循环就是12次。一个月30天,小循环就是30次。用for语句来表达,大意如下:
- for(m=1;m<=12;m++) //大循环。一年12个月。这里的m看作月,代表一年12个月的大循环。
- {
- for(d=1;d<=30;d++) //内嵌小循环。一月30天。这里的d看作天,代表一个月30天的小循环。
- {
- }
- }
复制代码
【51.2 循环嵌套的执行顺序。】
例子如下:
- for(i=0;i<2;i++) //大循环
- {
- 语句1;
- for(k=0;k<3;k++) //内嵌的小循环
- {
- 语句2;
- }
- 语句3;
- }
复制代码
上述例子中,带i的for称为大循环,带k的for称为小循环,单片机从大循环入口进来,由上往下执行,执行第1次大循环,先执行1次“语句1”,接着进入小循环,小循环要连续循环执行3次“语句2”才跳出小循环,之后执行1次“语句3”,然后再返回到大循环入口判断i条件是否满足,此时条件满足,继续执行第2次大循环,1次“语句1”,3次“语句2”,1次“语句3”,第2次循环结束后又返回到大循环入口判断i条件,此时i已经等于2不再小于2了,因此条件不满足,结束整个循环嵌套。上述执行的语句顺序如下:
- 语句1; //第1次大循环开始
- 语句2;
- 语句2;
- 语句2;
- 语句3;
- 语句1; //第2次大循环开始
- 语句2;
- 语句2;
- 语句2;
- 语句3;
复制代码
根据此顺序,再看一个具体的程序例子:
- a=0;
- b=0;
- for(i=0;i<2;i++) //大循环
- {
- a=a+1; //被执行了2次
- for(k=0;k<3;k++) //内嵌的小循环
- {
- b= b+1; //被执行了6次
- }
- }
复制代码
上述例子中,执行完程序后,a的值变成了2,b的值变成了6。重点分析b的变化,“b=b+1”在内嵌循环体里被执行了6次,6次从何而来?就是i乘以k等于6。这个乘法次数是循环嵌套一个很重要的特性。上述程序如果用while语句来实现,等效如下:
- a=0;
- b=0;
- i=0; //控制大循环的变量初始化
- while(i<2) //大循环
- {
- a=a+1; //被执行了2次
- k=0; //控制小循环的变量初始化
- while(k<3) //内嵌的小循环
- {
- b= b+1; //被执行了6次
- k=k+1;
- }
- i=i+1;
- }
复制代码
【51.3 循环嵌套的常见用途---二维数组的应用。】
二维数组a[2][3],它有6个变量,在没有学for语句之前,如果要依次把每个元素单独赋值清零真不容易,要写6次赋值语句如下:
- a[0][0]=0;
- a[0][1]=0;
- a[0][2]=0;
- a[1][0]=0;
- a[1][1]=0;
- a[1][2]=0;
复制代码
自从懂了for嵌套语句之后,可以让同样功能的代码简洁许多。上述代码等效于如下:
- for(i=0;i<2;i++) //大循环
- {
- for(k=0;k<3;k++) //内嵌的小循环
- {
- a[i][k]=0;
- }
- }
复制代码
【51.4 循环嵌套的常见用途---大延时。】
单片机项目会经常会用到delay这个延时函数,大部分都是利用for循环来实现,小延时的函数往往不用嵌套,直接如下编写:
上述的N是控制循环次数,每次循环都要消耗单片机一点时间,如果N越大需要消耗的时间就越多,起到延时的作用。但是N所能取的最大值受它所定义的类型所限制,比如unsigned char类型最大范围是255,unsigned int类型最大范围是65535,unsigned long类型最大范围是4294967295。如果要实现更大的延时怎么办?就可以用for的循环嵌套,利用循环嵌套可以使得循环总次数进行乘法翻倍的放大,很容易编写大延时的函数。比如:
- for(i=0;i<M;i++) //大循环
- {
- for(k=0;k<N;k++); //内嵌的小循环
- }
复制代码
此时循环的次数是N乘以M的乘积。如果N和M都是unsigned long类型,就意味着最大循环次数是4294967295的平方,次数大到惊人。
【51.5 例程练习和分析。】
现在编写一个循环嵌套的练习程序。
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- unsigned char a=0; //观察这个数最后的变化
- unsigned char b=0; //观察这个数最后的变化
- unsigned char c=0; //观察这个数最后的变化
- unsigned char i; //控制大循环体的条件判断变量
- unsigned char k; //控制内嵌小循环体的条件判断变量
- void main() //主函数
- {
- for(i=0;i<2;i++) //大循环
- {
- a=a+1; //被执行了2次
- for(k=0;k<3;k++) //内嵌小循环
- {
- b=b+1; //被执行了6次,也就是i乘以k,2乘以3等于6.
- }
- c=c+1; //被执行了2次
- }
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:2
- 十六进制:2
- 二进制:10
- 第2个数
- 十进制:6
- 十六进制:6
- 二进制:110
- 第3个数
- 十进制:2
- 十六进制:2
- 二进制:10
复制代码
分析:
变量a为2。
变量b为6。
变量c为2。
【51.6 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-1-8 11:24
本帖最后由 jianhong_wu 于 2017-1-8 11:42 编辑
第五十二节: 支撑程序框架的switch语句。
第五十二节_pdf文件.pdf
(83.37 KB, 下载次数: 1500)
【52.1 switch的重要性。】
switch是非常重要的语句,我所有的单片机项目都是用switch搭建程序主框架。如果说while和for是一对孪生兄弟,那么“if-else if”和switch也是一对孪生兄弟,凡是用“if-else if”能实现的功能都可以用switch实现。switch有条件分支的功能,当条件的分支超过3个以上时,switch会比“if-else if”更加直观清晰。
【52.2 switch的语法。】
switch常见的格式如下:
- switch(变量) //根据变量的数值大小从对应的case入口进来
- {
- case 0: //入口0
- 语句0;
- break; //switch程序体的出口之一
- case 1: //入口1
- 语句1;
- break; //switch程序体的出口之一
- case 2: //入口2
- 语句2;
- break; //switch程序体的出口之一
- } //最下面的花括号也是一个switch程序体的出口之一
复制代码
分析:单片机从第一行的switch(变量)进来,依次往下查询跟变量匹配的case入口,然后从匹配的case入口进来,往下执行语句,直到遇上break语句,或者return语句,或者“最下面的花括号”这三种情况之一,才跳出当前switch程序体。上述例子中,假如变量等于3,单片机从switch(变量)进来,往下查询跟3匹配的case入口,因为没有发现case 3,最后遇到“最下面的花括号”于是结束switch程序体,像这种变量等于3的情况,就意味着switch里面的有效语句没有被执行到。多补充一句,在case 2选项中,“语句2”后面紧跟的break可以省略,因为case 2是最后一个case,即使没有遇到break也会遇到“最下面的花括号”而结束switch程序体。上述程序功能如果用“if-else if”语句来实现,等效于如下:
- if(0==变量)
- {
- 语句0;
- }
- else if(1==变量)
- {
- 语句1;
- }
- else if(2==变量)
- {
- 语句2;
- }
复制代码
【52.3 switch的break。】
刚才的例子中,可以看到三个关键字:switch,case,break。其实并不是每个case都必须要跟break配套,break只是起到一个出口的功能。假如没有遇到break,程序会一直往下执行,直到遇到break或者switch“最下面的花括号”为止。比如:
- switch(变量) //根据变量的数值大小从对应的case入口进来
- {
- case 0: //入口0
- 语句0;
- break;
- case 1: //入口1
- 语句1;
- case 2: //入口2
- 语句2;
- break;
- case 3: //入口3
- 语句3;
- break;
- } //最下面的花括号也是一个switch程序体的出口之一
复制代码
分析:假如此时switch(变量)的变量等于1,单片机经过查询后,就从匹配的case 1入口进来,执行“语句1”后,居然没有遇到break语句,于是紧接着碰到“case 2”入口的语句,现在问题来了,单片机此时是退出switch程序体还是忽略“case 2”入口语句而继续执行后面的“语句2”?答案是:忽略“case 2”入口语句而继续执行后面的“语句2”。这里有点像坐地铁,你只关注一个入口和一个出口,进入地铁内之后,你中途再遇到无数个入口都可以忽略而继续前进,直到你到达目的地的出口才结束整个乘车过程。继续刚才的分析,单片机执行“语句2”之后,紧接着遇到break语句,这时才跳出整个switch程序体。回顾一下整个流程,本例子中case 1没有break语句,就继续往下执行下面case2里面的语句,直到遇到break或者“最下面的花括号”为止。
【52.4 case的变量有顺序要求吗?】
switch语句内部的case有规定顺序吗?必须连贯吗?switch程序体内部可以写很多case入口,这些case入口是不是必须按从小到大的顺序?是不是规定必须case数字连贯?答案是:没有规定顺序,也没有规定case数字连贯。case的数值只是代表入口,比如以下两种写法都是合法的:
第一种:case不按从小到大的顺序(这种格式是合法的):
- switch(变量)
- {
- case 2:
- 语句2;
- break;
- case 0:
- 语句0;
- break;
- case 1:
- 语句1;
- break;
- }
复制代码
第二种:case的数字不连贯(这种格式也是合法的):
- switch(变量)
- {
- case 0:
- 语句0;
- break;
- case 3:
- 语句3;
- break;
- case 9:
- 语句9;
- break;
- }
复制代码
【52.5 switch的default。】
default是入口语句,它在switch语句中也不是必须的,应根据程序需要来选择。default相当于“if-else if-else ”组合语句中的else,也就是当switch的入口变量没有匹配的case入口时,就会默认进入default入口,就像“if-else if-else ”语句中当前面所有的条件不满足时,就进入else语句的程序体,比如:
- switch(变量) //根据变量的数值大小从对应的case入口进来
- {
- case 0: //入口0
- 语句0;
- break; //switch程序体的出口之一
- case 1: //入口1
- 语句1;
- break; //switch程序体的出口之一
- case 2: //入口2
- 语句2;
- break; //switch程序体的出口之一
- default: //当所有的case不满足,就从default的入口进来
- 语句3;
- break;
- } //最下面的花括号也是一个switch程序体的出口之一
复制代码
分析:假如switch的入口变量等于35,单片机从上往下查询,因为没有找到case 35,所以就会从默认的default入口进来执行” 语句3”,然后遇到break语句才跳出switch程序体。上述程序功能如果用“if-else if-else”组合语句来实现等效于如下:
- if(0==变量)
- {
- 语句0;
- }
- else if(1==变量)
- {
- 语句1;
- }
- else if(2==变量)
- {
- 语句2;
- }
- else //相当于switch中的default
- {
- 语句3;
- }
复制代码
【52.6 switch中内嵌switch。】
if语句可以内嵌if语句,while语句也可以内嵌while语句,switch语句当然也可以内嵌switch。比如:
- switch(a)
- {
- case 1:
- switch(b) //内嵌的switch
- {
- case 1:
- Break;
- case 2:
- Break;
- }
- Break;
- case 2:
- Break;
- }
复制代码
分析:上述这种switch内嵌switch语句也是合法的,而且在实际项目中也很常用,大家目前先有个大概的了解即可,暂时不深入讲解。
【52.7 例程练习和分析。】
现在编写一个switch的练习程序。
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- unsigned char k; //switch的入口变量
- unsigned char a; //观察此变量的变化来理解switch的执行顺序
- void main() //主函数
- {
- a=0;
- k=2; //入口变量等于2
- switch(k)
- {
- case 0: //入口0
- a++;
- break; //跳出switch
- case 1: //入口1
- a++;
- case 2: //入口2,上述k等于2所以从这里进来
- a++;
- case 3: //入口3
- a++;
- case 4: //入口4
- a++;
- break; //跳出switch
- case 5: //入口5
- a++;
- break; //跳出switch
- default: //当前面没有遇到匹配的case入口时,就从此default入口进来
- a++;
- break; //跳出switch
- } //最后一个switch的花括号也是跳出switch
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:3
- 十六进制:3
- 二进制:11
复制代码
分析:
变量a为3。单片机从case 2入口进来,因为case 2和case 3都没有break语句,直到遇到case 4的break语句才结束switch程序体,因此整个过程遇到了3次“a++”语句,因此变量a的“自加一”执行了3次后从0变成了3。
【52.8 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-1-15 14:17
本帖最后由 jianhong_wu 于 2017-1-15 14:44 编辑
第五十三节: 使用函数的三要素和执行顺序。
第五十三节_pdf文件.pdf
(76.21 KB, 下载次数: 1462)
【53.1 函数的十大关联部件。】
函数是什么?我很难用一句话给它下定义,哪怕我真能用一句话定义了,初学者也很难从一句话的定义中“格”出函数之理。之所以函数有如此玄机,确实因为它包罗万象,涉及的内容非常多,就像要我去定义什么是中国,我也没法用一句话去定义,只有长大了慢慢了解它的地理文化历史,你才会对咱中国有深刻的认识。函数也是如此,虽然我不能用一句话定义函数,但是函数跟十大部件有关,只要今后不断学习和运用,对十大部件各个击破直到全部“通关”,总有一天你会感悟到函数的精髓。现在先把十大部件列出来,让大家有一个感性的认识,它们是:函数体,函数接口,return语句,堆栈,全局变量,普通局部变量,静态局部变量,单个变量的指针,数组的指针,结构体的指针。本节讲的“使用函数的三要素和执行顺序”就是属于“函数体”这个部件的内容。
【53.2 使用函数的三要素。】
有的人习惯把函数称为程序,比如主程序,子程序,这时的主程序对应主函数,子程序对应子函数,是一回事,只是每个人的表达习惯不一样而已。使用函数的三要素是声明,定义,调用。每次新构造一个函数时,尽量遵守这个三个要素来做就可以减少一些差错。什么叫函数的声明,定义,调用?为了让大家有一个感性的认识,请先看下面这个例子:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void HanShu(void); //子函数声明的第一区域
- unsigned char a; //全局变量定义的第二区域
- unsigned char b;
- unsigned char c;
- void HanShu(void) //子函数定义的第三区域
- {
- a++; //子函数的代码语句
- b=b+5;
- c=c+6;
- }
- void main() //主函数
- {
- a=0;
- b=0;
- c=0;
- HanShu() ; //子函数被调用的第四区域
- c=a+b;
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
分析:上述例子中,从书写代码区域的角度来寻找函数的大概规律,从上往下:
第一区域:写子函数HanShu声明。
第二区域:全局变量的定义。
第三区域:子函数HanShu的定义。
第四区域:在main函数里对子函数HanShu的调用。
【53.3 子函数被其它函数调用时候的执行顺序。】
子函数被其它函数调用时,子函数的名字就相当于一个跳转地址,而子函数的定义部分就是要跳转的实际地址,单片机在主函数里遇到子函数名字,就直接跳转到子函数定义那里执行子函数内部的代码,执行完子函数后再返回到主函数,此时返回到主函数哪里呢?答:因为子函数已经被执行了一次,所以返回到主函数中的子函数名字后面,然后继续往下执行main函数其它剩余的代码。请看下面这个代码的执行顺序,一目了然:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void HanShu(void); //子函数的声明
- void HanShu(void) //子函数的定义
- {
- 语句1;
- 语句2;
- }
- void main() //主函数
- {
- 语句3;
- HanShu() ; //子函数的被调用
- 语句4;
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
执行顺序分析:单片机从主函数main那里进来往下执行,先执行“语句3”,接着遇到HanShu名字的跳转地址,然后马上跳转到HanShu的定义部分,执行“语句1”,“语句2”,执行完子函数HanShu的定义部分,就马上返回到主函数,继续执行HanShu名字后面的“语句4”。整个执行语句的先后顺序如下:
【53.4 例程练习和分析。】
现在编写一个练习程序来体验一下函数的使用。
程序代码如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void HanShu(void); //子函数声明的第一区域
- unsigned char a; //全局变量定义的第二区域
- void HanShu(void) //子函数定义的第三区域
- {
- a++; //子函数的代码语句
- }
- void main() //主函数
- {
- a=0;
- a++;
- HanShu() ; //子函数被调用的第四区域
- a++;
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:3
- 十六进制:3
- 二进制:11
复制代码
分析:
变量a为3。单片机从main主函数进来,主函数里有2条“a++”,再加上子函数里也有1条“a++”,因此累加了3次,从0变成了3.
【53.5 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-1-23 13:58
本帖最后由 jianhong_wu 于 2017-1-23 14:13 编辑
第五十四节: 从全局变量和局部变量中感悟“栈”为何物。
第五十四节_pdf文件.pdf
(104.82 KB, 下载次数: 1407)
【54.1 本节阅读前的名词约定。】
变量可以粗略的分成两类,一类是全局变量,一类是局部变量。如果更深一步精细划分,全局变量还可以分成“普通全局变量”和“静态全局变量”,局部变量也可以分成“普通局部变量”和“静态局部变量”,也就是说,若精细划分,可以分成四类。其中“静态全局变量”和“静态局部变量”多了一个前缀“静态”,这个前缀“静态”是因为在普通的变量前面多加了一个修饰关键词“static”,这部分的内容后续章节会讲到。本节重点为了让大家理解内存模型的“栈”,暂时不考虑“静态变量”的情况,人为约定,本节所涉及的“全局变量”仅仅默认为“普通全局变量”,“局部变量”仅仅默认为“普通局部变量”。
【54.2 如何判定全局变量和局部变量?】
全局变量就是在函数外面定义的变量,局部变量就是在函数内部定义的变量,这是最直观的判定方法。下面的例子能很清晰地说明全局变量和局部变量的判定方法:
- unsigned char a; //在函数外面定义的,所以是全局变量。
- void main() //主函数
- {
- unsigned char b; //在函数内部定义的,所以是局部变量。
- b=a;
- while(1)
- {
- }
- }
复制代码
【54.3 全局变量和局部变量的内存模型。】
单片机内存包括ROM和RAM两部分,ROM存储的是单片机程序中的指令和一些不可更改的常量数据,而RAM存放的是可以被更改的变量数据,也就是说,全局变量和局部变量都是存放在RAM,但是,虽然都是存放在RAM,全局变量和局部变量之间的内存模型还是有明显的区别的,因此,分了两个不同的RAM区,全局变量占用的RAM区称为“全局数据区”,局部变量占用的RAM区称为“栈”,因为我后面会用宾馆来比喻“栈”,为了方便记忆,大家可以把“栈”想象成 “客栈”来记忆。它们的内存模型到底有什么本质的区别呢?“全局数据区”就像你自己家的房间,是唯一的,一个房间的地址只能你一个人住(假设你还没结婚的时候),而且是永久的,所以说每个全局变量都有唯一对应的RAM地址,不可能重复的。而“栈”就像宾馆客栈,一年下来每天晚上住的人不一样,每个人在里面居住的时间是有期限的,不是长久的,一个房间的地址一年下来每天可能住进不同的人,不是唯一的。“全局数据区”的全局变量拥有永久产权,“栈”区的局部变量只能临时居住在宾馆客栈,地址不是唯一的,有期限的。全局变量像私人区,局部变量像公共区。“栈”的这片公共区,是给程序里所有函数内部的局部变量共用的,函数被调用的时候,该函数内部的每个局部变量就会被分配对应到“栈”的某个RAM地址,函数调用结束后,该局部变量就失效,因此它对应的“栈”的RAM空间就被收回以便给下一个被调用的函数的局部变量占用。请看下面这个例子,我借用“宾馆客栈”来比喻局部变量所在的“栈”。
- void HanShu(void); //子函数的声明
- void HanShu(void) //子函数的定义
- {
- unsigned char a; //局部变量
- a=1;
- }
- void main() //主函数
- {
- HanShu() ; //子函数的调用
- }
复制代码
分析:上述例子,单片机从主函数main往下执行,首先遇到HanShu子函数的调用,所以就跳到HanShu函数的定义那里开始执行,此时的局部变量a开始被分配在RAM的“栈区”的某个地址,相当于你入住宾馆被分配到某个房间。单片机执行完子函数HanShu后,局部变量a在RAM的“栈区”所分配的地址被收回,局部变量a消失,被收回的RAM地址可能会被系统重新分配给其它被调用的函数的局部变量,此时相当于你离开宾馆,从此你跟那个宾馆的房间没有啥关系,你原来在宾馆入住的那个房间会被宾馆老板重新分配给其他的客人入住。全局变量的作用域是永久性不受范围限制的,而局部变量的作用域就是它所在函数的内部范围。全局变量的“全局数据区”是永久的私人房子(这里的“永久”仅仅是举一个例子,别拿“70年产权”来抬杠),局部变量的“栈”是临时居住的“客栈”。重要的事情说两遍,再次总结如下:
(1)每定义一个新的全局变量,就意味着多开销一个新的RAM内存。而每定义一个局部变量,只要在函数内部所定义的局部变量总数不超过单片机的“栈”区,此时的局部变量不开销新的RAM内存,因为局部变量是临时借用“栈”区的,使用后就还给“栈”,“栈”是公共区,可以重复利用,可以服务若干个不同的函数内部的局部变量。
(2)单片机每次进入执行函数时,局部变量都会被初始化改变,而全局变量则不会被初始化,全局变量是一直保存之前最后一次更改的值。
【54.4 三个常见疑问。】
第一个疑问:
问:“全局数据区”和“栈区“是谁在幕后分配的,怎么分配的?
答:是C编译器自动分配的,至于怎么分配,谁分配多一点,谁分配少一点,C编译器会有一个默认的比例分配,我们一般都不用管。
第二个疑问:
问:“栈”区是临时借用的,子函数被调用的时候,它内部的局部变量才会“临时”被分配到“栈”区的某个地址,那么问题来了,谁在幕后主持“栈区”这些分配的工作,难道也是C编译器?C编译器不是在编译程序的时候一次性就做完了编译工作然后就退出历史舞台了吗?难道我们程序已经在单片机内部运转的时候,编译器此时还在幕后指手画脚的起作用?
答:单片机已经上电开始运行程序的时候,编译器是不可能起作用的。所以,真相只有一个,“栈区”分配给函数内部局部变量的工作,确实是C编译器做的,唯一需要注意的地方是,它不是“现炒现卖”,而是在单片机上电前,C编译器就把所有函数内部的局部变量的分配工作就规划好了,都指定了如果某个函数一旦被调用,该函数内部的哪个局部变量应该分到“栈区”的哪个地址,C编译器都是事先把这些“后事”都交代完毕了才“结束自己的生命”,后面,等单片机上电开始工作的时候,虽然C编译器此时“不在”了,但是单片机都是严格按照C编译器交代的“遗嘱”开始工作和分配“栈区”的。因此,“栈区”的“临时分配”非真正严格意义上的“临时分配”。
第三个疑问:
问:函数内部所定义的局部变量总数不超过单片机的“栈”区的RAM数量,那,万一超过了“栈”区的RAM数量,后果严重吗?
答:后果特别严重。这种情况,专业术语叫“爆栈”。程序会出现异常,而且是莫名其妙的异常。为了避免这种情况,一般在编写程序的时候,函数内部都不能定义大数组的局部变量,局部变量的数量不能定义太多太大,尤其要避免刚才所说的定义开辟大数组局部变量这种情况。大数组的定义应该定义成全局变量,或者定义成“静态的局部变量”(“静态”这部分相关的内容后面章节会讲到)。有一些C编译器,遇到“爆栈”的情况,会好心跟你提醒让你编译不过去,但是也有一些C编译器可能就不会给你提醒,所以大家以后做项目写函数的时候,要对“爆栈”心存敬畏。
【54.5 全局变量和局部变量的优先级。】
刚才说到,全局变量的作用域是永久性并且不受范围限制的,而局部变量的作用域就是它所在函数的内部范围,那么问题来,假如局部变量和全局变量的名字重名了,此时函数内部执行的变量到底是局部变量还是全局变量?这个问题就涉及到优先级。注意,当面对同名的局部变量和全局变量时,函数内部执行的变量是局部变量,也就是局部变量在函数内部要比全局变量的优先级高。为了深刻理解“全局变量和局部变量的优先级”,强烈建议大家必须仔细看完下面列举的三个练习例子。
【54.6 例程练习和分析。】
请看下面第一个例子:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- unsigned char a=5; //此处第1个a是全局变量。
- void main() //主函数
- {
- unsigned char a=2; //此处第2个a是局部变量。跟上面全局变量的第1个a重名了!
- View(a); //把a发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
分析:
上述例子,有2个变量重名了!其中一个是全局变量,另外一个是局部变量。此时输出显示的结果是5还是2?正确的答案是2。因为在函数内部,函数内部的局部变量比全局变量的优先级更加高。此时View(a)是第2个局部变量的a,而不是第1个全局变量的a。虽然这里的两个a重名了,但是它们的内存模型不一样,第1个全局变量的a是分配在“全局数据区”是具有唯一的地址的,而第2个局部变量的a是被分配在临时的“栈”区的,寄生在main函数内部。
再看下面第二个例子:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void HanShu(void); //函数声明
- unsigned char a=5; //此处第1个a是全局变量。
- void HanShu(void) //函数定义
- {
- unsigned char a=3; //此处第2个a是局部变量。
- }
- void main() //主函数
- {
- unsigned char a=2; //此处第3个a也是局部变量。
- HanShu(); //子函数被调用
- View(a); //把a发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
分析:
上述例子,有3个变量重名了!其中一个是全局变量,另外两个是局部变量。此时输出显示的结果是5还是3还是2?正确的答案是2。因为,HanShu这个子函数是被调用结束之后,才执行View(a)的,就意味HanShu函数内部的局部变量(第2个局部变量a)是在执行View(a)语句的时候就消亡不存在了,所以此时View(a)的a是第3个局部变量的a(在main函数内部定义的局部变量的a)。
再看下面第三个例子:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void HanShu(void); //函数声明
- unsigned char a=5; //此处第1个a是全局变量。
- void HanShu(void) //函数定义
- {
- unsigned char a=3; //此处第2个a是局部变量。
- }
- void main() //主函数
- {
- HanShu(); //子函数被调用
- View(a); //把a发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
分析:
上述例子,有2个变量重名了!其中一个是全局变量,另外一个是局部变量。此时输出显示的结果是5还是3?正确的答案是5。因为,HanShu这个子函数是被调用结束之后,才执行View(a)的,就意味HanShu函数内部的局部变量(第2个局部变量)是在执行View(a)语句的时候就消亡不存在了,同时,因为此时main函数内部也没有定义a的局部变量,所以此时View(a)的a是必然只能是第1个全局变量的a(在main函数外面定义的全局变量的a)。
【54.7 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-1-24 16:38
本帖最后由 jianhong_wu 于 2017-1-24 16:57 编辑
第五十五节: 函数的作用和四种常见书写类型。
第五十五节_pdf文件.pdf
(84.39 KB, 下载次数: 1433)
【55.1 函数和变量的命名规则。】
函数的名字和变量的名字一样,一般是由“字母,数字,下划线”三者组成。第1个字符不能是数字,必须是字母或者下划线“_”,后面紧跟的第2个字符开始可以是数字。在C语言中名字所用的字母是区分大小写的。可以用下划线“_”,但是不可以用横杠“-”。名字不能跟C编译系统已经征用的关键字重名,比如不能用“unsigned ”,“char”,“static”等系统关键词,跟古代时不能跟皇帝重名一样,要避尊者讳。
【55.2 函数的作用和分类。】
函数的作用。通常把一些可能反复用到的算法或者过程封装成一个函数,函数就是一个模块,给它输入特定的参数,就可以输出想要的结果,比如一个加法函数,只要输入加数和被加数,然后就会输出相加结果之和,里面具体的算法过程只要写一次就可以重复调用,极大的节省单片机程序容量,也节省程序开发人员的工作量。还有一类函数,它从封装上看无所谓“输入输出”,这类函数往往是针对某一种可能重复使用的“过程”。
函数的分类。暂时排除指针的情况下(指针的内容后续章节会讲到),从输入输出的角度来看,有四种常见的书写类型。分别是“无输出无输入,无输出有输入,有输出无输入,有输出有输入”。“输出”是看函数名的前缀,前缀如果是void表示“无输出”,否则就是“有输出”。“输入”是看函数名括号里的内容,如果是void或者是空着就表示“无输入”,否则就是“有输入”。“输出”和“输入”是比较通俗的说法,专业一点的说法是,“有输出”表示函数“有返回”,“无输出”表示函数“无返回”。“有输入”表示函数“有形参”,“无输入”表示函数“无形参”。下面举一个加法函数的例子,分别用四种不同的函数类型来实现,通过对比它们之间的差别,来体会它们在书写方面有哪些不同,又有哪些规律。
【55.3 第1类:“无输出”“无输入”的函数。】
- unsigned char a; //此变量用来接收最后相加结果的和。
- unsigned char g=2;
- unsigned char h=3;
- void HanShu(void) //“无输出”“无输入”函数的定义。
- {
- a=g+h;
- }
- main()
- {
- HanShu(); //函数的调用。此处括号内的形参void要省略,否则编译不通过。
- }
复制代码
分析:void HanShu(void),此函数名的前缀是void,括号内也是void,属于“无输出”“无输入”函数。这类函数表面看是“无输出”“无输入”,其实内部是通过全局变量来输入输出的,比如上面的例子就是靠a,g,h这三个全局变量来传递信息,只不过这类表达方式比较隐蔽,没有那么直观。
【55.4 第2类:“无输出”“有输入”的函数。】
- unsigned char b; //此变量用来接收最后相加结果的和。
- void HanShu(unsigned char i,unsigned char k) //“无输出”“有输入”函数的定义。
- {
- b=i+k;
- }
- main()
- {
- HanShu(2,3); //函数的调用。
- }
复制代码
分析:void HanShu(unsigned char i,unsigned char k),此函数名的前缀是void,括号内是(unsigned char i,unsigned char k),属于“无输出”“有输入”的函数。括号的两个变量i和k是函数内的局部变量,也是跟对外的桥梁接口,它们有一个专业的名称叫形参。外部要调用此函数时,只要给括号填入对应的变量或者数值,这些变量和数值就会被复制一份传递给作为函数形参的局部变量(比如本例子中的i和k),从而外部调用者跟函数内部就发生了数据信息的传递。这种书写方式的特点是把输入接口封装了出来。
【55.5 第3类:“有输出”“无输入”的函数。】
- unsigned char c; //此变量用来接收最后相加结果的和。
- unsigned char m=2;
- unsigned char n=3;
- unsigned char HanShu(void) //“有输出”“无输入”函数的定义。
- {
- unsigned char p;
- p=m+n;
- return p;
- }
- main()
- {
- c=HanShu(); //函数的调用。此处括号内的形参void要省略,否则编译不通过。
- }
复制代码
分析:unsigned char HanShu(void),此函数名的前缀是unsigned char类型,括号内是void,属于“有输出”“无输入”的函数。函数前缀的unsigned char表示此函数最后退出时会返回一个unsigned char类型的数据给外部调用者。而且这类函数内部必须有一个return语句配套,表示立即退出当前函数并且返回某个变量或者常量的数值给外部调用者。这种书写方式的特点是把输出接口封装了出来。
【55.6 第4类:“有输出”“有输入”的函数。】
- unsigned char d; //此变量用来接收最后相加结果的和。
- unsigned char HanShu(unsigned char r,unsigned char s) //“有输出”“有输入”函数的定义
- {
- unsigned char t;
- t=r+s;
- return t;
- }
- main()
- {
- d=HanShu(2,3); //函数的调用。
- }
复制代码
分析:unsigned char HanShu(unsigned char r,unsigned char s),此函数名的前缀是unsigned char类型,括号内是(unsigned char r,unsigned char s),属于“有输出”“有输入”的函数。输入输出的特点跟前面介绍的函数一样,不多讲。这种书写方式的特点是把输出和输入接口都封装了出来。
【55.7 函数在被“调用”时需要注意的地方。】
函数的三要素是“声明,定义,调用”。函数在被“调用”的时候,对于“无输入”的函数,形参的void关键词要省略,否则编译不通过,这里仅仅是指在函数在被“调用”的时候。
【55.8 例程练习和分析。】
现在编写一个练习程序,要求编写4个不同“输入输出”封装的函数,它们每个函数所实现的功能都是一样的,都是加法的算法函数,它们之间仅仅是外观的封装接口不同而已。
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void hanshu_1(void);
- void hanshu_2(unsigned char i,unsigned char k);
- unsigned char hanshu_3(void);
- unsigned char hanshu_4(unsigned char r,unsigned char s);
- unsigned char a; //此变量用来接收第1个函数最后相加结果的和。
- unsigned char g=2;
- unsigned char h=3;
- unsigned char b; //此变量用来接收第2个函数最后相加结果的和。
- unsigned char c; //此变量用来接收第3个函数最后相加结果的和。
- unsigned char m=2;
- unsigned char n=3;
- unsigned char d; //此变量用来接收第4个函数最后相加结果的和。
- void hanshu_1(void) //第1类:“无输出”“无输入”。
- {
- a=g+h;
- }
- void hanshu_2(unsigned char i,unsigned char k) //第2类:“无输出”“有输入”。
- {
- b=i+k;
- }
- unsigned char hanshu_3(void) //第3类:“有输出”“无输入”。
- {
- unsigned char p;
- p=m+n;
- return p;
- }
- unsigned char hanshu_4(unsigned char r,unsigned char s) //第4类:“有输出”“有输入”。
- {
- unsigned char t;
- t=r+s;
- return t;
- }
- void main() //主函数
- {
- hanshu_1(); //第1类:“无输出”“无输入”的函数调用。这里的形参的void要省略。
- hanshu_2(2,3); //第2类:“无输出”“有输入”的函数调用。
- c=hanshu_3(); //第3类:“有输出”“无输入”的函数调用。这里的形参的void要省略。
- d=hanshu_4(2,3); //第4类:“有输出”“有输入”的函数调用。
- View(a); //把a发送到电脑端的串口助手软件上观察。
- View(b); //把b发送到电脑端的串口助手软件上观察。
- View(c); //把c发送到电脑端的串口助手软件上观察。
- View(d); //把d发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:5
- 十六进制:5
- 二进制:101
- 第2个数
- 十进制:5
- 十六进制:5
- 二进制:101
- 第3个数
- 十进制:5
- 十六进制:5
- 二进制:101
- 第4个数
- 十进制:5
- 十六进制:5
- 二进制:101
复制代码
分析:
变量a为5。
变量b为5。
变量c为5。
变量d为5。
【55.9 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-2-12 09:57
本帖最后由 jianhong_wu 于 2017-2-12 10:14 编辑
第五十六节: return在函数中的作用以及四个容易被忽略的功能。
第五十六节_pdf文件.pdf
(80.23 KB, 下载次数: 1261)
【56.1 return深入讲解。】
return在英语单词中有“返回”的意思,上一节提到,凡是“有输出”的函数,函数内部必须有一个“return+变量或者常量”与之配套,表示返回的结果给外部调用者接收,这个知识点很容易理解,但是容易被忽略的是另外四个功能:
第一个是return语句隐含了立即退出的功能。退出哪?退出当前函数。只要执行到return语句,就马上退出当前函数。即使return语句身陷多层while或者for的循环中,它也毫不犹豫立即退出当前函数。
第二个是return语句可以出现在函数内的任何位置。可以出现在第一行代码,也可以出现在中间的某行代码,也可以出现在最后一行的代码,它的位置不受限制。很多初学者有个错觉,以为return只能出现在最后一行,这是错的。
第三个是return语句不仅仅可以用在“有输出”的函数,也可以用在“无输出”的函数,也就是可以用在前缀是void的函数里。回顾上一节,在“有输出”的函数里,return后面紧跟一个变量或者常量,表示返回的数,但是在“无输出”的函数里,因为是“无输出”,此时return后面不用跟任何变量或者常量,这种写法也是合法的,表示返回的是空的。此时return主要起到立即退出当前函数的作用。
第四个是return语句可以在一个函数里出现N多次,次数不受限制,不一定必须只能一次。不管一个函数内有多少个return语句,只要任何一个return语句被单片机执行到,就立即退出当前函数。
【56.2 中途立即退出的功能。】
下面的书写格式是合法的:
- void HanShu(void) //“无输出”函数的定义。
- {
- 语句1;
- return; //立即退出当前函数。对于这类“无输出”函数,return后面没有跟任何变量或者常量。
- 语句2;
- return; //立即退出当前函数。对于这类“无输出”函数,return后面没有跟任何变量或者常量。
- 语句3;
- return; //立即退出当前函数。对于这类“无输出”函数,return后面没有跟任何变量或者常量。
- }
复制代码
分析:当HanShu此函数被调用时,单片机从“语句1”往下执行,当遇到第一个return语句后,马上退出当前函数。后面的“语句2”和“语句3”等代码永远不会被执行到。多说一句,大家仔细看看return后面跟了什么数没有?什么都没有。因为此函数的前缀是void的,是“无输出”的。
【56.3 身陷多层while或者for的循环时的惊人表现。】
下面的书写格式是合法的:
- void HanShu(void) //“无输出”函数的定义。
- {
- 语句1;
- while(1) //第一个循环
- {
- while(1) //第二个循环中的循环
- {
- return; //立即退出当前函数。
- }
- 语句2;
- return; //立即退出当前函数。
- }
- 语句3;
- return; //立即退出当前函数。
- }
复制代码
分析:当HanShu此函数被调用时,单片机从“语句1”往下执行,先进入第一个循环,接着进入第二个循环中的循环,然后遇到第一个return语句,于是马上退出当前函数。后面的“语句2”和“语句3”等代码永远不会被执行到。此函数中,虽然表面看起来有那么多可怕的循环约束着,但是一旦碰上return语句都是浮云,立刻退出当前函数。
【56.4 在“有输出”函数里的书写格式。】
把上面例子中“无输出”改成“有输出”的函数后:
- unsigned char HanShu(void) //“有输出”函数的定义。
- {
- unsigned char a=9;
- 语句1;
- while(1) //第一个循环
- {
- while(1) //第二个循环中的循环
- {
- return a; //返回a变量的值,并且立即退出当前函数。
- }
- 语句2;
- return a; //返回a变量的值,并且立即退出当前函数。
- }
- 语句3;
- return a; //返回a变量的值,并且立即退出当前函数。
- }
复制代码
分析:因为此函数是“有输出”的函数,所以return语句后面必须配套一个变量或者常量,此例子中配套的是a变量。当HanShu函数被调用时,单片机从“语句1”往下执行,先进入第一个循环,接着进入第二个循环中的循环,然后遇到第一个“return a”语句,马上退出当前函数。而后面的“语句2”和“语句3”等代码是永远不会被执行到的。再一次说明了,return语句不仅有返回某数的功能,还有立即退出的重要功能。
【56.5 项目中往往是跟if语句搭配使用。】
前面的例子只是为了解释return语句的执行顺序和功能,实际项目中,如果中间有多个return语句,中间的return语句不可能像前面的例子那样单独使用,它往往是跟if语句一起搭配使用,否则单独用return就没有什么意义。比如:
- void HanShu(void) //“无输出”函数的定义。
- {
- 语句1;
- if(某条件满足)
- {
- return; //立即退出当前函数。
- }
- 语句2;
- if(某条件满足)
- {
- return; //立即退出当前函数。
- }
- 语句3;
- }
复制代码
分析:单片机从“语句1”开始往下执行,至于在哪个“return”语句处退出当前函数,就要看哪个if的条件满不满足了,如果所有的if的条件都不满足,此函数会一直执行完最后的“语句3”才退出当前函数。
【56.6 例程练习和分析。】
写一个简单的除法函数,在除法运算中,除数不能为0,如果发现除数为0,就立即退出当前函数,并且返回运算结果默认为0。
- /*---C语言学习区域的开始。-----------------------------------------------*/
- //函数的声明。
- unsigned int ChuFa(unsigned int BeiChuShu,unsigned int ChuShu);
- //变量的定义。
- unsigned int a;//此变量用来接收除法的运算结果。
- unsigned int b;//此变量用来接收除法的运算结果。
- //函数的定义。
- unsigned int ChuFa(unsigned int BeiChuShu,unsigned int ChuShu)
- {
- unsigned int Shang; //返回的除法运算结果:商。
- if(0==ChuShu) //如果除数等于0,就立即退出当前函数,并返回0
- {
- return 0; // 退出当前函数并且返回0.此时后面的代码不会被执行。
- }
- Shang=BeiChuShu/ChuShu; //除法运算的算法
- return Shang; //返回最后的运算结果:商。并且退出当前函数。
- }
- void main() //主函数
- {
- a=ChuFa(128,0); //函数调用。128除以0,把商返回给a变量。
- b=ChuFa(128,2); //函数调用。128除以2,把商返回给b变量。
- View(a); //把a发送到电脑端的串口助手软件上观察。
- View(b); //把b发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:0
- 十六进制:0
- 二进制:0
- 第2个数
- 十进制:64
- 十六进制:40
- 二进制:1000000
复制代码
分析:
变量a为0。
变量b为64。
【56.7 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-2-19 09:58
本帖最后由 jianhong_wu 于 2017-2-19 10:17 编辑
第五十七节: static的重要作用。
第五十七节_pdf文件.pdf
(91.71 KB, 下载次数: 1803)
【57.1 变量前加入static后发生的“化学反应”。】
有两类变量,一类是全局变量,一类是局部变量。定义时,在任何一类变量前面加入static关键词,变量原有的特性都会发生某些变化,因此,static像化学的催化剂,具有神奇的功能。加static关键词的书写格式如下:
- static unsigned char a; //这是在全局变量前加的static关键词
- void HanShu(void)
- {
- static unsigned char i; //这是在局部变量前加的static关键词
- }
复制代码
【57.2 在全局变量前加static。】
static读作“静态”,全局变量前加static,称为静态全局变量。静态全局变量和普通全局变量的功能大体相同,仅在有效范围(作用域)方面有差异。假设整个工程有多个文件组成,普通全局变量的有效范围能覆盖全部文件,在任何一个文件里,以及跨文件与文件之间,在传递信息的层面上都畅通无阻。而静态全局变量只能在当前定义的那个文件里起作用,活动范围完全被限定在一个文件,彷佛被加了紧箍咒,由不得你任性,在传递信息的层面上仅仅局限于定义变量时所在的那一个文件。这部分的内容有个大致印象就可以,暂时不用深入研究,等以后学到“多文件编程”时再关注,因为我当前的程序例子只有一个源文件,还没涉及“多文件编程”。
【57.3 在局部变量前加static。】
这是本节重点。我常把局部变量比喻宾馆的客房,客人入住时被分配在哪间客房是随机临时安排的,第二天退房时宾馆会把客房收回继续分配给下一位其他的客人,是临时公共区。而加入static后的局部变量,发生了哪些变化?加入static后的局部变量,称为静态局部变量。静态局部变量就像宾馆的VIP客户,VIP客户财大气粗,把宾馆分配的客房永远包了下来,永远不许再给其它客人入住。总结了静态局部变量的两个重要特性:
第一个,静态局部变量不会在函数调用时被初始化,它只在单片机刚上电时被初始化了一次,因为它的内存模型不是分配在“栈”,而是跟全局变量一样放在“全局数据区”,拥有自己唯一的地址。因此,静态局部变量的数值跟全局变量一样,具有“记忆”功能,你每次调用某个函数,函数内部的静态局部变量的数值是维持最后一次被更改的数值,不会被“清零”的。但是跟全局变量又有差别,全局变量的有效范围(作用域)是整个工程,而静态局部变量毕竟是“局部”,在传递信息的层面仅局限于当前函数内。而普通局部变量,众所周知,每次被函数调用时,都会被重新初始化,会被“清零”的,没有“记忆”功能的。
第二个,每次函数调用时,静态局部变量比普通局部变量少开销一条潜在的“初始化语句”,原因是普通局部变量每次被函数调用时都要重新初始化,而静态局部变量不用进行这个操作。也就是说,静态局部变量比普通局部变量的效率高一点,虽然这个“点”的时间开销微不足道,但是写程序时不能忽略这个“点”。静态局部变量用到好处之时,能体现一个工程师的功力。
【57.4 静态局部变量的应用场合。】
静态局部变量适用在那些“频繁调用”的函数,比如main函数主循环while(1)里直接调用的所有函数,还有以后讲到的定时器中断函数,等等。因为静态局部变量每次被调用都不会被重新初始化,用在这类函数时就省去了每次初始化语句的时间。还有一类用途,就是那些规定不能被函数初始化的场合,比如在很多用switch搭建程序框架的函数里,这类switch程序框架俗称为状态机思路。
【57.5 能用全局变量替代静态局部变量吗?】
能用全局变量替代静态局部变量吗?能。哪怕在整个程序里全部用全局变量都可以。全局变量是一把牛刀,什么场合都用牛刀虽然也能解决问题,但是显得鲁莽没有条理。尽量把全局变量,普通局部变量,静态局部变量各自优势充分发挥出来才是编程之道。能用局部变量的尽量用局部变量,这样可以减少全局变量的使用。当局部变量帮分担一部分工作时,最后全局变量只起到一个作用,那就是在各函数之间传递信息。局部变量与全局变量的分工定位明确了,程序代码阅读起来就没有那么凌乱,思路也清晰很多。
【57.6 例程练习和分析。】
现在编写一个程序来熟悉static的性能。
- /*---C语言学习区域的开始。-----------------------------------------------*/
- //函数的声明。
- unsigned char HanShu(void);
- unsigned char HanShu_static(void);
- //变量的定义。
- unsigned char a; //用来接收函数返回的结果。
- unsigned char b;
- unsigned char c;
- unsigned char d;
- unsigned char e;
- unsigned char f;
- //函数的定义。
- unsigned char HanShu(void)
- {
- unsigned char i=0; //普通局部变量,每次函数调用都被初始化为0.
- i++; //i自加1
- return i;
- }
- unsigned char HanShu_static(void)
- {
- static unsigned char i=0; //静态局部变量,只在上电是此初始化语句才起作用。
- i++; //i自加1
- return i;
- }
- void main() //主函数
- {
- //下面函数内的i是普通局部变量,每次调用都会被重新初始化。
- a=HanShu(); //函数内的i每次重新初始化为0,再自加1,所以a等于1。
- b=HanShu(); //函数内的i每次重新初始化为0,再自加1,所以b等于1。
- c=HanShu(); //函数内的i每次重新初始化为0,再自加1,所以c等于1。
- //下面函数内的i是静态局部变量,第一次上电后默认为0,就不会再被初始化,
- d=HanShu_static(); //d由0自加1后等于1。
- e=HanShu_static(); //e由1自加1后等于2。
- f=HanShu_static(); //f由2自加1后等于3。
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- View(e); //把第5个数e发送到电脑端的串口助手软件上观察。
- View(f); //把第6个数f发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第2个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第3个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第4个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第5个数
- 十进制:2
- 十六进制:2
- 二进制:10
- 第6个数
- 十进制:3
- 十六进制:3
- 二进制:11
复制代码
分析:
变量a为1。
变量b为1。
变量c为1。
变量d为1。
变量e为2。
变量f 为3。
【57.7 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-2-26 12:11
本帖最后由 jianhong_wu 于 2017-2-26 12:29 编辑
第五十八节: const(或code)在定义数据时的作用。
第五十八节_pdf文件.pdf
(90.38 KB, 下载次数: 1805)
【58.1 const与code的关系。】
const与code都是语法的修饰关键词,放在所定义的数据前面时有“不可更改”之意。在C语言语法中,const像普通话全国通用,是标准的语言;而code像地方的方言,仅仅适合针对51单片机的C51编译器环境。而其它大多数单片机的C编译器并不支持code,只支持const。比如PIC,stm32等单片机的C编译器都是只认const而不认code的。通常情况下,const定义的数据都是放在ROM的,但是51单片机的C51编译器是例外,它并不把const定义的数据放在ROM区,只有用code关键词时它才会把数据放在ROM区,这一点相对其它大多数的单片机来说是不一样的。因为本教程是用51单片机的C51编译器,所以用code来替代const。本节教程所提到的const,在实际编程时都用code来替代。
【58.2 const(或code)在定义数据时的终极目的。】
在数据定义分配的应用中,const的终极目的是为了节省RAM的开销。从“读”和“写”的角度分析,数据有两种:“能读能写”和“只能读”这两种。 “能读能写”的数据占用RAM内存,叫变量,C语言语法上定义此类数据时“无”const前缀。 “只能读”的数据占用ROM内存,叫常量, C语言语法上定义此类数据时“有”const前缀。单片机的ROM容量比RAM容量往往大几十倍甚至上百倍,相比之下,RAM的资源显得比较稀缺。因此,把某些只需“读”而不需“写”的数据定义成const放在ROM,就可以节省RAM的开销。
【58.3 const(或code)的应用场合。】
const可以定义单个常量,也可以定义常量数组。定义单个常量时,通常应用在某个出现在程序多处并且需要经常调整的“阀值”参数,方便“一键更改”的操作。所谓“一键更改”,就是只要改一次const所定义初始化的某个常量,整个程序多次出现的此常量就自动更改了。定义常量数组时,通常应用在某个数据转换表,把某些固定的常量预先放到常量数组,通过数组的下标来“查表”。
【58.4 const(或code)的语法格式。】
定义单个常量和常量数组时的语法是以下这个样子的:
- const unsigned char x=10; //定义单个常量。加了const。
- const unsigned char y[12]={31,28,31,30,31,30,31,31,30,31,30,31}; //定义常量数组。加了const。
复制代码
【58.5 const(或code)的“能读”和“不可写”概念】
所谓“读”和“写”的能力,其实就是看某数能在赋值符号“=”的“右边”还是“左边”的能力。普通的变量,既可以在赋值符号“=”的“右边”(能读),也可以在赋值符号“=”的“左边”(能写)。比如,下面的写法是合法的:
- unsigned char k=1; //这是普通的变量,无const前缀。
- unsigned char n=2; //这是普通的变量,无const前缀。
- n=k; //k出现在赋值符号“=”的右边,表示能读。合法。
- k=n; //k出现在赋值符号“=”的左边,表示能写,可更改之意。合法。
复制代码
但是如果一旦在普通的变量前面加了const(或code)关键词,就会发生“化学变化”,原来的“变量”就变成了“常量”,常量只能“读”,不能“写”。比如:
- const unsigned char c=1; //这是常量,有const前缀。
- unsigned char n=2; //这是普通的变量,无const前缀。
- n=c; //c是常量,能读,这是合法的。这行代码是语法正确的。
- c=n; //c是常量,不能写,这是非法的,C编译器不通过。这行代码是语法错误的。
复制代码
【58.6 const(或code)能在函数内部定义吗?】
const(或code)能在函数内部定义吗?能。语法是允许的。当在函数内部定义数据成const(或者code),在数据的存储结构上,数据也是放在ROM区的(实际上在51单片机里想把数据放在ROM只能用code而不能用const),把数据定义在函数内部,就只能在这个函数里面用,不能被其它函数调用。在作用域的问题上,const(或者code)的常量数据跟其它变量的数据是一样的。比如:
- void HanShu(void)
- {
- const unsigned char c=1; //在函数内部定义的const常量也是放在ROM区存储。
- unsigned char n=2;
- n=c; //c是常量,在函数内部定义,只能在当前这个HanShu函数里调用。
- }
复制代码
【58.7 例程练习和分析。】
本教程使用的是51单片机的C51编译器,编写程序时为了让常量数据真正存储在ROM区,因此,本教程的程序例子都是用code替代const。
本例程讲两个例子,一个是单个常量,一个是常量数组。
(1)单个常量。举的例子是“阀值”的“一键更改”应用。根据考试的分数,分两个等级。凡是大于或者等于90分的就是“优”,串口助手输出显示“1”。凡是小于90分的就是“良”,串口助手输出显示“0”。这里的“90分”就是我所说的“阀值”概念,只要用一个const定义一个常量数据来替代“90”,当需要调整“阀值”时,只要更改一次此定义的常量数值就可以达到“一键更改”之目的。
(2)常量数组。举的例子是,查询2017年12个月的某个月的总天数,用两种思路实现,一种是switch分支语句来实现,另一种是const常量数组的“查表”思路来实现。通过对比这两种思路,你会发现const常量数组在做“转换表”这类“查表”项目时的强大优越性。
- /*---C语言学习区域的开始。-----------------------------------------------*/
- //函数的声明。
- unsigned char HanShu_switch(unsigned char u8Month);
- unsigned char HanShu_const(unsigned char u8Month);
- //数据的定义。
- code unsigned char Cu8Level=90; //需要调整“阀值”时,只需更改一次这里的“90”这个数值。
- code unsigned char Cu8MonthBuffer[12]= //每个月对应的天数。从数组下标0开始,0代表1月...
- {31,28,31,30,31,30,31,31,30,31,30,31};
- unsigned char a; //用来接收函数返回的结果。
- unsigned char b;
- unsigned char c;
- unsigned char d;
- //函数的定义。
- unsigned char HanShu_switch(unsigned char u8Month) //用switch分支来实现。
- {
- switch(u8Month)
- {
- case 1: //1月份的天数
- return 31;
- case 2: //2月份的天数
- return 28;
- case 3: //3月份的天数
- return 31;
- case 4: //4月份的天数
- return 30;
- case 5: //5月份的天数
- return 31;
- case 6: //6月份的天数
- return 30;
- case 7: //7月份的天数
- return 31;
- case 8: //8月份的天数
- return 31;
- case 9: //9月份的天数
- return 30;
- case 10: //10月份的天数
- return 31;
- case 11: //11月份的天数
- return 30;
- case 12: //12月份的天数
- return 31;
- default: //万一输错了其它范围的月份,就默认返回30天。
- return 30;
- }
- }
- unsigned char HanShu_const(unsigned char u8Month) //用const常量数组的“查表”来实现。
- {
- unsigned char u8GetDays;
- u8Month=u8Month-1; //因为数组下标是从0开始,0代表1月份,1代表2月份。所以减去1。
- u8GetDays=Cu8MonthBuffer[u8Month]; //这就是查表,马上获取常量数组表格里固定对应的天数。
- return u8GetDays;
- }
- void main() //主函数
- {
- //第(1)个例子
- if(89>=Cu8Level) //大于或者等于阀值,就输出1。
- {
- a=1;
- }
- else //否则输出0。
- {
- a=0;
- }
- if(95>=Cu8Level) //大于或者等于阀值,就输出1。
- {
- b=1;
- }
- else //否则输出0。
- {
- b=0;
- }
- //第(2)个例子
- c=HanShu_switch(2); //用switch分支的函数获取2月份的总天数。
- d=HanShu_const(2); //用const常量数组“查表”的函数获取2月份的总天数。
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:0
- 十六进制:0
- 二进制:0
- 第2个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第3个数
- 十进制:28
- 十六进制:1C
- 二进制:11100
- 第4个数
- 十进制:28
- 十六进制:1C
- 二进制:11100
复制代码
分析:
a为0。
b为1。
c为28。
d为28。
【58.8 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-3-5 13:10
本帖最后由 jianhong_wu 于 2017-3-5 13:31 编辑
第五十九节: 全局“一键替换”功能的#define。
第五十九节_pdf文件.pdf
(93.7 KB, 下载次数: 1743)
【59.1 #define作用和书写格式。】
上一节讲const的时候,讲到了当某个常量在程序中是属于需要频繁更改的“阀值”的时候,用const就可以提供“一键更改”的快捷服务。本节的#define也具有此功能,而且功能比const更加强大灵活,它除了可以应用在常量,还可以应用在运算式以及函数的“一键更改”中。所谓“一键更改”,其实是说,#define内含了“替换”的功能,此“替换”跟word办公软件的“替换”功能几乎是一模一样的。#define的“替换”功能,除了在某些场合起到“一键更改”的作用,还可以在某些场合,把一些在字符命名上不方便阅读理解的常量、运算式或函数先“替换”成容易理解的字符串,让程序阅读起来更加清晰更加方便维护。#define的常见三种书写格式如下:
- #define 字符串 常量 //注意,这里后面没有分号“;”
- #define 字符串 运算式 //注意,这里后面没有分号“;”
- #define 字符串 函数 //注意,这里后面没有分号“;”
复制代码
具体一点如下:
- #define AA 1 //常量
- #define BB (a+b+c) //运算式
- #define C add() //函数
复制代码
需要注意的时候,#define后面没有分号“;”,因为它是C语言中的“预处理”的语句,不是单片机运行的程序指令语句。
【59.2 #define的编译机制。】
#define是属于“预编译”的指令,所谓“预编译”就是在“编译”之前就开始的准备工作。编译器在正式编译某个源代码的时候,先进行“预编译”的准备工作,对于#define语句,编译器是直接把#define要替换的内容先在“编辑层面”进行机械化替换,这个“机械化替换”纯粹是字符串的替换,可以理解成word办公软件的“替换”编辑功能。比如以下程序:
- #define A 3
- #define B (2+6) //有括号
- #define C 2+6 //无括号
- unsigned long x=3;
- unsigned long a;
- unsigned long b;
- unsigned long c;
- void main() //主函数
- {
- a=x*A;
- b=x*B;
- c=x*C;
- while(1)
- {
- }
- }
复制代码
经过编译器“预编译”的“机械化替换”后,等效于以下代码:
- unsigned long x=3;
- unsigned long a;
- unsigned long b;
- unsigned long c;
- void main() //主函数
- {
- a=x*3;
- b=x*(2+6);
- c=x*2+6;
- while(1)
- {
- }
- }
复制代码
【59.3 #define在常量上的“一键替换”功能。】
上一节讲const(或code)的时候,举了一个“阀值”常量的例子,这个例子可以用#define来替换等效。比如,原来const(或code)的例子如下:
- code unsigned char Cu8Level=90; //需要调整“阀值”时,只需更改一次这里的“90”这个数值。
- unsigned char a;
- unsigned char b;
- void main() //主函数
- {
- if(89>=Cu8Level) //大于或者等于阀值,就输出1。
- {
- a=1;
- }
- else //否则输出0。
- {
- a=0;
- }
- if(95>=Cu8Level) //大于或者等于阀值,就输出1。
- {
- b=1;
- }
- else //否则输出0。
- {
- b=0;
- }
- while(1)
- {
- }
- }
复制代码
上述程序现在用#define来替换,等效如下:
- #define Cu8Level 90 //需要调整“阀值”时,只需更改一次这里的“90”这个数值。
- unsigned char a;
- unsigned char b;
- void main() //主函数
- {
- if(89>=Cu8Level) //大于或者等于阀值,就输出1。
- {
- a=1;
- }
- else //否则输出0。
- {
- a=0;
- }
- if(95>=Cu8Level) //大于或者等于阀值,就输出1。
- {
- b=1;
- }
- else //否则输出0。
- {
- b=0;
- }
- while(1)
- {
- }
- }
复制代码
【59.4 #define在运算式上的“一键替换”功能。】
#define在运算式上应用的时候,有一个地方要特别注意,就是必须加小括号“()”,否则容易出错。因为#define的替换是很“机械呆板”的,它只管“字符编辑层面”的机械化替换,举一个例子如下:
- #define B (2+6) //有括号
- #define C 2+6 //无括号
- unsigned long x=3;
- unsigned long b;
- unsigned long c;
- void main() //主函数
- {
- b=x*B; //等效于b=x*(2+6),最终运算结果b等于24。因为3乘以8(2加上6等于8)。
- c=x*C; //等效于c=x*2+6, 最终运算结果c等于12。因为3乘以2等于6,6再加6等于12。
- while(1)
- {
- }
- }
复制代码
上述例子中,“有括号”与“没括号”的运算结果差别很大,第一个是24,第二个是12。具体的分析已经在源代码的注释了。
【59.5 #define在函数上的“一键替换”功能。】
#define的应用很广,也可以应用在函数的“替换”上。例子如下:
- void add(void); //函数的声明。
- void add(void) //函数的定义。
- {
- a++;
- }
- #define a_zi_jia add() //用字符串a_zi_jia来替代函数add()。
- unsigned long a=1;
- void main() //主函数
- {
- a_zi_jia; //这里相当于调用函数add()。
- while(1)
- {
- }
- }
复制代码
【59.6 #define在常量后面添加U或者L的特殊写法。】
有些初学者今后可能在工作中遇到#define以下这种写法:
- #define 字符串 常量U
- #define 字符串 常量L
复制代码
具体一点如下:
- #define AA 6U
- #define BB 6L
复制代码
常量加后缀“U”或者“L”有什么含义呢?字面上理解,U表示该常量是无符号整型unsigned int;L表示该常量是长整型long。但是在实际应用中这样“多此一举”地去强调某个常量的数据类型有什么意义呢?我自己私下也做了一些测试,目前我本人暂时还没有发现这个秘密的答案。所以对于这个问题,初学者现在只要知道这种写法在语法上是合法的就可以,至于它背后有什么玄机,有待大家今后更深的发掘。
【59.7 #define省略常量的特殊写法。】
有些初学者今后在多文件编程中,在某些头文件.h中,会经常遇到以下这类代码:
- #ifndef _AAA_
- #define _AAA_
- #endif
复制代码
其中第2行代码“#define _AAA_”后面居然没有常量,这样子的写法也行,到底是什么意思?在这类写法中,当字符串“_AAA_”后面省略了常量的时候,编译器默认会给_AAA_添加一个“非0”的常量,也许是1或者其它“非0”的值,多说一句,所谓“非0”值就是“肯定不是0”。上述代码等效于:
- #ifndef _AAA_
- #define _AAA_ 1 //编译器会在这类默认添加一个1或者其它“非0”的常量
- #endif
复制代码
这个知识点大家只要先有一个感性的认识即可,暂时不用深入了解。
【59.8 例程练习和分析。】
现在编一个练习程序来熟悉#define的用法。
- /*---C语言学习区域的开始。-----------------------------------------------*/
- //第1个:常量的例子
- #define Cu8Level 90 //需要调整“阀值”时,只需更改一次这里的“90”这个数值。
- unsigned char a;
- unsigned char b;
- //第2个:运算式的例子
- #define C (2+6) //有括号
- #define D 2+6 //无括号
- unsigned char x=3;
- unsigned char c;
- unsigned char d;
- //第3个:函数的例子
- unsigned char e=1;
- void add(void);
- void add(void)
- {
- e++;
- }
- #define a_zi_jia add() //用字符串a_zi_jia来替代函数add()。
- void main() //主函数
- {
- //第1个:常量的例子
- if(89>=Cu8Level) //大于或者等于阀值,就输出1。
- {
- a=1;
- }
- else //否则输出0。
- {
- a=0;
- }
- if(95>=Cu8Level) //大于或者等于阀值,就输出1。
- {
- b=1;
- }
- else //否则输出0。
- {
- b=0;
- }
- //第2个:运算式的例子
- c=x*C; //等效于c=x*(2+6),最终运算结果c等于24。因为3乘以8(2加上6等于8)。
- d=x*D; //等效于d=x*2+6, 最终运算结果d等于12。因为3乘以2等于6,6再加6等于12。
- //第3个:函数的例子
- a_zi_jia; //这里相当于调用函数add()。e从1自加到2。
- a_zi_jia; //这里相当于调用函数add()。e从2自加到3。
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- View(e); //把第5个数e发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:0
- 十六进制:0
- 二进制:0
- 第2个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第3个数
- 十进制:24
- 十六进制:18
- 二进制:11000
- 第4个数
- 十进制:12
- 十六进制:C
- 二进制:1100
- 第5个数
- 十进制:3
- 十六进制:3
- 二进制:11
复制代码
分析:
a为0。
b为1。
c为24。
d为12。
e为3。
【59.9 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-3-12 12:32
本帖最后由 jianhong_wu 于 2017-3-12 12:49 编辑
第六十节: 指针在变量(或常量)中的基础知识。
第六十节_pdf文件.pdf
(92.17 KB, 下载次数: 1697)
【60.1 指针与普通变量的对比。】
普通变量和指针都是变量,都要占用RAM资源。普通变量的unsigned char类型占用1个字节,unsigned int类型占用2个字节,unsigned long类型占用4个字节。但是指针不一样,指针是一种特殊的变量,unsigned char*,unsigned int*,unsigned long*这三类指针都是一样占用4个字节。指针是普通变量的载体,平时我们处理普通变量,都是可以“直接”操作普通变量本身。而学了指针之后,我们就多一种选择,可以通过指针这个载体来“间接”操作某个普通变量。“直接”不是比“间接”更好更高效吗?为什么要用“间接”?其实在某些场合,指针的“间接”操作更加灵活更加高效,这个要看具体的应用。
指针既然是普通变量的“载体”,那么普通变量就是“物”。“载体”与“物”之间可以存在一对多的关系。也就是说,一个篮子(载体),可以盛放鸡蛋(物),也可以盛放青菜(物),也可以盛放水果(物)。
但是,在这里,一个篮子在一个时间段内,只能承载一种物品,如果想承载其它物品,必须先把当前物品“卸”下来,然后再“装”其它物品”。这里有两个关键动作“装”和“卸”,就是指针在处理普通变量时的“绑定”,某个指针与某个变量发生“绑定”,就已经包含了先“卸”后“装”这两个动作在其中。
题外话多说一句,刚才提到,unsigned int类型占用2个字节,这个是在C51编译器下的情况。如果是在stm32单片机的编译器下,unsigned int类型是占用4个字节。而“凡是指针都是4个字节”,这个描述仅仅适用于32位以下的单片机编译器(包括8位的单片机),而在某些64位的PC机,指针可能是8个字节,这些内容大家只要有个大概的了解即可。
【60.2 指针的定义。】
跟普通变量一样,指针也必须先定义再使用。为了与普通变量区分开来,指针在定义的时候多加了一个星号“*”,例子如下:
- unsigned char* pu8; //针对unsigned char类型变量的指针。凡是指针都是占4个字节!
- unsigned int* pu16; //针对unsigned int类型变量的指针。凡是指针都是占4个字节!
- unsigned long* pu32; //针对unsigned long类型变量的指针。凡是指针都是占4个字节!
复制代码
既然指针都是4个字节,为什么还要区分unsigned char*,unsigned int* pu16,unsigned long* pu32这三种类型?因为指针是为普通变量(或常量)而生,所以要根据普通变量(或常量)的类型定义对应的指针。
【60.3 指针与普通变量是如何关联和操作的?】
指针在操作某个变量的时候,必须先跟某个变量关联起来,这里的关联就是“绑定”。“绑定”后,才可以通过指针这个“载体”来“间接”操作变量。指针与普通变量在“绑定”的时候,需要用到“&”这个符号。例子如下:
- unsigned char* pu8; //针对unsigned char类型变量的指针。凡是指针都是占4个字节!
- unsigned char a=0; //普通的变量。
- pu8=&a; //指针与普通变量发生关联(或者说绑定)。
- *pu8=2; //通过指针这个载体来处理a这个变量,此时a从原来的0变成了2。
复制代码
【60.4 指针处理“批量数据”的基础知识。】
之所以有通过载体来“间接”操作普通变量的存在价值,其中很重要的原因是指针在处理“批量数据”时特别给力,这里的“批量数据”是有条件的,要求这些数据的地址必须挨家挨户连起来的,不能是零零散散的“散户”,比如说,数组就是由一堆在RAM空间里地址连续的变量组合而成,指针在很多时候就是为数组而生的。先看一个例子如下:
- unsigned char* pu8; //针对unsigned char类型变量的指针。凡是指针都是占4个字节!
- unsigned char Buffer[3]; //普通的数组,内含3个变量,它们地址是相连的。
- pu8=&Buffer[0]; //指针与普通变量Buffer[0]发生关联(或者说绑定)。
- *pu8=1; //通过指针这个载体来处理Buffer[0]这个变量,此时Buffer[0]变成了1。
- pu8=&Buffer[1]; //指针与普通变量Buffer[1]发生关联(或者说绑定)。
- *pu8=2; //通过指针这个载体来处理Buffer[1]这个变量,此时Buffer[1]变成了2。
- pu8=&Buffer[2]; //指针与普通变量Buffer[2]发生关联(或者说绑定)。
- *pu8=3; //通过指针这个载体来处理Buffer[2]这个变量,此时Buffer[2]变成了3。
复制代码
分析:上述例子中,并没有体现出指针的优越性,因为数组有3个元素,居然要绑定了3次,如果数组有1000个元素,难道要绑定1000次?显然这样是繁琐低效不可取的。而要发挥指针的优越性,我们现在必须深入了解一下指针的本质是什么,指针跟普通变量发生“绑定”的本质是什么。普通变量由“地址”和“地址所装的数据”构成,指针是特殊的变量,它是由什么构成呢?其实,指针是由“地址”和“地址所装的变量(或常量)的地址”组成。很明显,一个重要的区别是,普通变量装的数据,而指针装的是地址。正因为指针装的是地址,所以指针可以有两种选择,第一种可以处理“装的地址”,第二种可以处理“装的地址的所在数据”,这两种能力,就是指针的精华和本质所在,也是跟普通变量的区别所在。那么指针处理“装的地址”的语法是什么样子的?请看例子如下:
- unsigned char* pu8; //针对unsigned char类型变量的指针。凡是指针都是占4个字节!
- unsigned char Buffer[3]; //普通的数组,内含3个变量,它们地址是相连的。
- pu8=&Buffer[0]; //处理“装的地址”。把 Buffer[0]变量的地址装在指针这个载体里。
- *pu8=1; //处理“装的地址的所在数据”。此时Buffer[0]变成了1。
- pu8++; //处理“装的地址”。这里是“地址”自加1,相当于指针此时装的是Buffer[1]的地址。
- *pu8=2; //处理“装的地址的所在数据”。此时Buffer[1]变成了2。
- pu8++; //处理“装的地址”。这里是“地址”自加1,相当于指针此时装的是Buffer[2]的地址。
- *pu8=3; //处理“装的地址的所在数据”。此时Buffer[2]变成了3。
复制代码
上述例子中,利用“地址”自加1的操作,省去了2条赋值式的“绑定”操作(比如像pu8=&Buffer[0]这类语句),因此“绑定”本质其实就是更改指针所装的“变量(或常量)的地址”的操作。此例子中虽然还没体现了出指针在数组处理时的优越性,但是利用指针处理“装的地址”这项功能,在实际项目中很容易发现它的好处。
【60.5 指针与数组关联(绑定)时省略“&和下标[0]”的写法。】
指针与数组关联的时候,通常是跟数组的第0个元素的地址关联,此时,可以把数组的“&和下标[0]”省略,比如:
- unsigned char* pu8;
- unsigned char Buffer[3];
- pu8=Buffer; //此行代码省略了“&和下标[0]”,等效于pu8=&Buffer[0];
复制代码
【60.6 带const关键字的常量指针。】
指针也可以跟常量关联起来,处理常量,但是常量只能“读”不能“写”,所以通过指针操作常量的时候也是只能“读”不能“写”。操作常量的指针用const关键词修饰,强调此指针只有“读”的操作。例子如下:
- const unsigned char* pCu8; //常量指针
- code char Cu8Buffer[3]={5,6,7}; //常量数组
- unsigned char b;
- unsigned char c;
- unsigned char d;
- pCu8=Cu8Buffer; //此行代码省略了“&和下标[0]”,等效于pCu8=&Cu8Buffer[0];
- b=*pCu8; //读“装的地址的所在数据”。b等于5。
- pCu8++; //所装的地址自加1,跟Cu8Buffer[1]关联
- c=*pCu8; //读“装的地址的所在数据”。c等于6。
- pCu8++; //所装的地址自加1,跟Cu8Buffer[2]关联
- d=*pCu8; //读“装的地址的所在数据”。d等于7。
复制代码
【60.7 例程练习和分析。】
现在编一个练习程序来熟悉指针的基础知识。
- /*---C语言学习区域的开始。-----------------------------------------------*/
- unsigned char* pu8; //针对unsigned char类型变量的指针。凡是指针都是占4个字节!
- unsigned char a=0; //普通的变量。
- unsigned char Buffer[3]; //普通的数组,内含3个变量,它们地址是相连的。
- const unsigned char* pCu8; //常量指针
- code char Cu8Buffer[3]={5,6,7}; //常量数组
- unsigned char b;
- unsigned char c;
- unsigned char d;
- void main() //主函数
- {
- pu8=&a; //指针与普通变量发生关联(或者说绑定)。
- *pu8=2; //通过指针这个载体来处理a这个变量,此时a从原来的0变成了2。
- pu8=&Buffer[0]; //处理“装的地址”。把 Buffer[0]变量的地址装在指针这个载体里。
- *pu8=1; //处理“装的地址的所在数据”。此时Buffer[0]变成了1。
- pu8++; //处理“装的地址”。这里是“地址”自加1,相当于指针此时装的是Buffer[1]的地址。
- *pu8=2; //处理“装的地址的所在数据”。此时Buffer[1]变成了2。
- pu8++; //处理“装的地址”。这里是“地址”自加1,相当于指针此时装的是Buffer[2]的地址。
- *pu8=3; //处理“装的地址的所在数据”。此时Buffer[2]变成了3。
- pCu8=Cu8Buffer; //此行代码省略了“&和下标[0]”,等效于pCu8=&Cu8Buffer[0];
- b=*pCu8; //读“装的地址的所在数据”。b等于5。
- pCu8++; //所装的地址自加1,跟Cu8Buffer[1]关联
- c=*pCu8; //读“装的地址的所在数据”。c等于6。
- pCu8++; //所装的地址自加1,跟Cu8Buffer[2]关联
- d=*pCu8; //读“装的地址的所在数据”。d等于7。
- View(a); //把第1个数a发送到电脑端的串口助手软件上观察。
- View(b); //把第2个数b发送到电脑端的串口助手软件上观察。
- View(c); //把第3个数c发送到电脑端的串口助手软件上观察。
- View(d); //把第4个数d发送到电脑端的串口助手软件上观察。
- View(Buffer[0]); //把第5个数Buffer[0]发送到电脑端的串口助手软件上观察。
- View(Buffer[1]); //把第6个数Buffer[1]发送到电脑端的串口助手软件上观察。
- View(Buffer[2]); //把第7个数Buffer[2]发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:2
- 十六进制:2
- 二进制:10
- 第2个数
- 十进制:5
- 十六进制:5
- 二进制:101
- 第3个数
- 十进制:6
- 十六进制:6
- 二进制:110
- 第4个数
- 十进制:7
- 十六进制:7
- 二进制:111
- 第5个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第6个数
- 十进制:2
- 十六进制:2
- 二进制:10
- 第7个数
- 十进制:3
- 十六进制:3
- 二进制:11
复制代码
分析:
a为2。
b为5。
c为6。
d为7。
Buffer[0]为1。
Buffer[1]为2。
Buffer[2]为3。
【60.8 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-3-20 10:15
本帖最后由 jianhong_wu 于 2017-3-20 10:50 编辑
第六十一节: 指针的中转站作用,地址自加法,地址偏移法。
第六十一节_pdf文件.pdf
(87.5 KB, 下载次数: 1687)
【61.1 指针与批量数组的关系。】
指针和批量数据的关系,更像领导和团队的关系,领导是团队的代表,所以当需要描述某个团队的时候,为了表述方便,可以把由N个人组成的团队简化成该团队的一个领导,用一个领导来代表整个团队,此时,领导就是团队,团队就是领导。指针也一样,指针一旦跟某堆数据“绑定”了,那么指针就是这堆数据,这堆数据就是该指针,所以在很多PC上位机的项目中,往往也把指针称呼为“句柄”,字面上理解,就是一句话由N个文字组成,而“句柄”就是这句话的代表,实际上“句柄”往往是某一堆资源的代表。不管是把指针比喻成“领导”、“代表”还是“句柄”,指针在这里都有“中间站”这一层含义。
【61.2 指针在批量数据的“中转站”作用。】
指针在批量数据处理中,主要是能节省代码容量,而且是非常直观的节省代码容量。为什么能节省代码容量?是因为可以把某些重复性的具体实现的功能封装成指针来操作,请看下面的例子:
程序要求:根据一个选择变量Gu8Sec的值,要从三堆数据中选择对应的一堆数据放到数组Gu8Buffer里。当Gu8Sec等于1的时候选择第1堆,等于2的时候选择第2堆,等于3的时候选择第3堆。也就是“三选一”。
第1种实现的方法:没有用指针,最原始的处理方式。如下:
- code unsigned char Cu8Memory_1[3]={1,2,3}; //第1堆数据
- code unsigned char Cu8Memory_2[3]={4,5,6}; //第2堆数据
- code unsigned char Cu8Memory_3[3]={7,8,9}; //第3堆数据
- unsigned char Gu8Sec=2; //选择的变量
- unsigned char Gu8Buffer[3]; //根据变量来存放对应的某堆数据的数组
- unsigned char i; //for循环用到的变量i
- switch(Gu8Sec) //根据此选择变量来切换到对应的操作上
- {
- case 1: //第1堆
- for(i=0;i<3;i++) //第1次出现for循环,用来实现“赋值”的“搬运数据”的动作。
- {
- Gu8Buffer[i]=Cu8Memory_1[i];
- }
- break;
- case 2: //第2堆
- for(i=0;i<3;i++) //第2次出现for循环,用来实现“赋值”的“搬运数据”的动作。
- {
- Gu8Buffer[i]=Cu8Memory_2[i];
- }
- break;
- case 3: //第3堆
- for(i=0;i<3;i++) //第3次出现for循环,用来实现“赋值”的“搬运数据”的动作。
- {
- Gu8Buffer[i]=Cu8Memory_3[i];
- }
- break;
- }
复制代码
分析:上述程序中,没有用到指针,出现了3次for循环的“赋值”的“搬运数据”的动作。
第2种实现的方法:用指针作为“中间站”。如下:
- code unsigned char Cu8Memory_1[3]={1,2,3}; //第1堆数据
- code unsigned char Cu8Memory_2[3]={4,5,6}; //第2堆数据
- code unsigned char Cu8Memory_3[3]={7,8,9}; //第3堆数据
- unsigned char Gu8Sec=2; //选择的变量
- unsigned char Gu8Buffer[3]; //根据变量来存放对应的某堆数据的数组
- unsigned char i; //for循环用到的变量i
- const unsigned char *pCu8; //引入一个指针作为“中间站”
- switch(Gu8Sec) //根据此选择变量来切换到对应的操作上
- {
- case 1: //第1堆
- pCu8=&Cu8Memory_1[0]; //跟第1堆数据“绑定”起来。
- break;
- case 2: //第2堆
- pCu8=&Cu8Memory_2[0]; //跟第2堆数据“绑定”起来。
- break;
- case 3: //第3堆
- pCu8=&Cu8Memory_3[0]; //跟第3堆数据“绑定”起来。
- break;
- }
- for(i=0;i<3;i++) //第1次出现for循环,用来实现“赋值”的“搬运数据”的动作。
- {
- Gu8Buffer[i]=*pCu8; //把“指针所存的地址的数据”赋值给数组
- pCu8++; //“指针所存的地址”自加1,为下一个数据的“赋值”的“搬运”作准备。
- }
复制代码
分析:上述程序中,用到了指针作为中间站,只出现了1次for循环的“赋值”的“搬运数据”的动作。对比之前第1种方法,在本例子中,用了指针之后,程序代码看起来更加高效简洁清爽省容量。在实际项目中,数据量越大的时候,指针这种“优越性”就越明显。
【61.3 指针在书写上另外两种常用写法。】
刚才61.2处第2个例子中,有一段代码如下:
- for(i=0;i<3;i++) //第1次出现for循环,用来实现“赋值”的“搬运数据”的动作。
- {
- Gu8Buffer[i]=*pCu8; //把“指针所存的地址的数据”赋值给数组
- pCu8++; //“指针所存的地址”自加1,为下一个数据的“赋值”的“搬运”作准备。
- }
复制代码
很多高手,喜欢把上面for循环内部的那两行代码简化成一行代码,如下:
- for(i=0;i<3;i++) //第1次出现for循环,用来实现“赋值”的“搬运数据”的动作。
- {
- Gu8Buffer[i]=*pCu8++; //先把“数据”赋值给数组,然后“指针所存的地址”再自加1。
- }
复制代码
上面这种写法也是合法的,而且在高手的代码中常见,据说也是最高效的写法。还有一种是利用“指针的偏移地址”的写法,我常用这种写法,因为感觉这种写法比较直观,而且跟数组的书写很像。如下:
- for(i=0;i<3;i++) //第1次出现for循环,用来实现“赋值”的“搬运数据”的动作。
- {
- Gu8Buffer[i]=pCu8[i]; //这类是“偏移地址”的写法,i在这里相当于指针的偏移地址。
- }
复制代码
这种写法也是跟前面那两种写法在程序实现的功能上是一样的,是等效的,我常用这种写法。
【61.4 指针的“地址自加法”和“地址偏移法”的差别。】
刚才61.3处讲了3个例子,其中前面的两个例子都是属于“地址自加法”,而最后的那一个是属于“地址偏移法”。它们的根本差别是:“地址自加法”的时候,“指针所存的地址”是变动的;而“地址偏移法”的时候,“指针所存的地址”是不变的,“指针所存的地址”的“不变”的属性,就像某个原点,原点再加上偏移,就可以寻址到某个新的RAM地址所存的数据。例子如下:
第1种:“地址自加法”:
- pCu8=&Cu8Memory_2[0]; //假设赋值后,此时“指针所存的地址”是RAM的地址4。
- for(i=0;i<3;i++)
- {
- Gu8Buffer[i]=*pCu8++; //先把“数据”赋值给数组,然后“指针所存的地址”再自加1。
- }
复制代码
分析:上述代码,等程序执行完for循环后,指针所存的地址还是RAM地址4吗?不是。因为它是变动的,经过for循环,“指针所存的地址”自加3次后,此时“所存的RAM地址”从原来的4变成了7。
第2种:“地址偏移法”:
- pCu8=&Cu8Memory_2[0]; //假设赋值后,此时“指针所存的地址”是RAM的地址4。
- for(i=0;i<3;i++)
- {
- Gu8Buffer[i]=pCu8[i]; //这类是“偏移地址”的写法,i在这里相当于指针的偏移地址。
- }
复制代码
分析:上述代码,等程序执行完for循环后,指针所存的地址还是RAM地址4吗?是的。因为它存的地址是不变的,变的只是偏移地址i。此时“指针所存的地址”就像“原点”一样具有“绝对地址”的“参考点”的属性。
【61.5 例程练习和分析。】
现在编一个练习程序。
- /*---C语言学习区域的开始。-----------------------------------------------*/
- code unsigned char Cu8Memory_1[3]={1,2,3}; //第1堆数据
- code unsigned char Cu8Memory_2[3]={4,5,6}; //第2堆数据
- code unsigned char Cu8Memory_3[3]={7,8,9}; //第3堆数据
- unsigned char Gu8Sec=2; //选择的变量
- unsigned char Gu8Buffer[3]; //根据变量来存放对应的某堆数据的数组
- unsigned char i; //for循环用到的变量i
- const unsigned char *pCu8; //引入一个指针作为“中间站”
- void main() //主函数
- {
- switch(Gu8Sec) //根据此选择变量来切换到对应的操作上
- {
- case 1: //第1堆
- pCu8=&Cu8Memory_1[0]; //跟第1堆数据“绑定”起来。
- break;
- case 2: //第2堆
- pCu8=&Cu8Memory_2[0]; //跟第2堆数据“绑定”起来。
- break;
- case 3: //第3堆
- pCu8=&Cu8Memory_3[0]; //跟第3堆数据“绑定”起来。
- break;
- }
- // for(i=0;i<3;i++) //第1次出现for循环,用来实现“赋值”的“搬运数据”的动作。
- // {
- // Gu8Buffer[i]=*pCu8++; //先把“数据”赋值给数组,然后“指针所存的地址”再自加1。
- // }
- for(i=0;i<3;i++) //第1次出现for循环,用来实现“赋值”的“搬运数据”的动作。
- {
- Gu8Buffer[i]=pCu8[i]; //这类是“偏移地址”的写法,i在这里相当于指针的偏移地址。
- }
- View(Gu8Buffer[0]); //把第1个数Gu8Buffer[0]发送到电脑端的串口助手软件上观察。
- View(Gu8Buffer[1]); //把第2个数Gu8Buffer[1]发送到电脑端的串口助手软件上观察。
- View(Gu8Buffer[2]); //把第3个数Gu8Buffer[2]发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:4
- 十六进制:4
- 二进制:100
- 第2个数
- 十进制:5
- 十六进制:5
- 二进制:101
- 第3个数
- 十进制:6
- 十六进制:6
- 二进制:110
复制代码
分析:
Gu8Buffer[0]为4。
Gu8Buffer[1]为5。
Gu8Buffer[2]为6。
【61.6 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-3-26 10:38
本帖最后由 jianhong_wu 于 2017-3-26 10:54 编辑
第六十二节: 指针,大小端,化整为零,化零为整。
第六十二节_pdf文件.pdf
(82.29 KB, 下载次数: 1638)
【62.1 内存的大小端。】
C51编译器的unsigned int占2字节RAM(也称为内存),unsigned long占4字节RAM,这两种数据类型所占的字节数都超过了1个字节,而RAM内存是每一个地址对应一个字节的RAM内存,那么问题就来了,比如像unsigned long这种占4个字节RAM的数据变量,它这4个字节在RAM中的地址是“连续”的“挨家挨户”的“连号”的,这4个字节所存的一个数据,它的数据高低位在地址的排列上,到底是从低到高还是从高到低,到底是“正向”的还是“反向”?这两种不同的排列顺序,在C语言里用“大端”和“小端”这两个专业术语来描述。“大端”的方式是将高位存放在低地址,“小端”的方式是将低位存放在低地址。比如:
假设有一个unsigned long变量a等于0x12345678,是存放在RAM内存中的第4,5,6,7这四个“连号”的地址里,现在看看它在“大端”和“小端”的存储方式里的差别。如下:
(1)在“大端”的方式里,将高位存放在低地址。
0x12存在第4个地址,0x34存在第5个地址,0x56存在第6个地址,0x78存在第7个地址。
(2)在“小端”的方式里,将低位存放在低地址。
0x78存在第4个地址,0x56存在第5个地址,0x34存在第6个地址,0x12存在第7个地址。
问题来了,在单片机里,内存到底是“大端”方式还是“小端”方式?答:这个跟C编译器有关。比如,在51单片机的C51编译环境里是“大端”方式,而在stm32单片机的ARM_MDK编译环境里则是“小端”方式。那么问题又来了?如何知道一个C编译器是“大端”还是“小端”?答:有两种方式,一种是看C编译器的说明书,另一种是自己编写一个小程序测试一下就知道了(这种方法最简单可靠)。那么问题又来了?讲这个 “大小端”有什么用?答:这个跟指针的使用密切相关。
【62.2 化整为零。】
在数据的存储和通信中,往往要先把数据转换成以字节为单位的数组,才能进行数据存储和通信。比如unsigned long这种类型的数据,就要先转换成4个字节,这种把某个变量转换成N个字节的过程,就是“化整为零”。“化整为零”的过程,在代码上,有两种常见的方式,一种是原始的“移位法”,另一种是极具优越性的“指针法”。比如,现在以“大端”方式为例(因为本教程是用C51编译器,C51编译器是“大端”方式),有一个unsigned long变量a等于0x12345678,要把这个变量分解成4个字节存放在一个数组Gu8BufferA中,现在跟大家分享和对比一下这两种方法。
(1)原始的“移位法”。
- unsigned long a=0x12345678;
- unsigned char Gu8BufferA[4];
- Gu8BufferA[0]=a>>24;
- Gu8BufferA[1]=a>>16;
- Gu8BufferA[2]=a>>8;
- Gu8BufferA[3]=a;
复制代码
(2)极具优越性的“指针法”。
- unsigned long a=0x12345678;
- unsigned char Gu8BufferA[4];
- unsigned long *pu32; //引入一个指针变量,注意,这里是unsigned long类型的指针。
- pu32=(unsigned long *)&Gu8BufferA[0]; //指针跟数组“绑定”(也称为“关联”)起来。
- *pu32=a; //这里仅仅1行代码就等效于上述(1)“移位”例子中的4行代码,所以极具优越性。
复制代码
多说一句,“pu32=(unsigned long *)&Gu8BufferA[0]”这行代码中,其中小括号“(unsigned long *)”是表示数据的强制类型转换,这里表示强制转换成unsigned long的指针方式,以后这类代码写多了,就会发现这种书写方法的规律。作为语言来解读先熟悉一下它的表达方式就可以了,暂时不用深究它的含义。
【62.3 化零为整。】
从数据存储中提取数据出来,从通讯端接收到一堆数据,这里的“提取”和“接收”都是以字节为单位的数据,所以为了“还原”成原来的类型变量,就涉及“化零为整”的过程。在代码上,有两种常见的方式,一种是原始的“移位法”,另一种是极具优越性的“指针法”。比如,现在以“大端”方式为例(因为本教程是用C51编译器,C51编译器是“大端”方式),有一个数组Gu8BufferB存放了4个字节数据分别是:0x12,0x34,0x56,0x78。现在要把这4个字节数据“合并”成一个unsigned long类型的变量b,这个变量b等于0x12345678。现在跟大家分享和对比一下这两种方法。
(1)原始的“移位法”。
- unsigned char Gu8BufferB[4]={0x12,0x34,0x56,0x78};
- unsigned long b;
- b=Gu8BufferB[0];
- b=b<<8;
- b=b+Gu8BufferB[1];
- b=b<<8;
- b=b+Gu8BufferB[2];
- b=b<<8;
- b=b+Gu8BufferB[3];
复制代码
(2)极具优越性的“指针法”。
- unsigned char Gu8BufferB[4]={0x12,0x34,0x56,0x78};
- unsigned long b;
- unsigned long *pu32; //引入一个指针变量,注意,这里是unsigned long类型的指针。
- pu32=(unsigned long *)&Gu8BufferB[0]; //指针跟数组“绑定”(也称为“关联”)起来。
- b=*pu32; //这里仅仅1行代码就等效于上述(1)“移位”例子中的7行代码,所以极具优越性。
复制代码
【62.4 “指针法”要注意的问题。】
“化整为零”和“化零为整”其实是一个“互逆”的过程,在使用“指针法”的时候,一定要注意“大小端”的问题。“化整为零”和“化零为整”这两个“互逆”过程要么同时为“大端”,要么同时为“小端”,否则会因字节的排列顺序问题而引起数据的严重错误。
【62.5 例程练习和分析。】
现在编一个练习程序。
- /*---C语言学习区域的开始。-----------------------------------------------*/
- unsigned long a=0x12345678;
- unsigned char Gu8BufferA[4];
- unsigned char Gu8BufferB[4]={0x12,0x34,0x56,0x78};
- unsigned long b;
- unsigned long *pu32; //引入一个指针变量,注意,这里是unsigned long类型的指针。
- void main() //主函数
- {
- pu32=(unsigned long *)&Gu8BufferA[0]; //指针跟数组“绑定”(也称为“关联”)起来。
- *pu32=a; //化整为零
- pu32=(unsigned long *)&Gu8BufferB[0]; //指针跟数组“绑定”(也称为“关联”)起来。
- b=*pu32; //化零为整
- View(Gu8BufferA[0]); //把第1个数Gu8BufferA[0]发送到电脑端的串口助手软件上观察。
- View(Gu8BufferA[1]); //把第2个数Gu8BufferA[1]发送到电脑端的串口助手软件上观察。
- View(Gu8BufferA[2]); //把第3个数Gu8BufferA[2]发送到电脑端的串口助手软件上观察。
- View(Gu8BufferA[3]); //把第4个数Gu8BufferA[3]发送到电脑端的串口助手软件上观察。
- View(b); //把第5个数b发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:18
- 十六进制:12
- 二进制:10010
- 第2个数
- 十进制:52
- 十六进制:34
- 二进制:110100
- 第3个数
- 十进制:86
- 十六进制:56
- 二进制:1010110
- 第4个数
- 十进制:120
- 十六进制:78
- 二进制:1111000
- 第5个数
- 十进制:305419896
- 十六进制:12345678
- 二进制:10010001101000101011001111000
复制代码
分析:
Gu8BufferA[0]为0x12。
Gu8BufferA[1]为0x34。
Gu8BufferA[2]为0x56。
Gu8BufferA[3]为0x78。
b为0x12345678。
【62.6 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-4-2 10:16
本帖最后由 jianhong_wu 于 2017-4-2 10:34 编辑
第六十三节: 指针“化整为零”和“化零为整”的“灵活”应用。
第六十三节_pdf文件.pdf
(70.92 KB, 下载次数: 1588)
【63.1 化整为零的“灵活”应用。】
上一节讲“化整为零”的例子,指针是跟数组的首地址(下标是0)“绑定”的,这样,很多初学者就误以为指针跟数组“绑定”时,只能跟数组的“首地址”关联。其实,指针是可以跟数组的任何一个成员的地址“绑定”(只要不超过数组的长度导致越界),它不仅仅局限于首地址,指针的这个特征就是本节标题所说的“灵活”。请看下面这个例子:
有3个变量,分别是单字节unsigned char a,双字节unsigned int b,四字节unsigned long c,它们加起来一共有7个字节,要把这7个字节放到一个7字节容量的数组里。除了用传统的“移位法”,还有一种更加便捷的“指针法”,代码如下:
- unsigned char a=0x01;
- unsigned int b=0x0203;
- unsigned long c=0x04050607;
- unsigned char Gu8BufferABC[7]; //存放3个不同长度变量的数组
- unsigned char *pu8; //引入的unsigned char 类型指针
- unsigned int *pu16; //引入的unsigned int 类型指针
- unsigned long *pu32; //引入的unsigned long 类型指针
- pu8=&Gu8BufferABC[0]; //指针跟数组的第0个位置“绑定”起来。
- *pu8=a; //把a的1个字节放在数组第0个位置。
- pu16=(unsigned int *)&Gu8BufferABC[1]; //指针跟数组的第1个位置“绑定”起来。
- *pu16=b; //把b的2个字节放在数组第1、2这两个位置。
- pu32=(unsigned long *)&Gu8BufferABC[3]; //指针跟数组的第3个位置“绑定”起来。
- *pu32=c; //把c的4个字节放在数组第3、4、5、6这四个位置。
复制代码
【63.2 化零为整的“灵活”应用。】
刚才讲的是“化整为零”,现在讲的是“化零为整”。刚才讲的是“分解”,现在讲的是“合成”。请看下面这个例子:
有一个容量为7字节数组,第0字节存放的是unsigned char d变量,第1、2字节存放的是unsigned int e变量,第3、4、5、6字节存放的是unsigned long f变量,现在要从数组中“零散”的字节里提取并且合成为“完整”的3个变量。代码如下:
- unsigned char Gu8BufferDEF[7]={0x01,0x02,0x03,0x04,0x05,0x06,0x07}; //注意大小端的问题
- unsigned char d;
- unsigned int e;
- unsigned long f;
- unsigned char *pu8; //引入的unsigned char 类型指针
- unsigned int *pu16; //引入的unsigned int 类型指针
- unsigned long *pu32; //引入的unsigned long 类型指针
- pu8=&Gu8BufferDEF[0]; //指针跟数组的第0个位置“绑定”起来。
- d=*pu8; //从数组第0位置提取单字节完整的d变量。
- pu16=(unsigned int *)&Gu8BufferDEF[1]; //指针跟数组的第1个位置“绑定”起来。
- e=*pu16; //从数组第1,2位置提取双字节完整的e变量。
- pu32=(unsigned long *)&Gu8BufferDEF[3]; //指针跟数组的第3个位置“绑定”起来。
- f=*pu32; //从数组第3,4,5,6位置提取四字节完整的f变量。
复制代码
【63.3 例程练习和分析。】
现在编一个练习程序。
- /*---C语言学习区域的开始。-----------------------------------------------*/
- unsigned char a=0x01;
- unsigned int b=0x0203;
- unsigned long c=0x04050607;
- unsigned char Gu8BufferABC[7]; //存放3个不同长度变量的数组
- unsigned char Gu8BufferDEF[7]={0x01,0x02,0x03,0x04,0x05,0x06,0x07}; //注意大小端的问题
- unsigned char d;
- unsigned int e;
- unsigned long f;
- unsigned char *pu8; //引入的unsigned char 类型指针
- unsigned int *pu16; //引入的unsigned int 类型指针
- unsigned long *pu32; //引入的unsigned long 类型指针
- void main() //主函数
- {
- //第1类例子:化整为零。
- pu8=&Gu8BufferABC[0]; //指针跟数组的第0个位置“绑定”起来。
- *pu8=a; //把a的1个字节放在数组第0个位置。
- pu16=(unsigned int *)&Gu8BufferABC[1]; //指针跟数组的第1个位置“绑定”起来。
- *pu16=b; //把b的2个字节放在数组第1、2这两个位置。
- pu32=(unsigned long *)&Gu8BufferABC[3]; //指针跟数组的第3个位置“绑定”起来。
- *pu32=c; //把c的4个字节放在数组第3、4、5、6这四个位置。
- //第2类例子:化零为整。
- pu8=&Gu8BufferDEF[0]; //指针跟数组的第0个位置“绑定”起来。
- d=*pu8; //从数组第0位置提取单字节完整的d变量。
- pu16=(unsigned int *)&Gu8BufferDEF[1]; //指针跟数组的第1个位置“绑定”起来。
- e=*pu16; //从数组第1,2位置提取双字节完整的e变量。
- pu32=(unsigned long *)&Gu8BufferDEF[3]; //指针跟数组的第3个位置“绑定”起来。
- f=*pu32; //从数组第3,4,5,6位置提取四字节完整的f变量。
- View(Gu8BufferABC[0]); //把第1个数Gu8BufferABC[0]发送到电脑端的串口助手软件上观察。
- View(Gu8BufferABC[1]); //把第2个数Gu8BufferABC[1]发送到电脑端的串口助手软件上观察。
- View(Gu8BufferABC[2]); //把第3个数Gu8BufferABC[2]发送到电脑端的串口助手软件上观察。
- View(Gu8BufferABC[3]); //把第4个数Gu8BufferABC[3]发送到电脑端的串口助手软件上观察。
- View(Gu8BufferABC[4]); //把第5个数Gu8BufferABC[4]发送到电脑端的串口助手软件上观察。
- View(Gu8BufferABC[5]); //把第6个数Gu8BufferABC[5]发送到电脑端的串口助手软件上观察。
- View(Gu8BufferABC[6]); //把第7个数Gu8BufferABC[6]发送到电脑端的串口助手软件上观察。
- View(d); //把第8个数d发送到电脑端的串口助手软件上观察。
- View(e); //把第9个数e发送到电脑端的串口助手软件上观察。
- View(f); //把第10个数f发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第2个数
- 十进制:2
- 十六进制:2
- 二进制:10
- 第3个数
- 十进制:3
- 十六进制:3
- 二进制:11
- 第4个数
- 十进制:4
- 十六进制:4
- 二进制:100
- 第5个数
- 十进制:5
- 十六进制:5
- 二进制:101
- 第6个数
- 十进制:6
- 十六进制:6
- 二进制:110
- 第7个数
- 十进制:7
- 十六进制:7
- 二进制:111
- 第8个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第9个数
- 十进制:515
- 十六进制:203
- 二进制:1000000011
- 第:个数(这里是第10个数。本模块程序只支持显示第1到第9个,所以这里没有显示“10”)
- 十进制:67438087
- 十六进制:4050607
- 二进制:100000001010000011000000111
复制代码
分析:
Gu8BufferABC[0]为0x01。
Gu8BufferABC[1]为0x02。
Gu8BufferABC[2]为0x03。
Gu8BufferABC[3]为0x04。
Gu8BufferABC[4]为0x05。
Gu8BufferABC[5]为0x06。
Gu8BufferABC[6]为0x07。
d为0x01。
e为0x0203。
f为0x04050607。
【63.4 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-4-9 17:59
本帖最后由 jianhong_wu 于 2017-4-9 18:11 编辑
第六十四节: 指针让函数具备了多个相当于return的输出口。
第六十四节_pdf文件.pdf
(68.56 KB, 下载次数: 1572)
【64.1 函数的三类输出渠道。】
函数是模块,模块必须具备输入和输出的接口,从输入和输出的角度分析,函数对外部调用者传递信息主要有三类渠道,第一类是全局变量,第二类是return返回值,第三类是用指针。全局变量太隐蔽,没有那么直观,可读性稍差。return可读性强,缺点是一个函数只能有一个return,如果一个函数要输出多个结果,return就力不从心。指针作为函数的输出接口,就能随心所欲了,不但可读性强,而且输出的接口数量不受限制。
【64.2 只有一个输出接口的时候。】
现在举一个例子,要用函数实现一个加法运算,输出“一个”加法运算的和,求3加上5等于8。下面三个例子中分别使用“全局变量,return,指针”这三类输出接口。
第一类:全局变量。
- unsigned char DiaoYongZhe; //调用者
- unsigned char BeiJiaShu; //被加数
- unsigned char JiaShu; //加数
- unsigned char He; //输出的接口,加法运算的"和"。
- void JiaFa(void)
- {
- He=BeiJiaShu+JiaShu;
- }
- void main()
- {
- BeiJiaShu=3; //填入被加数3
- JiaShu=5; //填入加数5
- JiaFa(); //调用一次加法运算的函数
- DiaoYongZhe=He; //把加法运算的“和”赋值给调用者。
- }
复制代码
第二类:return。
- unsigned char DiaoYongZhe; //调用者
- unsigned char JiaFa(unsigned char BeiJiaShu,unsigned char JiaShu)
- {
- unsigned char He;
- He=BeiJiaShu+JiaShu;
- return He;
- }
- void main()
- {
- DiaoYongZhe=JiaFa(3,5); //把加法运算的“和”赋值给调用者,一气呵成。
- }
复制代码
第三类:指针。
- unsigned char DiaoYongZhe; //调用者
- void char JiaFa(unsigned char BeiJiaShu,unsigned char JiaShu,unsigned char *pu8He)
- {
- *pu8He=BeiJiaShu+JiaShu;
- }
- void main()
- {
- JiaFa(3,5,&DiaoYongZhe); //通过指针这个输出渠道,把加法运算的“和”赋值给调用者,一气呵成。
- }
复制代码
【64.3 有多个输出接口的时候。】
现在举一个例子,要用函数实现一个除法运算,分别输出除法运算的商和余数这“两个”数,求5除以3等于1余2。因为return只能输出一个结果,所以这里不列举return的例子,只使用“全局变量”和“指针”这两类输出接口。
第一类:全局变量。
- unsigned char DiaoYongZhe_Shang; //调用者的商
- unsigned char DiaoYongZhe_Yu; //调用者的余数
- unsigned char BeiChuShu; //被除数
- unsigned char ChuShu; //除数
- unsigned char Shang; //输出的接口,除法运算的"商"。
- unsigned char Yu; //输出的接口,除法运算的"余"。
- void ChuFa(void)
- {
- Shang=BeiChuShu/ChuShu; //求商。假设除数不会为0的情况。
- Yu=BeiChuShu%ChuShu; //求余数。假设除数不会为0的情况。
- }
- void main()
- {
- BeiChuShu=5; //填入被除数5
- ChuShu=3; //填入除数3
- ChuFa(); //调用一次除法运算的函数
- DiaoYongZhe_Shang=Shang; //把除法运算的“商”赋值给调用者的商。
- DiaoYongZhe_Yu=Yu; //把除法运算的“余数”赋值给调用者的余数。
- }
复制代码
第二类:return。
return只能输出一个结果,力不从心,所以这里不列举return的例子。
第三类:指针。
- unsigned char DiaoYongZhe_Shang; //调用者的商
- unsigned char DiaoYongZhe_Yu; //调用者的余数
- void ChuFa(unsigned char BeiChuShu,
- unsigned char ChuShu,
- unsigned char *pu8Shang,
- unsigned char *pu8Yu)
- {
- *pu8Shang=BeiChuShu/ChuShu; //求商。假设除数不会为0的情况。
- *pu8Yu=BeiChuShu%ChuShu; //求余数。假设除数不会为0的情况。
- }
- void main()
- {
- ChuFa(5,3,&DiaoYongZhe_Shang,&DiaoYongZhe_Yu);//通过两个指针的输出接口,一气呵成。
- }
复制代码
【64.4 例程练习和分析。】
现在编一个练习程序。
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void ChuFa(unsigned char BeiChuShu,
- unsigned char ChuShu,
- unsigned char *pu8Shang,
- unsigned char *pu8Yu); //函数声明
- unsigned char DiaoYongZhe_Shang; //调用者的商
- unsigned char DiaoYongZhe_Yu; //调用者的余数
- void ChuFa(unsigned char BeiChuShu,
- unsigned char ChuShu,
- unsigned char *pu8Shang,
- unsigned char *pu8Yu) //函数定义
- {
- *pu8Shang=BeiChuShu/ChuShu; //求商。假设除数不会为0的情况。
- *pu8Yu=BeiChuShu%ChuShu; //求余数。假设除数不会为0的情况。
- }
- void main() //主函数
- {
- ChuFa(5,3,&DiaoYongZhe_Shang,&DiaoYongZhe_Yu);//函数调用。通过两个指针的输出接口,一气呵成。
- View(DiaoYongZhe_Shang); //把第1个数DiaoYongZhe_Shang发送到电脑端的串口助手软件上观察。
- View(DiaoYongZhe_Yu); //把第2个数DiaoYongZhe_Yu发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第2个数
- 十进制:2
- 十六进制:2
- 二进制:10
复制代码
分析:
DiaoYongZhe_Shang为1。
DiaoYongZhe_Yu为2。
【64.5 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-4-16 11:37
本帖最后由 jianhong_wu 于 2017-4-16 12:03 编辑
第六十五节: 指针作为数组在函数中的入口作用。
第六十五节_pdf文件.pdf
(72.58 KB, 下载次数: 1623)
【65.1 函数的参数入口。】
要往函数内部传递信息,主要有两类渠道。第一类是全局变量。第二类是函数的参数入口,而参数入口可以分为“普通局部变量”和“指针”这两类。“普通局部变量”的参数入口一次只能传一个数据,如果一个数组有几十个甚至上百个数据,此时“普通局部变量”就无能为力,这时不可能也写几十个甚至上百个入口参数吧(这会累坏程序员),针对这种需要输入批量数据的场合,“指针”的参数入口就因此而生,完美解决了此问题,仅用一个“指针”参数入口就能解决一个数组N个数据的入口问题。那么,什么是函数的参数入口?例子如下:
- //函数声明
- unsigned long PinJunZhi(unsigned char a,unsigned char b,unsigned char c,unsigned char d);
- //变量定义
- unsigned char Gu8Buffer[4]={2,6,8,4}; //4个变量分别是2,6,8,4。
- unsigned long Gu32PinJunZhi; //求平均值的结果
- //函数定义
- unsigned long PinJunZhi(unsigned char a,unsigned char b,unsigned char c,unsigned char d)
- {
- unsigned long u32PinJunZhi;
- u32PinJunZhi=(a+b+c+d)/4;
- return u32PinJunZhi;
- }
- void main() //主函数
- {
- //函数调用
- Gu32PinJunZhi=PinJunZhi(Gu8Buffer[0],Gu8Buffer[1],Gu8Buffer[2],Gu8Buffer[3]);
- }
复制代码
上面是一个求4个数据平均值的函数,在这个函数中,函数小括号的(unsigned char a,unsigned char b,unsigned char c,unsigned char d)就是4个变量的“普通局部变量”参数入口,刚才说到,如果一个数组有上百个变量,这种书写方式是很累的。如果改用“指针”入口参数的方式,例子如下:
- //函数声明
- unsigned long PinJunZhi(unsigned char *pu8Buffer);
- //变量定义
- unsigned char Gu8Buffer[4]={2,6,8,4}; //4个变量分别是2,6,8,4。
- unsigned long Gu32PinJunZhi; //求平均值的结果
- //函数定义
- unsigned long PinJunZhi(unsigned char *pu8Buffer)
- {
- unsigned long u32PinJunZhi;
- u32PinJunZhi=(pu8Buffer[0]+pu8Buffer[1]+pu8Buffer[2]+pu8Buffer[3])/4;
- return u32PinJunZhi;
- }
- void main() //主函数
- {
- //函数调用
- Gu32PinJunZhi=PinJunZhi(&Gu8Buffer[0]);//也等效于Gu32PinJunZhi=PinJunZhi(Gu8Buffer)
- }
复制代码
上面例子中,仅用一个(unsigned char *pu8Buffer)指针入口参数,就可以达到输入4个变量的目的(这4个变量要求是同在一个数组内)。
【65.2 const在指针参数“入口”中的作用。】
指针在函数的参数入口中,既可以做“入口”,也可以做“出口”,而C语言为了区分这两种情况,提供了const这个关键字来限定权限。如果指针加了const前缀,就为指针的权限加了紧箍咒,限定了此指针只能作为“入口”,而不能作为“出口”。如果没有加了const前缀,就像本节的函数例子,此时指针参数既可以作为“入口”,也可以作为“出口”。加const关键字有两个意义,一方面是方便阅读,通过const就知道此接口的“入口”和“出口”属性,另一方面,是为了代码的安全,对于只能作为“入口”的指针参数一旦加了const限定,万一我们不小心在函数内部对const限定的指针所关联的数据进行了更改(“更改”就意味着“出口”),C编译器在编译的时候就会有提醒或者报错,及时让我们发现程序的bug(程序的漏洞)。这部分的内容后续章节会讲到,大家先有个大概的了解,本节暂时不深入讲。
【65.3 例程练习和分析。】
现在编一个练习程序。
- /*---C语言学习区域的开始。-----------------------------------------------*/
- //函数声明
- unsigned long PinJunZhi(unsigned char *pu8Buffer);
- //变量定义
- unsigned char Gu8Buffer[4]={2,6,8,4}; //4个变量分别是2,6,8,4。
- unsigned long Gu32PinJunZhi; //求平均值的结果
- //函数定义
- unsigned long PinJunZhi(unsigned char *pu8Buffer)
- {
- unsigned long u32PinJunZhi;
- u32PinJunZhi=(pu8Buffer[0]+pu8Buffer[1]+pu8Buffer[2]+pu8Buffer[3])/4;
- return u32PinJunZhi;
- }
- void main() //主函数
- {
- //函数调用
- Gu32PinJunZhi=PinJunZhi(&Gu8Buffer[0]);//也等效于Gu32PinJunZhi=PinJunZhi(Gu8Buffer)
- View(Gu32PinJunZhi); //把第1个数Gu32PinJunZhi发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:5
- 十六进制:5
- 二进制:101
复制代码
分析:
平均值变量Gu32PinJunZhi为5。
【65.4 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-4-23 09:46
本帖最后由 jianhong_wu 于 2017-4-23 10:07 编辑
第六十六节: 指针作为数组在函数中的出口作用。
第六十六节_pdf文件.pdf
(61.73 KB, 下载次数: 1448)
【66.1 指针作为数组在函数中的出口。】
函数对外部调用者传递信息主要有三类渠道,第一类是全局变量,第二类是return返回值,第三类是指针。之前讲指针对外传递信息的时候,只讲了单个变量的情况,现在重点讲讲数组的情况。要把一个四位数的个,十,百,千位分别提取出来成为4个数,依次存放在一个包含4个字节的数组里,代码如下:
- void TiQu(unsigned int u16Data,unsigned char *pu8Buffer) //“提取”函数
- {
- unsigned char u8Ge; //个位
- unsigned char u8Shi; //十位
- unsigned char u8Bai; //百位
- unsigned char u8Qian; //千位
- u8Ge=u16Data/1%10; //提取个位
- u8Shi=u16Data/10%10; //提取十位
- u8Bai=u16Data/100%10; //提取百位
- u8Qian=u16Data/1000%10; //提取千位
- //最后,把所提取的数分别传输到“指针”这个“出口通道”
- pu8Buffer[0]=u8Ge;
- pu8Buffer[1]=u8Shi;
- pu8Buffer[2]=u8Bai;
- pu8Buffer[3]=u8Qian;
- }
复制代码
上述代码,为了突出“出口通道”,我刻意多增加了u8Ge、u8Shi、u8Bai、u8Qian这4个局部变量,其实,这4个局部变量还可以省略的,此函数简化后的等效代码如下:
- void TiQu(unsigned int u16Data,unsigned char *pu8Buffer) //“提取”函数
- {
- pu8Buffer[0]=u16Data/1%10; //提取个位
- pu8Buffer[1]=u16Data/10%10; //提取十位
- pu8Buffer[2]=u16Data/100%10; //提取百位
- pu8Buffer[3]=u16Data/1000%10; //提取千位
- }
复制代码
【66.2 例程练习和分析。】
现在编一个练习程序。
- /*---C语言学习区域的开始。-----------------------------------------------*/
- //函数声明
- void TiQu(unsigned int u16Data,unsigned char *pu8Buffer);
- //全局变量定义
- unsigned char Gu8Buffer[4]; //存放提取结果的数组
- //函数定义
- void TiQu(unsigned int u16Data,unsigned char *pu8Buffer) //“提取”函数
- {
- pu8Buffer[0]=u16Data/1%10; //提取个位
- pu8Buffer[1]=u16Data/10%10; //提取十位
- pu8Buffer[2]=u16Data/100%10; //提取百位
- pu8Buffer[3]=u16Data/1000%10; //提取千位
- }
- void main() //主函数
- {
- TiQu(9876,&Gu8Buffer[0]); //把9876这个四位数分别提取6、7、8、9存放在数组Gu8Buffer里
- View(Gu8Buffer[0]); //把第1个数Gu8Buffer[0])发送到电脑端的串口助手软件上观察
- View(Gu8Buffer[1]); //把第2个数Gu8Buffer[1])发送到电脑端的串口助手软件上观察
- View(Gu8Buffer[2]); //把第3个数Gu8Buffer[2])发送到电脑端的串口助手软件上观察
- View(Gu8Buffer[3]); //把第4个数Gu8Buffer[3])发送到电脑端的串口助手软件上观察
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:6
- 十六进制:6
- 二进制:110
- 第2个数
- 十进制:7
- 十六进制:7
- 二进制:111
- 第3个数
- 十进制:8
- 十六进制:8
- 二进制:1000
- 第4个数
- 十进制:9
- 十六进制:9
- 二进制:1001
复制代码
分析:
Gu8Buffer[0]为6。
Gu8Buffer[1]为7。
Gu8Buffer[2]为8。
Gu8Buffer[3]为9。
【66.3 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-4-30 08:39
本帖最后由 jianhong_wu 于 2017-4-30 08:57 编辑
第六十七节: 指针作为数组在函数中既“入口”又“出口”的作用。
第六十七节_pdf文件.pdf
(63.51 KB, 下载次数: 1361)
【67.1 指针作为数组在函数中的“入口”和“出口”。】
前面分别讲了指针的入口和出口,很多初学者误以为指针是一个“单向”的通道,其实,如果指针前面没有加const这个“紧箍咒”限定它的属性,指针是“双向”的,不是“单向”的,也就是说,指针是可以同时具备“入口”和“出口”这两种属性的。现在讲一个程序例子,求一个数组(内含4元素)的每个元素变量的整数倍的一半,所谓整数倍的一半,就是除以2,但是不带小数点,比如4的整数倍的一半是2,7的整数倍的一半是3(不是3.5),代码如下:
- void Half(unsigned char *pu8Buffer) //“求一半”的函数
- {
- unsigned char u8Data_0; //临时中间变量
- unsigned char u8Data_1; //临时中间变量
- unsigned char u8Data_2; //临时中间变量
- unsigned char u8Data_3; //临时中间变量
- //从指针这个“入口”里获取需要“被除以2”的数据。
- u8Data_0=pu8Buffer[0];
- u8Data_1=pu8Buffer[1];
- u8Data_2=pu8Buffer[2];
- u8Data_3=pu8Buffer[3];
- //求数据的整数倍的一半的算法
- u8Data_0=u8Data_0/2;
- u8Data_1=u8Data_1/2;
- u8Data_2=u8Data_2/2;
- u8Data_3=u8Data_3/2;
- //最后,把计算所得的结果分别传输到指针这个“出口”
- pu8Buffer[0]=u8Data_0;
- pu8Buffer[1]=u8Data_1;
- pu8Buffer[2]=u8Data_2;
- pu8Buffer[3]=u8Data_3;
- }
复制代码
上述代码,为了突出“入口”和“出口”,我刻意多增加了u8Data_0,u8Data_1,u8Data_2,u8Data_3这4个临时中间变量,其实,这4个临时中间变量还可以省略的,此函数简化后的等效代码如下:
- void Half(unsigned char *pu8Buffer) //“求一半”的函数
- {
- pu8Buffer[0]=pu8Buffer[0]/2;
- pu8Buffer[1]=pu8Buffer[1]/2;
- pu8Buffer[2]=pu8Buffer[2]/2;
- pu8Buffer[3]=pu8Buffer[3]/2;
- }
复制代码
【67.2 例程练习和分析。】
现在编一个练习程序。
- /*---C语言学习区域的开始。-----------------------------------------------*/
- //函数声明
- void Half(unsigned char *pu8Buffer);
- //全局变量定义
- unsigned char Gu8Buffer[4]={4,7,16,25}; //需要“被除以2”的数组
- //函数定义
- void Half(unsigned char *pu8Buffer) //“求一半”的函数
- {
- pu8Buffer[0]=pu8Buffer[0]/2;
- pu8Buffer[1]=pu8Buffer[1]/2;
- pu8Buffer[2]=pu8Buffer[2]/2;
- pu8Buffer[3]=pu8Buffer[3]/2;
- }
- void main() //主函数
- {
- Half(&Gu8Buffer[0]); //计算数组的整数倍的一半。这里的“入口”和“出口”是“同一个通道”。
- View(Gu8Buffer[0]); //把第1个数Gu8Buffer[0])发送到电脑端的串口助手软件上观察
- View(Gu8Buffer[1]); //把第2个数Gu8Buffer[1])发送到电脑端的串口助手软件上观察
- View(Gu8Buffer[2]); //把第3个数Gu8Buffer[2])发送到电脑端的串口助手软件上观察
- View(Gu8Buffer[3]); //把第4个数Gu8Buffer[3])发送到电脑端的串口助手软件上观察
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:2
- 十六进制:2
- 二进制:10
- 第2个数
- 十进制:3
- 十六进制:3
- 二进制:11
- 第3个数
- 十进制:8
- 十六进制:8
- 二进制:1000
- 第4个数
- 十进制:12
- 十六进制:C
- 二进制:1100
复制代码
分析:
Gu8Buffer[0]为2。
Gu8Buffer[1]为3。
Gu8Buffer[2]为8。
Gu8Buffer[3]为12。
【67.3 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-5-7 10:24
本帖最后由 jianhong_wu 于 2017-5-7 10:39 编辑
第六十八节: 为函数接口指针“定向”的const关键词。
第六十八节_pdf文件.pdf
(70.79 KB, 下载次数: 1303)
【68.1 为函数接口指针“定向”的const关键词。】
在函数接口处的指针,是一个双向口,既可以作为“输入”也可以作为“输出”,换句话说,既能“读”也能“写”(被更改),这样一来,当你把一个数组(或者某变量)通过指针引入到函数内部的时候,当执行完此函数,这个数组的数值可能已经悄悄发生了更改(“是否被更改”取决于函数内部的具体代码),进来时是“摩托”出来后可能已变成“单车”,而实际项目上,很多时候我们只想传递数组(或者某变量)的数值,并不想数组(或者某变量)本身发生变化,这个时候,本节的主角const关键词就派上用场了。
只要在函数接口的指针前面加上const关键词,原来双向的指针就立刻变成了单向,只能输入不能输出。这个const有两个好处。第一个好处是方便阅读,通过const就知道此接口的“入口”和“出口”属性,如果你是用别人已经封装好的函数,一旦发现接口指针带了const标签,就足以说明这个指针只能作为输入接口,不用担心输入数据被意外修改。第二个好处是确保数据的安全,函数接口指针一旦加了const限定,万一你不小心在函数内部对指针所关联的数据进行了更改(“更改”就意味着“出口”),C编译器在编译的时候就会报错让你编译失败,及时让你发现程序的bug(程序的漏洞),这是编译器层面的一道防火墙。例子如下:
- unsigned char ShuRu(const unsigned char *pu8Data)
- {
- unsigned char a;
- a=*pu8Data; //这行代码是合法的,是指针所关联数据的“读”操作。
- *pu8Data=a; //这行代码是非法的,是指针所关联数据的“写”操作,违背const的约束。
- return a;
- }
复制代码
【68.2 例程练习和分析。】
在前面第65节讲函数入口的时候,用到一个求数组平均值的程序例子,这个数组是仅仅作为输入用的,不需要被更改,因此,现在借本节讲const的机会,为此函数的接口指针补上一个const关键词,让该函数更加科学规范,程序如下:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- unsigned long PinJunZhi(const unsigned char *pu8Buffer); //指针前增加一个const关键词
- unsigned char Gu8Buffer[4]={2,6,8,4};
- unsigned long Gu32PinJunZhi;
- unsigned long PinJunZhi(const unsigned char *pu8Buffer) //指针前增加一个const关键词
- {
- unsigned long u32PinJunZhi;
- u32PinJunZhi=(pu8Buffer[0]+pu8Buffer[1]+pu8Buffer[2]+pu8Buffer[3])/4; //求平均值
- return u32PinJunZhi;
- }
- void main() //主函数
- {
- Gu32PinJunZhi=PinJunZhi(&Gu8Buffer[0]);//不用担心Gu8Buffer数组的数据被意外更改。
- View(Gu32PinJunZhi); //把第1个数Gu32PinJunZhi发送到电脑端的串口助手软件上观察。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:5
- 十六进制:5
- 二进制:101
复制代码
分析:
平均值变量Gu32PinJunZhi为5。
【68.3 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-5-14 10:16
本帖最后由 jianhong_wu 于 2017-5-14 10:34 编辑
第六十九节: 宏函数sizeof()。
第六十九节_pdf文件.pdf
(74.22 KB, 下载次数: 1320)
【69.1 宏函数sizeof()的基础知识。】
宏函数sizeof()是用来获取某个对象所占用的字节数。既然是“宏”,就说明它不是单片机执行的函数,而是单片机之外的C编译器执行的函数(像#define这类宏语句一样),也就是说,在单片机上电之前,C编译器在电脑端翻译我们的C语言程序的时候,一旦发现了这个宏函数sizeof,它就会在电脑端根据C语言程序的一些关键字符(比如“unsigned char,[,]”这类字符)来自动计算这个对象所占用的字节数,然后再把我们C语言程序里所有的sizeof字符替换等效成一个“常量数字”,1代表1个字节,5代表5个字节,1000代表1000个字节。所谓在单片机之外执行的宏函数,就是说,在“计算”这些对象所占的字节数的时候,这个“计算”的工作只占用电脑的内存(C编译器是在电脑上运行的),并不占用单片机的ROM容量和内存。而其它在单片机端执行的“非宏”函数,是占用单片机的ROM容量和内存。比如:
- unsgiend char a; //变量。占用1个字节
- unsgiend int b; //变量。占用2个字节
- unsgiend long c; //变量。占用4个字节
- code unsgiend char d[9]; //常量。占用9个字节
- unsigned int Gu16GetBytes; //这个变量用来获取字节数
- Gu16GetBytes=sizeof(a); //单片机上电后,在单片机程序里等效于Gu16GetBytes=1;
- Gu16GetBytes=sizeof(b); //单片机上电后,在单片机程序里等效于Gu16GetBytes=2;
- Gu16GetBytes=sizeof(c); //单片机上电后,在单片机程序里等效于Gu16GetBytes=4;
- Gu16GetBytes=sizeof(d); //单片机上电后,在单片机程序里等效于Gu16GetBytes=9;
复制代码
上述的“sizeof字符”在进入到单片机的层面的时候,已经被编译器预先替换成对应的“常量数字”的,这个“常量数字”就代表所占用的字节数。
【69.2 宏函数sizeof()的作用。】
在项目中,通常用在两个方面:一方面是用在求一个数组的大小尺寸,另一方面是用在计算内存分配时候的偏移量。当然,sizeof并不是“刚需”,如果没有sizeof宏函数,我们也可以人工计算出一个对象所占用的字节数,只是,人工计算,一方面容易出错,另一方面代码往往“动一发而牵全身”,改一个变量往往就会涉及很多地方需要配合调整更改,没法做到“自由裁剪”的境界。下面举一个程序例子:要把3个不同长度的数组“合并”成1个数组。
第一种情况:在没有使用sizeof宏函数时,人工计算字节数和偏移量:
- unsigned char a[2]={1,2}; //占用2个字节
- unsigned char b[3]={3,4,5}; //占用3个字节
- unsigned char c[4]={6,7,8,9}; //占用4个字节
- unsigned char HeBing[9];//合并a,b,c在一起的数组。这里的9是人工计算a,b,c容量累加所得。
- unsigned char i; //循环变量i
- for(i=0;i<2;i++) //这里的2,是人工计算出a占用2个字节
- {
- HeBing[i+0]=a[i]; //从HeBing数组的偏移量第0个地址开始存放。
- }
- for(i=0;i<3;i++) //这里的3,是人工计算出b占用3个字节
- {
- HeBing[i+2]=b[i]; //这里的2是人工计算出的偏移量。a占用了数组2个字节。
- }
- for(i=0;i<4;i++) //这里的4,是人工计算出c占用4个字节
- {
- HeBing[i+2+3]=c[i]; //这里的2和3是人工计算出的偏移量,a和b占用了数组2+3个字节。
- }
复制代码
第二种情况:在使用sizeof宏函数时,利用C编译器自动来计算字节数和偏移量:
- unsigned char a[2]={1,2}; //占用2个字节
- unsigned char b[3]={3,4,5}; //占用3个字节
- unsigned char c[4]={6,7,8,9}; //占用4个字节
- unsigned char HeBing[sizeof(a)+sizeof(b)+sizeof(c)];//C编译器自动计算字节数
- unsigned char i;
- for(i=0;i<sizeof(a);i++) //C编译器自动计算字节数
- {
- HeBing[i+0]=a[i];
- }
- for(i=0;i<sizeof(b);i++) //C编译器自动计算字节数
- {
- HeBing[i+sizeof(a)]=b[i]; //C编译器自动计算偏移量
- }
- for(i=0;i<sizeof(c);i++) //C编译器自动计算字节数
- {
- HeBing[i+sizeof(a)+sizeof(b)]=c[i]; //C编译器自动计算偏移量
- }
复制代码
【69.3 例程练习和分析。】
现在编写一个练习的程序:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- unsigned char a[2]={1,2}; //占用2个字节
- unsigned char b[3]={3,4,5}; //占用3个字节
- unsigned char c[4]={6,7,8,9}; //占用4个字节
- unsigned char HeBing[sizeof(a)+sizeof(b)+sizeof(c)];//C编译器自动计算字节数
- unsigned char i;
- void main() //主函数
- {
- for(i=0;i<sizeof(a);i++) //C编译器自动计算字节数
- {
- HeBing[i+0]=a[i];
- }
- for(i=0;i<sizeof(b);i++) //C编译器自动计算字节数
- {
- HeBing[i+sizeof(a)]=b[i]; //C编译器自动计算偏移量
- }
- for(i=0;i<sizeof(c);i++) //C编译器自动计算字节数
- {
- HeBing[i+sizeof(a)+sizeof(b)]=c[i]; //C编译器自动计算偏移量
- }
- for(i=0;i<sizeof(HeBing);i++) //利用宏sizeof计算出HeBing数组所占用的字节数
- {
- View(HeBing[i]); //把HeBing所有数据挨个依次全部发送到电脑端观察
- }
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
开始...
第1个数
十进制:1
十六进制:1
二进制:1
第2个数
十进制:2
十六进制:2
二进制:10
第3个数
十进制:3
十六进制:3
二进制:11
第4个数
十进制:4
十六进制:4
二进制:100
第5个数
十进制:5
十六进制:5
二进制:101
第6个数
十进制:6
十六进制:6
二进制:110
第7个数
十进制:7
十六进制:7
二进制:111
第8个数
十进制:8
十六进制:8
二进制:1000
第9个数
十进制:9
十六进制:9
二进制:1001
分析:
HeBing[0]为1。
HeBing[1]为2。
HeBing[2]为3。
HeBing[3]为4。
HeBing[4]为5。
HeBing[5]为6。
HeBing[6]为7。
HeBing[7]为8。
HeBing[8]为9。
【69.4 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-5-21 10:14
本帖最后由 jianhong_wu 于 2017-5-21 10:24 编辑
第七十节: “万能数组”的结构体。
第七十节_pdf文件.pdf
(76.58 KB, 下载次数: 1165)
【70.1 结构体与数组。】
结构体是数组,但不是普通的数组,而是一种“万能数组”。普通数组,是依靠严格的数组下标(类似编号)来识别某个具体单元的(或者称“寻址”),期间,如果要往数组插入或者删除某些单元,后面所有单元的下标编号都会发生改变,牵一发而动全身,后面其它单元的下标序号自动重新排列,原来某个特定的单元的下标发生了改变,也就意味着“名字”发生了改变,这种情况在编写程序的时候,就意味着很多代码需要随着更改调整,给程序员带来很多不便。怎么办?结构体此时横空出世,扭转了这种“不便”的局面。之所以称结构体为“万能数组”,是因为结构体内部没有“下标编号”,只有名字。结构体与普通数组的本质区别是,结构体是靠“名字”来寻址的,不管你往结构体里插入或者删除某些单元,其它单元的“名字”不会发生改变,隔离效果好,左邻右舍不会受影响。除此之外,结构体内部的成员变量是允许出现不同的数据类型的,比如unsigned char,unsigned int,unsigned long这三种数据类型的变量都可以往同一个结构体里面“填充”,不受类型的局限,真正做到“万能”级。而普通数组就没有这个优越性,普通数组要么清一色都是unsigned char,要么清一色都是unsigned int,要么清一色都是unsigned long,不能像结构体这么“混合型”的。结构体的这种优越性,在大型程序的升级和维护时体现得非常明显。
【70.2 “造模”和“生成”和“调用”。】
结构体的使用,有三道标准工序“造模”和“生成”和“调用”。塑胶外壳,必须先开模具(造模),然后再用模具印出外壳(生成),再把外壳应用于日常生活中(调用)。结构体也一样,先“造”结构体的“模”(造模),再根据这个“模”来“生成”一个结构体变量(生成),然后在某函数里使用此变量(调用)。例子如下:
- struct StructMould //“造模”
- {
- unsigned char u8Data_A;
- unsigned int u16Data_B;
- unsigned long u32Data_C;
- };
- struct StructMould GtMould; //“生成”一个变量GtMould。
- void main()
- {
- GtMould.u8Data_A=1; //依靠成员的“名字”来“调用”
- GtMould.u16Data_B=2; //依靠成员的“名字”来“调用”
- GtMould.u32Data_C=3; //依靠成员的“名字”来“调用”
- while(1)
- {
- }
- }
复制代码
把上述程序转换成“普通数组”和“指针”的形式,给大家一个直观的对比,代码如下:
- unsigned char Gu8MouldBuffer[7]; //相当于结构体变量GtMould
- unsigned char *pu8Data_A;
- unsigned int *pu16Data_B;
- unsigned long *pu32Data_C;
- void main()
- {
- pu8Data_A=(unsigned char *)&Gu8MouldBuffer[0]; //依靠数组的下标[0]来“调用”
- *pu8Data_A=1;
- pu16Data_B=(unsigned int *)&Gu8MouldBuffer[1]; //依靠数组的下标[1]来“调用”
- *pu16Data_B=2;
- pu32Data_C=(unsigned long *)&Gu8MouldBuffer[3]; //依靠数组的下标[3]来“调用”
- *pu32Data_C=3;
- while(1)
- {
- }
- }
复制代码
分析:上述两种代码,目标都是把1,2,3这三个数字存放在一个数组里。第一种用结构体的方式,第二种用普通数组的方式。
【70.3 例程练习和分析。】
现在编写一个练习的程序:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- struct StructMould //“造模”
- {
- unsigned char u8Data_A;
- unsigned int u16Data_B;
- unsigned long u32Data_C;
- };
- struct StructMould GtMould; //“生成”一个变量GtMould。
- void main() //主函数
- {
- GtMould.u8Data_A=1; //依靠成员的“名字”来“调用”
- GtMould.u16Data_B=2; //依靠成员的“名字”来“调用”
- GtMould.u32Data_C=3; //依靠成员的“名字”来“调用”
- View(GtMould.u8Data_A); //把结构体成员GtMould.u8Data_A发送到电脑端观察
- View(GtMould.u16Data_B); //把结构体成员GtMould.u16Data_B发送到电脑端观察
- View(GtMould.u32Data_C); //把结构体成员GtMould.u32Data_C发送到电脑端观察
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第2个数
- 十进制:2
- 十六进制:2
- 二进制:10
- 第3个数
- 十进制:3
- 十六进制:3
- 二进制:11
复制代码
分析:
GtMould.u8Data_A为1。
GtMould.u16Data_B为2。
GtMould.u32Data_C为3。
【70.4 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-5-29 11:10
本帖最后由 jianhong_wu 于 2017-5-29 11:33 编辑
第七十一节: 结构体的内存和赋值。
第七十一节_pdf文件.pdf
(102.98 KB, 下载次数: 1174)
【71.1 结构体的内存生效。】
上一节讲到结构体有三道标准工序“造模”和“生成”和“调用”,那么,结构体在哪道工序的时候才会开始占用内存(或者说内存生效)?答案是在第二道工序“生成”(或者说定义)的时候才产生内存开销。第一道工序仅“造模”不“生成”是不会产生内存的。什么意思呢?请看下面的例子。
第一种情况:仅“造模”不“生成”。
- struct StructMould //“造模”
- {
- unsigned char u8Data_A;
- unsigned char u8Data_B;
- };
复制代码
分析:这种情况是没有内存开销的,尽管你已经写下了数行代码,但是C编译器在翻译此代码的时候,它会识别到你偷工减料仅仅“造模”而不“生成”新变量,此时C编译器会把你这段代码忽略而过。
第二种情况:先“造模”再“生成”。
- struct StructMould //“造模”
- {
- unsigned char u8Data_A;
- unsigned char u8Data_B;
- };
- struct StructMould GtMould_1; //“生成”一个变量GtMould_1。占用2个字节内存
- struct StructMould GtMould_2; //“生成”一个变量GtMould_2。占用2个字节内存
复制代码
分析:这种情况才会占用内存。你“生成”变量越多,占用的内存就越大。像本例子,“生成”了两个变量GtMould_1和GtMould_2,一个变量占用2个字节,两个就一共占用了4个字节。结论:内存的占用是跟变量的“生成”有关。
【71.2 结构体的内存对齐。】
什么是对齐?为了确保内存的地址能整除某个“对齐倍数”(比如4)。比如以4为“对齐倍数”,在地址0存放一个变量a,因为地址0能整除“对齐倍数”4,所以符合“地址对齐”,接着往下再存放第二个变量b,紧接着的地址1不能整除“对齐倍数”4,此时,为了内存对齐,本来打算把变量b放到地址1的,现在就要更改挪到地址4才符合“地址对齐”,这就是内存对齐的含义。“对齐倍数”是什么?“对齐倍数”就是单片机的位数除以8。比如8位单片机的“对齐倍数”是1(8除以8),16位单片机是2(16除以8),32位单片机是4(32除以8)。本教材所用的单片机是8位的51内核单片机,因此“对齐倍数”是1。1是可以被任何整数整除的,因此,8位单片机在结构体的使用上被内存对齐的“干扰”是最小的。
为什么要对齐?单片机内部硬件层面一条指令处理的数据宽度是固定的,比如,因为一个字节是8位,所以,8位的单片机一次处理的数据宽度是1个字节(8除以8等于1),16位的单片机一次处理的数据宽度是2个字节(16位除以8位等于2),32位的单片机一次处理的数据宽度是4个字节(32位除以8位等于4),如果字节不对齐,本来单片机一个指令能处理的数据可能就要分解成2个指令甚至更多的指令,所以C编译器为了让单片机处于最佳状态,在某些情况就会涉及内存对齐,结构体就涉及到内存对齐。
结构体的内存对齐表现在哪里呢?请看下面两个例子:
第一个例子:8位单片机。
- struct StructMould_1 //“造模”
- {
- unsigned char u8Data; //一个unsigned char占用1个字节。
- unsigned long u32Data; //一个unsigned long占用4个字节。
- };
- struct StructMould_1 GtMould_1; //占用多少个字节内存呢?
复制代码
分析:GtMould_1这个变量占用多少个内存字节呢?假设GtMould_1的首地址是0,那么地址0就存放成员u8Data,u8Data占用1个字节,所以接下来的地址是1(0+1),问题来了,地址1能直接存放占用4个字节的成员u32Data吗?因为8位单片机的“对齐倍数”是1(8除以8),那么地址1显然是可以整除“对齐倍数”1的,因此,地址1是可以果断存储u32Data成员的。因此,GtMould_1占用的总字节数是5(1+4),也就是u8Data和u32Data两者所占字节数之和。
第二个例子:32位单片机。
- struct StructMould_1 //“造模”
- {
- unsigned char u8Data; //一个unsigned char占用1个字节。
- unsigned long u32Data; //一个unsigned long占用4个字节。
- };
- struct StructMould_1 GtMould_1; //占用多少个字节内存呢?
复制代码
分析:GtMould_1这个变量占用多少个内存字节呢?假设GtMould_1的首地址是0,那么地址0就存放成员u8Data,u8Data占用1个字节,所以接下来的地址是1(0+1),那么问题来了,地址1能直接存放占用4个字节的成员u32Data吗?不能。因为32位单片机的“对齐倍数”是4(32除以8),那么地址1显然是不可以整除“对齐倍数”4的,因此,就要把地址1更改挪到地址4这里才符合“地址对齐”,这样,就意味着多插入了3个“填充的字节”,因此,GtMould_1占用的总字节数是8(1+3+4),也就是“1个字节u8Data,3个填充字节,4个u32Data”三者所占字节数之和。那么问题又来了,如果把结构体内部成员u8Data和u32Data的位置顺序更改一下,内存容量会有所改变吗?位置顺序更改后如下。
- struct StructMould_1 //“造模”
- {
- unsigned long u32Data; //一个unsigned long占用4个字节。
- unsigned char u8Data; //一个unsigned char占用1个字节。
- };
- struct StructMould_1 GtMould_1; //占用多少个字节内存呢?
复制代码
分析:更改u8Data和u32Data的位置顺序后,u32Data在前u8Data在后,GtMould_1这个变量占用多少个内存字节呢?假设GtMould_1的首地址是0,那么地址0就存放成员u32Data,u32Data占用4个字节,所以接下来的地址是4(0+4),那么问题来了,地址4能直接存放占用1个字节的成员u8Data吗?能。因为32位单片机的“对齐倍数”是4(32除以8),那么地址4显然是可以整除“对齐倍数”4的,因此,地址4是可以果断存储u8Data的。那么,是不是GtMould_1就占用5个字节呢?不是。因为结构体的内存对齐,还包括另外一条规定,那就是“一个结构体变量所占的内存总容量必须能整除该单片机的“对齐倍数”(单片机的位数除以8),如果不能,C编译器就会擅自在最后一个成员的后面插入若干个“填充字节”来满足这个规则”,根据这条规定,计算所得的总容量5是不能整除“对齐倍数”4的,必须再额外填充3个字节补足到8,才能整除“对齐倍数”4,因此,更改顺序后,GtMould_1还是占用8个字节(4+1+3),前4个字节是u32Data,中间1个字节是u8Data,后3个字节是“填充字节”。
因为本教程采用的是8位的51内核单片机,因此,在上述这个例子中,GtMould_1所占的字节数是符合“第一个例子”的情况,也就是占用5个字节。内存对齐是遵守几条严格的规则的,我只列出其中最关键的两条给大家大致阅读一下,有一个印象即可,不强求死记硬背,只需知道“结构体因为存在内存对齐,所以实际内存容量是有可能大于内部各成员类型字节数相加之和,尤其是16位或者32位这类单片机”就可以了。
- 第(1)条:结构体内部某个成员相对结构体首地址的偏移地址必须能整除该单片机的“对齐倍数”(单片机的位数除以8),如果不能,C编译器就会擅自在各成员之间插入若干个“填充字节”来满足这个规则。
- 第(2)条:一个结构体变量所占的内存总容量必须能整除该单片机的“对齐倍数”(单片机的位数除以8),如果不能,C编译器就会擅自在最后一个成员的后面插入若干个“填充字节”来满足这个规则。
复制代码
【71.3 如何获取某个结构体变量的内存容量?】
结构体存在内存对齐的问题,就说明它的内存占用情况不会像普通数组那样一目了然,那么,我们编写程序的时候怎么知道某个结构体变量占用了多少个字节数?答案是:用sizeof宏函数。比如:
- struct StructMould_1
- {
- unsigned long u32Data;
- unsigned char u8Data;
- };
- struct StructMould_1 GtMould_1;
- unsigned long a; //此变量用来获取结构体变量GtMould_1所占用的字节总数
- void main() //主函数
- {
- a=sizeof(GtMould_1); //利用宏函数sizeof获取结构体变量所占用的字节总数
- }
复制代码
【71.4 结构体之间的赋值。】
结构体之间的赋值有两种,第一种是成员之间“一对一”的赋值,第二种是整个结构体之间“面对面”的整体赋值。第一种成员赋值像普通变量赋值那样,没有那么多套路和忌讳,数据传递安全可靠。第二种整个结构体之间赋值在编程体验上带有“一键操作”的快感,但是要注意避开一些“雷区”,首先,整体赋值的前提是必须保证两个结构体变量都是同一个“结构体模板”造出来的变量,不同“模板”的结构体变量之间禁止“整体赋值”,其次,哪怕是“同一个模板”的结构体变量,也并不是所有的“同模板结构体”变量都能实现整个结构体之间的直接赋值,只有在结构体内部成员比较简单的情况下才适合“整体赋值”,如果结构体内部包含有“指针”或者“字符串”或者“其它结构体中的结构体”,这类情况就比较复杂,这时建议大家绕开有“雷区”的“整体赋值”而直接选用安全可靠的“成员赋值”。什么是“成员赋值”什么是“整体赋值”?请看下面两个例子。
第一种:成员赋值。把结构体变量GtMould_2_A赋值给GtMould_2_B。
- struct StructMould_2 //“造模”
- {
- unsigned long u32Data;
- unsigned char u8Data;
- };
- struct StructMould_2 GtMould_2_A; //生成第1个结构体变量
- struct StructMould_2 GtMould_2_B //生成第2个结构体变量
- void main() //主函数
- {
- //先给GtMould_2_A赋初值。
- GtMould_2_A.u32Data=1;
- GtMould_2_A.u8Data=2;
- //通过“成员赋值”,把结构体变量GtMould_2_A赋值给GtMould_2_B。
- GtMould_2_B.u32Data=GtMould_2_A.u32Data; //成员之间“一对一”的赋值
- GtMould_2_B.u8Data=GtMould_2_A.u8Data; //成员之间“一对一”的赋值
- }
复制代码
第二种:整体赋值。把结构体变量GtMould_2_A赋值给GtMould_2_B。
- struct StructMould_2 //“造模”
- {
- unsigned long u32Data;
- unsigned char u8Data;
- };
- struct StructMould_2 GtMould_2_A; //生成第1个结构体变量
- struct StructMould_2 GtMould_2_B //生成第2个结构体变量
- void main() //主函数
- {
- //先给GtMould_2_A赋初值。
- GtMould_2_A.u32Data=1;
- GtMould_2_A.u8Data=2;
- //通过“整体赋值”,把结构体变量GtMould_2_A赋值给GtMould_2_B。
- GtMould_2_B=GtMould_2_A; //整体之间“一次性”的赋值
- }
复制代码
上述例子中的整体赋值,是因为结构体内部的数据比较“简单”,没有包含“指针”或者“字符串”或者“其它结构体中的结构体”这类数据成员,如果包含这类成员,建议大家不要用整体赋值。比如遇到以下这类结构体就建议大家直接用安全可靠的“成员赋值”:
- struct StructMould //“造模”
- {
- unsigned char u8String[]=”String”; //字符串
- unsigned char *pu8Data; //指针
- struct StructOtherMould GtOtherMould; //结构体中的结构体
- };
复制代码
【71.5 例程练习和分析。】
现在编写一个练习的程序:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- struct StructMould_1 //“造模”
- {
- unsigned long u32Data; //一个unsigned long占用4个字节。
- unsigned char u8Data; //一个unsigned char占用1个字节。
- };
- struct StructMould_2 //“造模”
- {
- unsigned char u8Data;
- unsigned long u32Data;
- };
- struct StructMould_1 GtMould_1; //占用多少个字节内存呢?
- struct StructMould_2 GtMould_2_A;
- struct StructMould_2 GtMould_2_B;
- unsigned long a; //此变量用来获取结构体变量GtMould_1所占用的字节总数
- void main() //主函数
- {
- a=sizeof(GtMould_1); //利用宏函数sizeof获取结构体变量GtMould_1所占用的字节总数
- //先给GtMould_2_A赋初值。
- GtMould_2_A.u32Data=1;
- GtMould_2_A.u8Data=2;
- //通过“整体赋值”,把结构体变量GtMould_2_A赋值给GtMould_2_B。
- GtMould_2_B=GtMould_2_A; //整体之间“一次性”的赋值
- View(a); //把a发送到电脑端观察
- View(GtMould_2_B.u32Data); //把结构体成员GtMould_2_B.u32Data发送到电脑端观察
- View(GtMould_2_B.u8Data); //把结构体成员GtMould_2_B.u8Data发送到电脑端观察
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:5
- 十六进制:5
- 二进制:101
- 第2个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第3个数
- 十进制:2
- 十六进制:2
- 二进制:10
复制代码
分析:
GtMould_1所占的字节数a为5。
GtMould_2_B的结构体成员GtMould_2_B.u32Data为1。
GtMould_2_B的结构体成员GtMould_2_B.u8Data为2。
【71.6 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-6-4 11:59
本帖最后由 jianhong_wu 于 2017-6-4 12:10 编辑
第七十二节: 结构体的指针。
第七十二节_pdf文件.pdf
(79.71 KB, 下载次数: 1149)
【72.1 结构体指针的重要用途。】
结构体指针有两个重要用途,一个是结构体数据的拆分和打包,另一个是作为结构体数据在涉及函数时的参数入口。
什么是“结构体数据的拆分和打包”?结构体本质是一个数组,数组内可能包含了许多不同数据长度类型的成员,当我们直接操作某个具体的成员时,只改变某个成员的数值,不影响其它成员,这个就是“拆分”的角度。那么,什么是“打包”?当涉及整个结构体数据的存储或者传输(通信)给另外一个单片机时,这时候有两种选择,一种是一个成员一个成员的挨个处理,这种“拆分”的处理方式比较繁琐麻烦,另外一种就是把整个结构体当作一个以字节为单位的整体数组来处理,这种处理方式就是高速便捷的“打包”处理,但是关键的问题来了,我们把整个结构体数据以字节的方式“打包”传递给另外一个单片机,但是这个单片机接收到我们一组数据后,如何把这“一包”以字节为单位的数组转换成相同的结构体变量,以便在它的程序处理中也能以“拆分”的角度直接处理某个具体的成员变量,这时就涉及到结构体指针的作用。
什么是“作为结构体数据在涉及函数时的参数入口”?结构体数据一般内部包含了很多成员,当要把这一包数据传递给某个函数内部时,这个函数要给结构体数据预留参数入口,这时,如果函数以结构体成员的角度来预留入口,那么有多少个成员就要预留多少个函数的参数入口,可阅读性非常差,操作起来也麻烦。但是,如果以指针的角度来预留入口,那么不管这个结构体内部包含多少个成员,只需要预留一个指针参数入口就够用了,这就是绝大多32单片机在写库函数时都采样结构体指针作为函数的参数入口的原因。
结构体指针这两个重要用途后续章节会深入讲解,本节的重点是先让大家学会结构体指针的基础知识,为后续章节做准备。
【72.2 结构体指针的基础。】
操作结构体内部某个具体变量时,有两种方式,一种是成员调用的方式,另一种是指针调用的方式。C语言语法为了区分这两种方式,专门设计了两种不同的操作符号。成员调用方式采样小数点“.”的符号,指针调用方式采用箭头“->”的符号。例子如下:
- struct StructMould_1
- {
- unsigned char u8Data_A;
- unsigned long u32Data_B;
- };
- struct StructMould_1 GtMould_1; //“生成”一个变量。 //占用5个字节。
- struct StructMould_1 *ptMould_1; //定义一个结构体指针。 //占用3个字节。
- void main() //主函数
- {
- GtMould_1.u8Data_A=5; //“成员调用”的方式,用小数点符号“.”
- ptMould_1=&GtMould_1; //ptMould_1指针与变量GtMould_1建立关联。
- ptMould_1->u8Data_A=ptMould_1->u8Data_A+5; //“指针调用”的方式,用箭头符号“->”
- while(1)
- {
- }
- }
复制代码
分析:上述例子中,信息量很大,知识点有两个。
第一个知识点:为什么结构体变量GtMould_1占用5个字节,而结构体指针*ptMould_1只占用3个字节?结构体变量GtMould_1所占的内存是由结构体成员内部的数量决定的,而结构体指针*ptMould_1是由C编译器根据芯片硬件寻址范围而决定的,在一个相同的C编译器系统中,所有类型的指针所占用的字节数都是一样的,比如在本教程中所用8位单片机的C51编译器系统中,unsigned char *,unsigned int *,unsigned long *,以及本节的struct StructMould_1 *,都是占用3个字节(题外话,我前面第60节中所说的“凡是32位以下的单片机的指针都是占用4个字节”是有误的,抱歉)。32位单片机的指针往往都是4个字节,而某些64位的PC机,指针可能是8个字节,这些内容大家只要有个大概的了解即可。
第二个知识点:结构体成员GtMould_1.u8Data_A经过第一步的“成员调用”直接赋值5,紧接着经过“指针调用”的累加5操作,最后GtMould_1.u8Data_A的数值是10(5+5)。
【72.3 例程练习和分析。】
现在编写一个练习的程序:
- /*---C语言学习区域的开始。-----------------------------------------------*/
- struct StructMould_1
- {
- unsigned char u8Data_A;
- unsigned long u32Data_B;
- };
- struct StructMould_1 GtMould_1; //“生成”一个变量。 //占用5个字节。
- struct StructMould_1 *ptMould_1; //定义一个结构体指针。 //占用3个字节。
- void main() //主函数
- {
- GtMould_1.u8Data_A=5; //“成员调用”的方式,用小数点符号“.”
- ptMould_1=&GtMould_1; //ptMould_1指针与变量GtMould_1建立关联。
- ptMould_1->u8Data_A=ptMould_1->u8Data_A+5; //“指针调用”的方式,用箭头符号“->”
- View(sizeof(GtMould_1)); //在电脑端观察变量GtMould_1占用多少个字节。
- View(sizeof(ptMould_1)); //在电脑端观察指针ptMould_1占用多少个字节。
- View(GtMould_1.u8Data_A); //在电脑端观察结构体成员GtMould_1.u8Data_A的最后数值。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:5
- 十六进制:5
- 二进制:101
- 第2个数
- 十进制:3
- 十六进制:3
- 二进制:11
- 第3个数
- 十进制:10
- 十六进制:A
- 二进制:1010
复制代码
分析:
变量GtMould_1占用5个字节。
指针ptMould_1占用3个字节。
结构体成员GtMould_1.u8Data_A的最后数值是10。
【72.4 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-6-11 09:17
本帖最后由 jianhong_wu 于 2017-6-11 09:39 编辑
第七十三节: 结构体数据的传输存储和还原。
第七十三节_pdf文件.pdf
(68.5 KB, 下载次数: 1134)
【73.1 结构体数据的传输存储和还原。】
结构体本质是一个数组,数组内可能包含了许多不同数据长度类型的成员,当整个结构体数据需要存储或者传输(通信)给另外一个单片机时,这时候有两种选择,一种是一个成员一个成员的挨个处理,这种“以成员为单位”的处理方式比较繁琐麻烦,另外一种是把整个结构体变量当作一个“以字节为单位”的普通数组来处理,但是有两个关键的问题来了,第一个问题是如何把结构体“拆分”成“以字节为单位”来进行搬动数据,第二个问题是假如我们把整个结构体数据以“字节为单位”的方式“整体打包”传递给另外一个单片机,当这个接收方的单片机接收到我们这一组数据后,如何把这“一包”以字节为单位的数组再“还原”成相同的结构体变量,以便在程序处理中也能直接按“结构体的方式”来处理某个具体的成员。其实,这两个问题都涉及到“指针的强制转换”。具体讲解的例子,请直接阅读下面73.2段落的源代码例子和注释。
【73.2 例程练习和分析。】
现在编写一个练习程序,把一个结构体变量“以字节的方式”存储到另外一个普通数组里,然后再把这个“以字节为单位”的普通数组“还原”成“结构体的方式”,以便直接操作内部某个具体的成员。
- /*---C语言学习区域的开始。-----------------------------------------------*/
- struct StructMould_1
- {
- unsigned char u8Data_A;
- unsigned long u32Data_B;
- unsigned int u16Data_C;
- };
- struct StructMould_1 GtMould_1; //“生成”一个变量。
- unsigned char Gu8Buffer[sizeof(GtMould_1)]; //定义一个内存跟结构体变量大小一样的普通数组
- unsigned char *pu8; //定义一个把结构体变量“拆分”成“以字节为单位”的指针
- struct StructMould_1 *ptStruct; //定义一个结构体指针,用于“还原”普通数组为“结构体”
- unsigned int i; //定义一个用于for循环的变量
- void main() //主函数
- {
- //先把该结构体变量内部具体成员分别以“成员的方式”初始化为5,6,7
- GtMould_1.u8Data_A=5;
- GtMould_1.u32Data_B=6;
- GtMould_1.u16Data_C=7;
- pu8=(unsigned char *)&GtMould_1; //把结构体变量强制转换成“以字节为单位”的指针
- for(i=0;i<sizeof(GtMould_1);i++)
- {
- Gu8Buffer[i]=pu8[i]; //把结构体变量以字节的方式搬运并且存储到普通数组里。
- }
- ptStruct=(struct StructMould_1 *)&Gu8Buffer[0]; //再把普通数组强制“还原”成结构体指针
- ptStruct->u8Data_A=ptStruct->u8Data_A+1; //该变量从5自加1后变成6。
- ptStruct->u32Data_B=ptStruct->u32Data_B+1; //该变量从6自加1后变成7。
- ptStruct->u16Data_C=ptStruct->u16Data_C+1; //该变量从7自加1后变成8。
- View(ptStruct->u8Data_A); //在电脑端观察结构体成员u8Data_A的数值。
- View(ptStruct->u32Data_B); //在电脑端观察结构体成员u32Data_B的数值。
- View(ptStruct->u16Data_C); //在电脑端观察结构体成员u16Data_C的数值。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:6
- 十六进制:6
- 二进制:110
- 第2个数
- 十进制:7
- 十六进制:7
- 二进制:111
- 第3个数
- 十进制:8
- 十六进制:8
- 二进制:1000
复制代码
分析:
结构体成员u8Data_A的数值是6。
结构体成员u32Data_B的数值是7。
结构体成员u16Data_C的数值是8。
【73.3 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-6-17 17:43
本帖最后由 jianhong_wu 于 2017-6-17 18:02 编辑
第七十四节: 结构体指针在函数接口处的频繁应用。
第七十四节_pdf文件.pdf
(88.09 KB, 下载次数: 1111)
【74.1 重温“函数的接口参数”。】
函数的接口参数主要起到标识的作用。比如:
一个加法函数:
- unsigned char add(unsinged char a,unsigned char b)
- {
- return (a+b);
- }
复制代码
这里的a和b就是接口参数,它的作用是告诉人们,你把两个加数分别代入a和b,返回的就是你要的加法运算结果。这里的接口参数就起到入口标识的作用。注意,这句话的关键词是“标识”而不是“入口”,因为函数的“入口”不是唯一的,而是无数条路径。为什么这么说?我们把上面的例子改一下,改成全局变量,例子如下:
一个加法函数:
- unsinged char a; //加数
- unsigned char b; //加数
- unsigned char c; //和
- void add(void)
- {
- c=a+b;
- }
复制代码
上述例子中,尽管我用“两个”void(空的)关键词把原来加法函数的入口(接口参数)和出口(return返回)都堵得死死的,但是,全局变量是无法阻挡的,它进入一个函数的内部不受任何限制,也就是说,我们做项目的时候,如果把所有函数的接口参数和返回都改成void类型,所有的信息传递都改用全局变量,这样也是可以勉强把项目做完成的。但是,如果真的把所有函数的接口参数都改成void,全部靠全局变量来传递信息,那么最大的问题是函数多了之后,阅读非常不方面,你每看到一个被调用的函数,你不能马上猜出它大概跟哪些全局变量发生了关联,你必须一个一个的去查该函数的源代码才能理清楚,针对这个问题,C语言的设计者,给了函数非常丰富的接口参数,最理想的函数是:你把凡是与此函数相关的全局变量都经过接口参数的入口才进入到函数内部,尽量把接口参数的入口看作是函数的唯一合法入口(尽管不是唯一也不是必须),这样只要看函数的接口参数就知道这个函数跟哪些全局变量有关,函数的输入输出就非常清晰明了。但是问题又来了,如果有多少个全局变量就开多少个接口参数,接口参数就会变得非常多,接口参数多了,函数的门面就非常难看,无异于把本来应该“小而窄”的接口设在“宽而广”的平原上,还不如直接用原来那种全局变量强行进入呢。那么,要解决这个问题怎么办?本节的主角“结构体指针”可以解决这个问题。
【74.2 结构体指针在函数接口处的频繁应用。】
当函数的接口参数非常多的时候,可以把N个相关的全局变量“打包”成一个结构体数据,碰到函数接口的时候,可以通过“结构体指针”以“包”为单位的方式进入,这样就可以让函数的接口参数看起来非常少,这种方法,是很多32位单片机的库函数一直在用的方法,它最重要的好处是简化入口的通道数量。你想想,32位单片机有那么多寄存器,如果没有这种以“结构体指针”为接口参数的方式,它的入口可能需要几十个接口参数,那岂不是非常麻烦?库函数设计的成败与否,本来就在于接口的设计合不合理,“结构体指针作为函数接口参数”在此场合就显得特别有价值,使用了这种方法,函数与全局变量之间,它们的关联脉络再也不用隐藏起来,并且可以很清晰的表达清楚。现在举一个例子,比如有一个函数,要实现把5个全局变量“自加1”的功能,分别使用两种接口参数来实现,例子如下:
第一种方式:有多少个全局变量就开多少个接口参数。
- //函数的声明
- void Add_One( unsigned char *pu8Data_1, //第1个接口参数
- unsigned char *pu8Data_2, //第2个接口参数
- unsigned char *pu8Data_3, //第3个接口参数
- unsigned char *pu8Data_4, //第4个接口参数
- unsigned char *pu8Data_5); //第5个接口参数
- //5个全局变量的定义
- unsigned char a;
- unsigned char b;
- unsigned char c;
- unsigned char d;
- unsigned char e;
- //函数的定义
- void Add_One( unsigned char *pu8Data_1, //第1个接口参数
- unsigned char *pu8Data_2, //第2个接口参数
- unsigned char *pu8Data_3, //第3个接口参数
- unsigned char *pu8Data_4, //第4个接口参数
- unsigned char *pu8Data_5) //第5个接口参数
- {
- *pu8Data_1=(*pu8Data_1)+1; //实现自加1的功能
- *pu8Data_2=(*pu8Data_2)+1;
- *pu8Data_3=(*pu8Data_3)+1;
- *pu8Data_4=(*pu8Data_4)+1;
- *pu8Data_5=(*pu8Data_5)+1;
- }
- void main()
- {
- //5个全局变量都初始化为0
- a=0;
- b=0;
- c=0;
- d=0;
- e=0;
- //函数的调用,实现5个变量都“自加1”的功能。加“&”表示“传址”的方式进入函数内部。
- Add_One(&a, //第1个接口参数
- &b, //第2个接口参数
- &c, //第3个接口参数
- &d, //第4个接口参数
- &e); //第5个接口参数
- }
复制代码
第二种方式:把N个全局变量打包成一个结构体,以“结构体指针”的方式进入函数内部。
- //函数的声明
- void Add_One(struct StructMould *ptMould); //只有1个结构体指针,大大减小了接口参数。
- //结构体的“造模”
- struct StructMould
- {
- unsigned char a;
- unsigned char b;
- unsigned char c;
- unsigned char d;
- unsigned char e;
- };
- struct StructMould GtMould; //生成一个结构体变量,内部包含了5个全局变量a,b,c,d,e。
- //函数的定义
- void Add_One(struct StructMould *ptMould) //只有1个结构体指针,大大减小了接口参数。
- {
- ptMould->a=ptMould->a+1; //实现“自加1”的功能。
- ptMould->b=ptMould->b+1;
- ptMould->c=ptMould->c+1;
- ptMould->d=ptMould->d+1;
- ptMould->e=ptMould->e+1;
- }
- void main()
- {
- //5个全局变量的结构体成员都初始化为0
- GtMould.a=0;
- GtMould.b=0;
- GtMould.c=0;
- GtMould.d=0;
- GtMould.e=0;
- //函数的调用,实现5个变量都“自加1”的功能。加“&”表示“传址”的方式进入函数内部。
- Add_One(&GtMould); //只有1个结构体指针,大大减小了接口参数。
- }
复制代码
【74.3 例程练习和分析。】
现在编写一个“以结构体指针为函数接口参数”的练习程序。
- /*---C语言学习区域的开始。-----------------------------------------------*/
- //函数的声明
- void Add_One(struct StructMould *ptMould); //只有1个结构体指针,大大减小了接口参数。
- //结构体的“造模”
- struct StructMould
- {
- unsigned char a;
- unsigned char b;
- unsigned char c;
- unsigned char d;
- unsigned char e;
- };
- struct StructMould GtMould; //生成一个结构体变量,内部包含了5个全局变量a,b,c,d,e。
- //函数的定义
- void Add_One(struct StructMould *ptMould) //只有1个结构体指针,大大减小了接口参数。
- {
- ptMould->a=ptMould->a+1; //实现“自加1”的功能。
- ptMould->b=ptMould->b+1;
- ptMould->c=ptMould->c+1;
- ptMould->d=ptMould->d+1;
- ptMould->e=ptMould->e+1;
- }
- void main() //主函数
- {
- //5个全局变量的结构体成员都初始化为0
- GtMould.a=0;
- GtMould.b=0;
- GtMould.c=0;
- GtMould.d=0;
- GtMould.e=0;
- //函数的调用,实现5个变量都“自加1”的功能。加“&”表示“传址”的方式进入函数内部。
- Add_One(&GtMould); //只有1个结构体指针,大大减小了接口参数。
- View(GtMould.a); //在电脑端观察结构体成员GtMould.a的数值。
- View(GtMould.b); //在电脑端观察结构体成员GtMould.b的数值。
- View(GtMould.c); //在电脑端观察结构体成员GtMould.c的数值。
- View(GtMould.d); //在电脑端观察结构体成员GtMould.d的数值。
- View(GtMould.e); //在电脑端观察结构体成员GtMould.e的数值。
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第2个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第3个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第4个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第5个数
- 十进制:1
- 十六进制:1
- 二进制:1
复制代码
分析:
结构体成员GtMould.a的数值是1。
结构体成员GtMould.b的数值是1。
结构体成员GtMould.c的数值是1。
结构体成员GtMould.d的数值是1。
结构体成员GtMould.e的数值是1。
【74.4 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-6-25 10:36
本帖最后由 jianhong_wu 于 2017-6-25 11:14 编辑
第七十五节: 指针的名义(例:一维指针操作二维数组)。
第七十五节_pdf文件.pdf
(86.13 KB, 下载次数: 1012)
【75.1 指针的名义。】
刚开始接触指针往往有这种感觉,指针的江湖很乱,什么“乱七八糟”的指针都能冒出来,空指针,指针的指针,函数的指针,各种名目繁多的指针,似乎都可以打着指针的名义让你招架不住,而随着我们功力的提升,会逐渐拨开云雾,发现指针的真谛不外乎三个,第一个是所有的指针所占用字节数都一样,第二个是所有指针的操作本质都是“取地址”,第三个是所有各种不同类型的指针之间的转换都可以用“小括号的类型强制转换”。
【75.2 一维指针操作二维数组。】
C语言讲究门当户对,讲究类型匹配,什么类型的指针就操作什么类型的数据,否则C编译器在翻译代码的时候,会给予报错或者警告。如果想甩开因类型不匹配而导致的报错或者警告,就只能使用“小括号的类型强制转换”,这个方法在项目中应用很频繁,也很实用。一维指针想直接操作二维数组也是必须使用“小括号的类型强制转换”。实际项目中为什么会涉及“一维指针想直接操作二维数组”?二维数组更加像一个由行与列组合而成的表格,而且每行单元的内存地址是连续的,并且上下每行与每行之间的首尾单元的内存地址也是连续的,凡是内存地址连续的都是指针的菜。我曾遇到这样一种情况,要从一个二维表格里提取某一行数据用来显示,而这个显示函数是别人封装好的一个库函数,库函数对外的接口是一维指针,这样,如何把二维表格(二维数组)跟一维指针在接口上兼容起来,就是一个要面临的问题,这时有两种思路,一种是把二维数组的某一行数据先用原始的办法提取出来存放在一个中间变量的一维数组,然后再把这个一维数组代入到一维指针接口的库函数里,另一种思路是绕开中间变量,直接把二维数组的某一行的地址强制转换成一维指针的类型,利用“类型强制转换”绕开C编译器的报错或警告,实现二维数组跟一维指针“直通”,经过实验,这种方法果然可以,从此对指针的感悟就又上了一层,原来,指针的“取地址”是不仅仅局限于某个数组的首地址,它完全可以利用类型强制转换的小括号“()”与取地址符号“&”结合起来,让指针跟一维数组或者二维数组里面任何一个单元直接关联起来。请看下面两个例子,用一维指针提取二维数组里面某一行的数据,第一个例子是在程序处理中的类型强制转换的应用,第二个例子是在函数接口中的类型强制转换的应用。
【75.3 在程序处理中的类型转换。】
- unsigned char table[][3]= //二维数组
- {
- {0x00,0x01,0x02}, //二维数组的第0行数据
- {0x10,0x11,0x12}, //二维数组的第1行数据
- {0x20,0x21,0x22}, //二维数组的第2行数据
- };
- unsigned char *pGu8; //一维指针
- unsigned char Gu8Buffer[3]; //一维数组,存放从二维数组里提取出来的某一行数据
- unsigned char i; // for循环的变量
- void main()
- {
- pGu8=(unsigned char *)&table[2][0]; //利用类型强制转换使得一维指针跟二维数组关联起来。
- for(i=0;i<3;i++)
- {
- Gu8Buffer[i]=pGu8[i]; //提取二维数组的第2行数据,存入到一个一维数组里。
- }
- while(1)
- {
- }
- }
复制代码
【75.4 在函数接口中的类型转换。】
在函数接口中,也可以利用类型强制转换来实现函数接口的匹配问题,比如,下面这个写法也是合法的。
- void GetRowData(unsigned char *pu8); //函数的声明
- unsigned char table[][3]= //二维数组
- {
- {0x00,0x01,0x02}, //二维数组的第0行数据
- {0x10,0x11,0x12}, //二维数组的第1行数据
- {0x20,0x21,0x22}, //二维数组的第2行数据
- };
- unsigned char Gu8Buffer[3]; //一维数组,存放从二维数组里提取出来的某一行数据
- void GetRowData(unsigned char *pu8) //一维指针的函数接口
- {
- unsigned char i; // for循环的变量
- for(i=0;i<3;i++)
- {
- Gu8Buffer[i]=pu8[i]; //提取二维数组的某行数据,存入到一个一维数组里。
- }
- }
- void main()
- {
- GetRowData((unsigned char *)&table[2][0]); //利用类型强制转换来兼容一维指针的函数接口
- while(1)
- {
- }
- }
复制代码
【75.5 注意指针或者数组越界的问题。】
上述例子中,二维数组内部只有9个数据,如果指针操作的数据超过了这9个数据的地址范围,就会导致系统其它无辜的数据受到破坏,这个问题导致的后果是很严重的,这类指针或者数组越界的问题,大家平时做项目时必须留心注意。
【75.6 例程练习和分析。】
现在编写一个练习程序。
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void GetRowData(unsigned char *pu8); //函数的声明
- unsigned char table[][3]= //二维数组
- {
- {0x00,0x01,0x02}, //二维数组的第0行数据
- {0x10,0x11,0x12}, //二维数组的第1行数据
- {0x20,0x21,0x22}, //二维数组的第2行数据
- };
- unsigned char Gu8Buffer[3]; //一维数组,存放从二维数组里提取出来的某一行数据
- void GetRowData(unsigned char *pu8) //一维指针的函数接口
- {
- unsigned char i; // for循环的变量
- for(i=0;i<3;i++)
- {
- Gu8Buffer[i]=pu8[i]; //提取二维数组的某行数据,存入到一个一维数组里。
- }
- }
- void main() //主函数
- {
- GetRowData((unsigned char *)&table[2][0]); //利用类型强制转换来兼容一维指针的函数接口
- View(Gu8Buffer[0]); //在电脑端观察存放二维数组某行数据的一维数组的内容
- View(Gu8Buffer[1]); //在电脑端观察存放二维数组某行数据的一维数组的内容
- View(Gu8Buffer[2]); //在电脑端观察存放二维数组某行数据的一维数组的内容
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:32
- 十六进制:20
- 二进制:100000
- 第2个数
- 十进制:33
- 十六进制:21
- 二进制:100001
- 第3个数
- 十进制:34
- 十六进制:22
- 二进制:100010
复制代码
分析:
Gu8Buffer[0]是十六进制的0x20,提取了二维数组第2行中的某数据。
Gu8Buffer[1]是十六进制的0x21,提取了二维数组第2行中的某数据。
Gu8Buffer[2]是十六进制的0x22,提取了二维数组第2行中的某数据。
【75.7 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-7-9 09:09
本帖最后由 jianhong_wu 于 2017-7-9 09:13 编辑
第七十六节: 二维数组的指针。
第七十六节_pdf文件.pdf
(81.88 KB, 下载次数: 1006)
【76.1 二维数组指针的用途。】
前面章节讲了一维指针操作二维数组,本质是通过“类型强制转换”实现的,这种应用局限于某些特定的场合,毕竟一维有1个下标,二维有2个下标,一维和二维在队形感上是有明显差别的,强行用一维指针操作二维数组会破坏了代码原有的队形感,大多数的情况,还是用二维指针操作二维数组。
二维指针主要应用在两个方面,一方面是N个二维数组的“中转站”应用,另一方面是函数接口的应用。比如,当某项目有N个二维数组表格时,要通过某个变量来切换处理某个特定的表格,以便实现“N选一”的功能,此时,二维指针在这N个二维数组之间就起到中转站的作用。又,当某个函数接口想输入或者输出一个二维数组时,就必然要用到二维指针作为函数的接口参数。
【76.2 二维指针的“中转站”应用。】
举一个例子,有3个现有的二维数组,通过某个变量来选择切换,把某个二维数组的数据复制到指定的一个缓存数组中。
- code unsigned char table_1[3][3]= //第1个现有的二维数组
- {
- {0x00,0x01,0x02},
- {0x10,0x11,0x12},
- {0x20,0x21,0x22},
- };
- code unsigned char table_2[3][3]= //第2个现有的二维数组
- {
- {0xA0,0xA1,0xA2},
- {0xB0,0xB1,0xB2},
- {0xC0,0xC1,0xC2},
- };
- code unsigned char table_3[3][3]= //第3个现有的二维数组
- {
- {0xD0,0xD1,0xD2},
- {0xE0,0xE1,0xE2},
- {0xF0,0xF1,0xF2},
- };
- unsigned char SaveBuffer[3][3]; //指定的一个缓存数组
- unsigned char TableSec; //选择变量
- const unsigned char (*pTable)[3]; //“中转站”的二维指针
- unsigned char R,L; //复制数据时用到的for循环变量
- void main()
- {
- TableSec=2; //选择第2个现有的二维数组
- switch(TableSec) //根据选择变量来切换选择某个现有的二维数组
- {
- case 1: //选择第1个现有二维数组
- pTable=table_1; //二维指针pTable在这里关联指定的数组,起到中转站的作用。
- break;
- case 2: //选择第2个现有二维数组
- pTable=table_2; //二维指针pTable在这里关联指定的数组,起到中转站的作用。
- break;
- case 3: //选择第3个现有二维数组
- pTable=table_2; //二维指针pTable在这里关联指定的数组,起到中转站的作用。
- break;
- }
- //通过二维指针pTable来复制数据到指定的缓存数组SaveBuffer
- for(R=0;R<3;R++) //行循环
- {
- for(L=0;L<3;L++) //列循环
- {
- SaveBuffer[R][L]=pTable[R][L]; //这里能看到,二维指针维护了二维数组的队形感
- }
- }
- while(1)
- {
- }
- }
复制代码
【76.3 二维指针在“函数接口”中的应用。】
把上述例子“复制过程”的代码封装成一个函数,实现的功能还是一样,有3个现有的二维数组,通过某个变量来选择切换,把某个二维数组的数据复制到指定的一个缓存数组中。
- //函数声明
- void CopyBuffer(const unsigned char (*pTable)[3],unsigned char (*pSaveBuffer)[3]);
- code unsigned char table_1[3][3]= //第1个现有的二维数组
- {
- {0x00,0x01,0x02},
- {0x10,0x11,0x12},
- {0x20,0x21,0x22},
- };
- code unsigned char table_2[3][3]= //第2个现有的二维数组
- {
- {0xA0,0xA1,0xA2},
- {0xB0,0xB1,0xB2},
- {0xC0,0xC1,0xC2},
- };
- code unsigned char table_3[3][3]= //第3个现有的二维数组
- {
- {0xD0,0xD1,0xD2},
- {0xE0,0xE1,0xE2},
- {0xF0,0xF1,0xF2},
- };
- unsigned char SaveBuffer[3][3]; //指定的一个缓存数组
- unsigned char TableSec; //选择变量
- //*pTable是输入接口带const修饰,*pSaveBuffer是输出结果的接口无const。
- void CopyBuffer(const unsigned char (*pTable)[3],unsigned char (*pSaveBuffer)[3])
- {
- unsigned char R,L; //复制数据时用到的for循环变量
- for(R=0;R<3;R++) //行循环
- {
- for(L=0;L<3;L++) //列循环
- {
- pSaveBuffer[R][L]=pTable[R][L]; //这里能看到,二维指针维护了二维数组的队形感
- }
- }
- }
- void main()
- {
- TableSec=2; //选择第2个现有的二维数组
- switch(TableSec) //根据选择变量来切换选择某个现有的二维数组
- {
- case 1: //选择第1个现有二维数组
- CopyBuffer(table_1,SaveBuffer); //二维指针在这里分别体现了输入和输出接口作用
- break;
- case 2: //选择第2个现有二维数组
- CopyBuffer(table_2,SaveBuffer); //二维指针在这里分别体现了输入和输出接口作用
- break;
- case 3: //选择第3个现有二维数组
- CopyBuffer(table_3,SaveBuffer); //二维指针在这里分别体现了输入和输出接口作用
- break;
- }
- while(1)
- {
- }
- }
复制代码
【76.4 二维指针“类型强制转换”的书写格式。】
unsigned char *pu8,unsigned int *pu16,unsigned int *pu32这些指针的书写定义都是很有规则感的,相比之下,二维指针的定义显得缺乏规则感,比如定义的二维指针变量unsigned char (*pTable)[3],不规则在哪?就在于二维指针的变量pTable嵌入到了括号中去,跟符号“*”捆绑在一起,这时就会冒出一个问题,如果我要强制某个指针变量为二维指针怎么办?下面的例子已经给出了答案。
- unsigned char table[3][3]= //二维数组
- {
- {0xD0,0xD1,0xD2},
- {0xE0,0xE1,0xE2},
- {0xF0,0xF1,0xF2},
- };
- unsigned char (*pTable)[3];
- void main()
- {
- pTable=(unsigned char (*)[3])table; //这里,强制类型转换用unsigned char (*)[3]
- }
复制代码
总结:二维数组的强制类型转换用这种书写格式(unsigned char (*)[N]),这里的N是代表实际项目中某数组的“列”数。
【76.5 例程练习和分析。】
现在编写一个练习程序。
- /*---C语言学习区域的开始。-----------------------------------------------*/
- void CopyBuffer(const unsigned char (*pTable)[3],unsigned char (*pSaveBuffer)[3]);
- code unsigned char table_1[3][3]= //第1个现有的二维数组
- {
- {0x00,0x01,0x02},
- {0x10,0x11,0x12},
- {0x20,0x21,0x22},
- };
- code unsigned char table_2[3][3]= //第2个现有的二维数组
- {
- {0xA0,0xA1,0xA2},
- {0xB0,0xB1,0xB2},
- {0xC0,0xC1,0xC2},
- };
- code unsigned char table_3[3][3]= //第3个现有的二维数组
- {
- {0xD0,0xD1,0xD2},
- {0xE0,0xE1,0xE2},
- {0xF0,0xF1,0xF2},
- };
- unsigned char SaveBuffer[3][3]; //指定的一个缓存数组
- unsigned char TableSec; //选择变量
- //*pTable是输入接口带const修饰,*pSaveBuffer是输出结果的接口无const。
- void CopyBuffer(const unsigned char (*pTable)[3],unsigned char (*pSaveBuffer)[3])
- {
- unsigned char R,L; //复制数据时用到的for循环变量
- for(R=0;R<3;R++) //行循环
- {
- for(L=0;L<3;L++) //列循环
- {
- pSaveBuffer[R][L]=pTable[R][L]; //这里能看到,二维指针维护了二维数组的队形感
- }
- }
- }
- void main() //主函数
- {
- TableSec=2; //选择第2个现有的二维数组
- switch(TableSec) //根据选择变量来切换选择某个现有的二维数组
- {
- case 1: //选择第1个现有二维数组
- CopyBuffer(table_1,SaveBuffer); //二维指针在这里分别体现了输入和输出接口作用
- break;
- case 2: //选择第2个现有二维数组
- CopyBuffer(table_2,SaveBuffer); //二维指针在这里分别体现了输入和输出接口作用
- break;
- case 3: //选择第3个现有二维数组
- CopyBuffer(table_3,SaveBuffer); //二维指针在这里分别体现了输入和输出接口作用
- break;
- }
- View(SaveBuffer[0][0]); //在电脑端观察某个二维数组第0行数据第0个元素的内容
- View(SaveBuffer[0][1]); //在电脑端观察某个二维数组第0行数据第1个元素的内容
- View(SaveBuffer[0][2]); //在电脑端观察某个二维数组第0行数据第2个元素的内容
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:160
- 十六进制:A0
- 二进制:10100000
- 第2个数
- 十进制:161
- 十六进制:A1
- 二进制:10100001
- 第3个数
- 十进制:162
- 十六进制:A2
- 二进制:10100010
复制代码
分析:
SaveBuffer[0][0]是十六进制的0xA0,提取了第2个二维数组的第0行第0个数据。
SaveBuffer[0][1]是十六进制的0xA1,提取了第2个二维数组的第0行第1个数据。
SaveBuffer[0][2]是十六进制的0xA2,提取了第2个二维数组的第0行第2个数据。
【76.6 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-7-9 09:15
本帖最后由 jianhong_wu 于 2017-7-9 09:28 编辑
第七十七节: 指针唯一的“单向”输出通道return。
第七十七节_pdf文件.pdf
(70.74 KB, 下载次数: 1590)
【77.1 指针的“单向”输出通道。】
函数的接口有两个地方,一个是函数名“后面”的小括号所包含的接口参数,另一个是函数名“前面”通过函数内部return返回出来的“return返回类型”。比如:
- return返回类型 函数名(接口参数,接口参数...)
- unsigned char HanShu(unsigned char a,unsigned char b) //a和b是函数名“后面”的接口参数
- {
- unsigned char c;
- c=a+b;
- return c; //函数内部返回出来的“return返回类型”
- }
复制代码
指针在“函数名后面小括号所包含的接口参数”的地方时,可以是一个“双向”口(输入和输出),如果在指针前面加上const关键字修饰,可以把“双向”改为只能输入的“单向”口,注意,这里所说的“单向”是指“输入的单向”,但是做不到“输出的单向”,指针如果想做到“输出的单向”,就必须通过return这个通道。return返回指针这个功能很常用,比如用32位单片机想做比较漂亮的显示界面时,大家往往喜欢用到emWIN这个界面显示系统,而emWIN提供了很多库函数,这些库函数用了很多return返回的“句柄”,“句柄”其实就是指针,比如类似以下行代码:
- hItem = WM_GetDialogItem(hWin_FrameWin_GetClientWindow, ID_LISTVIEW_0); //获取某个控件的句柄
复制代码
其中hItem就是“句柄”,本质就是函数内部return返回出来的指针。
所以本节内容主要是想告诉大家,return不仅可以返回普通的变量,也是可以返回指针的,而且还很常用。具体内容请看下面77.2例子中的讲解。
【77.2 例程练习和分析。】
编写一个函数,要从一个二维表格的数组中提取其中某一行的数据,用return这个返回输出的通道来接收该行数据的地址(指针),然后再通过这个指针的间接调用,把该行数据全部显示出来。
- /*---C语言学习区域的开始。-----------------------------------------------*/
- unsigned char *GetRowData(unsigned char (*pu8Table)[3],unsigned char u8RowSec); //函数声明
- unsigned char table[][3]= //二维数组
- {
- {0x00,0x01,0x02}, //二维数组的第0行数据
- {0x10,0x11,0x12}, //二维数组的第1行数据
- {0x20,0x21,0x22}, //二维数组的第2行数据
- };
- //函数名前面是unsigned char *,代表内部return返回的是unsigned char *的指针。
- unsigned char *GetRowData(unsigned char (*pu8Table)[3],unsigned char u8RowSec)
- {
- unsigned char *pu8Row;
- pu8Row=(unsigned char *)&pu8Table[u8RowSec][0]; //提取某一行开始的地址(指针)
- return pu8Row; //经过return通道对外输出指针,pu8Row是一个指针类型的变量。
- }
- unsigned char *pGu8Row; //接收return输出的指针
- unsigned char Gu8Buffer[3]; //一维数组,存放从二维数组里提取出来的某一行数据
- unsigned char i; // for循环的变量
- void main() //主函数
- {
- pGu8Row=GetRowData(table,0);//这里的0是表示选择二维表格的第0行数据
- for(i=0;i<3;i++)
- {
- Gu8Buffer[i]=pGu8Row[i]; //通过指针pGu8Row来搬运数据到一维数组Gu8Buffer
- }
- View(Gu8Buffer[0]); //在电脑端观察存放二维数组某行数据的一维数组的内容
- View(Gu8Buffer[1]); //在电脑端观察存放二维数组某行数据的一维数组的内容
- View(Gu8Buffer[2]); //在电脑端观察存放二维数组某行数据的一维数组的内容
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:0
- 十六进制:0
- 二进制:0
- 第2个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第3个数
- 十进制:2
- 十六进制:2
- 二进制:10
复制代码
分析:
Gu8Buffer[0]是0,提取了二维数组的第0行第0个数据。
Gu8Buffer[1]是1,提取了二维数组的第0行第1个数据。
Gu8Buffer[2]是2,提取了二维数组的第0行第2个数据。
【77.3 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-7-16 12:34
本帖最后由 jianhong_wu 于 2017-7-16 13:09 编辑
第七十八节: typedef和#define和enum。
第七十八节_pdf文件.pdf
(92.03 KB, 下载次数: 1617)
【78.1 typedef和#define和enum。】
typedef称为“类型定义”,#define称为“宏定义”,enum称为“枚举”。三者都有“一键替换”的能力,但是应用的侧重点各有不同。请看下面的例子,要写一个函数,把学生的分数分为3个等级,第1等级是“优”(范围:“优”>=90分),第2等级是“中”(范围:70分<=“中”<90分),第3等级是“差”(范围:“差”<70分),实现此算法的函数需要一个输入口和一个输出口,用来输入分数和输出判断结果,判断的结果用三个数字常量0,1,2来表示,0代表“优”,1代表“中”,2代表“差”。代码如下:
- unsigned char GetGrade(unsigned char u8Score)
- {
- if(u8Score<70)
- {
- return 2; //2代表“差”
- }
- else if(u8Score>=70&&u8Score<90)
- {
- return 1; //1代表“中”
- }
- else
- {
- return 0; //0代表“优”
- }
- }
复制代码
上述代码没有添加任何“typedef,#define,enum”,是“素颜照”级别的原始代码。现在对上述代码做一些美容,加入“typedef,#define,enum”的元素,代码如下:
- #define BAD_MEDIUM 70 //宏定义。用BAD_MEDIUM来表示“差”和“中”分数的分界线
- #define MEDIUM_GOOD 90 //宏定义。用MEDIUM_GOOD来表示“中”和“优”分数的分界线
- typedef unsigned char u8; //用typedef为类型“unsigned char”增加一个名为“u8”的代言人
- enum {GOOD = 0,MEDIUM,BAD}; //用enum把“0,1,2”三个常量转换为“GOOD,MEDIUM,BAD”
- u8 GetGrade(u8 u8Score)
- {
- if(u8Score<BAD_MEDIUM) //等级分数分界线的判断
- {
- return BAD; //BAD就是常量2,代表“差”。
- }
- else if(u8Score>=BAD_MEDIUM&&u8Score<MEDIUM_GOOD) //等级分数分界线的判断
- {
- return MEDIUM; //MEDIUM就是常量1,代表“中”
- }
- else
- {
- return GOOD; //GOOD就是常量0,代表“优”
- }
- }
复制代码
代码赏析:
赏析片段一:
- #define BAD_MEDIUM 70 //宏定义。用BAD_MEDIUM来表示“差”和“中”分数的分界线
- #define MEDIUM_GOOD 90 //宏定义。用MEDIUM_GOOD来表示“良”和“优”分数的分界线
复制代码
这里,用宏定义#define来关联分界线判断的分数,给后续代码的升级维护带来了便捷,因为用户有可能会要求把“差”“中”“优”三者的分数线进行调整,这时直接更改70和90这个数值就可以实现分数线的调整。可见,宏定义#define经常用在涉及“分界线”判断的场合。
赏析片段二:
- typedef unsigned char u8; //用typedef为类型“unsigned char”增加一个名为“u8”的代言人
复制代码
用类型定义typedef为类型“unsigned char”增加一个名为“u8”的代言人,u代表unsigned的u,8代表此类型占用8位,比如unsigned char就是占用8位的unsigned类型,所以用u8。如果是16位的unsigned类型就用u16,32位则用u32,这都是单片机界的常用命名习惯。上述代码用了类型定义,今后代码中凡是想定义一个unsigned char变量,都可以直接用u8来替代。这样有两个好处:第一个好处,u8的字符个数明显比unsigned char少,省了敲代码的力气。第二个好处,方便代码在各种不同硬件平台上的移植,因为不同的单片机不同的编译器对unsigned char,unsigned int,unsigned long翻译所得的结果是不一样的,比如,51单片机的unsigned int是占用16位的,而很多32位单片机的unsigned int是占用32位的,它们的16位则用unsigned short int类型,而不是unsigned int。
当我们用51单片机写代码的时候,可以如下类型定义:
- typedef unsigned char u8;
- typedef unsigned int u16;
- typedef unsigned long u32;
复制代码
当我们用32位的单片机写代码的时候,可以如下类型定义:
- typedef unsigned char u8;
- typedef unsigned short int u16;
- typedef unsigned int u32;
复制代码
这样,当我们想把51单片机的代码移到32位的单片机上时,只需要修改类型定义typedef这部分的代码,就可以快速做到代码在不同编译器平台上的类型兼容。
赏析片段三:
- enum {GOOD = 0,MEDIUM,BAD}; //用enum把“0,1,2”三个常量转换为“GOOD,MEDIUM,BAD”
复制代码
用枚举enum把“0,1,2”三个常量转换为“GOOD,MEDIUM,BAD”英文单词,最大的好处就是方便代码的阅读和修改。再多补充一点枚举的基础知识,上述代码中,第一个英文单词GOOD,经过“GOOD = 0”这条初始化的语句后,等效于常量0,后面的MEDIUM和BAD则C编译器自动对它们进行“累加1”排序,所以MEDIUM和BAD分别为常量1,2,这是C语言的语法规则。枚举enum的应用侧重在某些涉及到“状态”的数据类型,但是也不绝对。
【78.2 enum和typedef的相结合。】
enum一旦搭载上typedef后,可以把各自的特性发挥得淋漓尽致,产生另外一种常见的用途,那就是“人造”数据类型的用途,这里的“人造”解读为“人为制造”之意。比如上述78.1的函数u8 GetGrade(u8 u8Score),输出接口接收的是u8类型,但是内部return返回的是枚举类型的“GOOD,MEDIUM,BAD”其中之一,而u8虽然也能接收和兼容常量“GOOD,MEDIUM,BAD”,但是总是感觉有点“类型不匹配”的“不适感”,如果想消除这点“不适感”,可以用enum和typedef相结合的办法,修改后代码如下:
- #define BAD_MEDIUM 70 //宏定义。用BAD_MEDIUM来表示“差”和“中”分数的分界线
- #define MEDIUM_GOOD 90 //宏定义。用MEDIUM_GOOD来表示“良”和“优”分数的分界线
- typedef unsigned char u8; //用typedef为类型“unsigned char”增加一个名为“u8”的代言人
- typedef enum {
- GOOD = 0,
- MEDIUM,
- BAD
- } Grade; //通过typedef 和enum的相结合,“人造”出一个新的数据类型 Grade。
- Grade GetGrade(u8 u8Score) //这里返回的类型是Grade,而“GOOD,MEDIUM,BAD”就是属于Grade
- {
- if(u8Score<BAD_MEDIUM) //等级分数分界线的判断
- {
- return BAD; //BAD就是常量2,代表“差”。
- }
- else if(u8Score>=BAD_MEDIUM&&u8Score<MEDIUM_GOOD) //等级分数分界线的判断
- {
- return MEDIUM; //MEDIUM就是常量1,代表“中”
- }
- else
- {
- return GOOD; //GOOD就是常量0,代表“优”
- }
- }
复制代码
【78.3 例程练习和分析。】
为了熟悉typedef,#define,enum的用法,现在要写一个函数,把学生的分数分为3个等级,第1等级是“优”(范围:“优”>=90分),第2等级是“中”(范围:70分<=“中”<90分),第3等级是“差”(范围:“差”<70分),实现此算法的函数需要一个输入口和一个输出口,用来输入分数和输出判断结果,判断的结果用三个数字常量0,1,2来表示,0代表“优”,1代表“中”,2代表“差”。
- /*---C语言学习区域的开始。-----------------------------------------------*/
- #define BAD_MEDIUM 70 //宏定义。用BAD_MEDIUM来表示“差”和“中”分数的分界线
- #define MEDIUM_GOOD 90 //宏定义。用MEDIUM_GOOD来表示“良”和“优”分数的分界线
- typedef unsigned char u8; //用typedef为类型“unsigned char”增加一个名为“u8”的代言人
- typedef enum {
- GOOD = 0,
- MEDIUM,
- BAD
- } Grade; //通过typedef 和enum的相结合,“人造”出一个新的数据类型 Grade。
- Grade GetGrade(u8 u8Score); //函数声明
- Grade a; //“人造”出Grade类型的变量a,用来接收函数的判断结果。
- Grade b; //“人造”出Grade类型的变量b,用来接收函数的判断结果。
- Grade c; //“人造”出Grade类型的变量c,用来接收函数的判断结果。
- Grade GetGrade(u8 u8Score) //这里返回的类型是Grade,而“GOOD,MEDIUM,BAD”就是属于Grade
- {
- if(u8Score<BAD_MEDIUM) //等级分数分界线的判断
- {
- return BAD; //BAD就是常量2,代表“差”。
- }
- else if(u8Score>=BAD_MEDIUM&&u8Score<MEDIUM_GOOD) //等级分数分界线的判断
- {
- return MEDIUM; //MEDIUM就是常量1,代表“中”
- }
- else
- {
- return GOOD; //GOOD就是常量0,代表“优”
- }
- }
- void main() //主函数
- {
- a=GetGrade(98); //输入98分,a来接收判断的结果
- b=GetGrade(88); //输入88分,b来接收判断的结果
- c=GetGrade(68); //输入68分,c来接收判断的结果
- View(a); //在电脑端观察98分的判断结果a
- View(b); //在电脑端观察88分的判断结果b
- View(c); //在电脑端观察68分的判断结果c
- while(1)
- {
- }
- }
- /*---C语言学习区域的结束。-----------------------------------------------*/
复制代码
在电脑串口助手软件上观察到的程序执行现象如下:
- 开始...
- 第1个数
- 十进制:0
- 十六进制:0
- 二进制:0
- 第2个数
- 十进制:1
- 十六进制:1
- 二进制:1
- 第3个数
- 十进制:2
- 十六进制:2
- 二进制:10
复制代码
分析:
98分的判断结果a为0,0代表“优”。
88分的判断结果b为1,1代表“中”。
68分的判断结果c为2,2代表“差”。
【78.4 如何在单片机上练习本章节C语言程序?】
直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。
作者: jianhong_wu 时间: 2017-7-23 10:23
本帖最后由 jianhong_wu 于 2017-7-23 10:44 编辑
第七十九节: 各种变量常量的命名规范。
第七十九节_pdf文件.pdf
(81.78 KB, 下载次数: 1527)
【79.1 命名规范的必要。】
一个大型的项目程序,涉及到的变量常量非常多,各种变量常量眼花缭乱,名字不规范就无法轻松掌控全局。若能一开始就遵守特定的命名规范,则普天之下,率土之滨,都被你牢牢地掌控在手里,天下再也没有难维护的代码。本节教给大家的是我多年实践所沿用的命名规范和习惯,它不是唯一绝对的,只是给大家参考,大家今后也可以在自己的实践中慢慢总结出一套适合自己的命名规范和习惯。
【79.2 普通变量常量的命名规范和习惯。】
在C51编译器的平台下,unsigned char ,unsigned int ,unsigned long三类常用的变量代表了“无符号的8位,16位,32位”,这类型的变量前缀分别加“u8,u16,u32”来表示。但是这种类型的变量还分全局变量和局部变量,为了有所区分,就在全局变量前加“G”来表示,不带“G”的就默认是局部变量。比如:
- unsigned char Gu8Number; //Gu8就代表全局的8位变量
- unsigned int Gu16Number; //Gu16就代表全局的16位变量
- unsigned long Gu32Number; //Gu32就代表全局的32位变量
- void HanShu(unsigned char u8Data) //u8就代表局部的8位变量
- {
- unsigned char u8Number; //u8就代表局部的8位变量
- unsigned int u16Number; //u16就代表局部的16位变量
- unsigned long u32Number; //u32就代表局部的32位变量
- }
复制代码
全局变量和局部变量继续往下细分,还分“静态”和“非静态”,为了有所区分,就在前面增加“ES”或“S”来表示,“ES”代表全局的静态变量,“S”代表局部的静态变量。比如:
- static unsigned char ESu8Number; //ESu8就代表全局的8位静态变量
- static unsigned int ESu16Number; //ESu16就代表全局的16位静态变量
- static unsigned long ESu32Number; //ESu32就代表全局的32位静态变量
- void HanShu(unsigned char u8Data) //u8就代表局部的8位变量
- {
- static unsigned char Su8Number; //Su8就代表局部的8位静态变量
- static unsigned int Su16Number; //Su16就代表局部的16位静态变量
- static unsigned long Su32Number; //Su32就代表局部的32位静态变量
- }
复制代码
刚才讲的只是针对“变量”,如果是“常量”,则前缀加“C”来表示,不管是全局的常量还是局部的常量,都统一用“C”来表示,不再刻意区分“全局常量”和“静态常量”,比如:
- const unsigned char Cu8Number=1; //Cu8就代表8位常量,不刻意区分“全局”和“局部”
- const unsigned int Cu16Number=1; //Cu16就代表16位常量,不刻意区分“全局”和“局部”
- const unsigned long Cu32Number=1; //Cu32就代表32位常量,不刻意区分“全局”和“局部”
- void HanShu(unsigned char u8Data) //u8就代表局部的8位变量
- {
- const unsigned char Cu8Number=1; //Cu8就代表8位常量,不刻意区分“全局”和“局部”
- const unsigned int Cu16Number=1; //Cu16就代表16位常量,不刻意区分“全局”和“局部”
- const unsigned long Cu32Number=1; //Cu32就代表32位常量,不刻意区分“全局”和“局部”
- }
复制代码
【79.3 循环体变量的命名规范和习惯。】
循环体变量是一个很特定场合用的变量,为了突出它的特殊,这类变量在命名上用单个字母,可以不遵守命名规范,这里的“不遵守命名规范”就是它的“命名规范”,颇有道家“无为就是有为”的韵味,它是命名界的另类。比如:
- unsigned char i; //超越了规则约束的循环体变量,用单个字母来表示。
- unsigned long k; //超越了规则约束的循环体变量,用单个字母来表示。
- void HanShu(unsigned char u8Data) //u8就代表局部的8位变量
- {
- unsigned int c; //超越了规则约束的循环体变量,用单个字母来表示。
- for(c=0;c<5;c++) //用在循环体的变量
- {
- u8Data=u8Data+1; //u8就代表局部的8位变量
- }
- for(i=0;i<5;i++) //用在循环体的变量
- {
- u8Data=u8Data+1; //u8就代表局部的8位变量
- }
- for(k=0;k<5;k++) //用在循环体的变量
- {
- u8Data=u8Data+1; //u8就代表局部的8位变量
- }
- }
复制代码
【79.4 数组的命名规范和习惯。】
数组有四种应用场合,一种是普通数组,一种是字符串,一种是表格,一种是信息。在命名上分别加入后缀“Buffer,String,Table,Message”来区分,但是它们都是数组。比如:
- unsigned int Gu16NumberBuffer[5]; //后缀是Buffer。16位的全局变量数组。用在普通数组。
- unsigned char Gu8NumberString[5]; //后缀是String。8位的全局变量数组。用在字符串。
- //根据原理图得出的共阴数码管字模表
- code unsigned char Cu8DigTable[]=//后缀是Table。这里的code是代表C51的常量(类似const)。
- {
- 0x3f, //0 序号0
- 0x06, //1 序号1
- 0x5b, //2 序号2
- 0x4f, //3 序号3
- 0x66, //4 序号4
- 0x6d, //5 序号5
- 0x7d, //6 序号6
- 0x07, //7 序号7
- 0x7f, //8 序号8
- 0x6f, //9 序号9
- 0x00, //不显示 序号10
- };
- void HanShu(unsigned char u8Data) //u8就代表局部的8位变量
- {
- unsigned char u8NumberMessage[5]; //后缀是Message。8位的局部变量数组。用在信息。
- }
复制代码
【79.5 指针的命名规范和习惯。】
指针的前缀加“p”来区分。再往下细分,指针有全局和局部,有“静态”和“非静态”,有“8位宽度”和“16位宽度”和“32位宽度”,有变量指针和常量指针。比如:
- unsigned char *pGu8NumberString; //pGu8代表全局的8位变量指针
- void HanShu(const unsigned char *pCu8Data) //pCu8代表局部的8位常量指针
- {
- unsigned char *pu8NumberBuffer; //pu8代表局部的8位变量指针
- static unsigned int *pSu16NumberBuffer; //pSu16代表局部的16位静态变量指针
- static unsigned long *pSu32NumberBuffer; //pSu32代表局部的32位静态变量指针
- }
复制代码
【79.6 结构体的命名规范和习惯。】
结构体的前缀加“t”来区分。再往下细分,指针有全局和局部,有“静态”和“非静态”,有结构体变量和结构体指针。比如:
- struct StructSignData //带符号的数
- {
- unsigned char u8Sign; //符号 0为正数 1为负数
- unsigned long u32Data; //数值
- };
- struct StructSignData GtNumber; //Gt代表全局的结构体变量。
- void HanShu(struct StructSignData *ptData) //pt代表局部的结构体指针
- {
- struct StructSignData tNumber; //t代表局部的结构体变量。
- static struct StructSignData StNumber; //St代表局部的静态结构体变量。
- }
复制代码
【79.7 宏常量的命名规范和习惯。】
所谓“宏常量”往往是指用#define语句定义的常量。宏常量的所有字符都用大写字母。比如:
- #define DELAY_TIME 30 //宏常量所有字符都用大写字母。DELAY_TIME代表延时的时间。
- void HanShu(void)
- {
- delay(DELAY_TIME); //相当于delay(30),这里的delay代表某个延时函数(这里没有具体写出来)
- }
复制代码
【79.8 首字符用大写字母以及下划线“_”的灵活运用。】
两个以上的英文单词连在一起命名时,每个单词的首字符用大写,其余用小写,这样可以把每个单词“断句”开来,方便阅读。如果遇到两个英文单词连在一起不好“断句”的情况(比如某个英文单词全部是大写字母的专用名词),只要在两个英文单词之间插入下划线“_”就可以清晰的“断句”了。比如:
- unsigned long Gu32GetFileLength; //GetFileLength寓意“获取某个文件的长度”。
- unsigned char Gu8ESD_Flag; //ESD是专业用名词,代表“静电释放”的意思。用下划线“_”断句。
复制代码
作者: jianhong_wu 时间: 2017-7-30 11:27
本帖最后由 jianhong_wu 于 2017-7-30 11:44 编辑
第八十节: 单片机IO口驱动LED。
第八十节_pdf文件.pdf
(135.81 KB, 下载次数: 1455)
【80.1 不再依赖第11节模板程序。】
前面大量的章节主要是讲C语言本身的基础知识,因此每次的练习例程都要依赖第11节的模板程序。从本节开始,正式进入到单片机主题,如果没有特殊说明,以后的练习程序就不再需要依赖第11节模板程序,可以脱离模板单飞了。
【80.2 寄存器。】
寄存器是跨越在软件与硬件之间的桥梁,单片机的C语言想控制单片机引脚输出0V或者5V的物理电压,本质就是通过往寄存器里填数字,往哪个寄存器填数字,填什么样的数字,对应的引脚就输出什么样的电压。至于“为什么往寄存器填数字就会在引脚上输出对应的电压”这个问题,对于我们“应用级”工程师来说是一个黑匣子。我们写软件的最底层就是操作到“寄存器”这个层面,至于“寄存器与物理电压之间是如何关联如何实现”的这个问题,其实是“芯片级”半导体工程师所研究的事,因为单片机本身其实就是一个成品,我们从“芯片级”半导体工程师那里拿到这个成品,这个成品的说明书告诉了我们该成品的每个寄存器的作用,我们只能在这个基础上去做更上层的应用。该说明书其实就是大家通常所说的芯片的datasheet。
寄存器在单片机C语言层面,是一个全局变量,是一个具备特定名字的全局变量,是一个被系统征用的全局变量。寄存器的名字就像古代皇帝的名字,所有普通老百姓的变量名字都要“避尊者讳”,不能跟寄存器的名字重名,否则C编译器就编译不通过。
图80.2.1 单片机的32个IO口引脚
本教程用的STC89C52单片机IO口寄存器有4个,分别是P0,P1,P2,P3这4个寄存器,每个寄存器都是一个8位的全局变量,每一位代表控制一个单片机的IO口引脚,因此,该单片机一共有32个(4乘以8)IO口引脚,每个引脚都是可以单独控制的(俗称位操作)。往该位填入0,对应的引脚就输出0V的物理电压。往该位填入1,对应的引脚就输出5V的物理电压。
【80.3 C语言操作IO口寄存器。】
C语言操作单片机IO口寄存器,以便在对应的引脚上输出对应的物理电压,有两种方式。一种是并口的方式,另外一种是位操作的方式。并口方式,一次操作8个位(8个引脚),往往用在并口数据总线上。位操作方式,一次操作1个位(1个引脚),该方式因为单独控制到某个引脚,所以应用更加灵活广泛。
并口方式。并口方式的时候,可以直接对P0,P1,P2,P3这4个寄存器赋值,就像对一个unsigned char的全局变量赋值一样。比如:
- #include "REG52.H"
- void main()
- {
- P0=0xF0; //直接对P0赋值0xF0,意味着P0口的8个引脚,高4位全部输出5V,低4位全部输出0V。
- while(1)
- {
- }
- }
复制代码
“P0=0xF0”这行代码,把十六进制的0xF0分解成二进制11110000来理解,P0.7,P0.6,P0.5,P0.4这4个引脚分别输出5V物理电压,而P0.3,P0.2,P0.1,P0.0这4个引脚分别输出0V物理电压。
位操作方式。并口方式因为一次操作就绑定了8个引脚,是非常不方便的,因此,位操作就显得特别灵活实用,你可以直接操作P0,P1,P2,P3这4组引脚中(共32个)的某1个引脚,而不影响其它引脚的状态。比如,P1.4引脚是属于P1组的8个引脚中的某1个引脚,如果想直接位操作P1.4引脚,要用到特定的关键词sbit和符号“^”这个组合,sbit和符号“^”的组合类似宏定义,使用方式如下。
- #include "REG52.H"
- sbit P1_4=P1^4; //利用sbit和符号“^”的组合,把变量名字P1_4与P1.4引脚关联起来
- void main()
- {
- P1_4=0; //P1.4引脚输出0V物理电压,而不影响其它P1口引脚的状态。
- while(1)
- {
- }
- }
复制代码
【80.4 点亮LED。】
LED灯要有电流通过,才会发光。要有电流通过,必须要有电压的“正压差”,“压差”可以用水压来比喻。
比如在2楼的水,对于1楼来说,它就有“正压差”(2减去1等于“正1”),因此只要构成回路(有水管),2楼的水是可以往1楼流动的。
比如在2楼的水,对于3楼来说,它虽然有压差,但是有的只是“负压差”(2减去3等于“负1”),因此哪怕构成回路(有水管),2楼的水也是不可以往3楼流动的。
比如在2楼的水,对于同楼层的2楼来说,它的压差是0压差(2减去2等于“0压差”),因此哪怕构成回路(有水管),2楼的水也是不可以在2楼之间流动的。
上面三个比喻很关键,精髓在于是否有“正压差”。要点亮一个LED灯,并不是说你单片机引脚直接输出一个5V的物理电压就能点亮的,还要看它构成的整个LED灯回路,也就是实际的电路图是什么样的。在本教程的原理图中,我们点亮LED灯是采样“灌入式”的电路,也就是单片机输出5V电压的时候LED灯是熄灭的,而输出0V物理电压时LED灯反而是被点亮的。如下两个图:
图80.4.1 灌入式驱动8个LED
图80.4.2 灌入式驱动4个LED
现在根据这原理图,编写一个并口和位操作的练习例子,直接把程序烧录进开发板,就可以看到对应的LED灯的状态。
- #include "REG52.H"
- sbit P1_4=P1^4; //利用sbit和符号“^”的组合,把变量名字P1_4与P1.4引脚关联起来
- void main()
- {
- P0=0xF0; //直接对P0赋值0xF0,意味着P0口的8个引脚,高4位全部输出5V,低4位全部输出0V。
- P1_4=0; //P1.4引脚输出0V物理电压,而不影响其它P1口引脚的状态。
- while(1)
- {
- }
- }
复制代码
现象分析:
“P0=0xF0”直接对P0赋值0xF0,意味着P0口的8个引脚,高4位全部输出5V(LED灯反而灭),低4位全部输出0V(LED灯反而被点亮)。
“P1_4=0”P1.4引脚输出0V物理电压(LED灯反而被点亮)。
作者: jianhong_wu 时间: 2017-8-6 11:33
本帖最后由 jianhong_wu 于 2017-8-6 11:44 编辑
第八十一节: 时间和速度的起源(指令周期和晶振频率)。
第八十一节_pdf文件.pdf
(120.48 KB, 下载次数: 1407)
【81.1 节拍。】
单片机的C语言经过C编译器后,翻译成很多条机器指令,单片机逐条执行这些指令,每执行一条指令都是按照固定的节奏进行的,两条指令之间是存在几乎固定的时间间隔(实际上不是所有指令的间隔时间都绝对一致,这里方便理解暂时看作是一致),这就是节拍,每个节拍之间的时间间隔其实就是指令周期,因此,指令周期越短,节拍就越短,单片机的运算速度就越快。指令周期是由什么决定的呢?指令周期是由“心跳速度”和“心跳个数”决定的。指令周期都是由固定的N个“心跳个数”组成的,指令周期到底由多少个“心跳个数”组成?每种单片机每类指令各不一样。我们用的51系列单片机,最短的单周期指令是由12个“心跳个数”组成,依次类推,双周期指令由24个“心跳个数”组成,4周期指令由48个“心跳个数”组成。但是光有“心跳个数”还不够,还必须搭配知道“心跳速度”才能最终计算出指令周期。这里的“心跳速度”就是晶振的频率,“心跳个数”就是累计晶振的起振次数。比如,假设我们用的51单片机是12MHz(本教程实际用的是11.0592MHz),那么每个单周期的指令执行的时间是:12x(1/12000000)秒=1微秒。这个公式左边的“12”代表“12个晶振起振的次数”,这个公式右边的“(1/12000000)”代表晶振每起振1次所需要的单位时间。二者结合,刚好就是“心跳个数”乘以“单个心跳周期”等于指令周期,而指令周期就是节拍的时间。
图81.1.1 单片机的晶振
【81.2 累计节拍次数产生延时时间。】
有了这个最原始的“节拍”概念,现在开始编写一个练习程序,让一个LED灯闪烁,闪烁的本质,就是让一个LED灯先亮一会(“一会”就是延时),然后紧接着让LED灯熄灭一会(“一会”就是延时),依次循环,在视觉上看到的连贯动作就是LED闪烁。这里的关键是如何产生这个“一会”的延时,本节教程所用的就是一个for循环来执行N条空指令,每执行一条空指令就需要消耗掉1个左右的指令周期的时间(大概1微秒左右),空指令执行的循环次数越多,产生的延时时间就越长。例子如下:
图81.2.1 灌入式驱动8个LED
- #include "REG52.H"
- sbit P0_0=P0^0; //利用sbit和符号“^”的组合,把变量名字P0_0与P0.0引脚关联起来
- unsigned long i; //for循环用的累计变量
- //unsigned int i; //如果把for循环的变量i改成unsigned int类型,闪烁的频率会加快。
- void main()
- {
- while(1)
- {
- //第(1)步
- P0_0=0; //LED灯亮。
- //第(2)步
- for(i=0;i<5000;i++) //累计的循环次数越大,这里的延时就越长,“亮”持续的时间就越长。
- {
- ; //分号代表一条空指令
- }
- //第(3)步
- P0_0=1; //LED灯灭。
- //第(4)步
- for(i=0;i<5000;i++) //累计的循环次数越大,这里的延时就越长,“灭”持续的时间就越长。
- {
- ; //分号代表一条空指令
- }
- //第(5)步:这里已经触碰到主循环while(1)的“底线”,所以接着跳转到第(1)步继续循环
- }
- }
复制代码
现象分析:
理论上,每执行1条指令大概1微秒左右,但是实际上,我们看到的实验现象,发现累计循环才5000次,按理论计算,应该产生0.005秒左右的延时才合理,但是实际上居然能产生类似0.5秒的闪烁效果,中间相差100倍!为什么?C语言跟机器指令之间是存在翻译的“中间商”环节,一条C指令并不代表一条机器指令,往往一条C指令翻译后产生N条机器指令,比如上面的代码,用到for循环变量i,用的是unsigned long变量,意味4个字节,即使一条C语言赋值指令估计可能也要消耗4条单周期指令,在加上for循环的判断指令,和累加指令,以及跳转指令,所以我们看到的for(i=0;i<5000;i++)并不代表是真正仅仅执行了5000个指令周期,而是有可能执行了500000条指令周期!假如我们把上述代码中的i改成unsigned int变量(2字节),是会看到闪烁的速度明显加快的,其中原因就是C编译器与机器指令之间存在翻译后的“1对N”的关系。
作者: jianhong_wu 时间: 2017-8-13 12:12
本帖最后由 jianhong_wu 于 2017-8-13 12:33 编辑
第八十二节: Delay阻塞延时控制LED闪烁。
第八十二节_pdf文件.pdf
(98.45 KB, 下载次数: 1379)
【82.1 “阻塞”与“非阻塞”。】
做项目写程序,大框架大思路上就是在“阻塞”与“非阻塞”这两种模式下不断切换。“阻塞”可以理解成“单任务处理”模式,“非阻塞”可以理解成“多任务并行处理”模式。“阻塞”的优点是它全神贯注不分心地专注于当下这一件事,它等待某个事件的响应速度是最快的,同时省去了“来回切换、反复扫描”的额外开销,而且在编程思路上不用太费脑力只需“记流水账式”的编程即可,但是它的缺点是当下只能干一件事,其它事情无法兼顾,做不到多任务并行处理。而“非阻塞”恰恰相反,它的有优点就是“阻塞”的缺点,它的缺点就是“阻塞”的优点,对于“非阻塞”本节暂时不多讲。在实际项目中,有时候“大 阻塞”中分支了N个“小 非阻塞”,也有时候“大 非阻塞”中分支了N个“小 阻塞”。能在“阻塞”与“非阻塞”之间运用自如者,谓之神。
“阻塞等待”是指单片机在某个死循环里(比如“while(1)”这类)一直不断循环地在等待某个标志变量的状态,如果这个标志变量满足条件才会跳出这个死循环然后才能干其它的事情,否则一直在死循环里死等,给人一种全神贯注心无旁骛的感觉,
“阻塞延时”是指单片机在产生“延时时间”的时候做不了别的事,延时多久它就要被“阻塞”多久,只有延时过后它才能解脱去干别的事。比如,在编程上,常用for循环产生N个空指令来达到产生“延时时间”的目的,这种编程方式就是最常见的“阻塞延时”。
【82.2 Delay阻塞延时的一个例子。】
现在利用“Delay阻塞延时”编写一个练习程序,让一个LED灯闪烁。例子如下:
图82.2.1 灌入式驱动8个LED
- #include "REG52.H"
- void Delay(unsigned long u32DelayTime); //函数的声明
- sbit P0_0=P0^0; //利用sbit和符号“^”的组合,把变量名字P0_0与P0.0引脚关联起来
- void Delay(unsigned long u32DelayTime) //产生“阻塞延时”的延时函数
- {
- static unsigned long i; //函数在频繁调用时,加static可以省去一条额外的初始化语句的开销。
- for(i=0;i<u32DelayTime;i++);
- }
- void main()
- {
- while(1)
- {
- //第(1)步
- P0_0=0; //LED灯亮。
- //第(2)步
- Delay(5000); //这里就是阻塞延时,时间就越长,“亮”持续的时间就越长。
- //第(3)步
- P0_0=1; //LED灯灭。
- //第(4)步
- Delay(5000); //这里就是阻塞延时,时间就越长,“灭”持续的时间就越长。
- //第(5)步:这里已经触碰到主循环while(1)的“底线”,所以接着跳转到第(1)步继续循环
- }
- }
复制代码
【82.3 累加型和累减型的两种Delay函数,哪家强?】
上述82.2例子中,用到一个Delay函数,该函数内部的for循环用的是“累加型”的,比如:
- void Delay(unsigned long u32DelayTime)
- {
- static unsigned long i; //“累加型”函数内部多开销了一个变量i。
- for(i=0;i<u32DelayTime;i++); //因为这里的“i++”是加法运算,所以称为“累加型”。
- }
复制代码
现在在跟大家分享一种“累减型”的Delay函数,例子如下:
- void Delay(unsigned long u32DelayTime)
- {
- //“累减型”函数内部节省了一个变量i。
- for(;u32DelayTime>0;u32DelayTime--); //“u32DelayTime--”意味着“累减型”。
- }
复制代码
仔细对比“累加型”和“累减型”,会发现在实现同样“阻塞延时”的功能下,因为“累减型”巧妙的借用了函数入口的局部变量u32DelayTime来充当for循环的变量,而省去了一个i变量。因此,“累减型”比“累加型”强一点。
【82.4 Delay函数让初学者容易犯的错误。】
初学者刚接触Delay函数,常常容易犯的错误就是忽略了for循环变量的类型,for循环变量的类型决定了你能输入的数值范围,比如上面例子中用到的是unsigned long变量,因此可以最大输入Delay(4294967295)。如果是unsigned int变量,最大可以输入Delay(65535)。如果是unsigned char变量,最大可以输入Delay(255)。
【82.5 Delay内部的for循环嵌套可产生无穷长的时间。】
刚才讲到,如果用最大的变量类型unsigned long ,最大的输入是Delay(4294967295),那么问题来,难道Delay函数的阻塞延时的时间有最大极限?其实不存在最大极限,理论上,你要多大的延时都可以,只需要在Delay函数内部用上for循环的嵌套,就可以产生“乘法级”的无穷长的时间,例子如下:
- void Delay(unsigned long u32DelayTime)
- {
- static unsigned long i;
- static unsigned long k;
- for(i=0;i<u32DelayTime;i++)
- {
- for(k=0;k<5000;k++); //内部嵌套的for循环,意味着乘法的关系u32DelayTime的5000倍!
- }
- }
复制代码
【82.6 “阻塞延时”与“非阻塞延时”的各自应用范围。】
“阻塞延时”一般应用在两个地方,一个是上电初始化进入主循环之前的延时,另一个是进入主循环之后,跟外部驱动芯片通信时候产生的时钟节拍小延时,而这个类延时一般是低于1ms的小延时。
“非阻塞延时”在项目中是被大量应用的,进入主循环之后,只要大于或等于1ms的延时,大多数都采样“非阻塞延时”,因为进入“任务框架级”的层面,只有“非阻塞延时”才能保证项目可以继续“多任务并行处理”。“非阻塞延时”的方式后续章节会讲到。
综上所述,1ms是“阻塞延时”与“非阻塞延时”的一个分解线,1ms这个时间不是绝对的,只是一个经验值。
作者: jianhong_wu 时间: 2017-8-20 09:35
本帖最后由 jianhong_wu 于 2017-8-20 09:55 编辑
第八十三节: 累计主循环的“非阻塞”延时控制LED闪烁。
第八十三节_pdf文件.pdf
(93.17 KB, 下载次数: 1361)
【83.1 累计主循环的“非阻塞”。】
上一节提到,当Delay的“阻塞”时间超过1ms并且被频繁调用的时候,由于Delay做“独占式无用功”而消耗的延时太长,会影响其它任务的并行处理,整个系统给人的感觉非常卡顿不流畅。为了解决此问题,本节引入累计主循环的“非阻塞”,同时,希望通过此例子,让大家第一次感受到switch语句在多任务并行处理时候的优越性。switch的精髓在于“根据某个判断条件实现步骤之间的灵活跳转”,这个思路是以后做所有大项目的框架性思路。
为什么“累计主循环”可以兼顾到其它任务的并行处理?因为单片机进入main函数以后,在一个主循环里要扫描N个任务,从头到尾,把N个任务扫描一遍,每扫描一遍算“一次主循环”,每一次“主循环”都是要消耗一点时间,累计的“主循环”次数越多,所要消耗的时间就越长,但是跟Delay唯一的差别是,Delay做延时的时候没有办法扫描其它任务,而“累计主循环”内部本身就是在不断扫描其它任务,产生时间越长扫描其它任务的次数就越多,两者是完全相互促进而没有矛盾的。具体内容,请看下面的例子。
【83.2 累计主循环“非阻塞”的一个例子。】
现在利用“累计主循环非阻塞”编写一个练习程序,让一个LED灯闪烁。例子如下:
图83.2.1 灌入式驱动8个LED
- #include "REG52.H"
- #define CYCLE_SUM 5000 //累计主循环次数的设定阀值,该值决定了LED闪烁频率
- sbit P0_0=P0^0; //利用sbit和符号“^”的组合,把变量名字P0_0与P0.0引脚关联起来
- unsigned char Gu8CycleStep=0; //switch的跳转步骤
- unsigned long Gu32CycleCnt=0; //累计主循环的计数器
- void main()
- {
- while(1)
- {
- switch(Gu8CycleStep)
- {
- case 0:
- Gu32CycleCnt++; //这里就是累计main函数内部的主循环while(1)的次数
- if(Gu32CycleCnt>=CYCLE_SUM) //累计的次数达到设定值CYCLE_SUM就跳到下一步骤
- {
- Gu32CycleCnt=0; //及时清零计数器,为下一步骤的新一轮计数准备
- P0_0=0; //LED灯亮。
- Gu8CycleStep=1; //跳到下一步骤
- }
- break;
- case 1:
- Gu32CycleCnt++; //这里就是累计main函数内部的主循环while(1)的次数
- if(Gu32CycleCnt>=CYCLE_SUM) //累计的次数达到设定值CYCLE_SUM就返回上一步骤
- {
- Gu32CycleCnt=0; //及时清零计数器,为返回上一步骤的新一轮计数准备
- P0_0=1; //LED灯灭。
- Gu8CycleStep=0; //返回到上一个步骤
- }
- break;
- }
- }
- }
复制代码
【83.3 累计主循环的不足。】
上述83.2例子中,“累计主循环次数”实现时间延时是一个不错的选择。这种方法能胜任多任务处理的程序框架,但是本身也有一个小小的不足,比如“阀值CYCLE_SUM到底应该取多少才能产生多长的时间”是没有标准的,只能依靠不断上机实验来拿到一个你所需要的数值,这种“不规范”,当程序要移植到其它单片机平台上的时候就特别麻烦,需要重新修正阀值CYCLE_SUM。除此之外,哪怕在同样的一个单片机里,随着主函数里任务量的增加,累计一次主循环所消耗的时间长度也会发生变化,意味着靠“累计主循环次数”所获得的时间也会发生变化而导致不准确,此时,为了保证延时时间的准确性,必须要做的就是再一次修正“设定累计主循环次数”的阀值CYCLE_SUM,这样显然给我们带来了一丝不便,怎么办?假设单片机没有“定时中断”这个资源,那么这种“累计主循环次数”在多任务处理中确实是不二之选,但是,因为现在几乎所有的单片机内部都有“定时中断”这个资源,所以,大家不用为这个“不足”而烦恼,我们只要用上本节的switch思路,再外加一个“定时中断”,就可以轻松解决此问题,下一节就跟大家讲“定时中断”的内容。
作者: jianhong_wu 时间: 2017-8-27 21:46
本帖最后由 jianhong_wu 于 2017-8-27 21:58 编辑
第八十四节: 中断与中断函数。
第八十四节_pdf文件.pdf
(80.44 KB, 下载次数: 1348)
【84.1 中断。】
单片机的“中断”跟日常生活的“中断”差不多,你正在做“常事”的时候,突然遇到优先级更高的“急事”,这时你必须先暂停手上的“常事”,马上去处理突如其来的“急事”,处理完“急事”再返回来继续做“常事”。要理解单片机的“中断”,有六个关键点,第一点是“配置中断”,第二点是“做常事”,第三点是“中断请求”,第四点是“保护中断现场”,第五点是“处理急事”,第六点是“返回中断现场”。举个例子如下:
第一点:你老婆随时都会打电话给你,所以你把你的手机24小时都打开处于待机的状态。(配置中断)
第二点:你正在读一本书《道德经》(做常事)。
第三点:当你读到第18页的时候,你老婆突然给你打电话,让你去幼儿园帮接一下小孩(中断请求)。
第四点:你在第18页里夹了一张书签做标记(保护中断现场)。
第五点:你放下手上的书去幼儿园接小孩(处理急事)。
第六点:接了小孩,你回来继续打开《道德经》,找到书签标记的第18页(返回中断现场),继续阅读。
上述六点,在单片机的C语言里,“配置中断”放在主函数的初始化那里,“做常事”放在主函数的主循环里(main函数内部的while(1)循环),“中断请求”单片机内部硬件检测到符合发生中断的条件,“保护中断现场”是单片机内部硬件电路自动处理的(不需要我们软件干涉),“处理急事”是单片机自动跳转到另外开辟的一个特殊中断函数处理(自动跳转是单片机的硬件自动完成不需要我们软件干涉),执行完一次中断函数后单片机再自动跳转到主函数的主循环的现场点继续从现场点开始继续做常事(返回中断现场)。在这六点中,其中第四点的“保护中断现场”与第六点的“返回中断现场”是要特别强调的,单片机从main函数的主循环while(1)准备跳转到中断函数之前,它会自动记录当前的位置(做好路标),以便处理完中断函数后再返回main函数的主循环while(1)时,能找到之前的被中断跳转前的位置,这样就可以接上原来的步骤去处理原来的“常事”,在步骤上既不提前也不滞后恰到好处,中断就不会影响到常事的完整性。代码分布图的模板描述如下:
- void main()
- {
- 配置中断;
- while(1)
- {
- 处理常事;
- }
- }
- void 中断函数() interrupt 序号 //中断函数后缀带“interrupt 序号”特别修饰
- {
- 急事;
- }
复制代码
奇怪!上述代码,为什么“main函数”与“中断函数”在软件上看不到任何关联,既不存在“main函数”调用“中断函数”,也不存在“中断函数”调用“main函数”的情况,在观感上,“main函数”与“中断函数”仿佛是隔离的毫无“物理连接”的,为什么单片机还能在“main函数”与“中断函数”两者中切换自如?没错,确实,“main函数”与“中断函数”在书写上是隔离的毫无关联的,但是它们之间之所以能相互切换,是因为背后有一只无形的手在自动操控这一切,这只手就是单片机硬件自身,这是一种特殊机制,也可以理解成一种特殊的游戏规则,我们只要遵守就好了,除了普通函数,其它凡是中断函数的,都不用跟main函数发生软件上的关联调用,它们之间的切换都是硬件自动完成的,这就是main函数与中断函数的特殊跳转机制(或者称为游戏规则也可以)。
【84.2 常用的中断函数有哪三类?】
单片机的中断有很多,但常用在项目上的有三类:
第一类是定时中断。配置中断后,使其每隔一段时间就产生一次中断,比如“1ms一次的定时中断”几乎是所有的系统里的标配,因为它对程序框架起到一个时间节拍的作用。
第二类是通讯中断。比如串口每接收完一个字节就会产生一个中断通知我们去处理。
第三类是电平变化的中断。下降沿或者上升沿的中断,常常用在采集高速的脉冲信号。
【84.3 我们如何操控中断?】
刚才84.1提到“单片机硬件自动”这个概念,但是说它“硬件自动”并不意味着它不可控。单片机本身出厂的时候内部就携带了很多种类的中断,这些中断是否开启取决于你的“配置中断”代码,你要开启或者关闭某类中断,只需编写对应的“配置中断”代码就可以,而“配置中断”的代码本质就是填写某些寄存器数值。
作者: jianhong_wu 时间: 2017-9-4 19:26
本帖最后由 jianhong_wu 于 2017-9-4 19:42 编辑
第八十五节: 定时中断的寄存器配置。
第八十五节_pdf文件.pdf
(128.48 KB, 下载次数: 1328)
【85.1 寄存器配置的本质。】
单片机内部可供我们选择的资源非常丰富,有定时器,有串口,有外部中断,等等。这些丰富的资源,就像你进入一家超市,你只需选择你所需要的东西就可以了,所以配置寄存器的关键在于选择,所谓选择就是往寄存器里面做填空题,单片机系统内部再根据你的“选择清单”,去启动对应的资源。那么我们怎么知道某个型号的单片机内部有哪些资源呢?看该型号“单片机的说明书”呀,“单片机的说明书”就是我们通常所说的“芯片的datasheet”,或者说是“芯片的数据手册”,这些资料单片机厂家会提供的。
跟单片机打交道,其实跟人打交道没什么区别,你要让单片机按照你的“意愿”走,你首先要把你的“意愿”表达清楚,这个“意愿”就是信息,信息要具备准确性和唯一性,不能模凌两可。比如,现在要让单片机“每1ms产生一次中断”,你想想你可能需要给单片机提供哪些信息?
(1)51单片机有2个定时器,一个是0号定时器,一个是1号定时器,我们要面临“二选一”的选择,本例子中用的是“0号定时器”。
(2)0号定时器内部又有4种工作方式:方式0,方式1,方式2,方式3,本例子中用的是“方式1”。
(3)定时器到底多长时间中断一次,这就涉及到填充与中断时间有关的寄存器的数值,该数值是跟时间成比例关系,本例子中配置的是1ms中断,就要填充对应的数值。
(4)默认状态下,定时器是不会被开启的,如果要开启,这里就是涉及到定时器的“开关”,本例子要开启此开关。
(5)定时器时间到了就要产生中断,中断也有“总开关”和“定时器的局部开关”,这两个开关都必须同时打开,中断才会有效。
要配置定时器“每1ms产生一次中断”,大概就上述这些信息,根据这些信息提示,下面开始讲解一下寄存器的具体内容。
【85.2 定时器/计数器的模式控制寄存器TMOD。】
寄存器TMOD是一个8位的特殊变量,里面每一位都代表了不同的功能选择。根据芯片的说明书,TMOD的8位从左到右依次对应从D7到D0(左高位,右低位),定义如下:
GATE C/T M1 M0 GATE C/T M1 M0
仔细观察,发现左4位与右4位是对称的,分别都是“GATE,C/T , M1 , M0”,左4位控制的是“定时器1”,右4位控制的是“定时器0”,因为本例子用的是“定时器0”,因此“定时器1”的左4位都设置为0的默认数值,我们只需重点关注右4位的“定时器0”即可。
GATE:定时器是否受“其它外部开关”的影响的标志位。定时器的开启或者停止,受到两个开关的影响,第一个开关是“自身原配开关”,第二个开关是“其它外部开关”。GATE取1代表定时器受“其它外部开关”的影响,取0代表定时器不受“其它外部开关”的影响。本例子中,定时器只受到“自身原配开关”的影响,而不受到“其它外部开关”的影响,因此,GATE取0。
C/T:定时器有两种模式,该位取1代表“计数器模式”,取0代表“定时器模式”。本例子是“定时器模式”,因此,C/T取0。
M1与M0:工作方式的选择。M1与M0这两位的01搭配,可以有4种组合(00,01,10,11),每一种组合就代表一种工作方式。本例子选用“方式1”,因此M1与M0取“01”的组合。
综上所述,TMOD的配置代码是:TMOD=0x01;
【85.3 决定时间长度的寄存器TH0与TL0。】
TH0与TL0,T代表定时器英文单词TIME的T,H代表高位,L代表低位,0代表定时器0。
TH0是一个8位宽度的寄存器,TL0也是一个8位宽度的寄存器,两者合并起来成为一个整体,实际上就是一个16位宽度的寄存器,TH0是高8位,TL0是低8位,它们合并后的数值范围是:0到65535。该16位寄存器取值越大,定时中断一次的时间反倒越小,为什么?TH0与TL0的初始值,就像一个水桶里装的水。如果这个桶是空桶(取值为0),“雨水”想把这个桶“滴满溢出”所需要的时间就很大。如果里面已经装了大半的水(取值为大于32767),“雨水”想把这个桶“滴满溢出”所需要的时间就很小。这里的关键词“滴满溢出”的“滴”与“满溢出”,“滴”的速度是由单片机晶振决定的,而“满溢出”一次就代表产生一次中断,执行完中断函数在即将返回主函数之前,我们重新装入特定容量的水(重装初值),为下一次的“滴满溢出”做准备,依次循环,从而连续不断地产生间歇的定时中断。
配置中断时间的大小是需要经验的,因为,每次定时中断的时间太长,就意味着时间的可分度太粗,而如果每次定时中断的时间太短,则会产生很频繁的中断,势必会影响主函数main()的执行效率,而且累记中断次数的时间误差也会增大。因此,配置中断时间是需要经验的,根据经验,定时中断取1ms一次,是几乎所有单片机项目的最佳选择,按我的理解,“1ms定时中断一次”已经是单片机界公认的一种“标配”。
要配置1ms定时中断,TH0与TL0如何取值?刚才提到一个形象的例子“桶,滴,满溢出”。TH0与TL0的最大取值范围是65535,可以理解成为最大65535“滴”,如果超过65535“滴”(比如加1“滴”后变成65536“滴”)就会“满溢出”,从而产生一次中断(65536是中断发生的临界值)。而“滴一次的时间”就刚好是单片机执行“一次单指令的时间”,“一次单指令的时间”等于12个晶振周期,比如12MHz的晶振,晶振周期是(1/12000000)秒,而“一次单指令的时间”就等于12乘以(1/12000000)秒,等于0.000001秒,也就是1us。1us“滴”一次,要产生1ms的时间就需要“滴”1000次。“满溢出”的前提条件是“桶里”一共需要装入65536滴才溢出,因此,在12MHz的晶振下要产生1ms的定时中断,TH0与TL0的初值应该是64536(65536减去1000等于64536),而64536变成十六进制0xfc17,再分解到高8位TH0为0xfc,低8位TL0为0x17。
刚才的例子是假如晶振在12MHz的情况下所计算出来的结果,而本教程所用的晶振是11.0592MHz,根据11.0592MHz产生1ms的定时中断,TH0与TL0应该取值多少?根据刚才的计算方式:
- 初值=[溢出值]-([0.001秒]/([晶振周期的12个]*([1秒]/[晶振频率])))
- 初值=65536-(0.001/(12*(1/11059200)))
- 初值=65536-922 (注:922是921.6的四舍五入)
- 初值=64614
- 初值=64614
- 初值=0xfc66
- 初值TH0=0xfc
- 初值TL0=0x66
复制代码
【85.4 中断的总开关EA与局部开关ET0。】
EA:中断的总开关。宽度是1位的位变量。此开关如果取0,就会强行屏蔽所有的中断,因此,只要用到中断,此开关必须取1。
ET0:专门针对定时器0中断的局部开关。宽度是1位的位变量。此开关如果取0,则会屏蔽定时器0的中断,如果取1则允许定时器0中断。如果要定时器0能产生中断,那么总开关EA与ET0必须同时都打开(都取1),两者缺一不可。
【85.5 定时器0的“自身原配开关”TR0。】
TR0:定时器的“自身原配开关”。宽度是1位的位变量。很多初学者会把EA,ET0,TR0三者搞不清。定时器可以工作在“查询标志位”和“中断”这两种状态,也就是说在没有中断的情况下定时器也可以单独使用的。TR0是定时器0自身的发动引擎,要不要把这个发动引擎所产生的能量传输到中断的渠道,则取决于中断开关EA和ET0。TR0是源头开关,EA是中断总渠道开关,ET0是中断分支渠道的定时器0开关。TR0取1表示启动定时器0,取0表示关闭定时器0。
【85.6 定时器0的中断函数的书写格式。】
- void 函数名() interrupt 1
- {
- ...中断程序内容;
- ...此处省去若干代码
- ...中断程序内容;
- ...最后面的代码,要记得重装TH0与TL0的初值;
- }
复制代码
函数名可以随便取,只要不是编译器已经征用的关键字。这里的1是定时器0的中断号。不同的中断号代表不同类型的中断,至于哪类中断对应哪个中断号,大家可以查找相关书籍和资料。本节用的定时器0处于工作方式1的情况下,在即将退出中断之前,需要重装TH0与TL0的初始值。
【85.7 寄存器的名字来源。】
前面讲的寄存器都有固定的名字,而且这些名字都是唯一的,拼写的时候少一个字母或者多一个字母,C编译器都会报错不让你通过,因此问题来了,初学者刚接触一款单片机的时候,如何知道某个寄存器它特定的唯一的名字?有两个来源。
第一个来源,可以打开C编译器的某个头文件(.h格式)查看这些寄存器的名字。比如51单片机可以查看REG52.H这个头文件。如何打开REG52.H这个文件?在keil源代码编辑器界面下,选中上面REG52.H这几个字符,在右键弹出的菜单下点击Open ducument“REG52.H”即可。
第二个来源是直接参考一些现成的范例程序,这些范例程序网上很多,有的是原厂提供的,有的是热心网友的分享,有的是技术书籍或者学习板开发板厂家提供的。
【85.8 如何快速配置寄存器。】
建议一边阅读芯片的数据手册,一边参考一些现成的范例程序,这些范例程序网上很多,有的是原厂提供的,有的是热心网友的分享,有的是技术书籍或者学习板开发板厂家提供的。
【85.9 练习例程。】
现在编写一个定时中断程序,让两个LED灯闪烁,一个是在主函数里用累计主循环次数的方式实现(P0.0控制),另一个是在定时中断函数里用累计定时中断次数的方式实现(P0.1控制)。这两个闪烁的LED灯,一个在main函数,一个是在中断函数,两路任务互不干涉独立运行,并行处理的“雏形”略显出来。
图85.9.1 灌入式驱动8个LED
- #include "REG52.H"
- #define CYCLE_SUM 5000 //主循环的次数
- #define INTERRUPT_SUM 500 //中断的次数
- sbit P0_0=P0^0; //在主循环里的LED灯
- sbit P0_1=P0^1; //在定时中断里的LED灯
- unsigned char Gu8CycleStep=0;
- unsigned long Gu32CycleCnt=0; //累计主循环的计数器
- unsigned char Gu8InterruptStep=0;
- unsigned long Gu32InterruptCnt=0; //累计定时中断次数的计数器
- void main()
- {
- TMOD=0x01; //设置定时器0为工作方式1
- TH0=0xfc; //产生1ms中断的TH0初始值
- TL0=0x66; //产生1ms中断的TL0初始值
- EA=1; //开总中断
- ET0=1; //允许定时0的中断
- TR0=1; //启动定时0的中断
- while(1) //主循环
- {
- switch(Gu8CycleStep)
- {
- case 0:
- Gu32CycleCnt++;
- if(Gu32CycleCnt>=CYCLE_SUM)
- {
- Gu32CycleCnt=0;
- P0_0=0; //主循环的LED灯亮。
- Gu8CycleStep=1;
- }
- break;
- case 1:
- Gu32CycleCnt++;
- if(Gu32CycleCnt>=CYCLE_SUM)
- {
- Gu32CycleCnt=0;
- P0_0=1; //主循环的LED灯灭。
- Gu8CycleStep=0;
- }
- break;
- }
- }
- }
- void T0_time() interrupt 1 //定时器0的中断函数,每1ms单片机自动执行一次此函数
- {
- switch(Gu8InterruptStep)
- {
- case 0:
- Gu32InterruptCnt++; //累计中断次数的次数
- if(Gu32InterruptCnt>=INTERRUPT_SUM) //次数达到设定值就跳到下一步骤
- {
- Gu32InterruptCnt=0; //及时清零计数器,为下一步骤的新一轮计数准备
- P0_1=0; //定时中断的LED灯亮。
- Gu8InterruptStep=1; //跳到下一步骤
- }
- break;
- case 1:
- Gu32InterruptCnt++; //累计中断次数的次数
- if(Gu32InterruptCnt>=INTERRUPT_SUM) //次数达到设定值就返回上一步骤
- {
- Gu32InterruptCnt=0; //及时清零计数器,为返回上一步骤的新一轮计数准备
- P0_1=1; //定时中断的LED灯灭。
- Gu8InterruptStep=0; //返回到上一个步骤
- }
- break;
- }
- TH0=0xfc; //重装初值,不能忘。
- TL0=0x66; //重装初值,不能忘。
- }
复制代码
作者: jianhong_wu 时间: 2017-9-10 10:48
本帖最后由 jianhong_wu 于 2017-9-10 11:08 编辑
第八十六节: 定时中断的“非阻塞”延时控制LED闪烁。
第八十六节_pdf文件.pdf
(134.61 KB, 下载次数: 1304)
【86.1 定时中断应用的四大关键词。】
本节主要内容有四大个关键词:1ms,互斥量,volatile,switch。
(1)1ms。把定时中断设置为1ms中断一次,几乎是单片机界公认的“标配”。这个1 ms是系统时间的节拍来源,有了1ms“标配”意识,你的程序在不同单片机平台上移植的时候会得心应手运用自如。
(2)互斥量。“主函数”与“定时中断函数”,本质上是两个独立进程在不断切换并行运行,两个进程之间不断切换,就会涉及到数据的安全保护,数据的安全保护主要是针对多字节的变量,比如int类型(2个字节),long类型(4个字节)。但是单字节的char变量不用额外保护,因为“字节”是变量中的最小单位(在不考虑“位”的情况下),这里的“最小单位不可分”就像“原子是最小单位不可分”一样,因此也有很多前辈把“互斥量”称为“原子锁”。为什么要用互斥量?因为,在多个线程同时访问同一个全局变量的时候,如果双方都是“读操作”,则不会出现问题,但是,如果双方都是“既有写操作也有读操作”的情况下,比如,我在主函数里正在修改(写操作)一个unsigned int类型的变量,unsigned int类型的变量占用2个字节,在更改数据的时候至少需要2条指令,当我刚执行完第1条指令还没来得及执行第2指令的时候,突然来了一个定时中断,并且在定时中断函数里也对这个变量进行了修改(写操作)并且还进行了读取判断操作,这个瞬间就可能给程序带来了隐患。话说回来,互斥量到底有没有必要,其实还是有点争议的,我曾经为这个问题纠结过很久,毕竟,如果不用互斥量,这么微观的隐患到底存不存在,目前很难做一个“让故障重现”的实验去证明,最后,我是本着“宁可信其有不可信其无”的态度,把互斥量应用在了我的工作中。
(3)volatile。volatile是一个前缀的修饰关键词,也是用来保护主函数与中断函数共用的全局变量的,只不过,volatile是针对C编译器的,预防“C编译器在优化代码的时候误伤一些重要的共享数据”,就像预防杀毒软件用力过猛把一些合法软件当作病毒而误杀。加了volatile修饰的全局变量,就能提醒C编译器不要对这类特殊变量擅作主张去优化。
(4)switch。switch是“非阻塞程序框架”的核心语句,在以switch为核心的框架下,进行不同步骤之间的程序跳转,是做大型裸机程序的常态。
【86.2 主函数与定时中断函数的程序框架。】
主函数与定时中断函数之间相互配合,主函数负责做什么,中断函数负责做什么,对于初学者来说可能是一头雾水,但是对于像我这种在单片机界深耕多年即将修炼成精的工程师来说,我心中是有很清晰的模板和套路的,这种模板和套路是经过多年沉淀下来的经验。比如,定时中断函数尽量放一些精简的计时器代码,一般不调用函数,但是“输入IO口的消抖动”(按键扫描)以及“蜂鸣器鸣叫”这两类特殊函数我是喜欢破例放在定时中断函数里调用的。定时中断如何产生时间,这个时间如何跟主函数关联起来,请看下面的框架代码:
- volatile unsigned char vGu8TimeFlag=0; //互斥量变量标志
- volatile unsigned int vGu16TimeCnt=0; //计时器变量
- void main()
- {
- vGu8TimeFlag=0; //在“写操作”vGu16TimeCnt全局变量之前,互斥量vGu8TimeFlag的“加锁”
- vGu16TimeCnt=1000; //全局变量的赋值,就是“写操作”
- vGu8TimeFlag=1; //互斥量vGu8TimeFlag的“解锁”。同时也起到“启动计时器”的开关作用
- while(1) //主循环
- {
- if(0==vGu16TimeCnt) //时间变量为0则表示时间到了
- {
- ...在这里执行具体的功能代码
- }
- }
- }
- void T0_time() interrupt 1 //每1ms中断一次的定时中断函数
- {
- if(1==vGu8TimeFlag&&vGu16TimeCnt>0) //判断vGu8TimeFlag是否等于1,就是互斥量的判断。
- {
- vGu16TimeCnt--; //“自减一”的操作
- }
- }
复制代码
分析:上述代码中,vGu8TimeFlag是一箭双雕,既起到互斥量的作用,也起到了计数器vGu16TimeCnt开始计时的启动开关作用。
【86.3 练习例程。】
现在根据上述程序框架,编写一个LED灯闪烁的程序。
图86.3.1 灌入式驱动8个LED
- #include "REG52.H"
- #define BLINK_TIME 500 //时间是500ms
- sbit P0_0=P0^0;
- volatile unsigned char vGu8TimeFlag=0; //互斥量变量标志
- volatile unsigned int vGu16TimeCnt=0; //计时器变量
- unsigned char Gu8Step=0; //switch的切换步骤
- void main()
- {
- TMOD=0x01; //设置定时器0为工作方式1
- TH0=0xfc; //产生1ms中断的TH0初始值
- TL0=0x66; //产生1ms中断的TL0初始值
- EA=1; //开总中断
- ET0=1; //允许定时0的中断
- TR0=1; //启动定时0的中断
- while(1) //主循环
- {
- switch(Gu8Step)
- {
- case 0:
- if(0==vGu16TimeCnt) //时间到
- {
- P0_0=0; //LED灯亮
- vGu8TimeFlag=0; //互斥量“加锁”
- vGu16TimeCnt=BLINK_TIME; //计时器的写操作。设定计时的长度
- vGu8TimeFlag=1; //互斥量“解锁”,同时蕴含了计时器“启动”的动作
- Gu8Step=1; //切换到case 1这个步骤
- }
- break;
- case 1:
- if(0==vGu16TimeCnt) //时间到
- {
- P0_0=1; //LED灯灭。
- vGu8TimeFlag=0; //互斥量“加锁”
- vGu16TimeCnt=BLINK_TIME; //计时器的写操作。设定计时的长度
- vGu8TimeFlag=1; //互斥量“解锁”,同时蕴含了计时器“启动”的动作
- Gu8Step=0; //切换到case 0这个步骤,依次循环
- }
- break;
- }
- }
- }
- void T0_time() interrupt 1 //定时器0的中断函数,每1ms单片机自动执行一次此函数
- {
- if(1==vGu8TimeFlag&&vGu16TimeCnt>0) //判断vGu8TimeFlag是否等于1,就是互斥量的判断
- {
- vGu16TimeCnt--; //“自减一”的操作
- }
- TH0=0xfc; //重装初值,不能忘
- TL0=0x66; //重装初值,不能忘
- }
复制代码
【86.4 解决闪烁出现不规则“非对称感”现象的方法。】
上述例子,实验现象应该是LED闪烁很有规则的每1s闪烁一次,但是也有一部分初学者可能会遇到闪烁出现不规则“非对称感”的现象,这个问题的解决办法如下:在keil2的project下拉菜单下,选择Options for Target选项,弹出的窗口中,切换到Target选项,在Memory Model选项中选择small:variables in Data。
图86.4.1 设置窗口
作者: jianhong_wu 时间: 2017-9-19 09:54
本帖最后由 jianhong_wu 于 2017-9-19 10:18 编辑
第八十七节: 一个定时中断产生N个软件定时器。
第八十七节_pdf文件.pdf
(84.23 KB, 下载次数: 1297)
【87.1 信手拈来的软件定时器。】
初学者会疑惑,51单片机只有2个定时器T0和T1,是不是太少了一点?2个定时器怎能满足实际项目的需要,很多项目涉及到的定时器往往十几个,怎么办?这个问题的奥秘就在本节的内容。
51单片机内置的2个定时器T0和T1,是属于硬件定时器,硬件定时器是一个母体,它可以孕育出N个软件定时器,实际项目中,我们需要多少个定时器只需要从同一个硬件定时器中断里构造出对应数量的软件定时器即可。构造N个软件定时器的框架如下:
- //“软件定时器1”的相关变量
- volatile unsigned char vGu8TimeFlag_1=0;
- volatile unsigned int vGu16TimeCnt_1=0;
- //“软件定时器2”的相关变量
- volatile unsigned char vGu8TimeFlag_2=0;
- volatile unsigned int vGu16TimeCnt_2=0;
- //“软件定时器3”的相关变量
- volatile unsigned char vGu8TimeFlag_3=0;
- volatile unsigned int vGu16TimeCnt_3=0;
- void main()
- {
- vGu8TimeFlag_1=0;
- vGu16TimeCnt_1=1000; //“软件定时器1”的定时时间是1000ms
- vGu8TimeFlag_1=1;
- vGu8TimeFlag_2=0;
- vGu16TimeCnt_2=500; //“软件定时器2”的定时时间是500ms
- vGu8TimeFlag_2=1;
- vGu8TimeFlag_3=0;
- vGu16TimeCnt_3=250; //“软件定时器3”的定时时间是250ms
- vGu8TimeFlag_3=1;
- while(1) //主循环
- {
- if(0==vGu16TimeCnt_1) //“软件定时器1”的时间到了
- {
- ...在这里执行具体的功能代码
- }
- if(0==vGu16TimeCnt_2) //“软件定时器2”的时间到了
- {
- ...在这里执行具体的功能代码
- }
- if(0==vGu16TimeCnt_3 //“软件定时器3”的时间到了
- {
- ...在这里执行具体的功能代码
- }
- }
- }
- void T0_time() interrupt 1 //每1ms中断一次的定时中断函数
- {
- if(1==vGu8TimeFlag_1&&vGu16TimeCnt_1>0) //在定时中断里衍生出“软件定时器1”
- {
- vGu16TimeCnt_1--;
- }
- if(1==vGu8TimeFlag_2&&vGu16TimeCnt_2>0) //在定时中断里衍生出“软件定时器2”
- {
- vGu16TimeCnt_2--;
- }
- if(1==vGu8TimeFlag_3&&vGu16TimeCnt_3>0) //在定时中断里衍生出“软件定时器3”
- {
- vGu16TimeCnt_3--;
- }
- //按上面的套路继续写,可以衍生出N个“软件定时器”,只要不超过单片机的RAM和ROM。
- }
复制代码
【87.2 练习例程。】
现在根据上述程序框架,编写3个LED灯闪烁的程序。第1个LED灯的一闪一灭的周期是2秒,第2个LED灯的一闪一灭的周期是1秒,第3个LED灯一闪一灭的周期是0.5秒。这3个灯的闪烁频率是不一样的,因此需要3个软件定时器。该例子其实也是一个多任务并行处理的典型例子,这3个LED灯就代表3个不同的任务,它们之间是通过switch这个关键语句进行多任务并行处理的。switch的精髓在于根据某个特定条件切换到对应的步骤(或称“跳转到对应的步骤”)。
图87.2.1 灌入式驱动8个LED
作者: jianhong_wu 时间: 2017-9-24 10:18
本帖最后由 jianhong_wu 于 2017-9-24 10:32 编辑
第八十八节: 两大核心框架理论(四区一线,switch外加定时中断)。
第八十八节_pdf文件.pdf
(91.21 KB, 下载次数: 1313)
【88.1 四区一线。】
提出“四区一线”理论,主要方便初学者理解单片机程序大概的“空间分区”。
“四区”代表四大主流函数,分别是:系统初始化函数,外设初始化函数,主程序的任务函数,定时中断函数。
“一线”是指“系统初始化函数”与“外设初始化函数”的“分割线”,这个“分割线”是一个delay的延时函数。
“四区一线”的布局如下:
- void main()
- {
- SystemInitial(); //“四区一线”的“第一区”
- Delay(10000); //“四区一线”的“一线”
- PeripheralInitial(); //“四区一线”的“第二区”
- while(1) //主循环
- {
- LedService(); //“四区一线”的“第三区”
- KeyService(); //“四区一线”的“第三区”
- UsartService(); //“四区一线”的“第三区”
- ... //凡是在主循环里的函数都是属于“第三区”
- }
- }
- void T0_time() interrupt 1 //“四区一线”的“第四区”
- {
- }
复制代码
“第一区”的函数SystemInitial(),是一个系统的初始化函数,专门用来初始化单片机自己的寄存器以及个别外围要求响应速度快的输出设备,防止刚上电之后,由于输出IO口电平状态不确定而导致外围设备误动作,比如驱动继电器的误动作等等。
“一线”的函数Delay(10000),是一个延时函数,为什么这里要插入一个延时函数?主要目的是为接下来的PeripheralInitial()做准备的。上电后先延时一段时间,再执行PeripheralInitial()函数,因为PeripheralInitial()函数专门用来初始化不要求上电立即处理的外设芯片和模块。比如液晶模块,AT24C02存储芯片,DS1302时钟芯片,等等。这些芯片在上电的瞬间,内部自身的复位需要一点时间,以及外部电压稳定也需要一点时间,只有过了这一点时间,这些芯片才处于正常的工作状态,这个时候单片机才能跟它正常通信,所以“一线”函数Delay(10000)的意义就在这里。
“第二区”的函数PeripheralInitial(),是一个外设的初始化函数。专门用来初始化不要求上电立即处理的外设芯片和模块。
“第三区”的函数LedService(),KeyService(),UsartService(),等等,是一些在主循环里不断扫描的任务函数。
“第四区”的函数void T0_time() interrupt 1,是一个定时中断函数,一个系统必须标配一个定时中断函数才算完美齐全,这个中断函数提供系统的节拍时间,以及处理扫描一些跟IO口消抖动相关的函数,以及跟蜂鸣器驱动相关的函数。
【88.2 switch外加定时中断。】
提出“switch外加定时中断”理论,主要方便初学者理解单片机程序大概的“逻辑框架”。
switch是一个万能语句,它外加while与for循环就可以做任何复杂的算法,比如,搜索算法,运动算法,提取关键词算法,等等。它外加定时中断,就可以搭建一个系统的基本框架。比如,做通信的程序框架,人机界面的程序框架,按键服务的程序框架,等等。switch的精髓在于“根据条件进行步骤的灵活切换”。具体内容请看本节的练习程序。
【88.3 练习例程。】
根据上述的两大核心框架理论,编写1个LED灯闪烁的程序。
图88.3.1 灌入式驱动8个LED
- #include "REG52.H"
- void T0_time();
- void SystemInitial(void) ;
- void Delay(unsigned long u32DelayTime) ;
- void PeripheralInitial(void) ;
- void LedService(void);
- #define BLINK_TIME_1 1000
- sbit P0_0=P0^0;
- volatile unsigned char vGu8TimeFlag_1=0;
- volatile unsigned int vGu16TimeCnt_1=0;
- void main()
- {
- SystemInitial(); //“四区一线”的“第一区”
- Delay(10000); //“四区一线”的“一线”
- PeripheralInitial(); //“四区一线”的“第二区”
- while(1) //主循环
- {
- LedService(); //“四区一线”的“第三区”
- }
- }
- void T0_time() interrupt 1 //“四区一线”的“第四区”
- {
- if(1==vGu8TimeFlag_1&&vGu16TimeCnt_1>0)
- {
- vGu16TimeCnt_1--;
- }
- TH0=0xfc;
- TL0=0x66;
- }
- void SystemInitial(void)
- {
- TMOD=0x01;
- TH0=0xfc;
- TL0=0x66;
- EA=1;
- ET0=1;
- TR0=1;
- }
- void Delay(unsigned long u32DelayTime)
- {
- for(;u32DelayTime>0;u32DelayTime--);
- }
- void PeripheralInitial(void)
- {
- }
- void LedService(void)
- {
- static unsigned char Su8Step=0; //加static修饰的局部变量,每次进来都会保留上一次值。
- switch(Su8Step)
- {
- case 0:
- if(0==vGu16TimeCnt_1) //时间到
- {
- P0_0=0;
- vGu8TimeFlag_1=0;
- vGu16TimeCnt_1=BLINK_TIME_1; //重装定时的时间
- vGu8TimeFlag_1=1;
- Su8Step=1; //切换到下一个步骤,精髓语句!
- }
- break;
- case 1:
- if(0==vGu16TimeCnt_1) //时间到
- {
- P0_0=1;
- vGu8TimeFlag_1=0;
- vGu16TimeCnt_1=BLINK_TIME_1; //重装定时的时间
- vGu8TimeFlag_1=1;
- Su8Step=0; //返回到上一个步骤,精髓语句!
- }
- break;
- }
- }
复制代码
作者: jianhong_wu 时间: 2017-10-2 11:45
本帖最后由 jianhong_wu 于 2017-10-2 12:01 编辑
第八十九节: 跑马灯的三种境界。
第八十九节_pdf文件.pdf
(105.25 KB, 下载次数: 1253)
【89.1 跑马灯的三种境界。】
跑马灯也称为流水灯,排列的几个LED依次循环的点亮和熄灭,给人“跑动起来”的感觉,故称为“跑马灯”。实现跑马灯的效果,编程上有三种思路,分别代表了跑马灯的三种境界,分别是:移位阻塞,移位非阻塞,状态切换非阻塞。
图89.1.1 灌入式驱动8个LED
本节用的是8个LED灯依次挨个熄灭点亮,如上图所示。
【89.2 移位阻塞。】
移位阻塞,“移位”用的是C语言的左移或者右移语句,“阻塞”用的是delay延时。代码如下:
- #include "REG52.H"
- void T0_time();
- void SystemInitial(void) ;
- void Delay(unsigned long u32DelayTime) ;
- void PeripheralInitial(void) ;
- void LedTask(void);
- void main()
- {
- SystemInitial();
- Delay(10000);
- PeripheralInitial();
- while(1)
- {
- LedTask();
- }
- }
- void T0_time() interrupt 1
- {
- TH0=0xfc;
- TL0=0x66;
- }
- void SystemInitial(void)
- {
- TMOD=0x01;
- TH0=0xfc;
- TL0=0x66;
- EA=1;
- ET0=1;
- TR0=1;
- }
- void Delay(unsigned long u32DelayTime)
- {
- for(;u32DelayTime>0;u32DelayTime--);
- }
- void PeripheralInitial(void)
- {
- }
- //跑马灯的任务程序
- void LedTask(void)
- {
- static unsigned char Su8Data=0x01; //加static修饰的局部变量,每次进来都会保留上一次值。
- static unsigned char Su8Cnt=0; //加static修饰的局部变量,每次进来都会保留上一次值。
- P0=Su8Data; //Su8Data的8个位代表8个LED的状态,0为点亮,1为熄灭。
- Delay(10000) ; //阻塞延时
- Su8Data=Su8Data<<1; //左移一位
- Su8Cnt++; //计数器累加1
- if(Su8Cnt>=8) //移位大于等于8次后,重新赋初值
- {
- Su8Cnt=0;
- Su8Data=0x01; //重新赋初值,继续下一次循环移动
- }
- }
复制代码
分析总结:这是第1种境界的跑马灯,这种思路虽然实现了跑马灯的效果,但是因为“阻塞延时”,整个程序显得僵硬机械,缺乏多任务并行的框架。
【89.3 移位非阻塞。】
移位非阻塞,“移位”用的是C语言的左移或者右移语句,“非阻塞”用的是定时中断衍生出来的软件定时器。代码如下:
- #include "REG52.H"
- void T0_time();
- void SystemInitial(void) ;
- void Delay(unsigned long u32DelayTime) ;
- void PeripheralInitial(void) ;
- void LedTask(void);
- #define BLINK_TIME_1 1000
- volatile unsigned char vGu8TimeFlag_1=0;
- volatile unsigned int vGu16TimeCnt_1=0;
- void main()
- {
- SystemInitial();
- Delay(10000);
- PeripheralInitial();
- while(1)
- {
- LedTask();
- }
- }
- void T0_time() interrupt 1
- {
- if(1==vGu8TimeFlag_1&&vGu16TimeCnt_1>0) //软件定时器
- {
- vGu16TimeCnt_1--;
- }
- TH0=0xfc;
- TL0=0x66;
- }
- void SystemInitial(void)
- {
- TMOD=0x01;
- TH0=0xfc;
- TL0=0x66;
- EA=1;
- ET0=1;
- TR0=1;
- }
- void Delay(unsigned long u32DelayTime)
- {
- for(;u32DelayTime>0;u32DelayTime--);
- }
- void PeripheralInitial(void)
- {
- }
- //跑马灯的任务程序
- void LedTask(void)
- {
- static unsigned char Su8Data=0x01; //加static修饰的局部变量,每次进来都会保留上一次值。
- static unsigned char Su8Cnt=0; //加static修饰的局部变量,每次进来都会保留上一次值。
- if(0==vGu16TimeCnt_1) //时间到
- {
- vGu8TimeFlag_1=0;
- vGu16TimeCnt_1=BLINK_TIME_1; //重装定时的时间
- vGu8TimeFlag_1=1;
- P0=Su8Data; //Su8Data的8个位代表8个LED的状态,0为点亮,1为熄灭。
- Su8Data=Su8Data<<1; //左移一位
- Su8Cnt++; //计数器累加1
- if(Su8Cnt>=8) //移位大于等于8次后,重新赋初值
- {
- Su8Cnt=0;
- Su8Data=0x01; //重新赋初值,继续下一次循环移动
- }
- }
- }
复制代码
分析总结:这是第2种境界的跑马灯,这种思路虽然实现了跑马灯的效果,也用到了多任务并行处理的基本元素“软件定时器”,但是因为还停留在“移位”语句的阶段,此时的程序并没有超越跑马灯本身,跑马灯还是跑马灯,处于“看山还是山”的境界。
【89.4 状态切换非阻塞。】
状态切换非阻塞,“状态切换”用的是switch语句中根据特定条件进行步骤切换,“非阻塞”用的是定时中断衍生出来的软件定时器。代码如下:
分析总结:这是第3种境界的跑马灯,很多初学者咋看此程序,表示不理解,人家一条赋值语句就解决8个LED一次性显示的问题,你非要拆分成8条按位赋值的语句,人家只用一个判断就实现了LED灯移动显示的功能,你非要整出8个步骤的切换,况且,整个程序的代码量明显增加了很多,这个程序好在哪?其实,我这么做是用心良苦呀。这个程序的代码量虽然增多了,但是仔细一看,并没有影响运行的效率。之所以把8个LED灯拆分成一个一个的LED灯单独赋值显示,是因为,在我眼里,这个8个LED灯代表的不仅仅是LED灯,而是8个输出信号!这8个输出信号未来驱动的可能是不同的继电器,气缸,电机,大炮,导弹,以及它们的各种千变万化的组合逻辑,拆分之后程序框架就有了无限可能的扩展性。之所以整出8个步骤的切换,也是同样的道理,为了增加程序框架无限可能的扩展性。这个程序虽然表面看起来繁琐,但是仔细一看它是“多而不乱”,非常富有“队形感”。因此可以这么说,这个看似繁琐的跑马灯程序,其实背后蕴藏了编程界的大智慧,它已经突破了“看山还是山”的境界。
作者: jianhong_wu 时间: 2017-10-9 09:51
本帖最后由 jianhong_wu 于 2017-10-9 10:10 编辑
第九十节: 多任务并行处理两路跑马灯。
第九十节_pdf文件.pdf
(95.16 KB, 下载次数: 1176)
【90.1 多任务并行处理。】
两路速度不同的跑马灯,代表了两路独立运行的任务,单片机如何“并行”处理这两路任务,就涉及到“多任务并行处理的编程思路”。
上图90.1.1 灌入式驱动8个LED 第1路跑马灯
上图90.1.2 灌入式驱动4个LED 新增加的第2路跑马灯
如上图,本节特别值得一提的是,新增加的第2路跑马灯用的是4个LED,这4个LED的驱动IO口是“散装的”,因为,前面3个是P1口的(P1.4,P1.5,P1.6),最后1个是P3口的(P3.3),这种情况下,肯定用不了“移位”的处理思路,只能用跑马灯第3种境界里所介绍的“状态切换非阻塞”思路,可见,“IO口拆分”和“switch状态切换”又一次充分体现了它们“程序框架万能扩展”的优越性。代码如下:
作者: jianhong_wu 时间: 2017-10-17 17:51
本帖最后由 jianhong_wu 于 2017-10-17 18:07 编辑
第九十一节: 蜂鸣器的“非阻塞”驱动。
第九十一节_pdf文件.pdf
(81.38 KB, 下载次数: 1160)
【91.1 蜂鸣器的硬件电路简介。】
上图91.1.1 PNP三极管驱动有源蜂鸣器
蜂鸣器有两种,一种是有源蜂鸣器,一种是无源蜂鸣器。有源蜂鸣器的驱动最简单,只要通电就一直响,断电就停,跟驱动LED灯一样。无源蜂鸣器则不一样,无源蜂鸣器一直断电不响,奇怪的是一直通电也不响,只有“通,关,通,关...”反复通电关电的状态,才会持续发生稳定的声音,此方式称为脉冲驱动方式,或者PWM驱动方式。本教程用的是有源蜂鸣器。
蜂鸣器的驱动电路也有两种常用的方式,一种是NPN三极管驱动,一种是PNP三极管驱动。NPN三极管驱动电路,单片机输出“1”(高电平)蜂鸣器导通,输出“0”(低电平)蜂鸣器关闭。而PNP三极管驱动电路恰恰相反,单片机输出“0”(低电平)蜂鸣器导通,输出“1”(高电平)蜂鸣器关闭。本教程所用的是PNP三极管驱动电路,如上图。
【91.2 “非阻塞”驱动程序。】
“驱动层”是相对“应用层”而言。“应用层”发号施令,“驱动层”负责执行。一个好的“驱动层”必须给“应用层”提供快捷便利的调用接口,此接口可以是函数或者全局变量。本节驱动蜂鸣器所用的是全局变量vGu16BeepTimerCnt。“应用层”只需给vGu16BeepTimerCnt赋值,就可以控制蜂鸣器发声,赋值越大,发声越长,500代表发声500ms,1000代表发声1000ms,具体细节实现,则由“驱动层”的驱动函数负责执行,驱动函数放在定时中断函数里定时扫描。为什么不把驱动函数放到main函数的循环里去?因为放在定时中断里,能保证蜂鸣器的声音长度是一致的,如果放在main循环里,声音的长度有可能在某些项目中受到某些必须一气呵成的任务干扰,得不到及时响应,影响声音长度的一致性。下面代码实现的功能是,单片机只要一上电,蜂鸣器就发出一次1000ms长度的“嘀”声音。
作者: jianhong_wu 时间: 2017-10-25 11:11
本帖最后由 jianhong_wu 于 2017-10-28 09:07 编辑
第九十二节: 独立按键的四大要素(自锁,消抖,非阻塞,清零式滤波)。
第九十二节_pdf文件.pdf
(116.24 KB, 下载次数: 1076)
【92.1 独立按键的硬件电路简介。】
上图92.1.1 独立按键电路
按键有两种驱动方式,一种是独立按键,一种是矩阵按键。1个独立按键要占用1个IO口,IO口不能共用。而矩阵按键的IO口是分时片选复用的,用少量的IO口就可以驱动翻倍级别的按键数量。比如,用8个IO口只能驱动8个独立按键,但是却可以驱动16个矩阵按键(4x4)。因此,按键少的时候就用独立按键,按键多的时候就用矩阵按键。这两种按键的驱动本质是一样的,都是靠识别输入信号的下降沿(或上升沿)来识别按键的触发。
独立按键的硬件原理基础,如上图,P2.2这个IO口,在按键K1没有被按下的时候,P2.2口因为单片机内部自带上拉电阻把电平拉高,此时P2.2口是高电平的输入状态。当按键K1被按下的时候,按键K1左右像一根导线连接到电源的负极(GND),直接把原来P2.2口的电平拉低,此时P2.2口变成了低电平的输入状态。编写按键驱动程序,就是要识别这个电平从高到低的过程,这个过程也叫下降沿。多说一句,51单片机的P1,P2,P3口是内部自带上拉电阻的,而P0口是内部没有上拉电阻的,需要外接上拉电阻。除此之外,很多单片机内部其实都没有上拉电阻的,因此,建议大家在做独立按键电路的时候,养成一个习惯,凡是按键输入状态都外接上拉电阻。
识别按键的下降沿触发有四大要素:自锁,消抖,非阻塞,清零式滤波。
“自锁”,按键一旦进入到低电平,就要“自锁”起来,避免不断触发按键,只有当按键被松开变成高电平的时候,才及时“解锁”为下一次触发做准备。
“消抖”,按键是一个机械触点器件,在接触的瞬间必然存在微观上的机械抖动,反馈到电平的瞬间就是“高,低,高,低...”这种不稳定的电平状态是一种干扰,但是,按键一旦按下去稳定了之后,这种状态就消失,电平就一直保持稳定的低电平。消抖的本质就是滤波,要把这种接触的瞬间抖动过滤掉,避免按键的“一按多触发”。
“非阻塞”,在处理消抖的时候,必须用到延时,如果此时用阻塞的delay延时就会影响其它任务的运行效率,因此,用非阻塞的定时延时更加有优越性。
“清零式滤波”,在消抖的时候,有两种境界,第一种境界是判断两次电平的状态,中间插入“固定的时间”延时,这种方法前后一共判断了两次,第一次是识别到低电平就进入延时的状态,第二次是延时后再确认一次是否继续是低电平的状态,这种方法的不足是,“固定的时间”全凭经验值,但是不同的按键它们的抖动时间长度是不同的,除此之外,前后才判断了两次,在软件的抗干扰能力上也弱了很多,“密码等级”不够高。第二种境界就是“清零式滤波”,“清零式滤波”非常巧妙,抗扰能力超强,它能自动过滤不同按键的“抖动时间”,然后再进入一个“稳定时间”的“N次识别判断”,更加巧妙的是,在“抖动时间”和“稳定时间”两者时间内,只要发现一次是高电平的干扰,就马上自动清零计时器,重新开始计时。“稳定时间”一般取20ms到30ms之间,而“抖动时间”是隐藏的,在代码上并没有直接描写出来,但是却无形地融入了代码之中,只有慢慢体会才能发现它的存在。
具体的代码如下,实现的功能是按一次K1或者K2按键,就触发一次蜂鸣器鸣叫。
【92.2 专题分析:else if(0==Su8KeyLock1)。】
疑问:
- if(0!=KEY_INPUT1)
- {
- Su8KeyLock1=0;
- Su16KeyCnt1=0;
- }
- else if(0==Su8KeyLock1)//有按键按下,且是第一次被按下。为什么?为什么?为什么?
- {
- Su16KeyCnt1++;
- if(Su16KeyCnt1>KEY_FILTER_TIME)
- {
- Su8KeyLock1=1;
- vGu8KeySec=1;
- }
- }
复制代码
解答:
首先,我们要明白C语言的语法中,
- if(条件1)
- {
- }
- else if(条件2)
- {
- }
复制代码
以上语句是一对组合语句,不能分开来看。当(条件1)成立的时候,它是绝对不会判断(条件2)的。当(条件1)不成立的时候,才会判断(条件2)。
回到刚才的问题,当程序执行到(条件2) else if(0==Su8KeyLock1)的时候,就已经默认了(条件1) if(0!=KEY_INPUT1)不成立,这个条件不成立,就意味着0==KEY_INPUT1,也就是有按键被按下,因此,这里的else if(0==Su8KeyLock1)等效于else if(0==Su8KeyLock1&&0==KEY_INPUT1),而Su8KeyLock1是一个自锁标志位,一旦按键被触发后,这个标志位会变1,防止按键按住不松手的时候不断触发按键。这样,按键只能按一次触发一次,松开手后再按一次,又触发一次。
【92.3 专题分析:if(0!=KEY_INPUT1)。】
疑问:为什么不用if(1==KEY_INPUT1)而用if(0!=KEY_INPUT1)?
解答:其实两者在功能上是完全等效的,在这里都可以用。之所以本教程优先选用后者if(0!=KEY_INPUT1),是因为考虑到了代码在不同单片机平台上的可移植性和兼容性。很多32位的单片机提供的是库函数,库函数返回的按键状态是一个字节变量来表示,当被按下的时候是0,但是,当没有按下的时候并不一定等于1,而是一个“非0”的数值。
【92.4 专题分析:把KeyScan函数放在定时器中断里。】
疑问:为什么把KeyScan函数放在定时器中断里?
解答:中断函数里放的函数或者代码越少越好,但是KeyScan函数是特殊的函数,是涉及到IO口输入信号的滤波,滤波就涉及到时间的及时性与均匀性,放在定时中断函数里更加能保证时间的一致性。比如,蜂鸣器驱动,动态数码管驱动,按键扫描驱动,我个人都习惯放在定时中断函数里。
【92.5 专题分析:if(0==vGu8KeySec)return。】
疑问:if(0==vGu8KeySec)return是不是多此一举?
解答:在KeyTask函数这里,if(0==vGu8KeySec)return这行代码删掉,对程序功能是没有影响的,这里之所以多插入这行判断语句,是因为,当按键多达几十个的时候,避免主函数每次进入KeyTask函数,都挨个扫描判断switch的状态进行多次判断,如果增加了这行if(0==vGu8KeySec)return代码,就可以直接退出省事,在理论上感觉更加运行高效。其实,不同单片机不同的C编译器可能对switch语句的翻译不一样,因此,这里的是不是更加高效我不敢保证。但是可以保证的是,加了这行代码也没有其它副作用。
作者: jianhong_wu 时间: 2017-10-29 10:46
本帖最后由 jianhong_wu 于 2017-10-29 11:45 编辑
第九十三节: 独立按键鼠标式的单击与双击。
第九十三节_pdf文件.pdf
(111.12 KB, 下载次数: 1140)
【93.1 鼠标式的单击与双击。】
上图93.1.1 独立按键电路
上图93.1.2 LED电路
上图93.1.3 有源蜂鸣器电路
鼠标的左键,可以触发单击,也可以触发双击。双击的规则是这样的,两次单击,如果第1次单击与第2次单击的时间比较“短”的时候,则这两次单击就构成双击。编写这个程序的最大亮点是如何控制好第1次单击与第2次单击的时间间隔。程序例程要实现的功能是:(1)单击改变LED灯的显示状态,单击一次LED从原来“灭”的状态变成“亮”的状态,或者从原来“亮”的状态变成“灭”的状态,依次循环切换。(2)双击则蜂鸣器发出“嘀”的一声。代码如下:
作者: jianhong_wu 时间: 2017-11-5 11:14
本帖最后由 jianhong_wu 于 2017-11-5 11:30 编辑
第九十四节: 两个独立按键构成的组合按键。
第九十四节_pdf文件.pdf
(109.85 KB, 下载次数: 1118)
【94.1 组合按键。】
上图94.1.1 独立按键电路
上图94.1.2 LED电路
上图94.1.3 有源蜂鸣器电路
组合按键的触发,是指两个按键同时按下时的“非单击”触发。一次组合按键的产生,必然包含了三类按键的触发。比如,K1与K2两个独立按键,当它们产生一次组合按键的操作时,就包含了三类触发:K1单击触发,K2单击触发,K1与K2的组合触发。这三类触发可以看作是底层的按键驱动程序,在按键应用层的任务函数SingleKeyTask和CombinationKeyTask中,可以根据项目的实际需要进行响应。本节程序例程要实现的功能是:(1)K1单击让LED变成“亮”的状态。(2)K2单击让LED变成“灭”的状态。(3)K1与K2的组合按键触发让蜂鸣器发出“嘀”的一声。代码如下:
【94.2 专题分析:else if(0==Su8CombinationKeyLock)。】
疑问:
- if(0!=KEY_INPUT1||0!=KEY_INPUT2)
- {
- Su8CombinationKeyLock=0;
- Su16CombinationKeyCnt=0;
- }
- else if(0==Su8CombinationKeyLock)//两个按键被同时按下,且是第一次被按下。为什么?
- {
- Su16CombinationKeyCnt++;
- if(Su16CombinationKeyCnt>=KEY_FILTER_TIME)
- {
- Su8CombinationKeyLock=1;
- vGu8CombinationKeySec=1;
- }
- }
复制代码
解答:
首先,我们要明白C语言的语法中,
- if(条件1)
- {
- }
- else if(条件2)
- {
- }
复制代码
以上语句是一对组合语句,不能分开来看。当(条件1)成立的时候,它是绝对不会判断(条件2)的。当(条件1)不成立的时候,才会判断(条件2)。
回到刚才的问题,当程序执行到(条件2) else if(0==Su8CombinationKeyLock)的时候,就已经默认了(条件1) if(0!=KEY_INPUT1||0!=KEY_INPUT2)不成立,这个条件不成立,就意味着0==KEY_INPUT1和0==KEY_INPUT2,也就是有两个按键被同时按下,因此,这里的else if(0==Su8CombinationKeyLock)等效于else if(0==Su8CombinationKeyLock&&0==KEY_INPUT1&&0==KEY_INPUT2),而Su8CombinationKeyLock是一个自锁标志位,一旦组合按键被触发后,这个标志位会变1,防止两个按键按住不松手的时候不断触发组合按键。这样,组合按键只能同时按下一次触发一次,任意松开其中一个按键后再同时按下一次两个按键,又触发一次新的组合按键。
作者: jianhong_wu 时间: 2017-11-12 15:22
本帖最后由 jianhong_wu 于 2017-11-12 15:37 编辑
第九十五节: 两个独立按键的“电脑键盘式”组合按键。
第九十五节_pdf文件.pdf
(106.57 KB, 下载次数: 1051)
【95.1 “电脑键盘式”组合按键。】
上图95.1.1 独立按键电路
上图95.1.2 LED电路
上图95.1.3 有源蜂鸣器电路
上一节也讲了由K1和K2构成的组合按键,但是这种组合按键是普通的组合按键,因为它们的K1和K2是不分先后顺序的,你先按住K1然后再按K2,或者你先按住K2然后再按K1,效果都一样。本节讲的组合按键是分先后顺序的,比如,像电脑的复制快捷键(Ctrl+C),你必须先按住Ctrl再按住C此时“复制快捷键”才有效,如果你先按住C再按住Ctrl此时“复制快捷键”无效。本节讲的例程就是要实现这个功能,用K1代表C这类“字符数字键”,用K2代表Ctrl这类“辅助按键”,因此,要触发组合键(K2+K1),必须先按住K2再按K1才有效。本节讲的例程功能如下:(1)K1每单击一次,LED要么从“灭”变成“亮”,要么从“亮”变成“灭”,在两种状态之间切换。(2)如果先按住K2再按K1,就认为构造了“电脑键盘式”组合键,蜂鸣器发出“嘀”的一声。代码如下:
作者: jianhong_wu 时间: 2017-11-19 10:57
本帖最后由 jianhong_wu 于 2017-11-19 11:30 编辑
第九十六节: 独立按键“一键两用”的短按与长按。
第九十六节_pdf文件.pdf
(106.69 KB, 下载次数: 1055)
【96.1 “一键两用”的短按与长按。】
上图96.1.1 独立按键电路
上图96.1.2 LED电路
上图96.1.3 有源蜂鸣器电路
某些项目,当外部按键的资源比较少的时候,一个按键也可以“一键多用”。“一键多用”有很多种玩法,比如,谍战片的无线电通信,依赖一个按键的“不同敲击频率”就可以发送内容丰富的情报。本节“一键两用”也是属于“一键多用”的众多玩法之一。“短按与长按”的原理是依赖“按键按下的时间长度”来区分识别。“短按”是指从按下的“下降沿”到松手的“上升沿”时间,“长按”是指从按下的“下降沿”到一直按住不松手的“低电平持续时间”。本节的例程功能如下:(1)K1每“短按”一次(25ms),LED要么从“灭”变成“亮”,要么从“亮”变成“灭”,在两种状态之间切换。(2)K1每“长按”一次(500ms),蜂鸣器发出“嘀”的一声。代码如下:
欢迎光临 独闷闷网 (http://dumenmen.com/) |
Powered by Discuz! X3.2 |