接下就是所有单片机都有的也是最简单的外设了,就是那个亮灯用的 GPIO 。从数据手册看出比较有意思的特性是带有输入电平翻转的功能、和 GPIO 寄存器内存地址映射。尤其是后者,第一次见到。至于其它功能都是常规操作了。
小白要留心这个小提示,可以先把所有引脚都统一配置为模拟模式,之后用到哪个引脚再对其重新配置,这样有利于降低整机功耗。
啥也不说了先亮个灯吧。这是必须得看硬件设计文档了,最好是看原理图了,不过既然官方画了这么好看的示意图,暂时以示意图为主。从图中看出 PORTC 端口基本没有做引脚复用,本着不浪费的原则,就用它点灯了。
一般来说,从功能构思,元件选型,电路设计到 PCB Layout ,最好焊接成型整个周期是以周来计算的。2020 年至 2021 年芯片采购变得极为困难,无疑会导致硬件制造流程变得更加漫长。单片机代码涉及的很多电路上的信息交互,传统的开发调试一般是等硬件电路制造完成后,在硬件电路上仿真调试。随着嵌入式软件开发技术的进步,可以对单片机代码采用全虚拟仿真和半实物仿真,这样就不要苦等硬件电路了。
数据手册动辄上百页,但我们只想用 GPIO 输出个高低电平。GPIO 章节的介绍可以慢慢瞅,现在要蹦到寄存器描述环节。里面有很多个相关寄存器,需要配置就 2 个而已。第一个是控制引脚数据方向的寄存器 DIR,要对外输出就得把 DIR 置 1。
第二个关键寄存器 OUT 就是控制输出值的高低。OUT 置 1 就是输出高电平,OUT 置 0 就是输出低电平。
现在又可以直接甩代码了。因为单片机一旦执行晚所有代码就会自动复位,因此 main 函数的最后得放置一个 while 循环防止单片机复位。会闪动的灯光才好看呀,所以得写有专门实现延时效果的 delay 函数。它就是让单片机做点无用功,好比小明走的太快了需要给她加点负重,让她走慢点。大家可以看到 delay 函数就是三个 for 循环。这里得补充个知识,此时 char 默认是有符号的字符型,但是这个不是通用的,有的嵌入式单片机编译器, char 默认是无符号的字符型。在 XC8 for AVR 里面可以通过修改编译器参数来 char 的默认类型。这个知识对小白来说可能有点深了。这个会影响 for 循环里 x,y,z 的最大值,不得不讲一下。在很多教程中,会用 #define 来 unsigned char 改为 uchar。因为宏定义不便于小白阅读和调试,所以我的教程里会尽量避免使用这个东西。
#include <avr/io.h>
#include <avr/cpufunc.h>
FUSES = {
.WDTCFG = 0x00, // WDTCFG {PERIOD=OFF, WINDOW=OFF}
.BODCFG = 0x00, // BODCFG {SLEEP=DISABLE, ACTIVE=DISABLE, SAMPFREQ=128Hz, LVL=BODLEVEL0}
.OSCCFG = 0xF8, // OSCCFG {CLKSEL=OSCHF}
.SYSCFG0 = 0xD2, // SYSCFG0 {EESAVE=CLEAR, RSTPINCFG=GPIO, CRCSEL=CRC16, CRCSRC=NOCRC}
.SYSCFG1 = 0xE8, // SYSCFG1 {SUT=0MS, MVSYSCFG=DUAL}
.CODESIZE = 0x00, // CODESIZE {CODESIZE=User range: 0x0 - 0xFF}
.BOOTSIZE = 0x00, // BOOTSIZE {BOOTSIZE=User range: 0x0 - 0xFF}
};
LOCKBITS = 0x5CC5C55C; // {KEY=NOLOCK}
void clock(void);
void delay(char a);
int main(void){
unsigned char i=0;
clock(); //初始化时钟
PORTC.DIR = 0xff; //设置引脚为输出模式
PORTC.OUT = 0xff; //设置引脚为输出为 0
while(1){
PORTC.OUT = i; //不断改变引脚输出值
i++;
delay(100); //适当延时方便后续观察
}
return 0;
}
void clock(void){
CLKCTRL.MCLKCTRLA = 0x80; //内部高速振荡器,时钟在对外输出
CLKCTRL.MCLKCTRLB = 0x00; //关闭预分频器分频
CLKCTRL.OSCHFCTRLA = 0xbc; //24MHz,一直开启,关闭时钟修正
}
void delay(char a){
unsigned char x,y,z;
for(x=0;x<a;x++){
for(y=0;y<200;y++){
for(z=0;z<200;z++);{
_NOP();
}
}
}
}
我直接从 PORTC 端口里随便挑了 4 个 PIN 口观察电平变化。下图用的就是软件仿真方式观察效果,而非传统的开发板亮灯。
说到这了,得介绍下我们开发必备的软硬件工具了。IDE 是 MPLAB X ,编译器是 XC8 ,这个和其它单片机常用的 Keil 不太一样。 MPLAB X 和编译器是 XC8 是分开的,两者都可以从 Microchip 的官网上免费下载和使用。MPLAB X 自带有软件仿真器,不过最后和硬件联调时还得用到硬件仿真调试器。这方面的工具有很多,我用到是 PIC Kit4,市场价三百多吧。
它长这个样子的,基本上可以覆盖 Microchip 所有的单片机系列。价格与功能上与友商的 ST Link V3 旗鼓相当吧。一般官方的开发板上集成有板载调试器,可以不用单独购买 PIC Kit4 。平时自己设计电路就必须拥有一款硬件调试器,虽然可以通过串口来完成烧录程序,但是这种方式是不能进行调试的。
接着看代码,一般延时函数根据延时时间的长短而有所不同。最短的是指令级延时,_NOP(); 就代表延时一个指令的意思。想用这行代码必须添加头文件 #include<avr/cpufunc.h> 。然后就是毫秒级和微秒级延时了,这时根据系统时钟适当的控制 for 循环或者 while 循环的嵌套深度来实现。这时还得搞明白一个东西,那就是执行一条指令需要几个时钟。PIC 单片机里面,大约四十多条指令,基本上一条指令需要四个时钟。而 AVR 就不一样了,大约 一百三十多条指令,每条指令需要的时钟数也不太一样。关于指令的时钟数需要自己在数据手册慢慢扒拉,上面所讲仅作参考。下一步,就可以根据 C 语言代码脑补出它的汇编指令,再默算出循环执行时间。对小白来说,还有个笨办法,可以直接用示波器抓出波形,看代码运行时间。像下面这样,直接看下 PORTC 下任何一个引脚的电平变化周期即可看出 delay 的延时的数量级。
while(1){
PORTC.OUT = 0x00; //不断改变引脚输出值
delay(1); //适当延时方便后续观察
PORTC.OUT = 0xff;
delay(1);
}
其实很多 IDE 里面有专门的计算程序代码执行时间的功能,在 MPLAB X 里面叫做跑表,可惜的是这个功能这个功能暂时还不支持这个功能。不过,编译器是 XC8 的可以直接调用系统集成的延时函数,第一步当然是添加头文件 #include <avr/delay.h> ,然后调用即可。建议延时函数里面的参数不要超过 1000 。假如要做秒级延时,既可以简单粗暴地加大循环周期,也可以更高级点。可以让单片机休眠,时间到了再唤醒,这种方式有利于降低功耗,适合物联网设备的开发。这里暂且不讲怎么实现,留给后面章节。
哈哈哈,那就继续用PIC的吧