avr-gcc与avr单片机c语言开发 pdf,AVR 单片机与GCC 编程使用方法
原标题:AVR 单片机与GCC 编程使用方法
WIN 是一个ATMEL AVR 系列 的开发工具集,它包含GNU C 和C++编译器 。
1.1 一个简单的例子
为了先有一个感性的认识,我们首先看一下如下一段程序和它的编译、链接过程。
文件demo1.c :
#include
int main( void )
{
unsigned char i, j, k,=0;
DDRB=0xff;
while (1)
{
if(led)
PORTB|=0X01;
else
PORTB&=0XFE;
led=!led;
//延时
for (i=0; i<255; i++)
for(j=0; j<255;j++)
k++;
}
}
这是一个使接在PB0 口的发光管闪烁的程序。有了源程序文件demo1.c,我们就可以编译它了。通过点击菜单开始->运行 在弹出的对话框中输入 “ command ” ,来打开控制台窗口,并在命令行输入:-gcc –mmcu=at90s2313 –c demo1.c
如图1-1 所示。
必需告诉编译器 程序的mcu 类型,这是我们通过命令行选项-mmcu 来指定的,我们指定的器件为at90s2313。-c 选项告诉编译器编译完成后不链接。
图1-1 控制台窗口
编译完成后在工作目录新生成了一个文件:demo1.o ,它是我们的目标文件,我们再使用链接器将它链接成可在器件上执行的二进制代码。
在命令行输入:avr-gcc –mmcu=at90s2313 –O demo1.elf demo1.o
之后我们会在工作目录看见链接器生成的demo1.elf。gcc 的链接后生成的文件为ELF 格式,在命令行我们通常用.elf 指定其扩展名。ELF 格式文件除了包含不同存储器的二进制格式内容外还包含一些调试信息,所以我们还要借助一个有用工具 avr-objcopy 来提取单片机内容。命令行输入:avr-objcopy -j .text -j .data -O ihex demo1.elf demo1.hex
gcc 把不同类型的数据分到不同的段落,相关程序存储器的段有 .text 和 .data ,我们用选项 –j 指定了要提取的段。选项 –O 用来指定输出格式,这里我们指定为ihex (intel HEX file)。
到此我们得到了最终可以写入单片机90S2313 FLASH 存储器的demo1.hex 文件。用编程器将demo1.hex 内空写入到单片机,便可看到接在PB0 口的LED 不断的闪烁。以上对一次编译过程的描述只是为了说明gcc 编译一个C 源程序的步骤,在实际的应用中我们很少用这种方式编译每一个源程序和每一个更新后的程序。而是借助一个叫make 的项目管理工具来进行编译操作。Make 由下一节介绍。
1.2 用MAKEFILE 管理项目
在我看来,通常一个编译器(泛指高级语言编译器、汇编器、链接器等等)、项目管理器和文本编辑器构成一个完整的编程环境。
WINAVR 没有像Keil uVision 那样的集成IDE,所以我们需要写一个叫做makefile 的文件来管理程序的编译链接。makefile 是个脚本文件,一个标准的(应该说经典的)可执行文件make.exe 负责解析它并根据脚本内容来调用编译器、链接器或其它的工具。
1.2.1 make 的使用
make 能够自动记忆各源文件间的依赖关系,避免重复编译。
Make 指令用法是:
Make [-f filename] [names]
方括号表示括号里边的内容可以省略。其中filename 代表make 所使用的项目描述文件,如果此项省略,则从当前目录下按下列顺序寻找默认的项目描述文件
GNUmakefile.
makefile
Makefile (当然在WINDOWS 下不份大小写文件名,也就无所谓了)
names 指定目标名或宏名。若不指定目标名,则make 命令总是把在makefile 文件中遇到的第一个目标当作默认目标。
1.2.2 Makefile 项目描述文件
一.目标
make 命令引入了目标(targets)的概念。Makefile 描述文件便是它的第一个目标,make 命令必须处理至少一个目标,否则不会得出任何结果。正如我们在一个没有默认描述文件的当前目录下敲入make 一样,make 会输出以下的结果:
MAKE: ***No targets specifiend no makefile found. Stop.
1.在项目描述文件中定义目标
一个目标通常从一行的开头开始,并后跟一个冒号。
最简单的MAKEFILE
#当前目录 D:\AVRGCC\TEST
all:
@echo hello!
#End makefile
all: 便是第一个目标
调用此描述文件结果:
D:\AVRGCC\TEST>make
hello!
2.默认目标(goal)
在上面提到过,如果调用make 时不指定目标名则make 总是假设在描述文件中遇到的第一个目标是默认目标。以下示例可以非常好的说明这一问题。
具有三个目标的makefile
#当前目录 D\AVRGCC\TEST
one:
@echo one.
Two:
@echo two.
Three:
@echo three.
#End makefile
调用1:
D:\AVRGCC\TEST>make
one.
由于在命令行没有指定目标,make 从makefile 中找到第一个目标(one)并执行后既退出。
调用2:
D:\AVRGCC\TEST>make two
two.
由于在命令行明确指定了要执行的目标(two),make 在makefile 中找到指定的目标,并执行后退出。
调用3:
D:\AVRGCC\TEST make three one two
three.
one.
two.
命令行指定了三个目标,make 一一寻找并执行。
在makefile 中非默认的目标称为可替换的目标,只有默认的目标与它们存在直接或间接的依赖关系时它们才有可能被调用。
二.依赖关系
makefile 文件按如下格式指定依赖关系:
目标1[目标2 … ] : [ : ][依赖1][依赖2] …
[命令]
如下例
#当前目录 D:\AVRGCC\TEST
one: Two
@echo one.
Two:
@echo two.
#End makefile
执行结果是:
d:\avrgcc\test>make
two.
one.
Make 首先找到第一个目标one ,之后发现目标one 依赖目标Two 就先执行Two 后才执行one 中的命令。
三.Makefile 内容
makefile 内容可分为如下五种类型
①规则定义
语法:
目标 : 依赖
命令
...
其中目标为一个文件名或以空格分开的多个文件名,可含通配符。
例如:
%.o : %.c
avr-gcc -c $< -o $@
以上规则定义了任意一个以 .o 结尾的文件依赖于相同前缀且以 .c 结尾的文件。并执行下边的命令获得。
规则中目标和依赖分别为 %.o 和%.c,在目标通配符 “%” 代表任意的字符串,而在依赖中代表与目标中代表的对应字符串。
②隐含规则
隐含规则是make 预先定义的规则,用选项 –r 可取消所有的隐含规则。
例如对于C 程序 %.o 可以自动的从 %.c 通过命令
$(CC) -c $(CPPFLAGS) $(CFLAGS)' 生成。
③变量
变量是在makefile 中描述一字符串的的名称。变量可用在目标、依赖、命令和makefile 其它部分中。变量名由除': '、'#'、'='之外的字符组成,对大小写敏感。
变量的定义并赋值格式:
变量名 = 变量代表字符串
变量的引用格式:
$(变量名)
例如:
CC = avr-gcc
%.o : %.c
$(CC) -c $< -o $@
④命令
命令部分是由make 传递到系统的字符格式的命令行的组合,在目标被创建时它们按顺序一行一行传递到系统并执行。
字符 '@'开始的命令 在系统的输出中不显示本次的指令行。
⑤注释
字符 '# ' 开头的行为注释行,如果注释需要换行需在行尾加 ' \ ',除包含其它MAKEFIEL 外在行的任意处可插入注释。
四.自动变量
在makefile 中有一组预定义的变量,当每一规则被执行时根据目标和依赖重新计算其值,叫作自动变量。
下面列出了常用的几个自动变量
$@ : 在规则中代表目标名,如果规则含有多个目标名它将列举所有目标。
$% : 仅在目标是存档文件的成员时起作用,代表目标。
如目标foo.a(bar.o)中$@ 代表foo.a $%代表bar.o
$< : 在规则中代表第一个依赖文件名
$? : 代表在规则中所有以空格隔开的依赖文件名,如果依赖是存档文件的成员则只有成员名被列出。
$^ : 代表在规则中所有以空格隔开的依赖文件名,如果依赖是存档文件的成员则只有成员名被列出。
WINAVR 提供一种简单makefile 生成工具叫mfile,如图1-2
利用它我们可方便的生成合适的makefile。
Main file name…菜单指定主程序文件,它将决定主源程序文件名及输出文件名。
Output format 菜单项用于选择最终生成的可指行代码格式,根据编程器支持格式选择即可。
Optimization leave 指定C 代码的优化级,s 代表按最小代码量编译。
C/C++ source file(s) 和Assembler source files(s) 用于在项目中添加其它C、C++、和汇编程序文件。
图1-2 mfile 生成makefile
通常我们选择了以上几项便可编译了。
1.3 开发环境的配置
一.软件环境
UltraEdit + WinAVR 打造超级开发IDE
UltraEdit 是个文本编辑器,它支持C 代码的高亮显示、项目管理及外部工具配置等功能。
首先要安装UltraEdit 和 WinAVR。
(1) UltraEdit 的个性化设置:
下面是我个人习惯的设置
视图->颜色设置 光标所在行文本 设置成黑,光标所在行背景设置成白
高级->配置->编辑 制表符宽度值和缩进空格娄均设成4。
高级->配置->备份 保存时备份文件里选择不备份。
视图->查看列表 选中函数列表
(2) 创建编译用的文件
先在硬盘上创建一个目录作为设计大本营,这里假设为 d:\devdir
UltraEdit 写主程序文件保存到此文件夹中 这里设为demo.c
用mfile 生成一个合适的makefile 保存到d:\devdir
UltraEdit 创建一项目,负责管理文件
项目->新建项目 目录选d:\devdir 输入项目名称(这里假设为prj)
在接下来的文件设置对话框中的项目文件目录区输入或选择d:\devdir
选中 相对路径 复选按钮
通过 添加文件 按钮将刚才的makefile 和demo.c 添加到项目中,之后按关闭。
(3)在UltraEdit 中make 我的项目
高级 -> 工具配置
在命令行区输入 make
在工作目录区输入 d:\devdir
在菜单项目名称区输入一个任意的菜单名称
选中 输出到列表框 和 捕获输出两个选择按钮后单击 插入按钮 确定。
至此你就可以在UltraEdit 内就可以make 你的程序了?
如果不愿意每次编译时找菜单可用快捷键 Ctrl+shift+0。
记得要在你的项目里添加源程序时,除了在UltraEdit 项目->文件设置里添加外还要在
makefile 的SRC 变量后列出来才可编译哦?
到此 我们的超级无敌AVR 编程环境打造完成 ,如图1-3。
图 1-3 配置后的UltraEdit 外观
二.硬件环境
SI-Prog + PonyProg 实现最廉价的下载实验器 AVR 系列单项机提供对程序存储器(FLASH)和数据存储器(EEPROM)的串行编程功能(ISP),使它的程序烧写变得方便。AVR 系列器件内部FLASH 存储器的编程次数通常可达到10000 次以上,所以使用多次烧写的方式调试程序时不必担心器件的损坏。
ISP 功能占用三个I/O 端口(MOSI 、MISO、 SCK)与外部编程逻辑通信,编程逻辑按指定时序将程序数据串行方式发送到器件,器件内的ISP 功能模块负责将数据写入到FLASH 或EEPROM。
在实际应用中通常利用PC 机的并行口或串行口加一个下载适配器(下载线)实现一个编程硬件,AVR 的下载线有很多种,这里向大家推荐SI-Prog,SI-Prog 具有制作方便(只需几个分立元件)、接线少(通过PC 9 针串行口编程),支持软件成熟(PonyProg)等特点。 si-prog 的完整电路可到http://www.LancOS.com 下载。图1-4
为简化后的图。
PonyPorg 是个串行器件编程软件,支持AVR 在内的多种器件的串行编程。该软件可到http://www.LanOS.com 下载。
图1-4 SI-Prog 电路原理图
有了一台安装有PonyPorg 的PC 机和SI-Prog ,就可以将程序写入到实际器件来验证了,想一想此方案的成本和一个AVR 芯片能烧写的次数,是不是觉得很值?
读到这里您对AVR 单片机的开发和WINAVR 编程应该有了一个基本的认识,也应当做好了开发或学习前软硬件的准备工作。从下一章开始我将进一步解析AVR 的GCC 程序设计。
第二章 存储器操作
2.1 AVR 单片机存储器组织结构
AVR 系列单片机内部有三种类型的被独立编址的存储器,它们分别为:Flash 程序存储器、内部SRAM 数据存储器和EEPROM 数据存储器。
Flash 存储器为1K~128K 字节,支持并行编程和串行下载,下载寿命通常可达10,000 次。
由于AVR 指令都为16 位或32 位,程序计数器对它按字进行寻址,因此FLASH 存储器按字组织的,但在程序中访问FLASH 存储区时专用指令LPM 可分别读取指定地址的高低字节。
寄存器堆(R0~R31)、I/O 寄存器和SRAM 被统一编址。所以对寄存器和I/O 口的操作使用与访问内部SRAM 同样的指令。其组织结构如图2-1 所示。
图2-1 AVR SRAM 组织
32 个通用寄存器被编址到最前,I/O 寄存器占用接下来的64 个地址。从0X0060 开始为内部SRAM。外部SRAM 被编址到内部SRAM 后。
AVR 单片机的内部有64~4K 的EEPROM 数据存储器,它们被独立编址,按字节组织。擦写寿命可达100,000 次。
2.2 I/O 寄存器操作
I/O 专用寄存器(SFR)被编址到与内部SRAM 同一个地址空间,为此对它的操作和SRAM 变量操作类似。
SFR 定义文件的包含:
#include
io.h 文件在编译器包含路径下的avr 目录下,由于AVR 各器件间存在同名寄存器地址有不同的问题,io.h 文件不直接定义SFR 寄存器宏,它根据在命令行给出的 –mmcu 选项再包含合适的 ioxxxx.h 文件。
在器件对应的ioxxxx.h 文件中定义了器件SFR 的预处理宏,在程序中直接对它赋值或引用的方式读写SFR,如:
PORTB=0XFF;
Val=PINB;
从io.h 和其总包含的头文件_defs.h 可以追溯宏PORTB 的原型
在io2313.h 中定义:
#define PORTB _SFR_IO8(0x18)
在sfr_defs.h 中定义:
#define _SFR_IO8(io_addr) _MMIO_BYTE((io_addr) + 0x20)
#define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))
这样PORTB=0XFF; 就等同于 *(volatile unsigned char *)(0x38)=0xff;
0x38 在器件AT90S2313 中PORTB 的地址
对SFR 的定义宏进一步说明了SFR 与SRAM 操作的相同点。
关键字volatile 确保本条指令不会因C 编译器的优化而被省略。
2.3 SRAM 内变量的使用
一个没有其它属性修饰的C 将被指定到内部SRAM,avr-libc 提供一个整数类型定义文件inttype.h,其中定义了常用的整数类型如下表:
定义值 长度(字节) 值范围
int8_t 1 -128~127
uint8_t 1 0~255
int16_t 2 -32768~32767
uint16_t 2 0~65535
int32_t 4 -2147483648~2147483647
uint32_t 4 0~4294967295
int64_t 8 -9.22*10^18~-9.22*10^18
uint64_t 8 0~1.844*10^19
根据习惯,在程序中可使用以上的整数定义。
定义、初始化和引用
如下示例:
uint8_t val=8; 定义了一个SRAM 变量并初始化成8
val=10; 改变变量值
const uint8_t val=8; 定义SRAM 区常量
register uint8_t val=10; 定义
2.4 在程序中访问FLASH 程序存储器
avr-libc 支持头文件:pgmspace.h
#include < avr/pgmspace.h >
在程序存储器内的数据定义使用关键字 __attribute__((__progmem__))。在pgmspace.h
中它被定义成符号 PROGMEM。
1. FLASH 区整数常量应用
定义格式:
数据类型 常量名 PROGMEM = 值 ;
如:
char val8 PROGMEM = 1 ;
int val16 PROGMEM = 1 ;
long val32 PROGMEM =1 ;
对于不同长度的整数类型 avr-libc 提供对应的读取函数:
pgm_read_byte(prog_void * addr)
pgm_read-word(prg_void *addr)
pgm_read_dword(prg_void* addr)
另外在pgmspace.h 中定义的8 位整数类型 prog_char prog_uchar 分别指定在FLASH
内的8 位有符号整数和8 位无符号整数。应用方式如下:
char ram_val; //ram 内的变量
const prog_char flash_val = 1; //flash 内常量
ram_val=pgm_read_byte(&flash_val); //读flash 常量值到RAM 变量
对于应用程序FLASH 常量是不可改变的,因此定义时加关键字const 是个好的习惯。
2. FLASH 区数组应用:
定义:
const prog_uchar flash_array[] = {0,1,2,3,4,5,6,7,8,9}; //定义
另外一种形式
const unsigned char flash_array[] RROGMEM = {0,1,2,3,4,5,6,7,8,9};
读取示例:
unsigend char I, ram_val;
for(I=0 ; I<10 ;I ++) // 循环读取每一字节
{
ram_val = pgm_read_byte(flash_array + I);
… … //处理
}
2. FLASH 区字符串常量的应用
全局定义形式:
const char flash_str[] PROGMEM = “Hello, world!”;
函数内定义形式:
const char *flash_str = PSTR(“Hello, world!”);
以下为一个FLASH 字符串应用示例
#include
#include
#include
const char flash_str1[] PROGMEM = “全局定义字符串”;
int main(void)
{
int I;
char *flash_str2=PSTR(“函数内定义字符串”);
while(1)
{
scanf(“%d”,&I);
printf_P(flash_str1);
printf(“\n”);
printf_P(flash_str2);
printf(“\n”);
}
}
2.5 EEPROM 数据存储器操作
#include EEPROM.h>
头文件声明了avr-libc 提供的操作EEPROM 存储器的API 函数。
这些函数有:
EEPROM_is_ready() //EEPROM 忙检测(返回EEWE 位)
EEPROM_busy_wait() //查询等待EEPROM 准备就绪
uint8_t EEPROM_read_byte (const uint8_t *addr) //从指定地址读一字节
uint16_t EEPROM_read_word (const uint16_t *addr) //从指定地址一字
void EEPROM_read_block (void *buf, const void *addr, size_t n) //读块
void EEPROM_write_byte (uint8_t *addr, uint8_t val) //写一字节至指定地址
void EEPROM_write_word (uint16_t *addr, uint16_t val) //写一字到指定地址
void EEPROM_write_block (const void *buf, void *addr, size_t n)//写块
在程序中对EEPROM 操作有两种方式
方式一:直接指定EERPOM 地址
示例:
/*此程序将0xaa 写入到EEPROM 存储器 0 地址处,
再从0 地址处读一字节赋给RAM 变量val */
#include
#include EEPROM.h>
int main(void)
{
unsigned char val;
EEPROM_busy_wait(); //等待EEPROM 读写就绪
EEPROM_write_byte(0,0xaa); //将0xaa 写入到EEPORM 0 地址处
EEPROM_busy_wait();
val=EEPROM_read_byte(0); //从EEPROM 0 地址处读取一字节赋给RAM 变量val
while(1);
}
方式二:先定义EEPROM 区变量法
示例:
#include
#include EEPROM.h>
unsigned char val1 __attribute__((section(".EEPROM")));//EEPROM 变量定义方式
int main(void)
{
unsigned char val2;
EEPROM_busy_wait();
EEPROM_write_byte (&val1, 0xAA); /* 写 val1 */
EEPROM_busy_wait();
val2 = EEPROM_read_byte(&val1); /* 读 val1 */
while(1);
}
在这种方式下变量在EEPROM 存储器内的具体地址由编译器自动分配。相对方式一,数据在EEPROM 中的具体位置是不透明的。
为EEPROM 变量赋的初始值,编译时被分配到.EEPROM 段中,可用avr-objcopy 工具从.elf文件中提取并产生ihex 或binary 等格式的文件。
2.6 avr-gcc 段(section)与再定位(relocation)
粗略的讲,一个段代表一无缝隙的数据块(地址范围),一个段里存储的数据都为同一性质,如“只读”数据。as (汇编器)在编译局部程序时总假设从0 地址开始,并生成目标文件。最后ld(链接器)在连接多个目标文件时为每一个段分配运行时(run-time)统一地址。这虽然是个简单的解释,却足以说明我门为为什么用段.
ld 将这些数据块正确移动到它们运行时的地址。 此过程非常严格,数据的内部顺序与长度均不能发生变化.这样的数据单元叫做段,为段分配运行时地址叫再定位,此任务根据目标文件内的参考地址将段数据调整到运行时地址。
Avr-gcc 中汇编器生成的目标文件(object-file)至少包含四个段,分别为: .text 段、.data段 、 .bss 段和.EEPROM 段,它们包括了程序存储器(FLASH)代码,内部RAM 数据,和EEPROM 存储器内的数据。这些段的大小决定了程序存储器(FLASH)、数据存储器(RAM)、EEPROM 存储器的使用量,关系如下:
程序存储器(FLASH)使用量 = .text + .data
数据存储器(RAM)使用量 = .data + .bss [+ .noinit] + stack [+ heap]
EEPROM 存储器使用量 = .EEPROM
一..text 段
.text 段包含程序实际执行代码。另外,此段还包含.initN 和.finiN 两种段,下面详细讨论。
段.initN 和段.finiN 是个程序块,它不会象函数那样返回,所以汇编或C 程序不能调用。
.initN、.finN 和绝对段(absolute section 提供中断向量)构成avr-libc 应用程序运行框架,用户编写的应用程序在此框架中运行。
.initN 段
此类段包含从复位到main()函数开始执行之间的启动(stap)代码。
此类段共定义10 个分别是.init0 到.init9。执行顺序是从.init0 到.init9。
.init0:
此段绑定到函数__init()。用户可重载__init(),复位后立即跳到该函数。
.init1:
未用,用户可定义
.init2:
初始化堆栈的代码分配到此段
.init3:
未用,用户可定义
.init4:
初始化.data 段(从FLASH 复制全局或静态变量初始值到.data),清零.bss 段。
像UNIX 一样.data 段直接从可执行文件中装入。Avr-gcc 将.data 段的初始值存储到flash
rom 里.text 段后,.init4 代码则负责将这些数据复制SRAM 内.data 段。
.init5:
未用,用户可定义
.init6:
C 代码未用,C++程序的构造代码
.init7:
未用,用户可定义
.init8:
未用,用户可定义
.init9:
跳到main()
avr-libc 包含一个启动模块(startup module),用于应用程序执行前的环境设置,链接时它被分配到init2 和init4 中,负责提供缺省中断程序和向量、初始化堆栈、初始化.data 段和清零.bss 段等任务,最后startup 跳转到main 函数执行用户程序。
.finiN 段
此类段包含main()函数退出后执行的代码。
此类段可有0 到9 个, 执行次序是从fini9 到 fini1。
.fini9
此段绑定到函数exit()。用户可重载exit(),main 函数一旦退出exit 就会被执行。
.fini8:
未用,用户可定义
.fini7:
未用,用户可定义
.fini6:
C 代码未用, C++程序的析构代码
.fini5:
未用,用户可定义
.fini4:
未用,用户可定义
.fini3:
未用,用户可定义
.fini2:
未用,用户可定义
.fini1:
未用,用户可定义
.fini0:
进入一个无限循环。
用户代码插入到.initN 或.finiN
示例如下:
void my_init_portb (void) __attribute__ ((naked)) \
__attribute__ ((section (".init1")));
void my_init_portb (void)
{
outb (PORTB, 0xff);
outb (DDRB, 0xff);
}
由于属性section(“.init1”)的指定,编译后函数my_init_portb 生成的代码自动插入到.init1段中,在main 函数前就得到执行。naked 属性确保编译后该函数不生成返回指令,使下一个初始化段得以顺序的执行。
二..data 段
.data 段包含程序中被初始化的RAM 区全局或静态变量。而对于FLASH 存储器此段包含在程序中定义变量的初始化数据。类似如下的代码将生成.data 段数据。
char err_str[]=”Your program has died a horrible death!”;
struct point pt={1,1};
可以将.data 在SRAM 内的开始地址指定给连接器,这是通过给avr-gcc 命令行添加
-Wl,-Tdata,addr 选项来实现的,其中addr 必须是0X800000 加SRAM 实际地址。例如 要将.data 段从0x1100 开始,则addr 要给出0X801100。
三..bss 段
没有被初始化的RAM 区全局或静态变量被分配到此段,在应用程序被执行前的startup过程中这些变量被清零。
另外,.bss 段有一个子段 .noinit , 若变量被指定到.noinit 段中则在startup 过程中不会被清零。将变量指定到.noinit 段的方法如下:
int foo __attribute__ ((section (“.noinit”)));
由于指定到了.noinit 段中,所以不能赋初值,如同以下代码在编译时产生错误:
int fol __attribute__((section(“.noinit”)))=0x00ff;
四..EEPROM 段
此段存储EEPROM 变量。
Static unsigned char eep_buffer[3] __attribute__((section(“.EEPROM”)))={1,2,3};
在链接选项中可指定段的开始地址,如下的选项将.noinit 段指定位到RAM 存储器
0X2000 地址处。
avr-gcc ... -Wl,--section-start=.noinit=0x802000
要注意的是,在编译时Avr-gcc 将FLASH、RAM 和EEPROM 内的段在一个统一的地址空间内处理,flash 存储器被定位到0 地址开始处,RAM 存储器被定位到0x800000 开始处,EEPROM 存储器被定位到0X810000 处。所以在指定段开始地址时若是RAM 内的段或EEPROM 内的段时要在实际存储器地址前分别加上0x800000 和0X810000。
除上述四个段外,自定义段因需要而可被定义。由于编译器不知道这类段的开始地址,又称它们为未定义段。必需在链接选项中指定自定义段的开始地址。如下例:
void MySection(void) __attribute__((section(".mysection")));
void MySection(void)
{
printf("hello avr!");
}
链接选项:
avr-gcc ... -Wl,--section-start=.mysection=0x001c00
这样函数MySection 被定位到了FLASH 存储器0X1C00 处。
第三章 功能模块编程示例
3.1 中断服务程序
avr-gcc 为中断提供缺省的入口例程,这些例程的名字已固定,用户可通过重载这些例程来处理中断。如果中断没有被用户重载,说明正常情况下不会产生该中断,缺省的中断例程将程序引导到0 地址处(既复位)。
Avr-gcc 为重载中断例程提供两个宏来解决细节的问题,它们是 SIGNAL(signame)和INTERRUPT(signame)。参数signame 为中断名称,它的定义在io.h 中包含。表3-1 列出了ATMega8 的signame 定义,其它器件的signame 定义可查阅相应的ioxxxx.h 文件。
表3-1 ATMega8 中断名称定义
signame 中 断 类 型
SIG_INTERRUPT0 外部中断INT0
SIG_INTERRUPT1 外部中断INT1
SIG_OUTPUT_COMPARE2 定时器/计数器比较匹配中断
SIG_OVERFLOW2 定时器/计数器2 溢出中断
SIG_INPUT_CAPTURE1 定时器/计数器2 输入捕获中断
SIG_OUTPUT_COMPARE1A 定时器/计数器1 比较匹配A
SIG_OUTPUT_COMPARE1B 定时器/计数器1 比较匹配B
SIG_OVERFLOW1 定时器/计数器1 溢出中断
SIG_OVERFLOW0 定时器/计数器0 溢出中断
SIG_SPI SPI 操作完成中断
SIG_UART_RECV USART 接收完成
SIG_UART_DATA USART 寄存器空
SIG_UART_TRANS USART 发送完成
SIG_ADC ADC转换完成
SIG_EEPROM_READY E2PROM 准备就绪
SIG_COMPARATOR 模拟中断
SIG_2WIRE_SERIAL TWI 中断
SIG_SPM_READY 写程序存储器准备好
以下是个外部中断0 的重载示例:
#include
#include
#include
SIGNAL(SIG_INTERRUPT0)
{
//中断处理程序
}
宏INTERRUPT 的用法与SIGNAL 类似,区别在于SIGNAL 执行时全局中断触发位被清除、其他中断被禁止,INTERRUPT 执行时全局中断触发位被置位、其他中断可嵌套执行。
另外avr-libc 提供两个API 函数用于置位和清零全局中断触发位,它们分别是:
void sei(void) 和 void cli(void)。
3.2 定时器/计数器应用
下面以定时器/计数器0 为例,说明定时器计数器的两种操作模式
定时器/计数器0 相关寄存器:
TCCR0 :定时器/计数器0 控制寄存器
计数使能,时钟源选择和CPU 时钟预分频设置
TCNT0 :定时器/计数器0 计数值寄存器
包含计数值 (0~255)
TIFR :标志寄存器(Timer Interrupt Flag Register)
TOV0 位 为定时器/寄存器0 溢出标志
TIMSK :定时器中断屏蔽寄存器(Timer Interrupt Mask Register)
TOIE0 位为定时器/寄存器0 中断使能/禁止控制位
查询模式举例:
/* mcu:AT90S2313 时钟:4MHz */
#include
#define uchar unsigned char
#define SET_LED PORTD&=0XEF //PD4 接发光管
#define CLR_LED PORTD|=0X10
int main(void)
{
uchar i,j=0;
DDRD=0X10;
PORTD=0X10;
TCNT0=0; // T/C0 开始值
TCCR0=5; // 预分频 ck/1024 ,计数允许
while(1)
{
//查询定时器方式等待一秒
//4000000 /1024 /256 /15 ≈ 1Hz
for(i=0;i<15;i++)
{
loop_until_bit_is_set(TIFR,TOV0);
sbi(TIFR,TOV0);//写入逻辑1 清零TOV0 位
}
if(j) //反向LED 控制脚
SET_LED,j=0;
else
CLR_LED,j=1;
}
}
中断模式举例:
/* mcu:AT90S2313 时钟:4MHz */
#include
#include
#include
#define uchar unsigned char
#define SET_LED PORTD&=0XEF //PD4 接发光管
#define CLR_LED PORTD|=0X10
static uchar g_bCount=0; //中断计数器
static uchar g_bDirection=0;
//T/C0 中断例程
SIGNAL(SIG_OVERFLOW0)
{
// 产生中断周期 T = 256 * 1024 / 4MHz
if(++g_bCount >14) //中断15 次约一秒
{
if(g_bDirection) //反向LED 控制脚
SET_LED,g_bDirection=0;
else
CLR_LED,g_bDirection=1;
g_bCount=0;
}
}
int main(void)
{
DDRD=0X10;
PORTD=0X10;
TCNT0=0; // T/C0 开始值
TCCR0=5; // 预分频 ck/1024 ,计数允许
TIMSK=_BV(TOIE0);
sei();
while(1);
}
3.3 看门狗应用
avr-libc 提供三个API 支持对器件内部Watchdog 的操作,它们分别是:
wdt_reset() // Watchdog 复位
wdt_enable(timeout) // Watchdog 使能
wdt_disable() // Watchdog 禁止
调用上述函数前要包含头文件 wdt.h ,wdt.h 中还定义Watchdog 定时器超时符号常量,它们用于为wdt_enable 函数提供timeout 值。符号常量分别如下:
符号常量 值含意
WDTO_15MS Watchdog 定时器15 毫秒超时
WDTO_30MS Watchdog 定时器30 毫秒超时
WDTO_60MS Watchdog 定时器60 毫秒超时
WDTO_120MS Watchdog 定时器120 毫秒超时
WDTO_250MS Watchdog 50 毫秒超时
WDTO_500MS Watchdog 定时器500 毫秒超时
WDTO_1S Watchdog 定时器1 秒超时
WDTO_2S Watchdog 定时器2 秒超时
Watchdog 测试程序:
/* mcu:AT90S2313 时钟:4MHz */
#include
#include
#include
#define uchar unsigned char
#define uint unsigned int
#define CLR_LED PORTD&=0XEF //PD4 接发光管
#define SET_LED PORTD|=0X10
//误差不会太大的延时1ms 函数
void DelayMs(uint ms)
{
uint i;
for(i=0;i
_delay_loop_2(4 *250);
}
int main(void)
{
DDRD=0X10;
PORTD=0X10; //SET_LED
wdt_enable(WDTO_1S);
wdt_reset();
DelayMs(500);
CLR_LED;
DelayMs(5000);//等待WDT 复位
SET_LED;
while(1)
wdt_reset();
}
执行结果:
接在PD4 脚下的LED 不断的闪烁,证明了Watchdog 使mcu 不断的复位。
3.4 UART 应用
查询方式:
/* mcu:AT90S2313 时钟:4MHz */
#include
#define uchar unsigned char
#define uint unsigned int
//发送一字节数据
void putc(uchar c)
{
loop_until_bit_is_set(UCR,UDRE);
UDR=c;
}
//uart 等待并接收一字节数据
uchar getc(void)
{
loop_until_bit_is_set(UCR,RXC);
return UDR;
}
int main(void)
{
//uart 初始化
UCR=(1<
UBRR=25; //baud=9600 UBRR=CK/(baud*16) -1
while(1)
{
putc(getc());
}
}
程序从UART 等待接收一字节,接收到数据后立即将数据又从UART 发送回去。
中断方式:
/* mcu:AT90S2313 时钟:4MHz */
#include
#include
#include
#define uchar unsigned char
#define uint unsigned int
uchar g_bTxdPos=0; //发送定位计数器
uchar g_bTxdLen=0; //等待发送字节数
uchar g_bRxdPos=0; //接收定位计数器
uchar g_bRxdLen=0; //等待接收字节数
uchar g_aSendBuf[16]; //发送数据绶冲区
uchar g_aRecvBuf[16]; //接收数据缓冲区
//接收中断
SIGNAL(SIG_UART_RECV)
{
uchar c=UDR;
if(g_bRxdLen>0)
{
g_aRecvBuf[g_bRxdPos++]=c;
g_bRxdLen--;
}
}
//发送中断
SIGNAL (SIG_UART_TRANS)
{
if(--g_bTxdLen>0)
UDR=g_aSendBuf[++g_bTxdPos];
}
//是否接收完成
uchar IsRecvComplete(void)
{
return g_bRxdLen==0;
}
//从发送缓冲区发送指定长度数据
void SendToUart(uchar size)
{
g_bTxdPos=0;
g_bTxdLen=size;
UDR=g_aSendBuf[0];
while(g_bTxdLen>0);
}
//接收指定长度数据到接收缓冲区
void RecvFromUart(uchar size,uchar bwait)
{
g_bRxdPos=0;
g_bRxdLen=size;
if(bwait)
while(g_bRxdLen>0);
}
int main( void )
{
uchar i;
//uart 初始化
//接收使能、发送使能、接收中断允许、发送中断允许
UCR=(1<
UBRR=25; // baud=9600 UBRR=CK/(baud*16) -1
sei();//总中断允许
while(1)
{
//异步接收16 字节数据
RecvFromUart(16,0);
//等待接收完成
while(!IsRecvComplete());
//将接收到的数据复制到发送缓冲区
for(i=0;i<16;i++)
g_aSendBuf[i]=g_aRecvBuf[i];
//发送回接收到的数据
SendToUart(16);
}
}
利用中断可实现数据的异步发送和接收,正如上面程序所示,调用RecvFromUart 后主程序可处理其它任务,在执行其它任务时可调用IsRecvComplete 检测是否接收完成。
3.5 PWM 功能编程
/*
avr-libc PWM 测试程序
mcu:at90S2313
时钟:4MHz
*/
#include
#include
#define uchar unsigned char
#define uint unsigned int
#define FREQ 4
//延时
void DelayMs(uint ms)
{
uint i;
for(i=0;i
_delay_loop_2(FREQ * 250);
}
int main (void)
{
uchar direction=1;
uchar pwm=0;
// 8 位PWM 模式 , 向上计数时匹配清除OC1
TCCR1A = _BV (PWM10) | _BV (COM1A1);
//PWM 引脚PB3 方向设置为输出
DDRB= _BV (PB3);
//启动PWM 时钟源:CK/8 PWM 频率为 4MHz/8/512=976Hz
TCCR1B = _BV (CS11);
//循环改变PWM 输出脉宽,使接在OC1 引脚上的发光管亮度发生变化
while(1)
{
if(direction)
{
if(++pwm==254)
direction=0;
}
else
{
if(--pwm==0)
direction=1;
}
OCR1=pwm;
DelayMs(10);
}
return 0;
}
3.6 模拟比较器
/*
模拟比较器测试程序
mcu:ATMega8
时钟:内部4MHz RC 振荡器
*/
#include
#include
#include
#define uchar unsigned char
#define SET_RED_LED PORTB&=0XFD //PB1 接红色发光管
#define CLR_RED_LED PORTB|=0X02
#define SET_YEL_LED PORTB&=0XFE //PB0 接黄色发光管
#define CLR_YEL_LED PORTB|=0X01
//模拟比较器中断函数
SIGNAL(SIG_COMPARATOR)
{
if(ACSR & _BV(ACO))
{
SET_YEL_LED;
CLR_RED_LED;
}
else
{
CLR_YEL_LED;
SET_RED_LED;
}
}
int main(void)
{
DDRB=0X03;
PORTB=0X03;
//模拟比较器上下均触发中断 ACIS1=ACIS0=0
//中断允许 ACIE=1
ACSR=_BV(ACIE);
sei();
//AIN0:正极 AIN1:负极 AIN0 脚上的电压高于AIN1 上电压时AC0=1
if(ACSR & _BV(ACO))
{
SET_YEL_LED;
CLR_RED_LED;
}
else
{
CLR_YEL_LED;
SET_RED_LED;
}
while(1);
}
以上程序实现了用LED 指示ATMega8 比较输入引脚 AIN0 和AIN1 上的电压的高低状态。
3.7 A/D 转换模块编程
查询方式:
/* 查询方式 A/D 转换测试程序 mcu:atmega8 时钟:4MHz 外部晶振 */
#include
#include
#include
#define uchar unsigned char
#define uint unsigned int
static uint g_aAdValue[8]; //A/D 转换缓冲区
void IoInit(void);
uint AdcConvert(void)
{
uchar i;
uint ret;
uchar max_id,min_id,max_value,min_value;
ADMUX=0Xc0;//内部2.56V 参考电压,0 通道
ADCSRA=_BV(ADEN);//使能ADC,单次转换模式
//连续转换8 次
for(i=0;i<8;i++)
{
ADCSRA|=_BV(ADSC);
_delay_loop_1(60);
while(ADCSRA&_BV(ADSC))
_delay_loop_1(60);
ret=ADCL;
ret|=(uint)(ADCH<<8);
g_aAdValue[i]=ret;
}
ret=0;
for(i=1;i<8;i++)
ret+=g_aAdValue[i];
//找到最大和最小值索引
ret/=7;
max_id=min_id=1;
max_value=min_value=0;
for(i=1;i<8;i++)
{
if(g_aAdValue[i]>ret)
{
if(g_aAdValue[i]-ret>max_value)
{
max_value=g_aAdValue[i]-ret;
max_id=i;
}
}
else
{
if(ret-g_aAdValue[i]>min_value)
{
min_value=ret-g_aAdValue[i];
min_id=i;
}
}
}
//去掉第一个和最大最小值后的平均值
ret=0;
for(i=1;i<8;i++)
{
if((i!=min_id)&&(i!=max_id))
ret+=g_aAdValue[i];
}
if(min_id!=max_id)
ret/=5;
else
ret/=6;
ADCSRA=0;//关闭ADC
return ret;
}
int main(void)
{
uchar i;
IoInit();
while(1)
{
scanf("%c",&i);
if(i=='c')
printf("%d\n",AdcConvert());
}
}
中断方式:
/* 中断方式 A/D 转换测试程序 mcu:atmega8 时钟:4MHz 外部晶振 */
#include
#include
#include
#include
#include
#define uchar unsigned char
#define uint unsigned int
static uint g_nAdValue=0;
void IoInit(void);
// A/D 转换完成中断
SIGNAL(SIG_ADC)
{
g_nAdValue=ADCL;
g_nAdValue|=(uint)(ADCH<<8);
}
int main(void)
{
uchar i;
//内部2.56V 参考电压,0 通道
ADMUX=0Xc0;
//使能ADC,中断允许,自由模式, 时钟:ck/8
ADCSRA=_BV(ADEN)|_BV(ADIE)|_BV(ADFR)|_BV(ADPS1)|_BV(ADPS0);
IoInit();//标准输入/输出初始化
ADCSRA|=_BV(ADSC);//自由模式开始转换
while(1)
{
//延时
for(i=0;i<100;i++)
_delay_loop_2(4 * 250 * 10);//10ms
cli();
printf("%d\n",g_nAdValue);
sei();
}
}
以上是ATMega8 A/D 转换程序的两种方式,在第一种查询方式中ADC 按单次转换模式工作,每次转换均由置位ADSC 触发。在中断方式示例中ADC 按自由模式工作,自第一次置位ADSC 起ADC 就连续不断的进行采样转换、进行数据更新。
第四章 使用C 语言标准I/O 流调试程序
4.1 avr-libc标准I/O流描述
avr-libc 提供标准I/O流stdin, stdout和stderr。但受硬件资源的限制仅支持标准C语言I/O流的部分功能。由于没有操作系统支持,avr-libc又不知道标准流使用的设备,在应用程序的startup过程中I/O流无法初始化。同样在avr-libc中没有文件的概念,它也不支持fopen()。做为替代 fdevopen()提供流与设备间的连接。fdevopen需要提供字符发送、字符接收两个函数,在avr-libc中这两个函数对于字符流与二进制流是没有区别的。
三个核心函数
fdevopen()
应用程序通过fdevopen函数为流指定实际的输入输出设备。
FILE* fdevopen( int(* put) (char), int(* get)(void), int opts __attribute__((unused))
前两个参数均为指向函数的指针,它们指向的函数分别负责向设备输出一字节和从设备输入一字节的函数。第三个参数保留,通常指定0。
如果只指定put指针,流按写方式打开,stdout 或 stderr成为流的引用名。
如果只指定get指针,流按只读方式打开,stdin成为流的引用名。
如果在调用时两者都提供则按读写方式打开,此时stdout、stderr和stdin相同,均可做为当前流的引用名。
(1)向设备写字符函数:
原型:
int put(char c)
{
? ?
return 0;
}
返回0表示字符传送成功,返回非零表示失败。
另外,字符'\n'被I/O流函数传送时直接传送一个换行字符,因此如果设备在换行前需要回车,应当在put函数里发送 '\n'前发字符'\r'。
以下是一个基于UART的 put 示例:
int uart_putchar(char c)
{
if(c=='\n')
uart_putchar('\r');
loop_until_bit_is_set(UCSRA,UDRE);
UDR=c;
return 0;
}
(2)从设备输入字符函数
原型:
int get(void)
{
? ?
}
get 函数从设备读取一字节并按 int 类型返回,如果读取时发生了错误需返回 –1。
vfprintf()
int vfprintf ( FILE * __stream, const char * __fmt, va_list __ap )
vfprintf 将__ap列出的值按__fmt 格式输出到流__stream。返回输出字节数,若产生错误返回EOF。
vfprintf是libc提供的I/O流格式化输出函数的基础,为避免应用中用不到的功能占用宝贵的硬件资源,vfprintf函数支持三种不同链接模式。
(1) 在默认情况下它包含除格式转换外的所有功能
(2) 最小模式仅包含基本整数类型和字符串转换功能要用最小模式链接此函数,使用的链接选项如下:
-Wl,-u,vfprintf -lprintf_min
(3) 完全模式支持浮点数格式转换在内的所有功能。
完全模式链接选项如下:
-Wl,-u,vfprintf -lprintf_flt –lm
vfscanf()
int vfscanf ( FILE * __stream, const char * __fmt, va_list __ap )
vfscanf 是libc 提供的I/O 流格式化输入函数的基础, 它从__stream 流按字符格式读取__fmt 内容后按转换规则将数据保存到__ap 内。与vfprintf 类似vfscanf 也支持三种不同链接模式。
(1)在默认情况下它支持除浮点数格式和格式“%[”外的所有转换。
(2)最小模式链接选项:
-Wl,-u,vfscanf -lscanf_min -lm
(3)完全模式链接选项:
-Wl,-u,vfscanf -lscanf_flt –lm
4.2 利用标准I/0流调试程序
在程序的调试阶段,提供数据格式化输入/输出功能的标准I/O函数是个非常有用的工具,而单片机UART接口是标准I/O的比较合适设备
一. 电路
图4-1 UART 实现I/O 流电路原理图
二.程序
/*
avr-libc 标准 i/o 测试程序
main.c
mcu:atmega8
芯艺 2004-09-09
*/
#include
#include
#include
char g_aString[81];
//uart 发送一字节
int usart_putchar(char c)
{
if(c=='\n')
usart_putchar('\r');
loop_until_bit_is_set(UCSRA,UDRE);
UDR=c;
return 0;
}
//uart 接收一字节
int usart_getchar(void)
{
loop_until_bit_is_set(UCSRA,RXC);
return UDR;
}
void IoInit(void)
{
//uart 初始化
UCSRB=_BV(RXEN)|_BV(TXEN);/*(1<
UBRRL=25; //9600 baud 6MHz:38 4MHz:25
//流与设备连接
fdevopen(usart_putchar,usart_getchar,0);
}
int main(void)
{
int tmp;
IoInit();
while(1)
{
//测试1
vfprintf(stdout,"测试1[输入数字]:\n",0);
vfscanf(stdin,"%d",&tmp);
vfprintf(stdout,"您的输入为:%d\n",tmp);
//测试2
printf("测试2[输入一字符串]:\n");
scanf("%s",g_aString);
printf("您的输入为:%s\n",g_aString);
//测试3
printf_P(PSTR("测试3[输入数字]:\n"));
scanf_P(PSTR("%d"),&tmp);
printf_P(PSTR("您的输入为:%d\n"),tmp);
}
}
三.监测工具
图4-2 PrintMonitor 运行界面
监测工具PrintMonitor 运行界面如图4-2 所示,它属于windows 应用程序,由VisualBasic6.0 编写,请到http://bitfu.zj.com 下载源代码。
第五章 AT89S52 下载器的制作
5.1 LuckyProg S52 概述
ATMEL 推出的89S 系列单片机具有类似AVR 的ISP 编程功能,单片机ISP 接口为用户提供了一种串行编程方法。ISP 功能就象操作串行EEPROM 存储器那样使单片机的编程变得简单方便。
本章将介绍一种用AVR(AT90S2313)实现的AT89S52 单片机ISP 编程器:LuckyProg S52。
LuckyProg S52 工作原理:
如图5-1 所示,编程单片机AT90S2313 与计算机用RS232 通信,2312 从串行口获得编程命令和数据后用ISP 程序下载接口对AT89S52 编程。
图5-1 LuckyProg S52 功能:
图5-2 为LuckyProg S52 的电中原理图,图中AT90S2313 的UART 口与计算机RS232标准串行接口之间的电平转换被省略,可参考图4-1。
用ISP 口下载程序时AT89S52 必需有时钟源,为此在XTAL0 与XTAL1 间接一个6MHz晶振,ISP 数据通信口MOSI、MISO 和SCK 均接在AT90S2313 的普通I/O 口上,而RST脚由I/O 口通过一个控制。
图5-2 LuckyProg S52 ISP 下载电路原理图
5.2 AT89S52 ISP 功能简介
串行数据的输入与输出时序
数据在SCK 的上升沿输入到52,SCK 的下降沿输出。另外必须保证串行时钟SCK 的周期至少大于是6 个CPU 时钟(XTAL1 上的)周期。
串行编程算法
1. 上电过程
在VCC 和GND 间加上的同时RST 脚加高电平。至少等待时10ms。
2.发送串行编程使能命令
如果通信失步则串行编程失败。如果同步则在编程时钟的第四个字节器件响应0X69,表示编程使能命令成功。不论响应正确与否,必需保证四字节的时钟周期。
3.写程序
通过写指令可对程序存储器的每一字节进行编程。一个写指令使单片机进入自定时的编程模式,在5V 编程电压下典型编程时间少于1ms。
4.读程序
任意位置的程序数据可通过读指令从引脚步MISO/P1.6 读出,实现定写入数据的校验。
5.编程操作结束后将RST 引脚拉低,使器件进入正常工作模式。
编程指令
表 5-1 AT89S52 ISP 下载命令
注:1.锁定位与模式对应
模式1(B1=0、B2=0):无锁定保护
模式2(B1=0、B2=1):内部锁定位1 有效
模式3(B1=1、B2=0):内部锁定位2 有效
模式4(B1=1、B2=1):内部锁定位3 有效
1.在模式3 和4 下不能读厂标
2.将Reset 拉高后SCK 至少保持64 个时钟周期才可执行编程允许命令,在页读写中命令和地址后数据由0到255 的顺序传送,只有接收完这256 字节的数据后下一个指令才能就绪。
5.3 程序设计
延时功能函数
通常在单片机C 程序里的延时模块为一个计数循环,延时时间的长短往往是先估计,后通过实验或仿真等方法来验证。avr-libc 提供了两个延时API 函数,利用这两个函数我们可以较精确的产生所需的延时函数。
第一个函数声明如下:
void _delay_loop_1(unsigned char count);
它的延时时间为count × 3 个系统时钟周期,计数器count 为8 位无符号整数,第二个函数声明格式为:
void _delay_loop_2(unsigned int count);
它将延时count × 4 个系统时钟周期,计数器count 为16 位无符号整数。
若要调用这两个函数,需先包函头文件 delay.h,实际上这两个函数的实现就在此文件里,我们可以从WINAVR安装目录里的\AVR\INCLUDE\AVR子目录里找到并查看它的内容,以下为_delay_loop_2 的源程序:
static inline void
_delay_loop_2(unsigned int __count)
{
asm volatile (
"1: sbiw %0,1" "\n\t"
"brne 1b"
: "=w" (__count)
: "0" (__count)
);
}
首先要说明的是,由于函数的实现写在了头文件里,所以被多个源文件包含是可能的,为此有必要将它声明成局部函数(static)。函数内容为内嵌汇编方式,有关内嵌汇编看第8章,这里我们只需知道它的执行需要count * 4 个时钟周期。要注意的是,inline 关键字说明了函数是内连函数,内连函数如同汇编程序里的宏,编译结果是在每一个调用的地方插入一次函数的内容。为此有程序空间要求且调用频率高的应用中再写一个延时函数是有必要的。
以下是为编程器主控单片机AT90S2313 写的延时程序,它以毫秒为单位执行延时任务。
void DelayMs(unsigned int t)
{
unsigned int i;
for(i=0;i
_delay_loop_2(FEQ * 250 - 1);
}
其中FEQ 为系统振荡频率(以M 为单位)。
AT90S2313 程序清单
#include
#include
#include
#include
#include
#define uchar unsigned char
#define uint unsigned int
#define SETLED PORTB&=0xF7
#define CLRLED PORTB|=0X80
#define FREQ 6 //时钟 6MHz
#define MOSI 4
#define MISO 6
#define SCK 0
#define RST 1
#define ACK 0xaa
#define ERR 0XBB
#define CMOD 0XCC;
uchar g_bTxdPos=0; //UART 发送定位数
uchar g_bTxdLen=0; //发送长度设置绶冲
uchar g_bRxdPos=0; //UART 接收定位数
uchar g_bRxdLen=0; //接收长度设置绶冲
uchar g_aMemBuf[32]; //数据绶冲
void DelayMs(uint t)
{
uint i;
for(i=0;i
{
_delay_loop_2(250*FREQ-1);//delay 1ms
wdt_reset();
}
}
void DelayBus(void)
{
_delay_loop_1(4);
wdt_reset();
}
void ISP_WriteByte(uchar dat)
{
unsigned char i;
for(i=0;i<8;i++)
{
if(dat&0x80)
sbi(PORTB,MOSI);
else
cbi(PORTB,MOSI);
sbi(PORTB,SCK);
DelayBus();
cbi(PORTB,SCK);
DelayBus();
dat<<=1;
}
}
uchar ISP_ReadByte(void)
{
uchar ret=0;
uchar i;
for(i=0;i<8;i++)
{
ret<<=1;
sbi(PORTB,SCK);
DelayBus();
if(PIND&0x40)
ret|=1;
cbi(PORTB,SCK);
DelayBus();
}
return ret;
}
///串口处理/
//接收中断
SIGNAL(SIG_UART_RECV)
{
uchar c=UDR;
if(g_bRxdLen>0)
{
g_aMemBuf[g_bRxdPos++]=c;
g_bRxdLen--;
}
}
//发送中断
SIGNAL (SIG_UART_TRANS)
{
if(--g_bTxdLen>0)
UDR=g_aMemBuf[++g_bTxdPos];
}
//等待接收完成
void WaitRecv(void)
{
while(g_bRxdLen>0)
DelayBus();
}
//发送指定字节
void SendToUart(uchar size)
{
g_bTxdPos=0;
g_bTxdLen=size;
UDR=g_aMemBuf[0];
while(g_bTxdLen>0)
DelayBus();
}
//接收指定字节
void RecvFromUart(uchar size,uchar bwait)
{
g_bRxdPos=0;
g_bRxdLen=size;
if(bwait)
WaitRecv();
}
//
//S52 编程允许
uchar PrgEn(void)
{
//MOSI、SCK 设为输出
cbi(PORTB,SCK);
cbi(PORTB,MOSI);
sbi(DDRB,MOSI);
sbi(DDRB,SCK);
cbi(PORTB,RST);
DelayMs(100);
ISP_WriteByte(0xac);
ISP_WriteByte(0x53);
ISP_WriteByte(0);
if(ISP_ReadByte()==0x69)
return 1;
else
return 0;
}
//S52 复位
void PrgDs(void)
{
//MOSI、SCK 设为输入高阻
cbi(PORTB,MOSI);
cbi(PORTB,SCK);
cbi(DDRB,MOSI);
cbi(DDRB,SCK);
cbi(PORTB,RST);
DelayMs(500);
sbi(PORTB,RST);
}
//读FLASH
void ReadDevice(void)// CMD : 1
{
uchar i,j,k;
uchar pageaddress=g_aMemBuf[1];
uchar pagecount=g_aMemBuf[2];
if(!PrgEn())
{
g_aMemBuf[0]=ERR;
return ;
}
g_aMemBuf[0]=ACK;
SendToUart(1);
for(k=0;k
{
ISP_WriteByte(0x30);
ISP_WriteByte(pageaddress++);//Write address
for(i=0;i<8;i++)
{
for(j=0;j<32;j++)
{
g_aMemBuf[j]=ISP_ReadByte();
}
SendToUart(32);
}
if(k&0x1)
SETLED;
else
CLRLED;
}
PrgDs();
CLRLED;
g_aMemBuf[0]=ACK;
}
//写FLASH
void WriteDevice(void)//CMD : 3
{
uchar i,j,k;
if(PrgEn()==0)
{
g_aMemBuf[0]=ERR;
return ;
}
uchar pageaddress=g_aMemBuf[1];
uchar pagecount=g_aMemBuf[2];
for(k=0;k
{
ISP_WriteByte(0x50);
ISP_WriteByte(pageaddress++); //Write address
SETLED;
for(i=0;i<8;i++)
{
g_aMemBuf[0]=3;
SendToUart(1);
RecvFromUart(32,1);
for(j=0;j<32;j++)
{
ISP_WriteByte(g_aMemBuf[j]);
DelayMs(1);
}
}
CLRLED;
//DelayMs(256);
}
PrgDs();
g_aMemBuf[0]=ACK;
}
//擦除
void EraseDevice(void) // CMD : 2
{
if(PrgEn()==0)
{
g_aMemBuf[0]=ERR;
return ;
}
ISP_WriteByte(0xac);
ISP_WriteByte(0x80);
ISP_WriteByte(0x0);
ISP_WriteByte(0x0);
DelayMs(1000);
PrgDs();
g_aMemBuf[0]=ACK;
}
//写锁定位
void WriteLockBits(void) // CMD: 4
{
uchar temp;
if(PrgEn()==0)
{
g_aMemBuf[0]=ERR;
return ;
}
temp=0xe0;
if(g_aMemBuf[1])
temp|=0x02;
if(g_aMemBuf[2])
temp|=0x1;
ISP_WriteByte(0xac);
ISP_WriteByte(temp);
ISP_WriteByte(0);
ISP_WriteByte(0);
g_aMemBuf[0]=ACK;
PrgDs();
}
//读锁定位
void ReadLockBits(void)//CMD :5
{
if(PrgEn()==0)
{
g_aMemBuf[0]=ERR;
return ;
}
ISP_WriteByte(0x24);
ISP_WriteByte(0);
ISP_WriteByte(0);
g_aMemBuf[0]=ISP_ReadByte();
g_aMemBuf[0]>>=2;
g_aMemBuf[0]&=7;
PrgDs();
}
//
//入口
int main( void )
{
DelayMs(1000);
//i/o 口初始化
PORTB=0X08;
DDRB=0X09;
DDRD=0;
PORTD=0XFF;//上拉开
//uart 初始化
UCR=(1<
UBRR=38; //UBRR=FCK/(9600*16) -1
wdt_enable(WDTO_1S);
//复位目标板
PrgDs();
//复位延时
DelayMs(1000);
//开中断
sei ();
//主盾环
while(1)
{
RecvFromUart(3,1);
SETLED;
switch(g_aMemBuf[0])
{
case 0:
PrgDs();
g_aMemBuf[0]=ACK;
break;
case 1:
ReadDevice();
break;
case 2:
EraseDevice();
break;
case 3:
WriteDevice();
break;
case 4:
WriteLockBits();
break;
case 5:
ReadLockBits();
break;
case ACK:
g_aMemBuf[0]=ACK;
break;
default:
break;
}//switch
SendToUart(1);
CLRLED;
}//main loop
}//main
在写FLASH 存储器时先从计算机读取程序数据到g_aMemBuf 缓冲区,然后用页模式(查表5-1)写入,由于2313 内部RAM 有限,缓冲区g_aMemBuf 的大小定义为32 字节,为此写一页时必需从计算机读程序数据8 次。
上位机程序
上位机程序界面如图5-3 所示,它是由VisualBasic6.0 编写。可执行文件或VisualBasic 源代码可到 http://bitfu.zj.com 下载。
第六章 硬件TWI 端口编程
6.1 TWI 模块概述
ATMega 系列单片机片内集成两线制串行接口模块,Atmel 文档称它为TWI 接口。事实上TWI 与PHILIPS 的I2C 是同一回事,之所以叫它TWI 是因为这样的命名可使Atmel 避免交术语版税。所以,TWI 兼容I2C 更一种说法。
关于I2C 协议参考PHILIPS 相关文档。
AVR 硬件实现的TWI 接口是面向字节和基于中断的,相对软件模拟I2C 总线有更好的实时性和代码效率,引脚输入部分还配有毛刺抑制单元,可去除高频干扰。另外,结合AVR I/0端口功能,在TWI 使能时可设置SCL 和SDA 引脚对应的I/O 口内部有效,
责任编辑:
avr-gcc与avr单片机c语言开发 pdf,AVR 单片机与GCC 编程使用方法相关推荐
- 《51单片机应用开发范例大全(第3版)》——第1章 单片机C语言开发基础
本节书摘来异步社区<51单片机应用开发范例大全(第3版)>一书中的第1章,作者:张杰 , 宋戈 , 黄鹤松 , 员玉良,更多章节内容可以访问云栖社区"异步社区"公众号查 ...
- c 语言单片机开发,单片机C语言开发离不开它
作为单片机爱好者,入门学习单片机编程一定要学会进制的基本概念,常见的有(二进制.八进制.十进制.十六进制),今天于晓超带大家入门一下单片机C语言编程的二进制和十六进制(技术文章阅读量惨淡,希望大家能够 ...
- 单片机 rs232 c语言,完全手册 51单片机C语言开发详解系列之第8章 串口通讯实战—RS232...
完全手册 51单片机C语言开发详解系列 技术凝聚实力 专业创新出版 第8章 串口通讯实战-RS232接口电路与软件设计 本章主要通过实际的例子讲解AT89S51单片机的RS232串行通讯接口设计. 随 ...
- 51单片机c语言试题及答案,单片机C语言期末考试题..
单片机C语言期末考试题..,单片机的c语言应用程序设计,单片机c语言编程,51单片机c语言程序100例,单片机c语言,单片机c语言编程思想,c语言开发单片机,c语言单片机程序,新概念51单片机c语言教 ...
- 单片机c语言模块化实例程序设计,单片机C语言模块化设计
原标题:单片机C语言模块化设计 keil中实现 模块化编程.doc 在使用 KEIL 的时候,我们习惯上在一个.c 的文件中把自己要写的东西按照 自己思路的顺序进行顺序书写.这样是很普遍的写法,当程序 ...
- 单片机C语言中判断按键是否按下,按键识别方法之一
单片机c语言 <>欢迎进入51单片机学习网论坛讨论 本站新域名www.8951.com开通WWW.51C51.COM 中国单片机编程技术普及推广第一站! 8.按键识别方法之一 1.实验任务 ...
- 用单片机C语言开发雨滴智能屏小程序
我们都听说过微信小程序,但是你听说过用单片机开发的小程序吗? 世界上没有什么不可以做,只是你没想到......见图,所见即所得: 图中的手机可不是一般的手机,其内部包含了stm32单片机,我们叫它智能 ...
- sbit单片机c语言,单片机C语言开发sbit使用方法.doc
单片机 C语音开发 sbit使用方法·· 1.bit和sbit都是C51扩展的变量类型. bit和int char之类的差不多,只不过char=8位, bit=1位而已.都是变量,编译器在编译过程中分 ...
- 单片机c语言快速除法运算,单片机开发厂家之单片机C语言快速精度除法方案
资源描述: 单片机C语言快速精度除法方案 目前的51单片机在进行带小数点结果的除法一般可以采用浮点数计算的方式,但是浮点数计算有一个缺点就是非常耗时,在对时间要求严格的工况就不太适用. 笔者最近做了个 ...
最新文章
- 报告解读 | 实现智能制造的五大关键环节和四大赋能路径
- RabbitMQ是什么东西?
- Python 推导,内含,迭代器
- 抽象类-(抽象类的样子)
- matlab 滤波器设计 coe_一种半带滤波器的低功耗实现方法
- xampp里mysql的环境变量配置_XAMPP集成环境中MySQL数据库的使用
- 武汉年会签到,抽奖,摇一摇,微信上墙,互动大屏
- EPLAN2022程序安装及注意事项
- ipad上能够编辑python_10 个可以在平板电脑上使用的 Python 编辑器
- Matlab中的plotyy用法总结
- 硬币组合问题-非递归实现
- Linux系统备份与还原-Clonezilla
- 皮蛋拌豆腐引发的——沟通管理的思考(丁仿)
- vue Elementui 引入第三方icon(阿里矢量库)
- 服务器(CentOS7)配置R以及R Studio Server
- npm 安装淘宝镜像报错 npm ERR! code EEXIST
- 亲测有效的清华源使用教程
- Python||报错:TypeError: can only join an iterable
- excel导出文件加密
- 理性预测,未来音视频开发前景将是这般光景