深圳市秦简计算机系统有限公司DJYOS驱动开发团队。
DJYOS的DjyBus总线模型为IIC、SPI之类的器件提供统一的访问接口,SPIBUS模块是DjyBus模块的一个子模块,为SPI器件提供统一的编程接口,实现通信协议层与器件层的分离。也标准化了SPI总线和 Device驱动接口,本手册指导驱动工程师编写SPI的接口程序。
SPI总线使用手册,请参见《都江堰操作系统用户手册》。
局限性:DJYOSV1.1.1版本的SPI驱动只提供主设备功能。
SPI通信协议是一种总线通信方式,这意味着一条总线上可以挂多个符合总线通信协议的设备,DjyBus资源组织结构就是符合这样一种物理的连接方式。如图 21所示资源组织结构图,总线类型 “SPI”、第n条总线“SPIn”、第n条总线上面的设备“Devn”,它们都是DjyBus资源树上面的资源节点,每次向总线“SPIn”上面增加一个设备,便向资源树上面增加了一个资源节点,它是“SPIn”的子节点。
图 2-1 总线资源结构
在编写SPI 器件驱动前,建议完成必要的准备工作,如:
1、认真阅读器件手册,了解通信协议、参数、操作流程等内容;
2、熟悉spibus.h头文件中提供的API,懂得参数的使用方法;
3、阅读SPI总线协议文档,熟练掌握SPI总线。
SPIBus是DjyBus模块的一个子模块,其结构如图 41所示,它为SPI器件提供标准的、一致的应用程序编程接口,并且规范了硬件驱动接口。驱动接口分为总线控制器接口和SPI器件接口两部分,驱动的重点是总线控制器,而器件接口实际上就是配置一下该器件的物理参数。
建议文件路径:在eclipse工程中的链接目录如下,如果是导入官方提供的example工程,那么该目录已经建立,在硬盘中添加文件后,只需要刷新工程即会自动添加进工程中。
src->OS_code->bsp->cpudrv->src->cpu_peri_spi.c。
相应的头文件目录为:
src->OS_code->bsp->cpudrv-> src->cpu_peri_spi.h。
在文件系统(硬盘)中的目录结构是:
djysrc\bsp\cpudrv\cpu_name\src\cpu_peri_spi.c。
djysrc\bsp\cpudrv\cpu_name\include\cpu_peri_spi.h。
根据以上命名,可以在DJYOS官方提供的代码中,找到大量范例。
以上文件命名并非绝对,例如LPC17xx的SPI模块,硬件被官方命名为SSP模块,DJYOS提供的源码中,其文件名就命名为cpu_peri_ssp.c。
SPI驱动程序编写重点有:
1、初始化SPI控制器,并且把SPI总线添加到DjyBus上。
2、实现图 41中的5个回调函数(哪些需要实现,参考后续章节)。
3、如果采用中断方式,须编写中断服务函数(实际上也是为4个回调函数服务)
图 4-1 SPIBus总线驱动架构
1、SPI控制器硬件的初始化,包括传输速度、IO配置、时钟等;
2、挂载SPI中断到中断系统,并配置中断类型,如配置为异步信号(若只采用轮询方式,则此功能可省略);
添加SPI总线的参数类型为struct tagSPI_Param,由函数SPI_BusAdd或SPI_BusAdd_s完成对SPI总线控制块的初始化和添加SPIn总线节点到DjyBus资源树。
代码 4-1 SPI参数结构体定义
struct tagSPI_Param
{
char *BusName; //总线名称,如SPI1
u8 *SPIBuf; //总线缓冲区指针
u32 SPIBufLen; //总线缓冲区大小,字节
ptu32_t SpecificFlag; //SPI私有标签,如控制寄存器基址
bool_t MultiCSRegFlag; //SPI控制寄存器是否有多套CS配置寄存器
TransferFunc pTransferTxRx; //发送接收回调函数,中断方式
TransferPoll pTransferPoll; //发送接收回调函数,轮询方式
CsActiveFunc pCsActive; //片选使能
CsInActiveFunc pCsInActive; //片选失能
SPIBusCtrlFunc pBusCtrl; //控制函数
};
SPI主设备同一时刻只能与一个从设备通信,收发同时进行,因此同一个SPI控制器中,多个片选可以共用缓冲区。
很多的SPI控制器对每个片选都提供一套配置通信参数寄存器,例如,CS0与从设备通信采用速度5Mbit/s,字符宽度为8比特,MSB,对应的配置片选CS0对应的寄存器,而CS1的从设备采用速度10Mbit/s,字符宽度为16比特,LSB,对应的配置片选CS1对应的寄存器。这种增强型的控制器对于一主多从,参数不一的通信能大大提高通信效率,简化参数配置。
SPI参数结构体的回调函数参数的原型如代码 42所示,其中PrivateTag就是结构体中SPI的私有标签,即SPIn寄存器基址。
代码 4-2 SPI回调函数类型申明
typedef ptu32_t (*TransferFunc)(ptu32_t SpecificFlag,u32 sendlen,u32 recvlen,u32 recvoff);
typedef bool_t (*TransferPoll)(ptu32_t SpecificFlag,u8* srcaddr,u32 sendlen,u8* destaddr,u32 recvlen,u32 recvoff);
typedef bool_t (*CsActiveFunc)(ptu32_t SpecificFlag, u8 cs);
typedef bool_t (*CsInActiveFunc)(ptu32_t SpecificFlag, u8 cs);
typedef ptu32_t (*SPIBusCtrlFunc)(ptu32_t SpecificFlag,u32 cmd,ptu32_t data1,ptu32_t data2);
有多少SPI总线是由具体的平台决定,因此,增加SPI总线到DjyBus上是由总线驱动程序员完成,成功添加的“SPIn”节点会成为“SPI”节点的子节点。
增加SPI总线的API函数可以调用SPI_BusAdd函数或SPI_BusAdd_s函数,两者的区别在于,SPI_BusAdd只需调用者提供已初始化好的参数结构体struct tagSPI_Param,而后者更需要提供struct tagSPI_CB结构体控制块(建议定义为静态变量)。
如果采用轮询方式收发,5个回调函数中只需要实现这一个,其他指针置为NULL即可。
轮询函数使用场合:
1、收发方式被设为轮询方式,则总是用轮询函数收发数据。默认值为中断方式,可调用SPI_BusCtrl函数设为轮询方式。
2、在禁止调度(即禁止异步信号中断)期间,强制使用轮询方式。
3、pTransferTxRx ==NULL,则使用轮询方式收发。
4、系统初始化未完成,多事件调度尚未启动期间。
如果使用中断方式收发,且不考虑在2~4三种情况下收发数据,则无须实现本函数,pTransferPoll指针设为NULL即可。
回调函数说明如下:
typedef bool_t (*TransferPoll)(ptu32_t SpecificFlag,u8* srcaddr,u32 sendlen,
u8* destaddr,u32 recvlen,u32 recvoff);
参数:
SpecificFlag:寄存器基址
srcaddr:发送数据存储地址。
sendlen:发送数据字节数。
destaddr:接收数据存储地址。
recvlen:接收数据字节数。
recvoff:接收偏移字节数,即认为接收到recvoff字节后的数据为有效,才存储。
返回:true,执行成功;false,执行失败。
说明:轮询方式主要应用于系统调度未启动或对实时性要求不高的场合,这种方法能够简化编程处理,快速实现通信功能。
启动收发是在使用中断方式收发数据必须实现的回调函数,若使用轮询方式,无须实现本函数,本函数指针设置为NULL即可。
回调函数说明如下:
typedef ptu32_t (*TransferFunc)(ptu32_t SpecificFlag,u32 sendlen,
u32 recvlen,u32 recvoff);
参数:
SpecificFlag,寄存器基址。
sendlen,发送数据长度,字节单位。
recvlen,接收数据长度,字节单位。
recvoff,接收偏移,接收到多少个字节后开始保护数据,即有用数据。
返回:true,中断方式启动通信成功,false,失败。
说明:该函数功能是配置SPI寄存器,并保存有关参数,使能中断,SPI总线采用的是四线的收发方式,收、发、时钟和片选。TransferFunc实质上是实现了相关的寄存器的配置,并中断使能。当然,SPI总线协议规定,操作设备前必须把对应从设备的CS线拉低。
对于TransferFunc需要完成的功能作如下说明:
1、保存静态变量,如发送接收数据长度,接收偏移(从接收到的第几个数据开始保存数据);
2、配置SPI寄存器,使其处于发送和接收的状态;
3、配置中断使能,并触发中断,在中断中将数据发送接收完成。
若使用轮询方式,本函数指针设置为NULL即可,但使能片选的功能须实现。
typedef bool_t (*CsActiveFunc)(ptu32_t SpecificFlag, u8 cs);
功能:片选拉低
参数:
SpecificFlag,寄存器基址
cs,片选号
返回:是否成功
虽然对于具体的芯片,该函数的实现过程不相同,但是功能是相同的,即拉低片选,选择CS对应的SPI从器件通信。若控制器具有硬件自动片选,硬件驱动可加以利用,提高效率。
若使用轮询方式,本函数指针设置为NULL即可,但失能片选的功能须实现。
typedef bool_t (*CsInActiveFunc)(ptu32_t SpecificFlag, u8 cs);
功能:片选拉高
参数:
SpecificFlag,寄存器基址
cs,片选号
返回:是否成功
目前,控制函数主要实现对总线的配置,如自动片选、传输速度、SPI时序配置等。应用层将通过调用SPI_Ctrl接口函数,传递不同的命令和参数,实现对总线的控制。
如果SPI控制器针对每个片选信号都有独立的配置寄存器,则在添加设备时(SPI_DevAddr),配置好每个片选寄存器;若多个片选共用一套配置寄存器,则每次传输都必须重新配置。在结构体SPI_CB中,成员multi_cs_reg是用来标记SPI控制器是否具有多套片选寄存器,在调用ModuleInstall_SPI时,硬件驱动会作相应的标记。
从SPI协议的时序来讲配置参数会更加的清晰。如图 42和图 43所示,SPI通信首先需产生CS片选有效,即拉低对应的CS片选。时钟信号SPI_CLK在未通信状态时的电平状态由CHOL决定,为高或者为低。而CPHA决定时序的相位,当CPHA为0时,在SPI_CLK的第一个边沿采样,第二个边沿输出数据;当CPHA为1时,在SPI_CLK的第一个边沿输出数据,第二个边沿采样。
图 4-2 SPI时序CPHA=0
图 4-3 SPI时序CPHA=1
CHOL和CPHA两种配置组成了四种模式,分别为模式0、1、2、3,如表 41所示,部分SPI从器件只支持部分模式,需根据具体器件配置成要求的模式。
表 4-1 SPI模式
控制命令用于应用层调用spibus.h的API的控制函数SPI_Ctrl实现参数配置或通信设置,与硬件相关的命令一览表如表 42所示。
表 4-2 SPI命令表
如果使用轮询方式实现驱动,则无须编写中断服务函数。
SPI接收和发送使用中断方式的好处在于,将发送任务由SPI控制器完成,节省CPU的处理负荷,因此提高了程序的运行效率,缺点在于编程相对复杂。现在绝大多数的主流CPU的中断系统都支持SPI中断,包括发送接收中断等。
SPI模块要求在中断服务函数内部完成的功能有如下:
1、清中断标志,处理好接收与发送数据同时进行的硬件机制;
2、接收数据从接收到recvoff字符数据后开始存储,即调用SPI_PortWrite;
3、发送数据从SPI_PortRead读取,若没有读到数据,则代表数据已经发送完成;若此时接收的数据还未完成,应该继续往寄存器中写数据,直到接收完成;
4、数据传输完成时,配置相应的寄存器,使其处于初始状态(视控制器而定);
下面以Atmel芯片为例,通过流程图的方式简要说明SPI中断服务函数的数据处理流程图。值得注意的是,中断服务函数中有些变量是通过__SPI_TransferTxRx传递参数到底层硬件驱动,底层驱动通过静态变量存储,并在中断服务函数中使用。如发送接收数据大小,信号量等。
图 4-4 中断服务函数流程图
为了简化编程,提高工作效率, BSP程序人员可采取下面的步骤快速的完成DJYOS驱动架构下SPI底层驱动的开发。
1、拷贝其他工程已测试通过的SPI驱动文件cpu_peri_spi.c/cpu_peri_spi.h;
2、添加SPI的中断号到critical.c文件下面tg_IntUsed数组;
3、修改cpu_peri_spi.c/cpu_peri_spi.h中与具体SPI寄存器相关的部分;
4、回调函数的具体实现和中断收发数据。
调用器件驱动程序前,确保已经调用ModuleInstall_DjyBus和ModuleInstall_SPIBus安装DjyBus和SPIBus模块。
建议将器件驱动的存放目录为djysrc\bsp\chip\xxx,其中,xxx是具体芯片的文件夹名称。
SPI总线初始化完成后,添加一个器件到总线上的过程,非常简单,就是初始化一下该器件的寻址特性参数,然后调用SPI_DevAdd_s或SPI_DevAdd函数把器件添加到总线上即可。需配置的参数,都在spibus.h文件中定义的struct tagSPI_Device中描述。struct tagSPI_Device结构定义如下:
代码 5-1 SPI器件结构体
//SPI总线器件结构体
struct tagSPI_Device
{
struct tagRscNode DevNode;
u8 Cs; //片选信号
bool_t AutoCs; //自动片选
u8 CharLen; //数据长度
u8 Mode; //模式选择
u8 ShiftDir; //MSB or LSB
u32 Freq; //速度,Hz
};
添加器件到总线的过程就是将器件节点挂到相应的“SPIn”总线节点的过程,同时,配置好相应的总线通信参数。现对添加SPI器件要点作如下说明:
1、若使用SPI_DevAdd_s挂载器件,定义static struct tagSPI_Device类型的静态变量;
2、若使用SPI_DevAdd_s挂载器件,初始化数据struct tagSPI_Param的各成员;
3、调用SPI_DevAdd_s或SPI_DevAdd添加设备到总线节点。
4、调用SPI_BusCtrl设置总线参数
SPI_DevAdd_s或SPI_DevAdd都可以把器件添加到总线上,但两者是有区别的:
1、使用SPI_DevAdd_s的话,你需要自己准备struct tagSPI_Device结构,并且自行初始化,特别是,当操作系统的spibus模块被修改导致该结构的定义发生变化时,器件驱动程序也需要修改。
2、使用SPI_DevAdd_s的好处是,该结构无须动态分配,符合像OSEK之类的严谨规范。
3、使用SPI_DevAdd的好处是,驱动程序非常简单。
下面用ATMEL公司的AT45的EEPROM芯片为例说明添加设备过程。如代码 52所示,将AT45芯片添加到总线“SPI”,并命名为“SPI_Dev_AT45”。
代码 5-2 添加SPI设备实例
bool_t AT45_HardInit(void)
{
bool_t result = false;
if(s_AT45_InitFlag == true)
return true;
static struct tagSPI_Device s_AT45_Dev;
s_AT45_Dev.AutoCs = false;
s_AT45_Dev.CharLen = 8;
s_AT45_Dev.Cs = CN_AT45_SPI_CS;
s_AT45_Dev.Freq = CN_AT45_SPI_FRE;
s_AT45_Dev.Mode = SPI_MODE_1;
s_AT45_Dev.ShiftDir = SPI_SHIFT_MSB;
if(NULL != SPI_DevAdd_s("SPI","SPI_Dev_AT45",&s_AT45_Dev))
{
ps_AT45_Dev = &s_AT45_Dev;
if(true == _at45db321_Check_ID()) //校验芯片ID
{
_at45db321_Binary_Page_Size_512();
s_AT45_InitFlag = true;
result = true;
}
}
return result;
}
器件装载到装载总线之后,可以通过访问SPI总线实现访问器件,具体就是调用spibus.h提供的API函数SPI_Transfer()。