客观运行的结果是 GP1管脚电平不停地一会高,一会低, 就输出了方波信号. 本文引用地址:http://www.eepw.com.cn/article/201803/376687.htm要计算方波的频率,我们必须知道单片机每运行一条指令需要多少时间.这个时间的单位不以通常的秒 毫秒 或微秒作为单位, 而是以”机器周期” 为单位. 以后凡是我们讨论单片机内部的时间问题都要以机器周期作为时间单位. 至于一个机器周期究竟是多少微妙或毫秒, 取决于单片机的品牌和振荡频率频率大小, 等一会我们再用公式计算我们PIC12CE512在4MHz震荡频率下的机器周期是多少个微妙。 我们先看看我们的程序中GP1脚的高电平低电平都是用了多少个机器周期. PIC单片机所有指令都是单机器周期的指令, 例外的情况是goto 语句要用2个机器周期 还有一个call指令用的时间也不完全是一个机器周期(待后续) 其他品牌的某些单片机可不是这样,一条指令往往要用几个周期…… 从bsf 到bcf有8个指令,都是单周期指令,所以GP1高电平时间长度是8个机器周期 从bcf 到bsf有7条指令,其中6条是单周期指令 1条双周期指令(goto). 所以GP1低电平时间长度也是8个机器周期 这样,我们输出方波的周期长度就是16 个机器周期. Pic品牌的机器周期 = 4/振荡频率 (公式) 所以,在我们的例子当中 1个机器周期=4/4MHz= 1 uS 也就是说,我们的例子中,执行一条指令仅需要1微秒的时间. 这样,我们输出的方波周期就是16微秒, 频率是 f =1/16 =0.0625 兆赫 =62.5 KHz 如果这个方波的频率比较低,你再接一个扬声器到GP1脚上你就可以听到声音了 频率降低到几赫兹的时候, 接一个led灯, 就会不停的闪烁. 当然, 频率太低你用的nop指令的数目会很多,程序虽简单但是臃肿, 这没有关系,我们主要是在学习程序, 弄清楚道理是目的。 要想使得程序不臃肿我们有的是办法,这就必须再学习新的指令. 如果此前我讲的你基本都弄明白了,那你现在已经抓住单片机入门的门把手了, 还需轻轻的推开. 当你坐在家里吃着月饼,惬意地用电视遥控器选择电视频道,不停地用 +/- 键盘调节电视音量到合适的时候,你可曾想过,此时崂山也许正钻在在温度高达35摄氏度以上的树丛里,忍耐蚊子蚂蚁的叮咬,研究用什么样的通信线更好地防止雨水侵蚀和动物的啃咬。 也许你从没有留意你按下的节目频道、音量等这些 标有 + / - 符号的键盘是怎样工作控制大小的。 下面我们学习两个新指令 incf 和 decf ,它们都是对某一个寄存器进行增1 或减1 操作,例句中假如我们要操作的寄存器是 09H movlw 02H '常数2进入W movwf 09H '把w 内的数2 复制到09H 这个寄存器 '现在09H 寄存器内存储的数是2 incf 09H '寄存器09H内存储的数 增加1 '现在09H内存储的数变成3 decf 09H '寄存器09H内存储的数 减掉1 '现在09H内存储的数变成2 movlw 0FFH '常数255进入W movwf 09H '把w 内的数255 复制到09H 这个寄存器 '现在09H 寄存器内存储的数是255 incf 09H '寄存器09H内存储的数 增加1 '现在09H内存储的数变成0 decf 09H '寄存器09H内存储的数 减掉1 '现在09H内存储的数又变成255 如果你事先定义好了地址为09H 的这个寄存器里存储的数字大小,代表电视机节目频道的话,你会很喜欢这两个指令的。并且当节目频道到达最大值255 或最小值0的时候无需担心,寄存器在0时减1 会得255, 255状态下增1 会得0 至于为什么会这样,学过环形计数器的人不会感到奇怪的。你要是没有学过计数器电路也不要紧,记住一个寄存器的最大存储数值是255 = 0FFH 就可以了,加减法都会导致它“进位” 当然控制音量时这个程序不能使用,因为它在0和255之间变化,音量忽大忽小怎们行。 为解决这个问题, 我们必须再学习两条指令 incfsz 和 decfsz 它们与上两个功能基本相同,不同的是: 寄存器增1 或减1操作以后,该指令会自动判定寄存器内的结果是否为零,如果不为零,继续正常执行该指令后面的语句. 但如果结果为零的话,则程序会 "跳一步" .绕过紧挨着它下面的一条指令,继续执行更下面的语句,举例子说明 假定我们操作的寄存器还是09H: movlw 0FDH '常数253进入W movwf 09H '把w 内的数253 复制到09H 这个寄存器 '现在09H 寄存器内存储的数是253 incfsz 09H '寄存器09H内存储的数 增加1,结果变成254 结果不等于0,故程序继续执行下一指令 nop '该句得到执行(因为上一句寄存器09H的计算结果不等于0) incf 09H '寄存器09H内存储的数 增加1,结果是255 incfsz 09H '寄存器09H内存储的数 增加1,结果变成0 '因为结果等于0,故程序要跳过下面的一句(不运行下面的一句). incf 09H '由于上一句的存在并结果为0,该句得不到执行,被忽略 incf 09H '程序跳入这一句继续运行 寄存器09H内存储的数 增加1 nop '因此现在 09H寄存器存储的数是1 nop '继续运行 . . . . . 思考题:设计一段程序代码,当用户连续按下音量减小键后,判定音量寄存器09H的存储音量数值, 防止该寄存器的值从0 变成255,以免震惊到用户。 . . . . SMALL_SOUND: nop '标号可以任意写的,此前用户一旦按下音量减,就把程 ' 序引导到这一句上来 decfsz 09H '寄存器09H内存储的数 减1,如果结果为0 就跳一步 goto OK '如果上一句结果不为0,执行该句后,程序去了ok语句 movlw 01H '跳到这一步说明寄存器结果是0 movwf 09 '强行把 09H内的数值写成1,仍然是小音量,这样音量不会被因为 减小而变成255 OK: nop '继续运行 . . 思考题:利用decfsz 指令设计一段延时代码,使得延时时间可以在10个机器周期到65535个机器周期之间, 可以通过程序任意控制 在这个例子中,设我们要控制的延时时间大约是24086个及其周期,用16进制表是就是 5E16 H. 如果用到通用寄存器,请使用 0AH, 0BH yanshi: movlw 5EH ' 常数5E进W 标号是延时 movwf 0BH , '0B寄存器数为5EH movlw 16H '常数16进W movwf 0AH '0A寄存器数为16H jixu: decfsz 0AH '0A寄存器内的数减1,如果结果为0跳步 goto jixu '结果不为0,继续 decfsz 0BH '0B寄存器内的数减1,如果结果为0跳步 goto jixu '结果不为0,继续 nop '延时完毕 . . . . 你现在可以只用这几个简单句子完成任意时间的延时程序了。 下面介绍单片机汇编语言里的一个概念 “子程序” 下面我介绍 “子程序” 我先打个比方,如果你做一顿饭,要做汤,炒菜,炖鱼,汆丸子, 奥,忘了还有炒小螃蟹(大螃蟹现在都叫人吃的逮不着了:))期间有一个动作在我看来不断的重复,这个动作就是放盐 放盐的过程描述是这样的: 放盐: 用一把小勺子深入盐罐 舀出氯化钠适量 。 把小勺子里的氯化钠 均匀洒在锅里。 完毕 如果我们把做饭定义为主任务 那么放盐这个动作就叫做 子任务。 这样定义的一个好处就是描述主任务的时候比较方便,当你用语言文字描述主任务的时候,无论哪一道菜,到了该加盐的时候不必细说用一把小勺子深入盐罐...... 因为很多菜都有同样的这个过程,所以,你用 “放盐” 两个字就可以了。但是在你使用 放盐 这个词之前或者之后,你应该解释一下放盐 这个词的具体过程是什么。 我们单片机的程序也是一样的,如果你设计一个电视机的自动搜索频道的程序,程序要求电视机每搜索成功一个频道,它面板上的发光二极管就眨一次眼睛,也就说,先熄灭一段时间然后再点亮。这样就会遇到很多这样的眨眼动作,为了简化主程序我们可以把眨眼这样一个过程定义为一段子程序,以后每次遇到需要眨眼的时候就调用一次子程序就可以了。 子程序的定义是这样的 Zhayan: bcf GPIO,GP1 '管脚GP1输出低电平关闭LED灯 做为子程序标号是必须有的 标号 ' 就是子程序的名字 nop nop nop nop nop ... . bsf GPIO,GP1 '管脚GP1输出高电平点亮LED灯 nop nop nop nop ... return '这个命令表示子程序的结束 是必需的 否则这个子程序没有结束 这样,子程序就定义完了 如果想在程序的某个位置需要led灯熄灭以下(眨眼一次),只需在那个程序位置调用一下子程序就可以了。 调用的方法是用 call 命令。 主程序: .... '这些点点表示主程序里的语句 .... ...... ...... '这个位置搜所成功一个台 需要“眨眼”一次 call Zhayan ...... '继续搜索下一个台的命令行 ...... ...... ...... ...... '这个位置搜所成功一个台 需要“眨眼”一次 call Zhayan ...... '继续搜索下一个台的命令行 ...... ...... ...... '这个位置搜所成功一个台 需要“眨眼”一次 call Zhayan ...... '继续搜索下一个台的命令行 ...... 疑问1 我在一个主程序里固然可以调用另一个子程序,而我在一个子程序里能不能调用另一个子程序? 答 可以的,这叫子程序嵌套,甚至还可以在另一个子程序中再继续调用别的子程序。 疑问2 嗯,那继续往下调用下去,有限制么? 答 有,这叫允许嵌套的层数 每个品牌 型号的单片机允许的嵌套层数都是有规定的 例如pic16f74 允许8层 pic12e519 允许两层 也就是说pic12e519的主程序里可以调用子程序,子程序里海可以再调用子程序,到此为止不要再往下调用 了,否则程序报错或者超出你预计的结果。 疑问3 在同一层程序空间里,例如在我的某个子程序之中,调用另一个子程序的次数有限制么? 回答 没有限制,只要你的程序寄存器装得下你的程序。 疑问4 我听说单片机在调用子程序以前,好像需要程序“堆栈”访问什么的,要进行一些程序计数器的保存保护,以保证子程序返回来得时候,程序能够正确回到原来位置和环境。是这样的么? 答 pic单片机不用管这些问题,它是硬件自动完成这些堆栈的事情,我们的指令里不用关心这些。尽管如此,中档pic单片机的例如 pic16等系列,它们的程序存储器地址是分页的,尽量调用本页的子程序,如果子程序不在本页,而是在另一个页面里存放,你还是要告诉单片机你的子程序所在的页面数据的,具体操作指令可以查相关指令说明。我们的pic12c519的程序存储器,没有分页,不用关心这事。 继续讲 我们学习到这里,就已经初窥门庭了,下一步还有一个重要的关口-------中断 单片机的中断,概念并不难以理解。只是要真正理解运用编程处理一些实际中断的例子,却也不是很容易,甚至是单片机学习、入门的拦路虎。要想学会实际的中断处理编程,也还需要清楚一些程序存储器,程序结构,程序计数器,硬件堆栈,现场保护等这些个另杂碎概念。 因此,我们在学习中断以前,以后和学习中断过程中,都有必要介绍回顾复习一些有关上述关键词的概念和知识,否则,尽管你学了中断,用起来可还是不能得心应手,以至于茫然。 我还是用比喻的方法介绍一下中断的概念: 你的主程序任务是做一桌可口的饭菜,期间可能要多次调用子程序“撒盐”。 尽管子程序下边还有更小的子任务,比如“计算食盐的量”等过程,尽管这些子过程很复杂,但他们的出场时间和顺序是可以预料的,是可以预先安排的。也就说你肯定知道在什么时候放盐。 有一类子程序,他的出场时间是不确定的,突然的,处理他们的时间刻不容缓,必须赶紧的。我们称这一类子程序为 中断子程序。 也就是我们所说的 中断 你正在做菜的过程中,隔壁邻居小孩突然敲门说 他的二大爷在他房间里摔倒了 请你帮忙把二大爷扶起来。这是急迫的,必须处理的事务。 你肯定关掉炉子一溜烟跑出去帮忙,等回来以后再点着炉子继续做菜。 这个事件的特点就是发生的时间你无法预先知道,而这个任务必须得停下当前工作去处理,并且是刻不容缓。 从开始关炉子到回来点着炉子的这段时间里以及你的救人行为,就叫做 “中断子程序”。 在中断子程序过程中,你关炉子的动作,叫做“中断现场保护” 点着炉子叫做“中断现场恢复”中间走出去扶起隔壁二大爷到回来 叫做“中断任务处理” 小孩子敲门就叫做“中断请求” 这就是中断的基本概念。 在单片机里,中断的例子也是很多的。我举一个你手里的手机的例子,你的GSM手机正工作在赋闲,屏幕上也就显示个时间日期中国电信什么的,表面看没有什么。其实它内部的cpu高速运行忙碌地工作在诸如联络无线网络,查询是否有短消息发来,计算当前信号强度,时间等任务中。 你突然按下数字键“8”,此时内部cpu必须停下它正在干的工作来应付你,也就是清屏,显示你按下的数字8,然后再回到它原来的任务接着运行。(当然,这个例子不一恰当,现在有操作系统Windows-ce windows-mobile的手机的工作机制远没有如此的简单) 下面我们要接触和复习一些另杂碎,学习中断必须要弄明白单片机这些另杂碎,所以你还得忍耐他们一阵子。 再说这不是教材,只是想为入门学习指划个门径,我的帖子里面有很多细节错误,例如内存页面问题,519也是分页的。 但为了入门,我们还是不先不要理会这些,等入门以后,还有很多细节需要搞明白,那时候就容易啦。 这不,我也是现学现卖,在想写个具体中断代码的例子的时候,才现行的查阅了pic12f519 的数据手册,竟然没有查到中断方面的说明,感情这款芯片没有中断的功能!不会是我英文水平低没有看懂吧,又拿出来中文的,同样的,程序存储器里没有中断说明,只有复位(复位也是单片机的概念) 。 总之,我们学习中断代码,这款芯片不适合我们啦。 怎么办?只好换一款中档的型号:PIC16C74. 那位说从低端芯片一下子到中端芯片跨度太大啦吧,能适应吗。我回答:肯定能! 高端芯片无非腿脚更多,片内资源也多,但是原理和方法,和低端的没有区别。我们只要掌握了单片机的使用操作方法入门,慢说中档,就是高端芯片pic18、24、99999 系列,那也是一样。我们仍然可以钻进去,出得来。 其实啊,所谓高端的语句,学起来更简单和使用起来更方便。要实现同样功能,如果限定仅使用低端35条,反而会比较罗索。 下面以 PIC16C74 这款芯片为例,仍然不出35条基本指令,写出一个完整的中断代码的例子,注意这个例子程序的总体结构。题目要求: 1、当一个键盘按动一下后,中断主程序,改变某管脚上的一只LED灯的状态,如果再次按动,再次改变。 2、主程序实际上和我们的中断任务处理没有关系,我们可以随便写个任务,例如主任务程序是计算:123 + 45 = ? 在这个例子里,计算123 + 45 = ?相当于我们在做菜,突然有人按动按键,相当于小孩子敲门请求中断,那么改变(点亮或者熄灭)某管脚上的一只LED灯的状态,就是我们刻不容缓的拯救行动。 为了理解中断代码,我们先看看硬件设计,下面是这个例子的电路图。(缺) 如图:11、12 管脚接电源和地线,13、14管脚接振荡器, 管脚 1 是复位管脚,只要它是高电平,程序就运行,只要是低电平,程序就马上停止,并回到程序特定的开始位置,也叫做“复位” 我们真正用到的是 管脚40 名字叫做 RB7 接一个键盘。可以看出,该脚平时为高电平,一旦有人按下键盘,就会变成低电平,从而导致主程序发生中断。它是作为输入 I 使用。 管教27 控制一个发光二极管,输出高电平点亮。 它是作为输出管脚. 剩下的那些管脚,先不管,实际使用的时候悬空好了。 根据我们以前所学,主程序和子程序已然明了。在以前的程序中,凡是涉及专用寄存器如 GPIO 或者通用寄存器如的时候, 我都是在程序注释里说明该符号如GPIO 、020H,是一个寄存器的地址。这里有一个差别,特殊寄存器都是用字符串表示,而通用寄存器使用它的地址数表示。这样做主要是为了便于理解寄存器的本质和使用。 事实上,通用寄存器也是可以用字符串来表示的,并且在实用的程序里往往是用字符串表示通用寄存器,而不是直接用它的地址,因为用字符串更能明确这个通用寄存的用途性质。仅用地址数是允许的,但是通用寄存器一旦多起来,连程序作者自己也搞不清楚哪一个是干什么用的了。 为了便于程序理解,阅读,容易编写。允许程序作者给通用寄存器 例如020H 起用一个漂亮直接的字符串名字。 我们来学习一个崭新的语句 EQU 我们看几个例子: BeijiaShu EQU 020H '定义字符串 BeijiaShu 的值是数字 020H '此前或者此后的程序代码中, 只要遇到 BeijiaShu 就可以用数字 020H取代 '尽管BeijiaShu也可以写在一行的最左顶格,但是别用冒号,它不是程序标号 有了上述的语句,下列语句 MOVLW 123H MOVWF 020H 就完全可以写成 MOVLW 123H MOVWF BeijiaShu 程序中下列两组语句是等价的,相同的意义。这样,所有的通用寄存器都可以有自己的名号可以使用了。 新语句中的 EQU 不属于35条指令里面的,因为它在单片机运行的时候,不能被执行,也不会占用单片机里的程序存储空间。 仅仅是为了方便我们人类阅读、编写源程序程序而起的代号,事实上,有了EQU指令,家用电脑在把我们编写的源程序“翻译”成单片机能够执行的机器码的时候,还徒然增加了编译的工作量,不利于提高编译速度。 因此,它也不是十分必须的语句,你如果记忆力好,对每个通用寄存器的地址你自己设计成什么用途了,都能够记住,完全可以不必多此一举的使用什么符号常数。话又说回来,谁有那么多多余的精力去记忆枯燥的地址数据呢。 象这样不能被单片机执行,仅仅能帮助我们编写阅读源程序方便、或者仅仅有助于辅助家用电脑编译源程序而设立的指令,叫做单片机的“伪指令” 从理论上来说,几乎所有伪指令都不是必须的,都是可有可无的,都能够用我们的35条指令就可以完成任务的。只是,那样的话我们的35条使用起来比较麻烦和不太方便甚至还需要增加额外的计算我们的程序所存在的地址等等工作量。 有了这些个伪指令,我们编写程序就会省却许多的麻烦,例如: 此前的例子中用到了许多行 NOP空操作指令,假如某段代码需要连续500行NOP语句,即便拷贝也是个大麻烦, 那么有一款或者两款伪指令,只需要说明你的行编辑重复次数以及行编辑重复的代码是NOP 就足够了, 编辑上看上去也就是几行的样子,实际家用电脑在编译生成单片机的实际执行码的时候,会自动添加500行NOP指令,单片机里面的程序存储空间,也是相应的增加等量500行代码的空间,那是一点也不能节约出来的。 一句话:伪指令和我们此前讲的 单引号 ’ 后面的程序注释类似,共同点是都不被单片机执行,也不会占用单片机内部的程序存储资源,都是帮助人们阅读和编写的方便。不同点是:伪指令要干预家用电脑对源程序的编译,而单引号则不会。 由此看,初学入门不宜在伪指令上徘徊,老手则是善于运用伪指令图个编程快捷简便。 下面我们编写我们的中断例子,并且作为文本文件来处理看待。汇编语言主程序文件的扩展名称,是汇编系统已经规定好了的,必须使用 *.ASM 例如:可以为: Shan_LED.ASM 用记事本新建一个文件,文件名称请使用例子 Shan_LED.ASM 文件内容如下 BeijiaShu EQU 020H '被加数 JiaShu EQU 021H '加数 HE1 EQU 022H '第一算式的和 HE2 EQU 023H '第二算式的和 尽管如此,初学者,有四个伪指令需要认识,一个是EQU 已经认识过了。如果需要的 EQU 的行很多,例如我们的主程序中要用到 被加数、加数、计算结果的和 以及第一个算式的计算结果寄存 以及第二个算式的计算结果暂存: 那么,下一步,就是要核计整个程序的结构,如果从程序的用途属性,程序存储区域等角度来看,存储来区分一个典型的程序结构例子要包含主程序,子程序,中断子程序,和伪指令说明程序。 以往教材,在教导程序结构的时候不是这么分类,而是大谈特谈什么循环结构,分支结构,递归结构,函数调用等等,先把学生搞晕再说,我认为那些属于程序技巧,是用,是末,理应排在以后的技巧升级后去研究。按照上述分类才是程序之本体,是入门的必然门径。 有四个伪指令需要认识的,我们已经学会一个 EQU,另外3个随着我们讲解这个中断例子添加。再看看 ORG 伪指令 ORG 00H '这是个伪指令,强调以下的源程序代码在存储到单片机的时候, '被存储在单片机程序存储器的 00H位置,也就是开始的位置 NOP '这个才是真实的第一条可执行单片机指令,它被存储在程序存储器的 0000H位置 NOP '该指令没有用ORG指令规定位置,只好按照默认的顺序 被存储在 0001H GOTO MAIN '顺序存储位置 002H 无条件挑转到标号 MAIN的地方。 '0003H这个地方没有任何语句,是个空洞,不被执行 '空洞在家用电脑编译器编译的时候,实际上也会被给出一个特定的数据存储到0003H '这个特定数据是什么是无所谓的,不同厂家的编译器会给不同的特定数 '特定数例如:NOP MOVLW等一般为可执行数据,以免万一单片机误入空洞,也能工作,不出错 ORG 04H GOTO INTSRV '由于上邻的ORG 04H的存在,该句会被存储到程序空间 0004H , 无条件转向标号 INTSRV NOP '按照默认顺序编译存储到 0005H NOP '按照默认顺序编译存储 直到遇到下一个ORG命令,才会打破顺序 学会ORG命令以后,就可以把整个源程序安排到单片机的程序存储器位置上,我们习惯叫做程序存储器地址。PIC12F509的片内程序存储器只有1024个单元行,地址从 0000H ---- 03FF H 而PIC16C74的片内程序存储器有4096个单元行。 整个程序存储器 有几个位置(地址)需要特别介绍,那就是复位地址0000H 和 中断入口地址 0004H。 0000H是 PIC单片机程序存储器的第一行,通电后首先执行这一行的指令。等于是单片机工作的开始位置。 通电后后也许需要单片机多次重新开始工作,例如一台电子称通电以后,可能需要称若干次物品的重量,那么,每换一次物品,单片机就需要重新回到 0000 H这个指令上来,开始新一轮的测量。只要单片机的指令,回到了 0000 H, 我们就成为单片机复位。 中断入口地址是怎么回事情呢? 原来啊,我们的主程序和调用的子程序已经很完美了,如果小孩子不敲门,或者在本例子里 那个键盘永远也不按下的话,是不会发生中断的,我们的程序也就不会跳出我们事先设计的主程序和子程序那些事情。 可是,一旦发生了按键事件,此时程序会暂时记住当前位置,转而去进行中断服务,去哪里服务呢?就是强行把程序指向 0004H 这个特殊地址,然后从0004 H开始进行中断的服务处理,因此我们设计的中断撒盐 或者开关LED灯的一些程序,就必须从0004H这个位置开始。因此 0004H就叫做中断入口 或者叫中断入口地址。 不同型号的单片机中断入口地址不一定都是 0004H, 就我们的例子PIC16C74是这样的,并且就这一个入口,因此中断程序较简单,而别的型号则不一定,需要查手册,如 PIC18F452就有两个中断入口,地址0008 H 以及 0018H |
|