今天来继续学习S3C2410的SPI接口和DS1390实时时钟。
SPI —— 低电压串行外设接口
DS1390 —— 实时时钟(RTC)芯片
DS1390
先来简单介绍一下DS1390。
低电压串行外设(SPI)实时时钟(RTC)芯片——DS1390——是能够提供百分之一秒、秒、分钟、小时、星期、日期、月份和年份等信息的时钟/日历芯片。少于31天的月份会自动调整月末日期,包括闰年日期调整。时钟可以工作在24小时格式下,也可以工作在带有AM/PM指示的12小时格式下。具有可编程的定时闹钟。可以通过SPI接口进行时间的设置和读取。
先来看看官方datasheet怎么介绍的。
基本和前文一样。有温度补偿式的参考电压监视VCC的状态,一旦掉电,就自动禁用总线,并启用备用电源。另外在DS1390上还有一个单个的开漏输出给CPU提供中断或者方波信号(四种可选频率)。
DS1390、DS1391、DS1394都是串行可编程的,SPI兼容、双向总线。
DS1392、DS1393通过3-线式的串行总线通信,另外一个引脚 RST¯¯¯¯¯¯¯ 可用于中断或者复位输出/去抖动输入。
特点:
- 一 百分之一秒、秒、分、时、天、日期、星期、月份、年份到2100年
- 二 输出引脚可以配置为中断模式或者方波输出(32.768kHz、8.192kHz、4.096kHz、1Hz)
- 三 当日实时时钟
- 四 掉电检测与切换电路
(以上四个是最常用的,后针对各个不同的型号略有差异,可查看相应数据手册)
再来看看DS1390的封装,常用接法,实物图
封装图:
常用接法:
实物图:
DS1390的时序图就不再往上放了,那样就成了罗列手册了。自己写驱动的时候,也没必要一行一行自己对照时序图写,那样也很难调。网上有现成的驱动代码,拿过来,能调通,存入、读出时间就好了。
SPI of S3C2410A
再来说说S3C2410A的SPI。
SPI—— Serial Peripheral Interface 串行外设接口
S3C2410A有两个SPI接口,每个接口包含两个8位移位寄存器,分别用于发送和接收。SPI数据传输期间,数据同时的移出(发送)和移入(移位寄存器)。8-位串行数据由相应的控制寄存器决定。如果你在发送,则接收数据寄存器中的值无效;如果在接收,则应该发送假的数据 ‘1’.
SPI的4个引脚信号:
- SCK
- MISO
- MOSI
- nSS0¯¯¯¯¯¯¯¯,nSS1¯¯¯¯¯¯¯¯
来看其中一个SPI接口的框图:
另一个接口的框图和这里一模一样。
SPI操作
讲讲SPI的操作。SPI主控端提供时钟,可以向SPPREn 这个寄存器写入合适的值来改变传输波特率。对于一个SPI从设备,则由其他主设备提供时钟。双方时钟一定是同步的。向SPTDATn 寄存器写入数据则开始发送或接收数据。某些情况下nSS应该在写入数据到 SPTDATn前被激活。
编程SPI模块的基本步骤:
- 设置波特率 SPPREn
- 配置控制寄存器 SPCONn
- 写 0xFF 到 SPTDATn 10次来初始化MMC或者SD卡
- 某个GPIO输入低拉低nSS来激活 MMC或者SD卡
- 发送数据:检查 REDY=1并写数据到SPTDATn 进行发送
- 接收数据:
- SPCONn的TAGD位禁用=正常模式:写0xFF到SPTDATn,然后等REDY=1后从读缓存中读数据
- SPCONn的TAGD位使能=Tx 自动清除数据模式:确认REDY=1即可直接读数据
- 拉高GPIO,清除 nSS,deactivate MMC or SD card
S3C2410A的SPI支持的4种传输格式
SPCONn 控制寄存器中有两个功能位 CPOL 和 CPHA.
CPOL控制IDLE状态下的时钟电平,0为低电平;
CPHA控制数据和时钟之间的延迟,0——无延迟;1——数据相对时钟上升沿半个周期出现在总线上
简单的看一下四种格式的波形图:
对于几种格式就不再作解释了。更详细的可以直接查看 2410的datasheet.
SPI 的特殊功能寄存器
SPCON0
SPCON1
控制寄存器进行模式选择、SCK使能、主/从选择、时钟极性(CPOL)选择、时钟相位(CPHA)选择、TAGD使能(Tx Auto Garbage Data mode)
SPSTA0
SPSTA1
状态寄存器只用了低三位作状态标志位,最常用的是最低位 (REDY)
SPPIN0
SPPIN1
SPI的引脚控制寄存器,也只用了低三位。具体功能查手册。
SPPRE0
SPPRE1
SPI的波特率预分频寄存器,共用低八位作为SPI时钟速率,计算工式为:
由此,该寄存器的值设置应为:
其中freq 为波特率值。注意:波特率值应该小于25MHz
SPIDTA0
SPIDAT1
发送数据寄存器
SPRDAT0
SPRDAT1
接收数据寄存器
VxWorks开发板S3C2410A上SPI驱动DS1390
在这之前,先来看看,DS1390与板子的连接。
S3C2410A的SPI1接口的4个引脚信号(GPG[5-8])
SPIMISO
SPIMOSI
SPICLK
SPICS
分别与实时时钟芯片DS1390的 DOUT、DIN、SCLK、
CS¯¯¯¯¯、
进行连接。
SPI驱动代码学习
先看几个功能性的常量和宏定义以及硬件访问寄存器。
// 几个常用的数据类型
typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef unsigned int UINT;
typedef unsigned int DWORD; /* 32-bit CPU */
typedef unsigned char BOOLEAN;
// 收发寄存器和REDY位, 此处用到的 rSPTDAT1等 在头文件<2410addr.h>中找
#define rSPTDAT (*(volatile unsigned int *)(rSPTDAT1))
#define rSPRDAT (*(volatile unsigned int *)(rSPRDAT1))
#define rDATREAD ((*(volatile unsigned int *)(rSPSTA1))&1)
#define CN_TIMEOUT_RW 10000
typedef struct { // BCD码方式时标数据结构
BYTE byYear_L; // 年低位
BYTE byYear_H; // 年高位
BYTE byMonth;
BYTE byDay;
BYTE byHour;
BYTE byMinute;
BYTE bySecond;
BYTE byMS_L; // 毫秒 低位
BYTE byMS_H; // 毫秒 高位
BYTE byDate; // 星期
}tagTimeBCD, *tagPTimeBCD;
tagPTimeBCD tTimeBcd; // 定义了一个全局的结构体指针变量
由于相应的S3C2410用的GPIO口为GPG口,先来看一下GPG口有没有什么特别之处。
上datasheet:
GPIOG口共16根口线,GPG[5-7]刚好有第二功能用来进行SPI口通信。
GPGCON[31:0] 控制寄存器共32位,每2-bit控制一根口线的功能模式。
来看驱动代码:
// 函数名称: SpiTimeInit
// 函数功能: SPI口初始化
static void SpiTimeInit()
{
DWORD dwValReg;
// 配置GPG[5-7]为利用的SPI功能、GPG[8]为普通输出
M_CPU_REG_READ( rGPGCON, dwValReg );
dwValReg = dwValReg & (~(3<<10)) & (~(3<<12)) & (~(3<<14)) & (~(3<<16));
dwValReg = dwValReg | (3<<10) | (3<<12) | (3<<14) | (1<<16);
M_CPU_REG_WRITE( rGPGCON, dwValReg );
SpiTimeDrv_En(FALSE);
// SPPRE1
dwValReg = 24; // 设置波特率为 1Mbsp,需要用上面提到的公式提前计算好
M_CPU_REG_WRITE( rSPPRE1, dwValReg );
// SPCON1
dwValReg = 0|(1<<1)|(0<<2)|(1<<3)|(1<<4)|(0<<5);
M_CPU_REG_WRITE( rSPCON1, dwValReg );
return;
}
//
///??? 没有搞懂这里返回值为什么要用 static 类型 ???
//
其中:
// 不同类型数据的硬件访问(读写)方式
#define M_CPU_REG_READ(r,result) ((result)=*(volatile unsigned int *)(r))
#define M_CPU_REG_WRITE(r,data) (*((volatile unsigned int *)(r)) = (data))
#define M_CPU_BYTE_READ(r,result) ((result)=*(volatile unsigned char *)(r))
#define M_CPU_BYTE_WRITE(r,data) (*((volatile unsigned char *)(r)) =(data))
#define M_CPU_REG_OR(r,result) (*(volatile unsigned int *)(r) |= result))
#define M_CPU_REG_AND(r,result) (*(volatile unsigned int *)(r) &=result)
#define M_CPU_REG_GET( r ) (*(volatile unsigned int *)(r))
// GPG[8]普通输出实现的片选功能
static void SpiTimeDrv_En( BOOLEAN bEn )
{
if( bEn ) // 拉低使能
{
M_CPU_REG_AND( rGPGDAT, (~(1<<8)) );
}
else // 拉高禁用
{
M_CPU_REG_OR ( rGPGDAT, (1<<8) );
}
return;
}
// 函数名称:SpiTimeGet
// 函数功能:获取时间
static BOOLEAN SpiTimeGet( tagPTimeBCD ptTimeBcd )
{
WORD wLoop,j;
BYTE byData[8];
BOOLEAN bOk = TRUE;
memset(ptTimeBcd,0,sizeof(tagTimeBCD)); // 清空结构体指针指向的内存
SpiTimeDrv_En(TRUE); // 片选DS1390实时时钟芯片
rSPTDAT = 0x00; // 写入的第一个字节高位表示读写(0读,1写)后续位为地址
for(wLoop=0;wLoop<CN_TIMEOUT_RW;wLoop++) // 超时判断
{
if( rDATREAD ) break;
}
if( wLoop ==CN_TIMEOUT_RW ) bOk = FALSE; // 参照SPI时序修改CN_TIMEOUT_RW
// 写0xFF到SPTDAT,并读回来。实际的读操作,循环移位寄存器
// 有一次超时,则返回 FALSE
for(j=0;j<8;j++)
{
if( bOk == FALSE ) break;
rSPTDAT = 0xFF; // 从2410写数据到DS1390
for(wLoop=0;wLoop<CN_TIMEOUT_RW;wLoop++)
{
if( rDATREAD ) break;
}
if( wLoop ==CN_TIMEOUT_RW ) bOk = FALSE;
else byData[j] = rSPRDAT; // 读回时间数据
}
if( bOk )
{
ptTimeBcd->byYear_H = 0x20;
ptTimeBcd->byMS_L = (byData[0]&0x0F)<<4; // 毫秒低位
ptTimeBcd->byMS_H = (byData[0]&0xF0)>>4; // 毫秒高位
ptTimeBcd->bySecond = byData[1];
ptTimeBcd->byMinute = byData[2];
ptTimeBcd->byHour = byData[3];
ptTimeBcd->byDate = byData[4]; // 星期
ptTimeBcd->byDay = byData[5];
ptTimeBcd->byMonth = byData[6];
ptTimeBcd->byYear_L = byData[7];
}
// 从这里可以看出 DS1390 的存储格式:
// 毫秒低---毫秒高,秒,分,时,星期,日,月,年低位 共8个unsigned char的数据
SpiTimeDrv_En(FALSE); // 读完一次实时时间就不再操作DS1390,即片选禁用
return(bOk); // 与VxWorks的函数保持统一的返回状态形式
}
有了获取时间,那设置时间就是一样的了,只不过方向反过来。
// 函数名称:SpiTimeSet
// 函数功能:设置 DS1390 实时时钟芯片的当前时刻时间
static BOOLEAN SpiTimeSet( tagPTimeBCD ptTimeBcd )
{
WORD wLoop,j;
BYTE byData[8];
BOOLEAN bOk = TRUE;
SpiTimeDrv_En(TRUE); // 片选DS1390芯片
byData[0] = 0x81; // 写入的第一个字节高位表示读写(0读,1写)后续位为地址
byData[1] = ptTimeBcd->bySecond;
byData[2] = ptTimeBcd->byMinute;
byData[3] = ptTimeBcd->byHour;
byData[4] = 0x00; // 星期,信息无效,随便写入
byData[5] = ptTimeBcd->byDay; // 日
byData[6] = ptTimeBcd->byMonth;
byData[7] = ptTimeBcd->byYear_L; // 年低位
for(j=0;j<8;j++)
{
rSPTDAT = byData[j]; // 注意写入DS1390顺序和读出顺序比较
for(wLoop=0;wLoop<CN_TIMEOUT_RW;wLoop++)
{
if( rDATREAD ) break;
}
if( wLoop ==CN_TIMEOUT_RW ) // 一次写入超时则直接返回失败
{
bOk = FALSE;
break;
}
}
SpiTimeDrv_En(FALSE); // 禁用片选 DS1390
return bOk;
}
外部接口
同前面的KEY一样,SPI驱动程序也要为其他任务提供接口函数。这里主要是针对DS1390写的几个操作函数。
// 函数名称:timeget
// 函数功能:获取并输出DS1390的当前时间
STATUS timeget(void)
{
tagPTimeBCD pTime= (UINT8 *)malloc(10); // 把10改成 sizeof(tagTimeBCD) 可读性会更强些
SpiTimeGet(pTime);
// 要注意 logMsg 和 printf 的区别
logMsg( "现在时间20%x年%x月%x日%x时%x分%x秒\n",
pTime->byYear_L,pTime->byMonth, pTime->byDay,
pTime->byHour,pTime->byMinute,pTime->bySecond);
return OK;
}
logMsg
此处来讲讲 logMsg 函数。在VxWorks API Reference 的 OS Libraries 里面按字母序排好了所有的库函数。其中L打头的有
- ledLib —— line-editing library
- loadLib —— object module loader
- loginLib —— user login/password subroutine library
- logLib —— message logging library
- lstLib —— doubly linked list subroutine library
先直接来看 logLib,帮助文件地址为X:/Tornado2.2/docs/vxworks/ref/logLib.html#top
其中X为Tornado开发环境安装目录。
每一个库的帮助信息中包含 NAME, ROUTINES, DESCRIPTION, … , INCLUDE FILES, SEE ALSO等项,基本和MATLAB等的帮助信息类似,容易学习使用。
5.5版本的VxWorks的logLib库包含了六个函数,分别是:
logInit( ) - initialize message logging library
logMsg( ) - log a formatted error message
logFdSet( ) - set the primary logging file descriptor
logFdAdd( ) - add a logging file descriptor
logFdDelete( ) - delete a logging file descriptor
logTask( ) - message-logging support task
可以看到其中 logMsg()函数主要作用是用来日志记录一个格式化的错误消息。看一下logMsg的具体描述:
logMsg()
int logMsg
(
char * fmt, /* format string for print */
int arg1, /* first of six required args for fmt */
int arg2,
int arg3,
int arg4,
int arg5,
int arg6
)
DESCRIPTIOIN
This routine logs a specified message via the logging task. This routine's syntax is similar to printf( ) -- a format string is followed by arguments to format. However, the logMsg( ) routine takes a char * rather than a const char * and requires a fixed number of arguments (6).
The task ID of the caller is prepended to the specified message.
// 此处可以看到了,logMsg 的语法和 printf 的语法很相似。但 logMsg 以 char* 为参数,而 printf 以 const char* 为参数,并且 logMsg 要求固定的6个参数,调用者任务的ID也被注入到指定的消息中。
还应该注意的是,logMsg()并不直接产生输出到 logging stream,而是输出到 logging task 的消息队列,因此 logMsg() 可以用到中断函数中。
同时,logTask() 并不在调用 logMsg()函数的时候 解释参数,而是在实际 logging 的时刻解释参数,因此,logMsg() 的参数不应当指向 volatile 实体对象。
非常重要的是, logMsg() 会检测自身是否是运行在中断上下文。如果是在中断环境中,则继续运行;如果是在某个任务中调用了 logMsg() ,则该任务会被阻塞在 logMsg() 函数中。logMsg 用于中断环境中输出字符串消息。
作为对比的是IO函数 printf 则不可以在中断函数中使用, printf 输出到一个串口(经重定向)。
printf()是将信息输出到标准输出设备(STDIN/STDOUT)中,如果此时设备正在工作,那么就会发生阻塞.
logMsg()是使用消息队列的方式,它将信息地址发送到队列,由专门的任务将信息打印出来.
EXAMPLE
If the following code were executed by task 20:
{
name = "GRONK";
num = 123;
logMsg ("ERROR - name = %s, num = %d.\n", name, num, 0, 0, 0, 0);
}
the following error message would appear on the system log:
0x180400 (t20): ERROR - name = GRONK, num = 123.
可以看到,当调用logMsg时实际的参数不足6个时,即可用 0 代入。
RETURN
The number of bytes written to the log queue, or EOF if the routine is unable to write a message.
返回信息几乎和 printf 一致。
这里再加一小段他人的使用经验小结:
再来看看第二个接口函数 timeset :
STATUS timeset(UINT8 year,UINT8 month,UINT8 day,UINT8 hour,UINT8 minute,UINT8 second)
{
tagPTimeBCD pTime = (UINT8 *)malloc(10); // malloc(sizeof(tagTimeBCD))
logMsg("DrvTime_SetTime!\n",0,0,0,0,0,0); // 格式串后面没有实际参数,6个全0即可
pTime->bySecond = second;
pTime->byMinute = minute;
pTime->byHour = hour;
pTime->byDay = day;
pTime->byMonth = month;
//pTime->byWeek = 0x00; // byDate 星期 信息不用写入
pTime->byYear_L = year;
logMsg("输入时间20%x年%x月%x日%x时%x分%x秒\n",
pTime->byYear_L,pTime->byMonth,pTime->byDay,
pTime->byHour,pTime->byMinute,pTime->bySecond);
SpiTimeSet(pTime);
return OK;
}
/
/// 按照上面的说法,这里两个接口函数中的 logMsg 是不是都应该换成 printf 更合适一些,现在还不懂,后续搞明白了再做说明
/
两个接口函数完了,再来看看SPI时钟任务主函数:
// 函数功能:SPI时钟任务主函数
void Time_Main( void )
{
tTimeBcd =(UINT8 *)malloc(10); // malloc(sizeof(tagTimeBCD));
SpiTimeInit();
while(1)
{
SpiTimeGet(tTimeBcd); // 获取时间后存到全局指针 tTimeBcd 指向的内存中
taskDelay(6); // 任务主动延时,让出CPU
}
}
好了,SPI驱动DS1390就暂时讲到这里。