初始化AVR单片机

通过配置AVR单片机的fuse bits,并对AVR的内部RC时钟进行校准,使AVR单片机达到所需要的工作状态

--by Captdam @ Oct 23, 2022

最近做毕业设计在用AVR单片机,另外帮一个同学的教授改写一块PCB的核心也是AVR。使用单片机的第一步当然是根据项目需求配置单片机。

正常来说,在AVR单片机开机启动时,会先进入bootloader。所谓bootloader,其实是一段将串口收到的数据写入程序储存空间的代码。也就是说,在AVR单片机开机时向AVR单片机发送以程序为内容的数据包,就可以对AVR单片机进行编程了。一般配置AVR单片机的做法是,在程序开始的部分,通过SRAM中的SFR(Special function register),即改变单片机的配置。例如,RC时钟矫正,中断向量的位置。

但是,对于一些靠底层的配置,则无法通过修改SFR达到。这时,则需要通过串行编程接口(SPI接口)或并行编程接口对AVR的fuse进行编程。比如说,AVR单片机可以使用外部晶体作为时钟,或是使用内部RC时钟,如果要修改时钟源,则需要对fuse进行编程。

至于为什么要把一些配置项目放入fuse而不是SFR,个人的理解是:

1.这些配置是非常偏底层的,或者说是与电路有关的,软件不太可能会需要修改这些配置。

2.在程序执行过程中修改这些底层配置可能会造成程序不稳定,例如,修改时钟源可能造成芯片内部硬件失去时钟同步。

这一篇博客,我会写一些我如何对我毕设所用的ATmega328P进行串行编程。

编程器

ICSP接口

要对AVR单片机进行编程,则需要访问SPI接口。

首先,做出一个ICSP接口(In-Circuit Serial Programmed)。一般来说,要生产一套单片机系统,下游厂家会将电路图发给PCB厂,并联系单片机厂直接向PCB厂发货。PCB厂会将单片机(此时是空白的,没有编程的)与外围电路先焊接在PCB上。下游厂家收到PCB后,通过ICSP接口对单片机进行编程后就可以入库等待销售了。这样做的好处是,避免了单片机厂家-下游厂家编程-上游PCB厂-下游厂家入库的繁复过程,也可以防止PCB厂逆向出单片机内部程序。

下面是一块以ATmega328P为核心的最小系统,仅包括了单片机核心,供电,电源检测和差分信号UART发送/接收电路。

如图,J2就是这一块单片机的ICSP接口。

上图是这一张电路的电路原理图。如图,ICSP包括了SPI的MISO,MOSI,CLK,单片机的供电与RESET,共6根线。

当然,ICSP只是为了方便在后期对系统程序进行升级所用的。实际上,如果不考虑后期方便升级的话,也可以直接在面包板上将编程器与单片机的SPI,供电和RESET连接即可。

Arduino编程器

要对AVR单片机进行串行编程,必须使用编程器。可以使用专业的编程器,例如avrispMkII。

然而专业的编程器虽然好用,但就是太贵。一个备用方案是使用Arduino作为编程器。电脑通过UART(串口)将程序发送给Arduino,Arduino再将串口的数据帧重新组合成SPI的数据帧发送给被编程单片机。

下面是我制作的一个ArduinoISP。其实就是用洞洞板走了几条线而已。

使用ArduinoISP编程时,需要使用AVRdude软件:avrdude.exe -v -P com4 -c arduino -b 115200 -p m328p -U {target}(ArduinoISP的波特率我强制设定的115200)

Arduino的网站上对ArduinoISP进行了详细的解释:https://www.arduino.cc/en/Tutorial/ArduinoISP

自制SPI编程器

最先开始制作这个毕设的时候,手上并没有多的Arduino。于是乎,就用8051做了一个软件SPI。

如果只是修改一下fuse的话,还没什么问题。要是要进行编程的话,就比较麻烦了。后来用上ArduinoISP后,我就“真香”了。

厂家默认Fuse bits

在ATmega328P出厂时,厂家对fuse的设置为:E:0xFF, H:0xD9, L:0x62

关于fuse中每一位的具体含义,参考http://ww1.microchip.com/downloads/en/DeviceDoc/ATmega48A-PA-88A-PA-168A-PA-328-P-DS-DS40002061A.pdf#page=289

Extended fuse bits

默认的Extended fuse bits为0xFF,二进制码为0b11111111。其中,最高五位是无效位。

E[2:0]:BODLEVEL:111:供电低电压检测器被禁用。供电是12V加7805,没太大的供电不足电压被拉低的可能性。

High fuse bits

默认的High fuse bits为0xD9,二进制码为0b11011001。

H[7]:RETDISBL:1:RESET引脚作为RESET键使用,不作为GPIO。如果软件失效,并且软件复位和看门狗也失效的话,那么RESET键就是最后的解决方案了。所以,RESET键需要留着。

H[6]:DWEN:1:debugWIRE(功能类似于JTAG,用于调试)功能被禁用。_(:3 _| <)__那么贵,我又买不起,我留着干嘛……

H[5]:SPIEN:0:允许通过SPI进行编程。被禁用了的话,就只能通过并行编程了。

