基于51单片机的SPI总线
单片机与其它芯片或设备之间的数据传输在单片机的应用中具有重要的地位,单片机本身的数据传输接口过去主要为8位并行数据接口或异步串行通信接口,但电子技术的迅速发展使得许多新的数据传输接口标准不断涌现,大多数的51单片机并没有在硬件中集成这些新的数据传输接口。
SPI(Serial Peripheral Interface)总线是由Motorola公司提出的一种同步串行外围接口,采用三或四根信号线 。51单片机一般并没有在硬件中集成这种新的接口,所以要用软件来进行模拟。
1 硬件设计
DS1302是涓流充电时钟芯片,内含有一个实时时钟/日历和31字节静态RAM,实时时钟/日历电路提供秒、分、时、日、星期、月、年的信息,每月的天数和闰年的天数可自动调整,时钟操作可通过AM/PM指示决定采用24或12小时格式。DS1302与单片机之间能简单地采用SPI同步串行的方式进行通信,仅需用到三根信号线:RES(复位),I/O(数据线),SCLK(同步串行时钟)。通过1602LCD显示日期和时间,其电路如下所示。
<?xml:namespace prefix = v ns = "urn:schemas-microsoft-com:vml" />
在桌面上双击图标,打开ISIS 7 Professional窗口(本人使用的是v7.4 SP3中文版)。单击菜单命令“文件”→“新建设计”,选择DEFAULT模板,保存文件名为“SPI.DSN”。在器件选择按钮中单击“P”按钮,或执行菜单命令“库”→“拾取元件/符号”,添加如下表所示的元件。
51单片机AT89C51 一片 | 晶体CRYSTAL 12MHz 一只 |
瓷片电容CAP 22pF 二只 | 电解电容CAP-ELEC 10uF 一只 |
电阻RES 10K 一只 | 排阻 RESPAC-8 10K 一只 |
1602液晶显示器 LM016L 一只 | 晶体CRYSTAL 32.768KHz 一只 |
时钟芯片DS1302 一片 | 电池BATTERY 3V 一只 |
若用Proteus软件进行仿真,则上图中的两只晶体、U1的复位电路和U1的31脚以及电池都可以不画,它们大都是默认的。
在ISIS原理图编辑窗口中放置元件,再单击工具箱中元件终端图标,在对象选择器中单击POWER或GROUND放置电源或地。放置好元件后,布好线。左键双击各元件,设置相应元件参数,完成电路图的设计。
2 软件设计
采用AT89C51以及日历芯片DS1302和1602LCD组成时钟的流程图如下所示。
本例主要目的是如何用软件模拟SPI总线对DS1302进行读、写,其详细详细C51程序如下。
//实例:基于DS1302的日历时钟
#include<reg51.h> //包含单片机寄存器的头文件
#include<intrins.h> //包含_nop_()函数定义的头文件
/*********************************
以下是DS1302芯片的操作程序
**********************************/
unsigned char code digit[10]={"0123456789"};
//定义字符数组显示数字
sbit DATA="P1"^1; //位定义1302的数据输出端定义在P1.1引脚
sbit RST="P1"^2; //位定义1302的复位端口定义在P1.2引脚
sbit SCLK="P1"^0; //位定义1302的时钟输出端口定义在P1.0引脚
/*****************************
函数功能:延时若干微秒
入口参数:n
******************************/
void delaynus(unsigned char n)
{
unsigned char i;
for(i=0;i<n;i++)
;
}
/**********************************
函数功能:向1302写一个字节数据
入口参数:dat
***********************************/
void Write1302(unsigned char dat)
{
unsigned char i;
SCLK=0; //拉低SCLK,为脉冲上升沿写入数据做好准备
delaynus(2); //稍微等待,使硬件做好准备
for(i=0;i<8;i++) //连续写8个二进制位数据
{
DATA=dat&0x01; //取出dat的第0位数据写入1302
delaynus(2); //稍微等待,使硬件做好准备
SCLK=1; //上升沿写入数据
delaynus(2); //稍微等待,使硬件做好准备
SCLK=0; //重新拉低SCLK,形成脉冲
dat>>=1; //将dat的各数据位右移1位,准备写入下一个数据位
}
}
/***********************************************
函数功能:根据命令字,向1302写一个字节数据
入口参数:Cmd,储存命令字;dat,储存待写的数据
************************************************/
void WriteSet1302(unsigned char Cmd,unsigned char dat)
{
RST=0; //禁止数据传递
SCLK=0; //确保写数居前SCLK被拉低
RST=1; //启动数据传输
delaynus(2); //稍微等待,使硬件做好准备
Write1302(Cmd); //写入命令字
Write1302(dat); //写数据
SCLK=1; //将时钟电平置于已知状态
RST=0; //禁止数据传递
}
/********************************
函数功能:从1302读一个字节数据
出口参数:dat
*********************************/
unsigned char Read1302(void)
{
unsigned char i,dat;
delaynus(2); //稍微等待,使硬件做好准备
for(i=0;i<8;i++) //连续读8个二进制位数据
{
dat>>=1; //将dat的各数据位右移1位
if(DATA==1) //如果读出的数据是1
dat|=0x80; //将1取出,写在dat的最高位
SCLK=1; //将SCLK置于高电平,为下降沿读出
delaynus(2); //稍微等待
SCLK=0; //拉低SCLK,形成脉冲下降沿
delaynus(2); //稍微等待
}
return dat; //将读出的数据返回
}
/**********************************************
函数功能:根据命令字,从1302读取一个字节数据
入口参数:Cmd 出口参数:dat
**********************************************/
unsigned char ReadSet1302(unsigned char Cmd)
{
unsigned char dat;
RST=0; //拉低RST
SCLK=0; //确保写数居前SCLK被拉低
RST=1; //启动数据传输
Write1302(Cmd); //写入命令字
dat=Read1302(); //读出数据
SCLK=1; //将时钟电平置于已知状态
RST=0; //禁止数据传递
return dat; //将读出的数据返回
}
/********************************
函数功能: 1302进行初始化设置
*********************************/
void Init_DS1302(void)
{
WriteSet1302(0x8E,0x00); //写入不保护指令
WriteSet1302(0x80,((0/10)<<4|(0%10))); //写入秒的初始值
WriteSet1302(0x82,((0/10)<<4|(0%10))); //写入分的初始值
WriteSet1302(0x84,((12/10)<<4|(12%10))); //写入小时的初始值
WriteSet1302(0x86,((24/10)<<4|(24%10))); //写入日的初始值
WriteSet1302(0x88,((4/10)<<4|(4%10))); //写入月的初始值
WriteSet1302(0x8c,((10/10)<<4|(10%10))); //写入年的初始值
}
/*******************************
以下是对液晶模块的操作程序
********************************/
sbit RS="P2"^0; //寄存器选择位,将RS位定义为P2.0引脚
sbit RW="P2"^1; //读写选择位,将RW位定义为P2.1引脚
sbit E="P2"^2; //使能信号位,将E位定义为P2.2引脚
sbit BF="P0"^7; //忙碌标志位,,将BF位定义为P0.7引脚
/*******************************************************************
函数功能:延时1ms
(3j+2)*i=(3×33+2)×10=1010(微秒),可以认为是1毫秒
********************************************************************/
void delay1ms()
{
unsigned char i,j;
for(i=0;i<10;i++)
for(j=0;j<33;j++)
;
}
/*****************************
函数功能:延时若干毫秒
入口参数:n
*******************************/
void delaynms(unsigned char n)
{
unsigned char i;
for(i=0;i<n;i++)
delay1ms();
}
/***********************************************
函数功能:判断液晶模块的忙碌状态
返回值:result。result=1,忙碌;result=0,不忙
***********************************************************/
bit BusyTest(void)
{
bit result;
RS=0; //根据规定,RS为低电平,RW为高电平时,可以读状态
RW=1;
E=1; //E=1,才允许读写
_nop_(); //空操作
_nop_();
_nop_();
_nop_(); //空操作四个机器周期,给硬件反应时间
result=BF; //将忙碌标志电平赋给result
E=0; //将E恢复低电平
_nop_();
_nop_();
_nop_();
_nop_();
return result;
}
/**************************************************************
函数功能:将模式设置指令或显示地址写入液晶模块
入口参数:dictate
***************************************************************/
void WriteInstruction (unsigned char dictate)
{
while(BusyTest()==1); //如果忙就等待
RS=0; //根据规定,RS和R/W同时为低电平时,可以写入指令
RW=0;
E=0; //E置低电平,为了让E从0到1发生正跳变,所以应先置"0"
_nop_();
_nop_(); //空操作两个机器周期,给硬件反应时间
P0=dictate; //将数据送入P0口,即写入指令或地址
_nop_();
_nop_();
_nop_();
_nop_(); //空操作四个机器周期,给硬件反应时间
E=1; //E置高电平
_nop_();
_nop_();
_nop_();
_nop_(); //空操作四个机器周期,给硬件反应时间
E=0; //当E由高电平跳变成低电平时,液晶模块开始执行命令
_nop_();
_nop_();
_nop_();
_nop_();
}
/*********************************************
函数功能:指定字符显示的实际地址
入口参数:x
************************************************/
void WriteAddress(unsigned char x)
{
WriteInstruction(x|0x80); //显示位置的确定方法为"80H+地址码x"
}
/***************************************************************
函数功能:将数据(字符的标准ASCII码)写入液晶模块
入口参数:y(为字符常量)
******************************************************************/
void WriteData(unsigned char y)
{
while(BusyTest()==1);
RS=1; //RS为高电平,RW为低电平时,可以写入数据
RW=0;
E=0; //E置低电平,为了让E从0到1发生正跳变,所以应先置"0"
P0=y; //将数据送入P0口,即将数据写入液晶模块
_nop_();
_nop_();
_nop_();
_nop_(); //空操作四个机器周期,给硬件反应时间
E=1; //E置高电平
_nop_();
_nop_();
_nop_();
_nop_(); //空操作四个机器周期,给硬件反应时间
E=0; //当E由高电平跳变成低电平时,液晶模块开始执行命令
_nop_();
_nop_();
_nop_();
_nop_();
}
/*******************************************************
函数功能:对LCD的显示模式进行初始化设置
*************************************************************/
void LcdInitiate(void)
{
delaynms(15); //首次写指令时应给LCD一段较长的反应时间
WriteInstruction(0x38);
//显示模式设置:16×2显示,5×7点阵,8位数据
delaynms(5); //给硬件一点反应时间
WriteInstruction(0x38);
delaynms(5); //给硬件一点反应时间
WriteInstruction(0x38); //连续三次,确保初始化成功
delaynms(5); //给硬件一点反应时间
WriteInstruction(0x0c);
//显示模式设置:显示开,无光标,光标不闪烁
delaynms(5); //给硬件一点反应时间
WriteInstruction(0x06); //显示模式设置:光标右移,字符不移
delaynms(5); //给硬件一点反应时间
WriteInstruction(0x01); //清屏幕指令,将以前的显示内容清除
delaynms(5); //给硬件一点反应时间
}
/**********************************
以下是1302数据的显示程序
***********************************/
/************************
函数功能:显示秒
入口参数:x
*************************/
void DisplaySecond(unsigned char x)
{
unsigned char i,j; //i,j分别储存秒的十位和个位
i=x/10; //取十位
j=x%10; //取个位
WriteAddress(0x49); //写显示地址,将在第2行第7列开始显示
WriteData(digit); //将十位数字的字符常量写入LCD
WriteData(digit[j]); //将个位数字的字符常量写入LCD
delaynms(50); //延时1ms给硬件一点反应时间
}
/************************
函数功能:显示分钟
入口参数:x
**************************/
void DisplayMinute(unsigned char x)
{
unsigned char i,j; //i,j分别储存分钟的十位和个位
i=x/10; //取十位
j=x%10; //取个位
WriteAddress(0x46); //写显示地址,将在第2行第7列开始显示
WriteData(digit); //将十位数字的字符常量写入LCD
WriteData(digit[j]); //将个位数字的字符常量写入LCD
delaynms(50); //延时1ms给硬件一点反应时间
}
/*************************
函数功能:显示小时
入口参数:x
***************************/
void DisplayHour(unsigned char x)
{
unsigned char i,j; //i,j分别储存小时的十位和个位
i=x/10; //取十位
j=x%10; //取个位
WriteAddress(0x43); //写显示地址,将在第2行第7列开始显示
WriteData(digit); //将十位数字的字符常量写入LCD
WriteData(digit[j]); //将个位数字的字符常量写入LCD
delaynms(50); //延时1ms给硬件一点反应时间
}
/*********************
函数功能:显示日
入口参数:x
**********************/
void DisplayDay(unsigned char x)
{
unsigned char i,j; //i,j分别储存日的十位和个位
i=x/10; //取十位
j=x%10; //取个位
WriteAddress(0x0d); //写显示地址,将在第1行第14列开始显示
WriteData(digit); //将十位数字的字符常量写入LCD
WriteData(digit[j]); //将个位数字的字符常量写入LCD
delaynms(50); //给硬件一点反应时间
}
/*********************
函数功能:显示月
入口参数:x
************************/
void DisplayMonth(unsigned char x)
{
unsigned char i,j; //i,j分别储存月的十位和个位
i=x/10; //取十位
j=x%10; //取个位
WriteAddress(0x0a); //写显示地址,将在第1行第11列开始显示
WriteData(digit); //将十位数字的字符常量写入LCD
WriteData(digit[j]); //将个位数字的字符常量写入LCD
delaynms(50); //给硬件一点反应时间
}
/**********************
函数功能:显示年
入口参数:x
***********************/
void DisplayYear(unsigned char x)
{
unsigned char i,j; //i,j分别储存年的十位和个位
i=x/10; //取十位
j=x%10; //取个位
WriteAddress(0x07); //写显示地址,将在第1行第8列开始显示
WriteData(digit); //将十位数字的字符常量写入LCD
WriteData(digit[j]); //将个位数字的字符常量写入LCD
delaynms(50); //给硬件一点反应时间
}
/***********************
函数功能:主函数
************************/
void main(void)
{
unsigned char second,minute,hour,day,month,year;
//分别储存秒、分、小时,日,月,年
unsigned char ReadValue; //储存从1302读取的数据
LcdInitiate(); //将液晶初始化
WriteAddress(0); //写Date的显示地址,将在第1行第1列开始显示
WriteData('D'); //将字符常量写入LCD
WriteData('a'); //将字符常量写入LCD
WriteData('t'); //将字符常量写入LCD
WriteData('e'); //将字符常量写入LCD
WriteData(':'); //将字符常量写入LCD
WriteData('2'); //将字符常量写入LCD
WriteData('0'); //将字符常量写入LCD
WriteAddress(0x09); //写年月分隔符的显示地址
WriteData('-'); //将字符常量写入LCD
WriteAddress(0x0c); //写月日分隔符的显示地址
WriteData('-'); //将字符常量写入LCD
WriteAddress(0x45); //写小时与分钟分隔符的显示地址
WriteData(':'); //将字符常量写入LCD
WriteAddress(0x48); //写分钟与秒分隔符的显示地址
WriteData(':'); //将字符常量写入LCD
Init_DS1302(); //将1302初始化
while(1)
{
ReadValue = ReadSet1302(0x81); //从秒寄存器读数据
second=((ReadValue&0x70)>>4)*10 + (ReadValue&0x0F);
//将读出数据转化
DisplaySecond(second); //显示秒
ReadValue = ReadSet1302(0x83); //从分寄存器读数据
minute=((ReadValue&0x70)>>4)*10 + (ReadValue&0x0F);
//将读出数据转化
DisplayMinute(minute); //显示分
ReadValue = ReadSet1302(0x85); //从时寄存器读数据
hour=((ReadValue&0x70)>>4)*10 + (ReadValue&0x0F);
//将读出数据转化
DisplayHour(hour); //显示小时
ReadValue = ReadSet1302(0x87); //从日寄存器读数据
day=((ReadValue&0x70)>>4)*10 + (ReadValue&0x0F);
//将读出数据转化
DisplayDay(day); //显示日
ReadValue = ReadSet1302(0x89); //从月寄存器读数据
month=((ReadValue&0x70)>>4)*10 + (ReadValue&0x0F);
//将读出数据转化
DisplayMonth(month); //显示月
ReadValue = ReadSet1302(0x8d); //从年寄存器读数据
year=((ReadValue&0x70)>>4)*10 + (ReadValue&0x0F);
//将读出数据转化
DisplayYear(year); //显示年
}
}
打开Keil程序(本人使用的是Keil8.05中文版),执行菜单命令“工程”→“新建工程”创建“SPI”项目,并选择单片机型号为AT89C51。执行菜单命令“文件”→“新建”创建文件,输入C语言源程序,保存为“SPI.C”。在Project Workspace窗口中右击源代码组1,选择“添加文件到组‘源代码组 l’”将源程序“SPI.C”添加到项目中。
在Keil中执行执行菜单命令“工程”→“创建目标”(或点击“创建目标”快捷按钮),编译源程序。如果编译成功,则在“Output Window”的“创建”窗口中显示没有错误,并创建了“SPI.HEX”文件。
3 仿真与调试
关于Proteus与Keil的联合仿真调试,可参见我以前所写的博文或其它参考资料。
启动Proteus的ISIS,并将其放在屏幕的右上角(可将原理图放大到合适大小);再启动Keil的μVision3,并将其放在屏幕的左下角。
在Keil中执行菜单命令“调试”→“启动/停止调试”,或直接单击图标,进入Keil调试环境。同时,在Proteus ISIS的窗口中可看出Proteus也进入了程序调试状态。
在Keil代码编辑窗口中设置相应断点,断点的设置方法:在需要设置断点语句前双击鼠标左键,可设置断点;再次双击,可取消该断点。
在Keil中按F5键(或点击“运行”快捷按钮)运行程序。1602LCD第一行显示日期“Date:20XX-XX-XX”,第二行显示时间“XX(时):XX(分):XX(秒),如下图所示。
或可以点击单步、运行到光标处、全速运行等快捷按钮,以及同时观察工程窗口寄存器页面、存储器窗口等,来进行仿真调试。若屏蔽掉主函数中DS1302初始化语句(Init_DS1302()), 则用Proteus仿真时将在1602LCD上显示当前的日期和时间。
本人邮箱:txxyc104@163.com,欢迎来信讨论.
用户1550226 2010-8-19 13:27
tengjingshu_112148725 2010-4-26 18:02