最近因为工作关系,需要开始重新捡起单片机的技能。实验室负责人比较熟悉8051系列单片机,也勉强给LPC111x系列写过程序。 而当前的目标,则是完整地了解一款PIC18系列的芯片:PIC18F46Q10。
选用这一款芯片的理由,既和工作的内容相关,也来自实验室负责人的一点期望:学习一个比51性能好一些的8比特单片机。 8051系列的设计来自于1980年代,虽然至今备受青睐而且是很多新设计的基础,但作为爱好者,如果只是停留在这个型号上,就会错失很多新的芯片的丰富功能。 而Microchip不仅仅生产繁多的8/16/32比特单片机型号,还免费提供它的IDE和编译器(MPLAB和XC8等)给用户使用,所以实验室负责人决定成为Microchip的粉丝~
自学的小目标
- 熟悉编程环境,学习语法
- 熟悉操作IO口的办法
- 学习PIC18的定时器和定时器中断两个模块
PIC18F46Q10 vs AT89S51/52
- 电压范围:PIC18可以使用1.8V-5.5V电压,以前的AT89S51只好使用5V。
- 时钟频率:PIC18可以上到64MHz,指令频率是Fosc/4=16MHz,而51单片机是Fosc/12,就算用32MHz的时钟,指令频率也不过2MHz多一点。
- ……
使用的硬件和软件
软件:MPLAB X,可以直接从microchip的网站上下载,安装免费。
——需要单独安装编译器,至少安装XC8。如果希望以后学习其他的16/32位单片机,可以安装相应的软件。
硬件:买了一个PICKIT 4的调试器,大约50欧元,支持ICSP和JTAG,除了PIC系列,也可以给其他支持JTAG标准的单片机编程,颇为值得购买。
另外,为了减少硬件上的烦劳,我买了一个OLIMEX PIC-USB-455x 的开发板,如图:
板上有一个按钮,一个LED灯。其他位置留给我们自行开发。
用MPLAB和XC8给单片机编程
XC8是C语言的编译器,结合MPLAB的开发环境,就可以很方便地编写和编译程序。PICKIT 4在Ubuntu下是即插即用的,无须安装驱动,MPLAB可以自动识别。
编程本身,其实C语言技术上还是大同小异。而重点是要仔细阅读单片机的数据手册。 很多问题的答案都在这份700多页的大部头文件里。
1. 用 MPLAB+XC8 进行基本的项目设置
在MPLAB中新建项目之后,注意选择器件型号。这样才能对相应的单片机生成正确的机器码。 作为一个弯路,我曾经试图在Protel中给PIC18F4550的虚拟器件加载PIC18F46Q10的代码,并不可行,因为不同器件内部的地址分配是不同的。(以前8051单片机相对宽松,因为遵循的老标准已经成为了传统……)
需要包含的文件
新项目需要包含的代码,至少是有main()
这个函数。这一点和C语言相同。
因为不可避免地需要操作单片机的各种寄存器,需要引用xc.h
这个文件。
除此之外,就是需要设定必要的配置位,让单片机运行在符合其电路设计的状态下。
单片机的配置位
配置位(Configure Bits)用于设定单片机运行的初始状态。这里面最重要的是时钟源的设置。如果设置不当,单片机将无法正确启动。
除此之外,单片机运行监控,看门狗,程序存储器和代码保护的设定,也都在配置位里。PIC18有CONFIG1
到CONFIG5
共5个配置位,参数繁多。为了简化各种型号单片机的配置位设定,MPLAB自带Set Configuration Bits
这个工具(在Production
菜单下),可以据此生成适当的代码,粘贴到程序文件中即可。
2. 控制一个IO口,点亮开发板上的LED,然后闪烁
查阅开发板的资料,我们知道,LED连接在RD3这个引脚上。当引脚为0,接受灌电流的时候,灯就会点亮。
由51单片机的经验,我想当然地认为:类似 RD3=0
这样的语法就可以了。换作XC8的语法就是:
PORTDbits.RD3 = 0;
但是,灯并没有点亮。查阅数据手册第5章,得知PIC18的IO口非常复杂,功能繁多。下图是这个单片机一个IO口的逻辑关系图:
注意到IO口的读写有4条线路:
- 写入控制IO口的数据锁存器,
LATx
- 写入IO口本身,
PORTx
- 读取IO口
PORTx
- 读取IO口对应的数据锁存器,
LATx
其中,1和2是同一条线路,实际上效果是相同的。这就是数据手册里说的,写入IO口总是写入IO口对应的数据锁存器上。
但是3和4区别在于,读取PORTx
会读到IO口上的实际逻辑电位,而读取LATx
读到的是之前写入的IO口上应具有的逻辑电位。
同时注意到,在输出时,锁存器要经过TRISx这个逻辑门。后者在设定为0的时候才可以启用。所以,我们需要至少设定IO口的输出驱动,TRISx寄存器,才可以让数字写入IO口:TRISD.TRISD3 = 0
而为了获取IO口上的逻辑电位,我们注意到在Read PORTx之前,还有一个受ANSELx
控制的与门。因此,如果想要这个功能,也要考虑ANSELx
的设定。
那么,如何翻转一个LED的状态呢?
我们希望能让LED闪烁。如果没有经验,我们可能试图使用这样的代码:
但这样的代码并不能成功。可以看到IO口维持在同一个状态不动。
根据前面的分析,应该可以看出这段代码的问题。我们希望让RD3
的值每次与上次不同。这涉及到一个先读后写(read-and-write)的过程。
程序需要首先读出RD3
的值,然后将其翻转,写回RD3
中。
然而,读取RD3
是有条件的,必须让ANSELx=0
。否则,读取到的不是对应引脚的实际逻辑电平。但另一方面,我们也无需读取实际引脚的逻辑电平,只要翻转IO口上的锁存器,也可以达到一样的效果。
所以,我们可以用如下代码:
3. 定时器和中断
在前面的热身练习完成后,我们尝试学习一点复杂而重要的东西:PIC18的中断系统。
作为一个小目标,我们希望利用定时器——而不是基于循环的延时功能——实现灯的闪烁。
阅读数据手册
认识中断系统最重要的“地图”,就是数据手册上的中断逻辑图,如下:
PIC18的中断系统有很多特点,比较重要的是:
PIC18的中断可以配置为具有高低优先级的工作模式,或者兼容低端单片机的模式。 在启用优先级的时候,高优先级的中断一旦出现,会屏蔽低优先级的中断。 这是因为在图的右侧,高优先级的中断输出,同时经过一个非门,输入给了低优先级的中断路径上的一个与门。 只有当高优先级的中断执行完毕,才有可能执行低优先级的中断。
所有的中断源,比如定时器、外部中断、各种片上设备产生的中断,都可以被分配到两个优先级。
这是因为在图的左边,各种中断源都有对应一个IPR
控制位(Interrupt Priority Register),分配中断源的优先级归属。
一个中断源会否产生中断,由3个控制位共同决定:中断优先级,中断启用(PIE)和中断请求(PIR)。然而,产生中断的逻辑实际上分为两路,互为镜像,分别是(PIE and PIR) and IPR
和(PIE and PIR) and !IPR
,
所以实际上一个中断的产生,并不受中断优先级控制,只和中断启用与中断请求相关。
特别是注意到,在IPEN
(中断优先级启用控制)位为0的时候,所有的中断源都能产生中断并向右发送。
中断优先级的作用只是控制中断信号的路径,但IPEN=0
时所有路径都是开放的。只有IPEN=1
时,路径才会根据优先级不同经过复杂的控制逻辑输出。
PIR0与其他中断不同。
我们注意到PIR0一路的中断,绕过了PEIE
的控制,只受到GIE
(中断全局开关)的控制。只要GIE=1
,PIR0
上的中断源都可以产生中断。
PEIE
可以理解成为内部中断,其不包括PIR0中的5个中断源:定时器0,引脚电平变化(Interrupt on Change, IOC)和3个外部输入中断。
相比之下,为了启用片上其他功能的中断,比如EUSART(串行口)、其他的定时器等,
不仅要设定对应中断源的PIE
,还可能需要启用PEIE
这个开关(内部中断启用)——但只是在IPEN=0
,即没有启用中断优先级的时候,才需要做这一点。
方案:定时器0中断实现灯的闪烁
定时器0是一个8位或者16位的定时器/计数器,可以从多个输入源获得计数脉冲。可能的输入有:
- CLC1(可配置的逻辑模块)的输出
- 多种内部振荡器的脉冲
- 芯片时钟/4
- 根据
T0CKIPPS
的设定所决定的外部引脚
我们这里只考虑使用芯片时钟作为计数脉冲,并使用定时器的16位模式。在这种模式下,定时器0会不停的从0x0000
计数到0xFFFF
,然后重新开始。在溢出的时候可以产生一个中断。为此,配置
因为时钟的速度比较快,比如我们本例中使用32MHz的振荡器(内部64MHz振荡器经过分频2:1),脉冲频率=振荡器频率/4=8MHz,即使计数到0xFFFF=65535
也只需要0.008秒。
为了让效果明显一点,我们使用时钟脉冲的预分频器
让计数器溢出的频率变为8秒。
最后,使用
即可启用时钟。
中断:启用中断,编写服务程序
仅仅启用时钟,并不能实现我们的目标。为了让时钟产生中断,并在中断中更改LED的状态,首先要编写中断服务程序。
中断服务程序(Interrupt Service Routine)是一个特殊的函数,我们这里命名为isr
,其声明的格式和内容如下:
在这个程序中,我们需要用if
首先判断是谁引起了中断。和一些高档的单片机可能不同,PIC18的中断优先级只有2个。所以,每个级别上的中断都可能由多个中断源引发。
这样,我们的函数就必须是一个“多面手”,可以应对所有可能的情况。
其次,注意到在if
判断的结尾,我们重新将PIR0bits.TMR0IF
清零。
这一步是非常重要的!一个中断请求产生后,必须手动消除这个中断请求。否则,下一个中断将还是由它引发。
p.s. 我们知道GIE是全局中断开关。实际上这个开关在中断发生后也有变化:它会自动清零。 但是,我们需不需要在ISR中重新启用GIE呢?这一点数据手册上说得很明白:在退出ISR之后,GIE会重新置1。 这也提示我们有一些操作不能在ISR中完成:比如关闭中断全局开关!
最后一步,就是启用中断了。根据前面的分析,我们需要注意如下几个控制位: