第5讲 AVR的仿真

在上一章中,我们已经体验了使用Atmel Studio 6.1集成开发环境为AVR单片机编写程序。本章主要介绍的是如何对AVR单片机进行位操作,以及使用开发环境内建的模拟器,模拟AVR CPU的运行,以观察程序运行时寄存器的状态,修正程序中存在的问题。

4.1

AVR的位操作

4.1.1

打开已有的项目

接下来,我们要对第3章已经写好的点亮LED灯的程序进行一些修改,使其可以交替点亮两个LED灯。将第3章的程序文件复制到chapter4文件夹中,打开Atmel Studio 6.1软件,在“File”菜单里面选择“Open”-“Project/Solution…”,弹出的对话框如图4-1所示。

在D:\MEGA32\chapter4下面找到名为“LED2”的项目文件,鼠标单击“打开”按钮,打开第3章中所写的程序。为了交替点亮两个流水灯,需要一个延时函数来控制LED的点亮和熄灭时间。对于延时函数,通常的做法是使用循环语句在程序中自定义一个函数,不过在Atmel Studio 6.1集成开发环境里,可以使用已经定义好的延时函数,只要在源程序中包含特定的头文件即可。

通过前面的学习,我们对于头文件的保存位置有了一定的了解。按照头文件的默认保存路径,可以找到这些头文件,例如在C:\program files\atmel\atmel toolchain\avr8

gcc\native\3.4.2.1002\avr8-gnu-toolchain\avr\include文件夹下面,有多个文件和文件夹,其中就有上一章中编写程序所引用的“io.h”头文件,另外还有E2PROM读写头文件“eeprom.h”、中断相关头文件“interrupt.h”等。包含这些头文件后,程序的相关功能会扩充,使其能完成更多的计算和处理任务。使用这些由开发环境定义好的头文件不仅有利于代码的简化,还可以降低程序开发的难度。以下我们以包含“delay.h”头文件为例,介绍一下由编译器所定义的延时函数的使用方法,具体详见代码清单4-1。

代码清单4-1

流水灯程序

/*

*

LED2.c *

*

Created: 2013/7/9

15:29:30

*

Author: GAO */

#include 
//
包含AVR 头文件
#define F_CPU 16000000UL //
定义系统时钟
#include 
//
包含延时函数头文件
int main(void)
{
DDRC=0x0F; //
将PORTC 端口低四位设为输出
while(1)
{
PORTC=0x01; //PORTC 端口输出
0000
0001
_delay_ms(300); //
延时300ms PORTC=0x02; //PORTC 端口输出
0000
0010
_delay_ms(300); //
延时300ms }
}
编译上面的程序,并将编译生成的HEX文件烧写到AVR系统板上的目标单片机中,程序运行后,两个流水灯LED1和LED2会交替闪烁。在上面的程序中,使用了由“delay.h”头文件定义的延时函数。为了得到精确的延时时间,编译器需要根据CPU的时钟频率调整设置,所以在代码中使用了如下的语句来定义CPU的时钟:
#define F_CPU 16000000UL //
定义系统时钟
在这一行由宏定义构成的代码中,F_CPU xxxxxxUL为固定格式,UL前面的数字是以Hz为单位的CPU时钟频率,如果不指定CPU的时钟频率,编译器会使用默认的1MHz作为CPU的频率。程序中使用以下代码来包含支持延时函数的头文件“delay.h”,其中“util/”是这个头文件所在文件夹的名称。
#include 
//
包含延时函数头文件
在“delay.h”头文件中,分别定义了两个延时函数,用于毫秒级和微秒级的延时。
void _delay_ms (double __ms); //
毫秒级延时函数
void _delay_us (double __us); //
微秒级延时函数
在调用这两个延时函数时,入口参数要使用常量,数据类型默认为“double”型。在使用延时函数时,需要特别注意的是延时时间有一个精度范围。调用毫秒级延时函数时,在262.14
ms/(F_CPU/1e6)的范围内(当主频为16MHz时,最大值为16
ms),延时是高精度的,超出此范围后,延时的精度会有所降低,分辨率降低为1/10ms,毫秒级延时函数最大可能的延迟时间为4294967.295
ms/(F_CPU/1e6);调用微秒级延时函数时,在768μs/(F_CPU/1e6)范围内(当主频为16MHz时,最大值为48μs),延时是高精度的,最大延迟时间为4294967.295μs/(F_CPU/1e6)。
4.1.2
位操作
在代码清单4-1中,对I/O口寄存器的操作采用的是整体赋值的方法。然而很多时候,我们需要单独对寄存器的某一位进行操作,并且希望对这一位的操作不会影响到该寄存器的其他位。AVR单片机的头文件中并没有对寄存器的位操作做出定义,但可以采用如下的方法对寄存器进行位操作。
AVR单片机位操作最简单的方法是采用“与运算”和“或运算”。例如要将PC1端口置1,可以使用如下的方法:
PORTC=PORTC|0x02; //0000
0010
这种方法也可以简写为:
PORTC|=0x02

