目录
与官方的方式不同,这里采用纯GPIO模式的方式实现SPI,这样可以定义任意GPIO为SPI,也可以实现QSPI。
typedef struct
{
struct ftdi_context *ftdi;
uint8_t sck;
uint8_t mosi_io0;
uint8_t miso_io1;
uint8_t io2;
uint8_t io3;
uint8_t freq;
spi_type_e type;
spi_mode_e mode;
bool msb;
uint8_t *pCommand;
int iCommand;
}mpsse_spi_s;
sck/mosi_io0/miso_io1/io2/io3分别对应SPI/QSPI的io,同样,所有io都必须属于同一组。注意,CS脚单独控制,用GPIO的方式控制。
type表示该SPI是哪种类型:
typedef enum
{
SPI_TYPE_SPI = 0,
SPI_TYPE_QSPI,
}spi_type_e;
mode则表示spi的模式
由于多个SPI从设备可以共用sck和数据口,只使用不同的cs即可。所以这里只定义2个spi设备
typedef enum
{
SPI_PORT_0 = 0,
SPI_PORT_1,
SPI_PORT_MAX,
}mpsse_spi_port_e;
mpsse_spi_s spi[SPI_PORT_MAX];
1. 初始化
void spiInit(uint8_t port, mpsse_spi_s init)
{
if(port >= SPI_PORT_MAX)
return;
spi[port] = init;
spi[port].freq += 1;
}
2. SCK默认电平设置
根据SPI的模式设置SCK默认电平,如果是模式0和模式1,SCK默认是低电平(CPOL = 0),如果是模式2和模式3(CPOL = 1),SCK默认是高电平。
void spiCPOL(uint8_t port)
{
if(spi[port].mode == SPI_MODE0 || spi[port].mode == SPI_MODE1)
mpsseGpioWrite(spi[port].sck, 0);
else
mpsseGpioWrite(spi[port].sck, 1);
}
这个函数要在CS设置为低前调用。
3. GPIO控制
将GPIO控制定义好宏
#define spiSCKHigh(port) do{\
gpio.level |= ((uint16_t)1 << spi[port].sck);\
}while(0)
#define spiSCKLow(port) do{\
gpio.level &= (uint16_t)(~((uint16_t)1 << spi[port].sck));\
}while(0)
#define spiMOSIHigh(port) do{\
gpio.level |= ((uint16_t)1 << spi[port].mosi_io0);\
}while(0)
#define spiMOSILow(port) do{\
gpio.level &= (uint16_t)(~((uint16_t)1 << spi[port].mosi_io0));\
}while(0)
#define spiMISORead(port) do{\
spi[port].pCommand[spi[port].iCommand++] = gpioReadCommand[gpioCommand(port)];\
}while(0)
4. spi全双工通信
spi类型下传输数据接口函数:
int spiTransferBytes(uint8_t port, uint8_t* wrBuf, uint8_t* rdBuf, uint16_t len)
这种模式下是全双工的模式,和选择的模式有关。CPHA表示选择第几个沿来
当CPHA = 0时,表示第一个沿采集数据(即读入数据);当CPHA = 1时,表示第二个沿采集数据。由于CPOL确定了SCK初始电平,所以这2个设置就决定了第一个是上升沿还是下降沿。
mode | CPOL | CPHA |
---|---|---|
mode 0 | 0 | 0 |
mode 1 | 0 | 1 |
mode 2 | 1 | 0 |
mode 3 | 1 | 1 |
mode 0: SCK初始电平为低电平, 在第一个上升沿前MOSI输出,然后在上升沿后MISO读入,完成第一个位的传输。
mode 1: SCK初始电平为低电平, 在第一个上升沿前MOSI输出,等到下降沿后MISO读入
mode 2: SCK初始电平为高电平, 在第一个下降沿前MOSI输出,然后在下降沿后MISO读入,完成第一个位的传输。
mode 3: SCK初始电平为高电平, 在第一个下降沿前MOSI输出,等到上升沿后MISO读入
int size = len;
int j = 0;
while(size > 0)
{
int i;
uint8_t wrDat;
if(wrBuf == NULL)
wrDat = 0xff;
else
wrDat = wrBuf[j];
uint8_t level = 0;
for(i = 0; i < 8; i++)
{
if(spi[port].msb == false)
{
level = (wrDat & 0x01) ? 1 : 0;
wrDat >>= 1;
}
else
{
level = (wrDat & 0x80) ? 1 : 0;
wrDat <<= 1;
}
switch(spi[port].mode)
{
case SPI_MODE0:
default:
spi0TransferBit(port, level);
break;
case SPI_MODE1:
spi1TransferBit(port, level);
break;
case SPI_MODE2:
spi2TransferBit(port, level);
break;
case SPI_MODE3:
spi3TransferBit(port, level);
break;
}
}
j++;
size--;
}
4.1 MSB/LSB
在发送前,先根据高位在前MSB还是低位在前LSB准备好要发送的位
for(i = 0; i < 8; i++)
{
if(spi[port].msb == false)
{
level = (wrDat & 0x01) ? 1 : 0;
wrDat >>= 1;
}
else
{
level = (wrDat & 0x80) ? 1 : 0;
wrDat <<= 1;
}
switch(spi[port].mode)
4.2 分配command缓存
一个位写需要3 * 3 * freq个字节命令,最后需要加一个0x87命令,所以一共需要分配的字节数为:
int commandlength = size * 3 * 3 * spi[port].freq + 1;
spi[port].pCommand = (uint8_t *)malloc(commandlength);
spi[port].iCommand = 0;
4.3 spi0TransferBit
模式0下传输一个位,在上升沿前MOSI输出位信息,然后产生上升沿,从设备准备好数据,主机读入数据,最后拉低SCK,完成一个位的传输。
void spi0TransferBit(uint8_t port, uint8_t level)
{
if(level == 0)
spiMOSILow(port);
else
spiMOSIHigh(port);
spiCommandWrite(port, 1);
spiSCKHigh(port);
spiCommandWrite(port, 1);
spiMISORead(port);
spiSCKLow(port);
spiCommandWrite(port, 1);
}
4.3 spi1TransferBit
和模式0最大的区别是在第二个时钟沿读入MISO
void spi1TransferBit(uint8_t port, uint8_t level)
{
if(level == 0)
spiMOSILow(port);
else
spiMOSIHigh(port);
spiCommandWrite(port, 1);
spiSCKHigh(port);
spiCommandWrite(port, 1);
spiSCKLow(port);
spiCommandWrite(port, 1);
spiMISORead(port);
}
4.4 spi2TransferBit
和模式0相比,sck方向反过来就是模式2.
void spi2TransferBit(uint8_t port, uint8_t level)
{
if(level == 0)
spiMOSILow(port);
else
spiMOSIHigh(port);
spiCommandWrite(port, 1);
spiSCKLow(port);
spiCommandWrite(port, 1);
spiMISORead(port);
spiSCKHigh(port);
spiCommandWrite(port, 1);
}
4.5 spi3TransferBit
和模式1相比,sck方向反过来就是模式3
void spi3TransferBit(uint8_t port, uint8_t level)
{
if(level == 0)
spiMOSILow(port);
else
spiMOSIHigh(port);
spiCommandWrite(port, 1);
spiSCKLow(port);
spiCommandWrite(port, 1);
spiSCKHigh(port);
spiCommandWrite(port, 1);
spiMISORead(port);
}
4.6 写命令序列
最后加一个0x87命令
spi[port].pCommand[spi[port].iCommand++] = 0x87;
同样,判断一下command缓存是否足够
if(spi[port].iCommand > commandlength)
{
printf("spi transfer error: command buffer is overflow %d:%d\n",
spi[port].iCommand, commandlength);
if(spi[port].pCommand)
free(spi[port].pCommand);
return -2;
}
发送command数据
int writetimeout = spi[port].ftdi->usb_write_timeout;
spi[port].ftdi->usb_write_timeout = spi[port].iCommand * spi[port].freq;
int ret = ftdi_write_data(spi[port].ftdi, spi[port].pCommand, spi[port].iCommand);
if(spi[port].pCommand)
free(spi[port].pCommand);
spi[port].ftdi->usb_write_timeout = writetimeout;
if(ret < 0)
{
printf("usb write fail %d\n", ret);
return -3;
}
4.7 读数据
读入所有的位数据
int rdLen = len * 8;
uint8_t *pReadBuf = (uint8_t *)malloc(rdLen);
int readtimeout = spi[port].ftdi->usb_read_timeout;
spi[port].ftdi->usb_read_timeout = spi[port].iCommand + rdLen * spi[port].freq;
ret = ftdi_read_data(spi[port].ftdi, pReadBuf, rdLen);
printf("read data number:%d\n", ret);
if(ret < rdLen)
{//try again
printf("retry read:%d\n", ret);
int remain;
if(ret < 0)
{
remain = rdLen;
ret = ftdi_read_data(spi[port].ftdi, pReadBuf, rdLen);
}
else
{
remain = rdLen - ret;
ret = ftdi_read_data(spi[port].ftdi, pReadBuf + ret, remain);
}
if(ret + remain < rdLen)
{
if(pReadBuf)
free(pReadBuf);
spi[port].ftdi->usb_read_timeout = readtimeout;
return -3;
}
}
spi[port].ftdi->usb_read_timeout = readtimeout;
4.8 组合实际数据
因为读入的数据是整个8位数据,所以将对应位的数据组合为8位数据。如果rdBuf是空指针,那么就不需要读数据,这一步就跳过。
if(rdBuf != NULL)
{
int j = 0;
for(int i = 0; i < rdLen; i++)
{
int max = i + 8;
uint8_t tmp = 0;
for(; i < max; i++)
{
if(spi[port].msb == true)
{
tmp <<= 1;
if ((pReadBuf[i] & ((uint8_t)(1 << (spi[port].miso_io1 % 8))))
== (uint8_t)(1 << (spi[port].miso_io1 % 8)))
{
tmp |= 0x01;
}
}
else
{
tmp >>= 1;
if ((pReadBuf[i] & ((uint8_t)(1 << (spi[port].miso_io1 % 8))))
== (uint8_t)(1 << (spi[port].miso_io1 % 8)))
{
tmp |= 0x80;
}
}
}
rdBuf[j++] = tmp;
i--;
}
if(pReadBuf)
free(pReadBuf);
}
最后释放缓存,返回即可。
if(pReadBuf)
free(pReadBuf);
printf("SPI Transfer Bytes OK\n");
4. 9 读写函数
可以通过宏定义定义好SPI接口的读写函数
#define spiWriteBytes(port, wrBuf, len) spiTransferBytes(port, wrBuf, NULL, len)
#define spiReadBytes(port, rdBuf, len) spiTransferBytes(port, NULL, rdBuf, len)
5. 验证
SPI接口接SPI NOR Flash,其中ADBUS0接MOSI,ADBUS1接MISO,ADBUS2接SCK,ADBUS3接CS。
5.1 初始化FTDI设备模式
int ret;
ret = ftdi_set_bitmode(ftdi, 0, BITMODE_MPSSE);
if(ret < 0)
{
printf("Set Mode Fail: %d\n", ret);
return EXIT_FAILURE;
}
5.2 初始化GPIO
#define SFLASH_CS_PIN 3
mpsse_gpio_s gpioSetting;
gpioSetting.ftdi = ftdi;
gpioSetting.dir = 0x0000; //All input
gpioSetting.dir |= ((uint16_t)GPIO_DIR_OUT << spiSetting.sck)
| ((uint16_t)GPIO_DIR_OUT << spiSetting.mosi_io0)
| ((uint16_t)GPIO_DIR_OUT << SFLASH_CS_PIN); //CS
gpioSetting.level = 0xFFFF;
mpsseGpioInit(gpioSetting);
5.3 控制CS
直接使用gpio的api控制CS的高低电平。
#define SFLASH_CS_PIN 3
void sflash0CS(uint8_t port, bool enable)
{
spiCPOL(port);
mpsseGpioWrite(SFLASH_CS_PIN, !enable);
}
5.4 读flash的id
#define CMD_READ_ID 0x9F
uint8_t cmd[4] = {CMD_READ_ID, 0xff, 0xff, 0xff};
uint32_t id;
sflash[port].pCSEnable(port, true);
spiWriteBytes(sflash[port].spiPort, cmd, 1);
spiReadBytes(sflash[port].spiPort, cmd + 1, 3);
sflash[port].pCSEnable(port, false);
id = ((uint32_t)cmd[1] << 16) | (uint32_t)(cmd[2] << 8) | (uint32_t)(cmd[3]);
printf("sflash id is:%x\n", id);
5.5 结果
sflash id is:ef4018
这个波形如下图:
而最大频率大概为450KHz,有点低
如果需要更高频率,应该要采用官方的方式,固定SCK,MISO和MOSI的方式。
还有一种优化方式,就是将所有的命令一次发出,例如pCSEnable + spiWriteBytes + spiReadBytes + pCSEnable这4个的IO控制序列组合起来,一次写给设备。例如:
sflash[port].pCSEnable(port, true);
//spiWriteBytes(sflash[port].spiPort, cmd, 1);
//spiReadBytes(sflash[port].spiPort, cmd + 1, 3);
spiTransferBytes(sflash[port].spiPort, cmd, cmd, 4);
sflash[port].pCSEnable(port, false);
它的波形就变为: