1、简介
SPI(Serial Peripheral interface)又名串行外围设备接口。是一种高速的、全双工、同步的通信总线。SPI总线主要应用于EEPROM,FLASH,实时时钟,AD转换器等。在芯片中只占用四根管脚,节约了芯片管脚数目也为PCB布局节省了空间。正是出于这种简单易用的特性,现在越来越多芯片上都集成了SPI技术。
引脚名称 | 含义 | 描述 |
SCK | Serial Clock、时钟信号线 | Master设备往Slave设备传输时钟信号,同步控制数据交换的时机以及速率 |
MOSI | Master Output Slave Input、主设备输出从设备输入 | 在Master上面也被称为Tx-Channel,作为数据的出口,用于SPI主设备发送数据 |
MISO | Master Input Slave Output、主设备输入从设备输出 | 在Master上面也被称为Rx-Channel,作为数据的入口,主要用于SPI主设备接收数据 |
SS_n | Slave Select、从设备选择线,低电平有效 | Master设备片选Slave设备,被选中的Slave设备能够被Master设备所访问 |
SPI主从设备连接方式:
2、SPI传输时序
SPI有四种不同的工作模式,通过CPOL(时钟极性)和CPHA(时钟相位)来控制。并且主设备和从设备通信双方的工作模式需保持一致,也就是CPOL和CPHA的参数设置要保持一致。如果有多个从设备,并且它们使用了不同的工作模式,那么主设备必须在读写不同从设备时需要重新修改对应从设备的模式。
CPOL(时钟极性)定义了时钟空闲时的电平状态:
- CPOL=0,表示主设备空闲状态时SCK线上为低电平。
- CPOL=1,表示主设备空闲状态时SCK线上为高电平。
CPHA(时钟相位)定义数据采集的触发时刻:
- CPHA=0,在时钟的第一个跳变沿进行数据采样,在第二个边沿发送数据。
- CPHA=1,在时钟的第二个跳变沿进行数据采样,在第一个边沿发送数据
若CPOL=0,CPHA=0,那么设备在第一个时钟上升沿之前和时钟下降沿会传输1bit数据。在时钟上升沿会采集数据。
若CPOL=1,CPHA=0,那么设备在第一个时钟下降沿之前和时钟上升沿会传输1bit数据,在时钟下降沿会采集数据。
若CPOL=0,CPHA=1,那么设备在时钟上升沿传输1bit数据,在时钟下降沿采集数据。
若CPOL=1,CPHA=1,那么设备在时钟下降沿传输1bit数据,在时钟上升沿采集数据。
以下是当CPOL=1,CPHA=1时主从设备完成8bit数据交换的时序图:
值得注意的是,SPI是一个环形总线结构,只有主模式和从模式之分,没有读和写的说法。SPI设备间的数据传输之所以又被称为数据交换,是因为SPI协议规定一个SPI设备不能在数据通信过程中仅仅只充当一个“发送者”或者“接收者”。在每个clk周期内,SPI设备都会发送并接收一个bit大小的数据,相当于该设备有一个bit数据被交换了。
3、SPI总线回环测试
测试代码基于linux操作系统,开发板为 Bananapi-r2-pro。
首先将开发板spi3的MISO和MOSI短接起来,进行spi回环测试。通过查看开发板40pin引脚信息可以得知,spi3总线相关引脚包括:CON2-P19、CON2-P21、CON2-P23、CON2-P24。将CON2-P19和CON2-P21短接起来即可。
测试时,由于spi总线每读取1bit数据到寄存器中时,都需要发送1bit数据,因此回环测试需要使用spi_sync函数。发现打印结果为:“5020”,可得知spi总线在正常工作。
ssize_t spi_test(struct file *filp, char *buf, size_t size, loff_t *offset) { int err; char *kern_buf,rx_buf[10]; int id,i; kern_buf = kmalloc(max(4,size),GFP_KERNEL); err = spi_write_enable(); if(err<0)return -1; kern_buf[0] = 0x05; kern_buf[1] = 0x00; kern_buf[2] = 0x02; kern_buf[3] = 0x00; struct spi_message m; struct spi_transfer t= { .tx_buf=kern_buf, .rx_buf=rx_buf, .len=4, .speed_hz=1000000, .bits_per_word=8 }; spi_message_init(&m); spi_message_add_tail(&t,&m); spi_sync(g_spi_controller,&m); for(i=0;i<4;i++) { printk("%d",rx_buf[i]); } kfree(kern_buf); return size; };
4、w25q16存储芯片
spi总线测试成功后即使用w25q16进行测试。w25q16是一款基于SPI接口的Flash存储器 。其存储地址如下图所示。
w25q16的读时序如下图所示,其数据格式为:0x03h + 24bit地址 。随后返回预设个数字节数据。当芯片寄存器BUSY=1时,读操作无效。使用spi_sync进行数据交换。操作地址为0x000000h。
ssize_t tmc_spi_read(struct file *filp, char *buf, size_t size, loff_t *offset) { int err; char *kern_buf,*rx_buf; int id,i; printk("spi_read\n"); kern_buf = kmalloc(max(4,size+4),GFP_KERNEL); rx_buf = kmalloc(max(4,size+4),GFP_KERNEL); id = tmc_ReadDeviceID(); printk("get id : 0x%X\n ",id); /*给出要读取的flash地址*/ err = tmc_write_enable(); if(err<0)return -1; kern_buf[0] = 0x03; kern_buf[1] = 0x00; kern_buf[2] = 0x00; kern_buf[3] = 0x00; /* 使用spi_sync进行数据交换 */ for(i=4;i<size+4;i++)kern_buf[i] = 0x00; struct spi_message m; struct spi_transfer t= { .tx_buf=kern_buf, .rx_buf=rx_buf, .len=size+4, .speed_hz=1000000, .bits_per_word=8 }; spi_message_init(&m); spi_message_add_tail(&t,&m); spi_sync(g_spi_controller,&m); err = copy_to_user(buf,rx_buf+4,size); kfree(kern_buf); kfree(rx_buf); return size; };
w25q16的写时序如下图所示,其数据格式为 0x02h + 24bit地址 + 要写入的数据。注意一次最多只能写入256字节数据。当BUSY=1时写操作无效。在执行spi_write对flash进行写入时,要预先进行Write Enable,下文会提到。否则无法进行写入操作。
以下便是写操作相关代码。很简单,在构造好规定的数据格式后,使用spi_write函数即可。
int write_data(int addr, size_t size,const char *buf) { char *kern_buf; int err,i; err = tmc_write_enable(); if(err<0)return -1; kern_buf = kmalloc(size+4,GFP_KERNEL); kern_buf[0] = 0x02; kern_buf[1] = (addr>>16)&0xFF; kern_buf[2] = (addr>>8)&0xFF; kern_buf[3] = addr&0xFF; for(i=4;i<4+size;i++) { kern_buf[i]=buf[i-4]; } err = spi_write(g_spi_controller,kern_buf,size+4); printk("spi_write"); if(err<0) { printk("spi write address error! : %d\n",err); return -1; } kfree(kern_buf); return 0; }
由于一次最多只能写入256字节,即一页。另外还要注意数据过长写入不同扇区的情况。以下给出简易的针对数据超过256字节情况下,数据的写入代码。tmc_spi_write为入口函数。
void spi_write_data(int addr,char *buf, size_t size) { char *kern_buf = buf; int page_remain,write_remain,write_size,err,count=0,i; page_remain = 256 - addr%256; write_remain = size; if(write_remain<page_remain) { write_size=write_remain; } else { write_size = page_remain; } while(1) { err = write_data(addr,write_size,kern_buf); if(write_size==write_remain)break; write_remain-=write_size; addr+=write_size; kern_buf+=write_size; if(write_remain>256)write_size=256; else write_size = write_remain; count++; if(count>100)break; } } void spi_write_sector(int addr,void *buf, size_t size) { int sector_remain,sector_write,count=0; sector_remain = 4096 - addr%4096; if(size < sector_remain) { sector_write = size; } else sector_write = sector_remain; while(1) { sector_erase(addr); spi_write_data(addr,buf,sector_write); addr += sector_write; buf += sector_write; size -= sector_write; sector_remain-=sector_write; if(size<4096) sector_write = size; else sector_write = 4096; if(size==0)break; count++; if(count>100)break; } } ssize_t tmc_spi_write(struct file *filp, const char *buf, size_t size, loff_t *offset) { int err,i; char *kern_buf; kern_buf = kmalloc(size,GFP_KERNEL); if(err<0)return -1; err = copy_from_user(kern_buf,buf,size); spi_write_sector(0,kern_buf,size); kfree(kern_buf); return size; };
Write_Enable时序如下图,其数据格式为:0x06h。即向w25q16使用spi_write发送0x06即可。该操作会把寄存器中WEL的值修改为1。
int tmc_write_enable(void) { char kern_buf[1]; int err; printk("tmc_write_enable"); kern_buf[0] = 0x06; err = spi_write(g_spi_controller,kern_buf,1); if(err<0) { printk("write enable error! : %d\n",err); return -1; } return 0; }
另外,flash存储芯片还有一个特点,即只能将1写为0,而不能将0改变为1。 因此想要对同一块地址区域进行数据写入时,要进行扇区擦除.擦除操作要求WEL=1。并且在擦除操作执行完后,WEL自动被清除为0。Sector Erase时序如下图,其数据格式为:0x20h + 24bit地址。注意当擦除操作执行时,BUSY=1,当擦除操作结束后,BUSY被置为0,当BUSY=0时代表可以执行其他操作。
void sector_erase(int addr) { int err; char *kern_buf; kern_buf = kmalloc(4,GFP_KERNEL); tmc_write_enable(); tmc_spi_GetBusyStat(); kern_buf[0] = 0x20; kern_buf[1] = (addr>>16)&0xff; kern_buf[2] = (addr>>8)&0xff; kern_buf[3] = addr&0xff; char rx_buf[4]; struct spi_message m; struct spi_transfer t= { .tx_buf=kern_buf, .rx_buf=rx_buf, .len=4, .speed_hz=1000000, .bits_per_word=8 }; spi_message_init(&m); spi_message_add_tail(&t,&m); spi_sync(g_spi_controller,&m); kfree(kern_buf); tmc_spi_GetBusyStat(); return ; }
该模块也提供了BUSY位的读取方法Read Status Register。需要读取该芯片的Register1。其读取时序如下,数据格式为:0x05h\0x35h,随后读取寄存器值。使用0x05h读取Register1,使用0x35h读取Register2。
Register1和Register2中分别为:
void tmc_spi_GetBusyStat(void) { unsigned char stat=1; /*使用硬件SPI同步读写时序*/ char tx_buf[2]={0x05,0xFF}; char rx_buf[2]; while(stat&0x01) //判断状态最低位 { struct spi_message m; struct spi_transfer t= { .tx_buf=tx_buf, .rx_buf=rx_buf, .len=2, .speed_hz=1000000, .bits_per_word=8 }; spi_message_init(&m); spi_message_add_tail(&t,&m); spi_sync(g_spi_controller,&m); stat=rx_buf[1]; //得到状态寄存器 } }