如果想将PC1端口清零,可以使用如下的方法:
PORTC=PORTC&0xFD; //1111
1101
这种方法同样也可以简写为:
PORTC&=0xFD; 按照上述代码所示的位操作方法,我们可以在程序的开始处加入宏定义,分别定义出使某一位置1或清零的算法,以增强代码的可读性。
#define LED2SET (PORTC|=0x02)
//
置位PC1
位,点亮LED2
#define LED2CLR (PORTC&=0xFD)
//
清零PC1
位,熄灭LED2
有了这样的定义,我们在程序中点亮或熄灭LED2就可以使用如下方法了:
LED2SET ;
//
点亮LED2
LED2CLR ;
//
熄灭LED2
接下来,我们要再次将流水灯的程序进行修改,使用位操作的方法来控制流水灯。将D:\MEGA32\chapter3\LED2\LED2的LED3.c源文件复制并重新命名为LED3TMP.c,如图4-2所示。
将重命名的C源文件添加到LED2项目中,方法是在“Solution Explorer”窗口中,右击“LED2.c”源文件,在弹出的对话框中选择“Remove”项,如图4-3所示。
这时弹出一个确认对话框,在此对话框中单击“Remove”按钮,即可将“LED2.c”源文件从LED2项目中移除,如图4-4所示。
之后将复制好的源文件添加到LED2项目中。在“Solution Explorer”窗口里,右击“LED2”项目,在弹出的对话框中选择“Add”-“Existing Item...”项,如图4-5所示。
在弹出的对话框中选择“LED2TMP.c”源文件,单击“Add”按钮,即可将“LED2TMP.c”源文件添加到“LED2”项目中来,如图4-6所示。
源文件添加好后,对程序代码进行修改,具体详见代码清单4-2。
代码清单4-2
位操作I/O口(一)
/*
*
LED2.c *
*
Created: 2013/7/9
15:29:30
*
Author: GAO */
#include 
//
包含AVR 头文件
#define F_CPU 16000000UL //
定义系统时钟
#include 
//
包含延时函数头文件
#define LED2SET (PORTC|=0x02)
//
置位PC1
,点亮LED2
#define LED2CLR (PORTC&=0xFD)
//
清零PC1
,熄灭LED2
#define LED1SET (PORTC|=0x01)
//
置位PC0
,点亮LED1
#define LED1CLR (PORTC&=0xFE)
//
清零PC0
,熄灭LED1
int main(void)
{
DDRC=0x0F; //
将PORTC 端口低四位设为输出
while(1)
{
LED1SET; //LED1
点亮
LED2CLR; //LED2
熄灭
_delay_ms(300); //
延时300ms LED2SET; //LED2
点亮
LED1CLR; //LED1
熄灭
_delay_ms(300); //
延时300ms }
}
编译这段代码,并将生成的HEX文件再次烧写到系统板上的目标单片机中,程序运行后可以看到流水灯又一次交替闪烁了,不同的是这一次是使用位操作的方法来控制I/O口的。
除了以上介绍的位操作方法外,我们还有另外的方法。这种方法与头文件中对寄存器位序号的相关定义有关。在与ATmega32单片机对应的头文件“iom32a.h”中,对单片机各个寄存器位名称与位序号做了关联定义。例如,对PORTC寄存器有如下定义:
*****
部分代码略*****
#define PORTC _SFR_IO8(0x15)
#define PORTC7
7
#define PORTC6
6
#define PORTC5
5
#define PORTC4
4
#define PORTC3
3
#define PORTC2
2
#define PORTC1
1
#define PORTC0
0
*****
部分代码略*****
有了以上的定义方式,如果在代码中出现“PORTC1”,则代表的是数字“1”。这样,要将PC1和PC0位置“1”就可以使用如下的写法。
PORTC =
(1< PORTC|=(1<
将PORTC 的PC1
位置1
PORTC&=~(1<
将PORTC 的PC0
位清零
使用上述方法编写的流水灯程序详见代码清单4-3。
代码清单4-3
位操作I/O口(二)
/*
*
LED2.c *
*
Created: 2013/7/9
15:29:30
*
Author: GAO */
#include 
//
包含AVR 头文件
#define F_CPU 16000000UL //
定义系统时钟
#include 
//
包含延时函数头文件
int main(void)
{
DDRC=0x0F; //
将PORTC 端口低四位设为输出
while(1)
{
PORTC|=
(1<
将PORTC 的PC1
位置1
PORTC&=~(1<
将PORTC 的PC0
位清零
_delay_ms(300); //
延时300ms PORTC|=
(1<
将PORTC 的PC0
位置1
PORTC&=~(1<
将PORTC 的PC1
位清零
_delay_ms(300); //
延时300ms }
}
掌握AVR单片机位操作的方法不仅可以方便编写程序,而且还可以帮助你更好地理解别人已经写好的代码,这些对于学好AVR单片机是十分重要的,相信你有了上面的这些例子作为基础,一定会自己总结出更多更好的位操作的办法。

 


评论:

AVR单片机基础教程

作者:高显生   共18讲

AVR系列单片机是8位单片机中第一个真正的RISC结构单片机,它采用了大型快速存取寄存器组、快速的单周期指令系统以及单级流水线等诸多先进技术,使得AVR单片机具备了高达1MIPS/MHz的运行处理能力。