前言:
因为工作需要用到SPI驱动Flash芯片,且SPI硬件接口无法使用,因此采用GPIO来模拟SPI和Flash建立连接。但因为之前没有接触过如何根据时序图来编写代码,下面详细展示我是如何根据芯片手册来编写代码驱动Flash芯片的。
硬件介绍:
处理芯片:AWR6843ARBGALPQ1
嵌入式平台:CCS
Flash芯片:MX25R1635F
GPIO模拟SPI:
SPI根据CPOL( Clock Polarity,时钟极性)和CPHA(Clock Phase,时钟相位)不同共分为四种工作模式,这导致时序会有一些差异,具体请查找对应的芯片。
如图是MX25R1635F的相关介绍,其介绍到:
- MX25R1635F的支持Mode 0 和Mode 3,这表明CPOL可以自行设置,下文是按照CPOL为0进行设置(即时钟信号线在空闲状态是低电平)。
- MX25R1635F的数据变化是发生在时钟的下降沿,那么数据的采样则是在时钟的上升沿,结合CPOL为0和上升沿采样,那么可以得到CPHA为0(即时钟的奇数边沿采样,偶数边沿数据变化)
- SPI的启动是片选信号线CS拉低,结束则是CS拉高。
根据上述信息就可以编写GPIO模拟SPI的驱动代码。具体如下:
为了简化代码编写,在进行驱动代码之前先进行硬件引脚的定义及引脚控制函数编写,由于不同硬件和不同编程平台(底层驱动函数不一致),因此这边对基础的功能函数仅做介绍,从而方便阅读后续的驱动代码:
static void delay_us (unsigned char length); //延时函数
static void GPIO_In_Out_Config(unsigned char gpioNum,unsigned char gpiotype); //IO口输入输出设置
static void SPI_CS(unsigned char gpiovalue); //配置片选信号线CS的高低电平
static void SPI_SCLK(unsigned char gpiovalue); //配置时钟信号线SCLK的高低电平
static void SPI_SI(unsigned char gpiovalue); //配置MOSI信号线的高低电平
static int32_t READ_SO(void); //读取MISO信号的电平状态
void init_qspiflash(void); //引脚配置,并设置CS、SCLK、SI为输出,SO为输入
#define DummyBytes 0xFF //Dummy Bytes
SPI启动
根据芯片手册可知,SPI的启动是片选信号从高变成低,因此,启动代码具体如下:
//产生SPI起始信号
static void SPI_Start(void)
{
SPI_CS(1);
SPI_SCLK(0); //由于我选择的是CPOL为0,因此时钟在空闲状态下是低电平
delay_us(4);
SPI_CS(0); //片选信号线拉低,SPI启动
delay_us(4);
}
SPI关闭
根据芯片手册可知,SPI的关闭是片选信号从低变高,因此,关闭代码具体如下:
//产生SPI起始信号
static void SPI_Stop(void)
{
SPI_CS(0);
SPI_SCLK(0); //由于我选择的是CPOL为0,因此时钟在空闲状态下是低电平
delay_us(4);
SPI_CS(1); //片选信号线拉高,SPI关闭
delay_us(4);
}
SPI读字节
根据芯片手册,SPI的读字节是在上升沿采样,下降沿产生变化,那么可以将一个字节的读取时序图解释如下:
空闲状态时钟为低->延时->时钟拉高->数据读取1bit->延时->时钟拉低->延时->时钟拉高->数据读取1bit->延时->…->时钟拉低->延时->时钟拉高->数据读取1bit->延时->时钟拉低(读完8个bit后将时钟拉低)
根据上述的时序逻辑,SPI的读字节驱动代码具体如下:
//数据读取
uint8_t SPI_ReadByte(void) { //SPI读1 Byte,循环8次,每次接收1 Bit;
uint8_t i = 0;
uint8_t read_data = 0xFF;
for(i=0; i<8; i++) {
read_data = read_data << 1; //“腾空” read_data最低位,8次循环后,read_data将高位在前;
SPI_SCLK(0); //拉低时钟,即空闲时钟为低电平;
delay_us(2);
SPI_SCLK(1);
if(READ_SO())
{
read_data = read_data + 1;
}
delay_us(2);
}
SPI_SCLK(0); //最后SPI读取完后,拉低时钟,进入空闲状态
return read_data;
}
SPI写字节
和读字节的逻辑基本一致,不过一个是在上升沿的时候改变SI的电平,一个是在上升沿的时候读取SO的电平。
具体的SPI写字节的驱动代码如下:
void SPI_WriteByte(uint8_t data)
{
uint8_t i = 0;
uint8_t temp = 0;
for(i=0; i<8; i++) {
temp = ((data&0x80)==0x80)? 1:0; //将data最高位保存到temp;
data = data<<1; //data左移一位,将次高位变为最高位,用于下次取最高位;
SPI_SCLK(0); //CPOL=0 //拉低时钟,即空闲时钟为低电平, CPOL=0;
SPI_SI(temp); //根据temp值,设置MOSI引脚的电平
delay_us(2); //简单延时,可以定时器或延时函数实现
SPI_SCLK(1); //CPHA=0 //拉高时钟,这样就设置成上升沿数据没有改变,实现在时钟上升沿采样,下降沿数据变化
delay_us(2);
}
SPI_SCLK(0); //最后SPI发送完后,拉低时钟,进入空闲状态;
}
读设备ID:
基于上述的基本驱动函数,下面就是根据芯片手册详细的时序逻辑图完成对应的功能,例如MX25R1635F读ID的操作,其具体的时序逻辑就是:
SPI启动->发送读设备ID命令0x90->发送两个DummyBytes->发送读ID地址->读取ManufactureID->读取DeviceID->SPI停止
DummyBytes可以是任意值,关于DummyBytes的具体功能网上查到的是说为了减少中间过程的偏差。
int32_t SPI_Write_Sequence_Debug(uint8_t DebugCommand,uint8_t DebugCommandAddress)
{
int32_t retVal = 0;
uint8_t ManufactureID;
uint8_t DeviceID;
SPI_Start();
SPI_WriteByte(DebugCommand);
SPI_WriteByte(DummyBytes);
SPI_WriteByte(DummyBytes);
SPI_WriteByte(DebugCommandAddress);
DeviceID = SPI_ReadByte();
ManufactureID = SPI_ReadByte();
SPI_Stop();
retVal++;
return retVal;
}
SPI_Write_Sequence_Debug(0x90,0x01); //主程序调用
最终的Debug测试结果如下:成功读取到两个ID分别是0x15,0xC2,具体结果如下:
至于该款Flash的其他功能也是一样,根据相应的功能时序图进行修改即可。当然如果是Dual Read或者是Quad Read模式则相应的驱动函数需要根据时序图进一步修改才能使用。
欢迎大家批评指正!