H[4]:WDTON:1:看门狗默认关闭。看门狗应该在系统初始化后,由软件打开。

H[3]:EESAVE:1:EEPROM会在擦除芯片时被擦除。因为这里不需要使用EEPROM,所以这个设置无关紧要。

H[2:1]:BOOTSZL:00:Bootloader的大小为最大值——1024个字(2048个字节,AVR的程序为16bit/字)。因为不考虑用户后期通过bootloader编程,所以需要更改为11,即使用最小空间——128字。

H[0]:BOOTRST:1:RESET向量位于0x0000。单片机启动时,从程序区间开始执行,而不是bootloader区间。

Low fuse bits

默认的Extended fuse bits为0x62,二进制码为0b01100010。

L[7]:CKDIV8:0:系统时钟将会被8分频。因为使用内部RC时钟,受环境干扰,时钟频率不稳定。使用8分频后,可以让时钟输出稳定一些。另外,时钟越快,芯片发热与功耗越大。因此,使用分频可以降低功耗与发热。

L[6]:CKOUT:1:CLKO引脚作为GPIO,而不是系统时钟输出。在多机同步工作的情况下,可以让主机输出系统时钟,作为从机的时钟源。另外,因为晶体生产的误差,因此每一块芯片的RC时钟都是不一样的。在校准RC时钟时,将这个bit改写为0,可以方便测量内部时钟频率。

L[5:4]:SUT:10:芯片启动会使用65ms外加14个时钟周期,唤醒会使用6个时钟周期。缓慢的启动芯片可以保证芯片内部所有配件都准备好,防止运行中各部件失去同步。

L[3:0]:CKSEL:0010:使用内部8MHz的RC振荡器作为时钟。

校准时钟

关于时钟源

AVR单片机可以使用多种时钟作为CPU时钟。总的来说分为外部和内部两种。

外部时钟可以是晶体振荡器,一般用于对时钟精度要求高的场景,比如说电子表(虽然现在电子表可以通过网络/授时中心无线电校准时钟所以精度要求没那么严格了)。只要晶体的加工精度够高,CPU就能以非常精确的频率工作。

另一个外部时钟源是震荡,一般用于多机协同工作。因为多台单片机使用同一个时钟,因此所有的单片机都会有完全相等的速度。因此,在通信时就不需要考虑同步问题了。

内部时钟有两个,第一个是128kHz的看门狗专用时钟。这个时钟的工作频率大致在128kHz(看门狗不需要高精度时钟),太慢了,基本没什么用。当然,如果对运算性能要求不高,使用这个时钟可以节约功耗,因为功耗与CPU频率成正比。

另一个外部时钟是可校准的RC时钟。在AVR单片机内部,有一个RC电路组成的振荡器,大致工作频率处于8MHz。通过修改SFR OSCCAL,可以修改这个时钟的频率。一般来说,在出厂时,这个时钟已经被校准过了。但是,为了让这篇文章不是废话让这个时钟更精确,还是进行测量校准一下比较好。

RC时钟也被用于EEPROM。因为写EEPROM需要提供足够的延时才能保证写的内容被真正的写进去了,所以RC时钟不能校准到超过8.8MHz。

一个个人的观点:使用晶体作为时钟时,直接使用就可以了。使用RC作为时钟的话,因为RC时钟其实很不稳定,所以打开八分频,对时钟周期进行平均。这样虽然降低了运算性能,但是可以得到更稳定的时钟。

进行校准

现在的气温是30摄氏度,湿度40%。

在校准之前,首先确保fuse设置时钟源为内部RC时钟,并且输出内部时钟。换言之,low fuse设定为0x22。

接下来,使用示波器连接ATmega328P的14脚。对于其他AVR单片机,连接CLKO脚。

连接电源,此时时钟输出是这样。可见,这个误差还是不小的:

要校准,使用在程序中插入如下代码。其中,修改define的RC_CLOCK_CALIBRATE就可以改变时钟频率:

校准的可靠性:RC时钟的频率是会受环境温度影响的!温度发生变化时,RC振荡器的频率也会相应发生变化。因此,需要在实际工作温度环境下校准RC时钟,并且RC时钟不宜用于对时钟精度要求高的环境。

校准时的电压:校准时,最好使用系统工作时将会使用的电压。但是,理论上,RC振荡器的频率和电压无关。测试了一下,实际工作电压5V供电时的频率是1MHz,编程器供电电压3.3V供电时频率是995kHz,误差0.5%。误差是可以忽略不计的,但是为了保险还是使用实际工作电压比较好。

程序中,CLKPR = 0x80;CLKPR = 0x03;这两个指令实际上是不需要的,因为在fuse中已经设定时钟将会被八分频。但是,为了防止以后忘记这茬并犯一些低级错误,还是把这行写在这里比较好(也就两个周期,反正初始化的时候对延迟并不敏感嘛)。

校准后,得到了精确的1MHz频率:

为了方便后续使用,也为了降低单片机对环境的EMI,需要关闭CLKO输出。要关闭CLKO,需要对fuse进行修改。换言之,修改low fuse到0x62。

现在,单片机的前期设定就完成了。接下来,就可以在已经初始化的单片机上开始编写程序了。