3.8 访问AVR单片机硬件的编程
1.访问AVR单片机的低层硬件
AVR系列单片机使用高级语言编程时有很高的C语言密度,它允许对访问目标MCU的底层硬件进行访问。由于AVR单片机性能,除了要最大程序地优化代码外很少使用汇编。偶然情况下目标MCU的硬件特点在C语言中不能很好地使用,很显然使用在线汇编和预处理宏能访问这些特点。
头文件io*.h(如io8515.h iom603.h等)定义了指定AVR单片机MCU的IO寄存器细节。这些文件是从ATMEL官方发布的文件经过修改,以匹配这个编译器的语法要求。文件macros.h定义了许多有用的宏,例如宏UART_TRANSMIT_ON()能使UART开始工作。
这个编译器的效率很高,当访问由IO寄存器映射的内存时能产生单周期指令,如in、out、sbis、sbi等。参考IO寄存器。
注意:旧的头文件avr.h定义IO寄存器的bit有一些模糊,尽管io*.h定义了它们的bit的位置。因此使用io*.h和IO寄存器的bit,很多时候将需要使用定义在macros.h文件中的BIT()宏。例如:
2.位操作
一个共同的任务是编程微控制器(MCU)打开或关闭IO寄存器的一些位(bit)。很幸运,标准C有较好的和适用的位操作功能,而没有借助于汇编指令或其他非标准C结构,C定义了一些按位进行的运算是很有用的。
a|b:按位或,这个表达式指示中a被表达式中的b按位进行或运算。这惯用于打开某些位,尤其常用|=的形式,例如,PORTA|=0x80;//打开位7(最高位)
a&b:按位与,这个运算在检查某些位是否置1时有用,例如,If((PORTA & 0x81)==0)//检查位7和位0
注意,圆括号需要在&运算符的周围,因为它与==相比运算优先级较低,这是C程序中很多错误的原因之一。
a^b:按位异或,这个运算对一个位取反有用,例如,在下面的例子中,位7是被翻转的:PORTA^=0x80;//翻转位7
~a:按位取反,在表达式中这个运算执行一个取反,当用按位与运算关闭某些位时,与这个运算组合使用尤其有用,如:PORTA &=~0x80;//关闭位7
这个编译器对这些运算能产生最理想的机器指令,例如,sbic指令可以用在根据位的状态进行条件分支的按位与运算中。
3.程序存储器和常量数据
AVR单片机是哈佛结构的MCU,它的程序存储器和数据存储器是分开的,这样的设计是有一些优点的。例如,分开的地址空间允许AVR单片机装置比传统结构访问更多的存储器;例如,ATmega系列允许有超过64KW(字)的程序存储器和64KB的数据存储器。将来的MCU装置可能用到更多的程序存储器,而程序计数器仍保留在16位上。
不幸的是,C不是在这种机器上发明的。特别地,C指针是任意一个数据指针或函数指针,C规则已经指定不可以假设数据和函数指针能被向前和向后修改,可是同是哈佛结构的AVR单片机,要求数据指针能指向任一个数据内存和程序内存。
非标准C解决了这个问题,ImageCraft AVR编译器使用“const”限定词表示项目是在程序存储器中。注意对指针描述,这个const限定词可以应用于不同的场合,不管是限定指针变量自己还是指向项目的指针。例如:
“table”是表格式样分配在程序存储器,“ptr1”是一个项目在数据存储器而指向数据的指针在程序存储器,“ptr2”是一个项目在程序存储器而指向数据的指针在数据存储器,最后“ptr3”是项目在程序存储器而指向数据的指针也在程序存储器,在大多数的例子中“ta-ble”和“ptr1”是很典型的,C编译器生成LPM指令来访问程序存储器。
注意,C标准不要求“const”数据是放入只读存储器中,而且在传统结构中,除了正确访问就没有要紧的了。因而在承认参数的C标准中使用const限定是非传统的,无论如何这样做与标准C函数定义是有一定冲突的。
例如,标准“strcpy”的原型是strcpy(char*dst,const char*src),带有const限定的第2个参数表示函数不能修改参数,然而在ICCAVR下,const限定词表示第2个参数指向程序存储器是不合适的。因此这些函数定义设有const限制。
最后,注意只有常数变量以文件存储类型放入FLASH中,例如定义在函数体外的变量或有静态存储类型限制的变量,如果使用有const限制的局部变量,将不被放入FLASH中而可能导致不明确的结果。
4.字符串
在哈佛结构的AVR单片机中,程序内存和数据内存分开,给程序内存和数据内存的说明带来了一定的复杂性,现在来讨论一下字符串。
这个编译器将带有const说明的表和项目放入程序存储器中。最困难的是字符串的分配和处理,问题在于C中将字符串转换为char指针。如果字符串是分配在程序存储器中,那么所有字符串库函数中的任意一个必须被复制成不同于指针的操作,或者字符串也必须被分配在数据存储器中。ImageCraft编译器提出了解决这个问题的两个方法:
(1)默认的字符串
分配这个默认的方法是同时分配字符串在数据和程序存储器中,所有涉及的字符串是复制进数据存储器的,为了确保它们的值是正确的,在程序启动时字符串是由程序存储器复制进数据存储器中的。因此只有单一的字符串复制函数是必须的(编译器执行全局变量初始化也是这样处理的)。
如果希望节省空间,能使用常量字符型数组来将字符串只分配进程序存储器中。例如:
const char hello[]="Hello World";
在这个例子中,hello可以在上下文中作为字符串使用,但不能用作标准C库中字符串函数的参数。
Printf已被扩展成带%S格式字符来输出只存储于FLASH中的字符串。另外,新的字符串函数已加入了对只存储于FLASH中字符串的支持。
(2)只分配全部字符串到FLASH存储器中
当对应“Project->Options->Target->Strings In FLASH Only”检查框被选中时,可以指挥编译器字符串只放在FLASH中,这时必须很小心地调用库函数。当这个选项是选中的,字符串类型“const char*”是有效的,并且必须保证函数获得了合适的参数类型。除了新的“const char*”与字符串有关系外,创建了cprintf和csprintf函数承认字符串格式的类型。读者可以参考标准输入输出函数。
注意,当选项2(只分配全部字符串到FLASH存储器中)时,应使用cprintf()。对const char*及const char ptr[]类型字符串,并且加%S参数。
当选项1时,应使用printf()。对const char*及const char ptr[]类型字符串,并且加%S参数。
5.堆栈
生成代码使用两个堆栈:一个是用于子程序调用和中断操作的硬件堆栈,另一个是用于以堆栈结构传递的参数、临时变量和局部变量的软件堆栈。
硬件堆栈起初是用于存储函数返回的地址,它代表了许多小的软件堆栈。通常,如果程序没有子程序调用,也不调用像带有%f格式的printf()等库函数。那么默认的16B应该在大多数的例子中能良好工作,在绝大多数程序中除了很繁重的递归调用程序(再入式函数),最多40B的硬件堆栈应该是足够的。
硬件堆栈是从数据内存的顶部开始分配的,而软件堆栈是在它下面一定数量字节处分配。硬件堆栈和数据内存的大小是受在编译器选项中的目标装置项设定限制的,数据区从0x60开始分配,在IO空间后面是正确的,允许数据区和软件堆栈彼此相向生长。
如果选择的目标装置带有32KB或64KB的外部SRAM,那么堆栈是放在内部SRAM的顶部,而且向低内存地址方向生长。可以参考程序和数据内存的使用。
任意一个程序失败的重要原因是堆栈溢出到其他数据内存的范围。两个堆栈中的任意一个都可能溢出,并且当一个堆栈溢出时会偶尔产生坏的事情,可以使用堆栈检查函数检测溢出情况。
6.在线汇编
除了在汇编文件中写汇编函数外,在线汇编允许写汇编代码进C文件中(当然,在工程中使用汇编源文件作为一个部件是良好的)。在线汇编的语法如下:
asm("<string>");
多个汇编声明可以被符号\n分隔成新的一行,“String”可以被用来指定多个声明,除了额外增加的ASM关键词。为了在汇编声明中访问一个C的变量,可使用%<变量名>格式,如:
register unsigned char uc;
asm("mov %uc,R0\n""sleep\n");
任意一个C变量都可以被引用。如果在汇编指令中需使用一个CPU寄存器,必须使用寄存器存储类(register)来强制分配一个局部变量到CPU寄存器中。
通常,使用在线汇编引用局部寄存器的能力是有限的。如果在函数中描述了太多的寄存器变量,就很可能没有寄存器可用。在这种情况下,将从汇编程序得到一个错误,那时也不能控制寄存器变量的分配,所以在线汇编指令很可能失败。作为例子,使用LDI指令需要使用R16~R31中的一个寄存器,但这里没有请求使用在线汇编,同样也没有引用上半部分的整数寄存器。
在线汇编可以被用在C函数的内部或外部,编译器将在线汇编的每行都分解成可读的,不像AVR汇编器,ImageCraft汇编器允许标签放置在任意地方,所以可以在在线汇编代码中创建标签。当汇编声明在函数外部时,可能得到一个警告,不要理睬这个警告。
7.IO寄存器
IO寄存器,包含状态寄存器SREG,可以被两条路线访问。IO地址在0x00~0x3F之间,可以使用IN和OUT指令读写IO寄存器;或者使用在0x20~0x5F之间的数据内存地址,可以使用普通数据访问指令和地址模式。两种方法在C中都可使用:
数据内存地址,一个直接地址可以通过加指针类型符号直接访问。例如,SREG的数据内存在地址是0x5F:
注意,数据内存地址0~31涉及CPU寄存器,注意不要改变CPU寄存器。
当访问在IO寄存器范围中的数据内存时,编译器自动生成低级指令如in、out、sbrs、sbrc等是首选的方法。
IO地址,可以使用在线汇编和预处理宏来访问IO地址:
注意,旧的头文件avr.h定义IO寄存器的bit有一些模糊,尽管io*.h定义了它们的bit的位置。因此在使用io*.h和IO寄存器的bit时,将需要使用定义在macros.h文件中的BIT()宏。例如:
8.绝对内存地址
程序可能需要使用绝对内存地址,例如外部IO设备通常被映射成特殊的内存。这些可能包括LCD界面和双口SRAM,通常可以使用在线汇编或单独的汇编文件来描述那些定位在特殊内存地址的数据。
在下面有例子中,假设有一个两字节的LCD控制寄存器定位在0x1000地址,一个两字节的LCD数据寄存器定位在0x1002地址,并且有一个100字节的双口SRAM定位在0x2000的地址。
使用汇编模式,在一个汇编文件中输入以下内容。
在C文件中必须这样描述:
extern unsigned int LCD_control_register,LCD_data_register;
extern char dual_port_SRAM[100];
注意,界面规定在汇编文件中外部变量名称是带‘_’前缀的并且使用两个冒号定义为全局变量。
也可以选择在线汇编,在线汇编遵守同样的汇编语法规则,除了它被附加了一个asm()伪函数。在C文件中,关于上面的汇编代码被变为如下代码:
在C中仍然要使用“extern”描述变量,正像上面使用单独的汇编文件那样,否则C编译器不会真正知道在asm中的声明。
9.C任务(Tasks)
作为汇编界面的描述和调用规则,编译器通常生成代码来保存和恢复保护的寄存器。在一些情况下,这些行为可能是不合适的。例如,如果使用RTOS(实时操作系统),RTOS管理着寄存器的保存和恢复并作为任务切换处理的一部分,编译器如果再插入这些代码就变得多余了。
为了禁止这种行为,可以使用“#pragma ctask”,例如:
这个附注(pragma)必须被用在函数定义之前。注意作为默认的情况,从不返回的程序“main”是有这个属性的,它也没有必要为返回保存和恢复任意一个寄存器。
10.中断操作
在中断操作中,C中断可以使用,无论函数定义在文件的什么地方,必须用一个附注(pragma)在函数定义之前通知编译器这个函数是一个中断操作:
#pragma interrupt_handler<name>:<vector number>*
“vector number”中断的向量号,注意向量号是从1开始的,那是复位向量。这个附注有两个作用:对中断操作函数,编译器生成RETI指令代替RET指令,而且保存和恢复在函数中用过的全部寄存器;编译器生成以向量号和目标MCU为基础的中断向量。
例如:
编译器生成的指令为
rjmp_timer_handler ;对普通AVR单片机MCU
或者
jmp_timer_handler ;对Mega单片机MCU
上述指令定位在0x06(字节地址,针对普通装置)和0x0c(字节地址,针对Mega装置)。Mega使用2个字作为中断向量,非Mega使用1字作为中断向量。如果希望对多个中断入口使用同一个中断操作,可以在一个interrupt_handler附注中放置多个用空格分开的名称,分别带有多个不同的向量号。例如:
#pragma interrupt_handler timer_ovf:7 timer_ovf:8
汇编中断操作,可以用汇编语言写中断操作。如果在汇编操作内部调用C函数,无论如何要小心。汇编程序要保存和恢复挥发寄存器(参考汇编界面),C函数不做这些工作。
如果使用汇编中断操作,那么必须自己定义向量。使用“abs”属性描述绝对区域,用“.org”来声明rjmp或jmp指令的正确地址。注意这个“.org”声明使用的是字节地址。
;对全部除ATmega以外的MCU
.area vectors(abs);中断向量
.org 0x6
rjmp_timer
;对ATmega MCU
.area vectors(abs);中断向量
.org 0xC
jmp_timer
11.访问UART
默认的库函数getchar和putchar使用查寻模式从UART中进行读写。在\icc\exam-ples.avr目录,有一个以中断方式工作的IO程序可以代替默认的程序。
12.访问EEPROM
EEPROM在运行时可以使用库函数访问,在调用这些函数之前加入#include<eeprom.h>。
EEPROM_READ(int location,object),这个宏调用了EEPROMReadBytes函数,从EE-PROM指定位置读取数据送给数据对象,“object”可以是任意程序变量包括结构和数组。例如:
int i;
EEPROM_Read(0x1,i);//读2B给i
EEPROM_WRITE(int location,object),这个宏调用了EEPROMWriteBytes函数,将数据对象写入到EEPROM的指定位置,“object”可以是任意程序变量包括结构和数组。例如:
int i;
EEPROM_WRITE(0x1,i);//写2B至0x1
这些宏和函数可以用于任意AVR单片机装置。可是对EEPROM单元少于256B的MCU,即使不需要高地址字节它们也是欠佳的,因为它仍然是要写的。如果它关系重大,可以为EEPROM较少的目标装置重新编译库源代码。
EEPROM可以在程序源文件中初始化,在C源文件中它作为一个全局变量被分配到特殊调用区域“eeprom”中的,这是可以用附注实现的,结果是产生扩展名为.eep的输出文件。例如:
EEPROM_READ((int)&foo,i);//i等于0x1234
第2个附注是必须的,为返回默认的“data”区域需要重设数据区名称。注意因为AVR单片机的硬件原因,初始化EEPROM数据至0地址是不可以使用的。注意当使用外部描述(比如访问在另一个文件中的foo),不需要加入这个附注。例如:
如果需要下列函数可以直接使用,上面关于宏的描述对大多数装置应该是有能力的。
unsigned char EEPROMread(int location),从EEPROM指定位置读取1B。
int EEPROMwrite(int location,unsigned char byte),写1B到EEPROM指定位置,如果成功返回0。
void EEPROMReadBytes(int location,void*ptr,int size),从EEPROM指定位置处开始读取“size”个字节至由“ptr”指向的缓冲区。
void EEPROMWriteBytes(int location,void*ptr,int size),从EEPROM指定位置处开始写“size”个字节,写的内容由“ptr”指向的缓冲区提供。
13.访问SPI
一个以查寻模式访问SPI的函数是提供的,更多的信息参考spi.h。
14.相对转移/调用的地址范围
一个带8KB程序存储器的装置,全部范围内的跳转可以使用相对转移和调用指令(rjmp和rcall)。为实现这个目的,相对转移和调用的范围是以8KB为分界的。例如,一个较远的跳转跳转到0x2100字节处(0x2000为8KB),实际上会跳转到地址0x100处。这个选项是由工程管理器自动检测的,只要目标装置的程序存储器是8KB的。