SourceURL:file:///home/ui/Linux下CH376S的SPI接口驱动实现细节.docx
CH376厂商提供的时序图
从时序图可知,当CS信号由高电平变低电平后,CH376S被选定。在SPI模式3下,单片机向CH376发送数据,在SCK的上升沿,CH376S读取输入的命令和数据;于此同时CH376S向单片机输出数据。
以下是使用51单片机IO口模拟SPI时序的数据写入:
void Spi376OutByte( UINT8 d ) /* SPI输出8个位数据 */
{
/* 如果是硬件SPI接口,应该是先将数据写入SPI数据寄存器,然后查询SPI状态寄存器以等待SPI字节传输完成 */
UINT8 i;
for ( i = 0; i < 8; i ++ )
{
CH376_SPI_SCK = 0;
if ( d & 0x80 ) CH376_SPI_SDI = 1;
else CH376_SPI_SDI = 0;
d <<= 1; /* 数据位是高位在前 */
CH376_SPI_SCK = 1; /* CH376在时钟上升沿采样输入 */
}
}
先是准备好单片机上MOSI引脚要输出的数据位的电平,然后再将SCK(时钟)电平由低拉高,形成一个时钟上升沿,这时CH376读取MOSI引脚上的电平并转换成0或者1。
下面是以51单片机IO口模拟SPI时序的数据读取:
UINT8 Spi376InByte( void ) /* SPI输入8个位数据 */
{
/* 如果是硬件SPI接口,应该是先查询SPI状态寄存器以等待SPI字节传输完成,然后从SPI数据寄存器读出数据 */
UINT8 i, d;
d = 0;
for ( i = 0; i < 8; i ++ )
{
CH376_SPI_SCK = 0;
d <<= 1; /* 数据位是高位在前 ,可以理解为这是一个向前的移位动作*/
if ( CH376_SPI_SDO ) d ++; /*d++是“个位”“置1”操作*/
CH376_SPI_SCK = 1;
}
return( d ); //最终根据SDO的电平判断返回1字节数据
}
下面是对输入输出操作还有进一步封装:
#define xEndCH376Cmd( ) { CH376_SPI_SCS = 1;}
/* SPI片选无效,结束CH376命令,仅用于SPI接口方式 */
void xWriteCH376Cmd( UINT8 mCmd ) /* 向CH376写命令 */
{
CH376_SPI_SCS = 1; /* 防止之前未通过xEndCH376Cmd禁止SPI片选 */
mDelay0_5uS( );
/* 对于双向I/O引脚模拟SPI接口,那么必须确保已经设置SPI_SCS,SPI_SCK,SPI_SDI为输出方向,SPI_SDO为输入方向 */
CH376_SPI_SCS = 0; /* SPI片选有效 */
Spi376OutByte( mCmd ); /* 发出命令码 */
mDelay0_5uS( ); mDelay0_5uS( ); mDelay0_5uS( );
/* 延时1.5uS确保读写周期大于1.5uS,或者用上面一行的状态查询代替*/
}
void xWriteCH376Data( UINT8 mData ) /* 向CH376写数据 */
{
Spi376OutByte( mData );
//mDelay0_5uS( ); /* 确保读写周期大于0.6uS */
}
UINT8 xReadCH376Data( void ) /* 从CH376读数据 */
{
//mDelay0_5uS( ); /* 确保读写周期大于0.6uS */
return( Spi376InByte( ) );
}
从以上代码可以看出每次单片机与CH376S通信,都要先拉低CS电平,然后传送1字节命令,其后再传送若干字节数据,最后再拉高CS电平,完成一次完整的通信。
那么如果我们想写一个Linux下CH376S的驱动模块,针对这些SPI的底层通信,我们该如何实现?
有两种方法:
方法一、对照上面的51单片机代码,写一个以GPIO引脚模拟的SPI底层通信程序,包括控制CS引脚做出高、低电平变化的程序段、模拟MOSI输出和MISO输入的程序段;
方法二、利用linux内核提供的SPI框架(包括SPI Core、SPI Master、SPI Device、SPI transfer、SPI message等等)完成CH376S的SPI接口程序设计。
两方法各有优缺点,方法一实现简单,控制过程完全由编程实现,并不需要单片机提供专用的SPI端口,但没有利用到单片机集成的SPI硬件接口、也没有发挥出linux已有的SPI架构优势。方法二所涉及的理论知识多而繁杂,阅读源代码的工作量较大,实现上基本看不到硬件操控的痕迹,但通信效率方面也相当出色。
那么如何以方法二实现CH376S的SPI接口通信呢?笔者经多日摸索,终于找到了一点规律,现在分享给大家,代码可能不是很完美,希望能给有需要的读者以帮助,同时如果大家有什么好的建议也请不吝赐教。
话不多说,直接上代码:
//多字节写入操作
void Spi376OutBytes(unsigned char *d,unsigned char cnt){
int ret;
ret = spi_write(spi_device,d,cnt);
if (ret != 0)
printk(KERN_NOTICE"outbyte,spi_write failed.\n");
return;
}
//一字节写入ch376
void Spi376OutByte(unsigned char d){
int ret;
ret = spi_write(spi_device,&d,1);
if (ret !=0)
printk(KERN_NOTICE"in outbyte,spi_write failed.\n");
return;
}
对输入输出操作进一步封装:
void xWriteCH376Cmd(unsigned char mCmd)
{
Spi376OutByte(mCmd);
//延时1.5uS确保读写周期大于1.5uS
udelay(3);
}
//向CH376写入命令+数据的多字节指令
void xWriteCH376Cmds(unsigned char *d,unsigned char cnt){
Spi376OutBytes(d,cnt);
}
//向CH376写入单字节数据
void xWriteCH376Data(unsigned char mData)
{
Spi376OutByte(mData);
/* 确保读写周期大于0.6uS */
udelay(1); //应该是等待CH376处理数据并在SDO口准备输出所用的时间.
}
//从CH376读取反馈结果
int xReadCH376Data(void)
{
char ret;
if(spi_read(spich376_dev.spi_device,&ret,1)==0)
return ret;
else
return -1; //返回-1表示读取失败
}
对于以上代码,看起来很简单吧?但有一些事还是需要说一下:
- xWriteCH376Cmds()能单独使用,在该函数调用期间,Linux内核会自动完成CS、CLK、MOSI引脚的联合动作,以完成从单片机到CH376S方向(MOSI)的多字节写入操作;再配合xReadCH376Data()从CH376读取反馈(MISO),就能完成整套的通信过程;
- 拉低CS电平->xWriteCH376Cmd()->xWriteCH376Data()->xReadCH376Data()->
拉高CS电平,是一套完整的通信过程。相当于上面xWriteCH376Cmds()和xReadCH376Data()的调用过程。
3、xWriteCH376Cmd()通常不单独调用,它完成的是单片机到CH376S方向(MOSI)的单字节命令写入操作,如果xWriteCH376Cmd()被单独调用说明后续没有需要写入CH376S的数据,只是单字节指令;
4、xWriteCH376Data()不能单独调用,它完成的是单片机到CH376S方向(MOSI)的单字节数据写入操作,与xWriteCH376Cmd() 共用一个CS拉低过程,而且必须xWriteCH376Cmd() 在前,xWriteCH376Data() 在后。
5、关于CS线的控制问题。查看上述代码你会发现,Linux内核使用spi_write() 和 spi_read()封装了对SPI接口的通信操作,里面包含了对CH376S的CS线的电平的自动控制,通常一个spi_write()或者一个spi_read()过程中Linux内核就会使CS线的电平“高->低->高”变换一次,所以对CH376S的通信操作推荐使用xWriteCH376Cmds()来完成,如果非要使用xWriteCH376Cmd()和xWriteCH376Data()组合来完成,那么CS引脚就必须自己编程控制了。另外多说一句,不要在Linux里控制单片机内置spi接口的CS引脚,自己另寻一个空闲的GPIO引脚作为CH376S的CS,实现起来要方便的多,因为什么,留给你自己思考吧!另外再补充一句,不要再多路复用的SPI接口上自己编程控制CS引脚,因为什么,也留给你自己思考吧!