同SPI Flash一样,对于MCU端来说,EVE芯片也仅仅是一个SPI外设,对EVE芯片的操作可以简化为SPI写地址,然后读写数据。因此,先定义2个API函数ft8xxWriteBuf和ft8xxReadBuf作为SPI通信的接口函数:
ft8xxWriteBuf的步骤是:首先FT8xx的CS脚拉低,然后写入3字节的地址,接着写入数据,最后将CS脚拉高结束此次传输。
bool_t ft8xxWriteBuf(uint32_t addr, uint8_t *pbuf, uint32_t len)
{
uint8_t addrBuf[3];
ft8xxCSEn();
addrBuf[0] = ((uint8_t)(addr >> 16));
addrBuf[1] = ((uint8_t)(addr >> 8));
addrBuf[2] = ((uint8_t)(addr));
spiWriteBytes(addrBuf, 3);
spiWriteBytes(pbuf, len);
ft8xxCSDis();
return TRUE;
}
ft8xxReadBuf的步骤与ft8xxWriteBuf类似,区别是写入3字节地址后需要多写入一个dummy byte才能开始读数据。
bool_t ft8xxReadBuf(uint32_t addr, uint8_t *pbuf, uint32_t len)
{
uint8_t addrBuf[4];
ft8xxCSEn();
addrBuf[0] = ((uint8_t)(addr >> 16));
addrBuf[1] = ((uint8_t)(addr >> 8));
addrBuf[2] = ((uint8_t)(addr));
addrBuf[3] = 0xff; //dummy read, ft8xx require
spiWriteBytes(addrBuf, 4);
spiReadBytes(pbuf, len);
ft8xxCSDis();
return TRUE;
}
参数addr表示需要写入或读出数据的地址,pbuf是读写的数据buffer,len表示读写的长度。
其中addr地址对应EVE芯片内部Memory地址空间(BT81x的地址空间与FT81x的相同)如下图,即对EVE芯片的读写地址对应RAM_G/RAM_PAL(只有FT80x)/RAM_DL/RAM_REG/RAM_CMD,这些名字都在头文件FT_Gpu.h(该文件在FTDI官网例程里面均有)里面有定义。
SPI数据协议:
1. Host Command
Host Command需要写入3字节命令,因为FT8xx的地址也是3字节,所以可以把Host Command理解为一组特殊的地址。
另外,第二个字节可以默认为0,因为我们一般不会设置这个参数(这个参数目前只有FT81x有,FT80x是默认为0的)。
注意第一个自己的bit7、bit6,这里为01表示该地址是Command。FT8xx就是通过这2个bit来确认当前的操作是属于什么类型。
用宏定义实现:
#define ft8xxWrCmd(cmd) ft8xxWriteBuf(((uint32_t)cmd) << 16, 0, 0)
这里采用右移16bit的原因是FTDI官网的例程中的头文件FT_Gpu_Hal.h定义的cmd都是字节表示,所以需要移位。
比如选择内部晶振还是外部晶振的命令,datasheet上描述是:
FT_Gpu_Hal.h中的定义是
typedef enum {
FT_GPU_INTERNAL_OSC = 0x48,
FT_GPU_EXTERNAL_OSC = 0x44,
}FT_GPU_PLL_SOURCE_T;
写入命令的方式是;
ft8xxWrCmd(FT_GPU_INTERNAL_OSC);
ft8xxWrCmd(FT_GPU_EXTERNAL_OSC);
2. Host Memory Read
读操作的数据格式如下:
注意第一个字节的bit7、bit6为00,表示当前操作为Host Memory Read,写入30字节的地址,写完地址后需要发送一个Dummy byte,后面的数据才是真正的有用数据。
同样可以用宏定义实现:
#define ft8xxRdMemBuf(addr, buf, len) ft8xxReadBuf(addr, buf, len)
举例从RAMG地址0x00000000中读取10字节数据,数据放在数组buf[10]中:
ft8xxRdMemBuf(RAMG + 0x00000000, buf, 10);
3. Host Memory Write
写操作的数据格式如下:
注意第一个字节的bit7、bit6为10,表示当前操作为Host Memory Write,另外同Read不同的是地址后无需Dummy Byte写入。
其宏定义为:
#define ft8xxWrMemBuf(addr, buf, len) ft8xxWriteBuf(((uint32_t)addr) | ((uint32_t)0x80 << 16), buf, len)
这里地址或上((uint32_t)0x80 << 16)是为了设置第一个字节的bit7、bit6为10。
举例从RAMG地址0x00000000中写入10字节数据,数据放在数组buf[10]中:
ft8xxWrMemBuf (RAMG + 0x00000000, buf, 10);
读写EVE芯片的Memory常用的API函数:
void ft8xxWrMem32(uint32_t addr, uint32_t dat)
{
uint8_t buf[4];
buf[0] = (uint8_t)dat;
buf[1] = (uint8_t)(dat >> 8);
buf[2] = (uint8_t)(dat >> 16);
buf[3] = (uint8_t)(dat >> 24);
ft8xxWriteBuf(addr | ((uint32_t)0x80 << 16), buf, 4);
}
uint32_t ft8xxRdMem32(uint32_t addr)
{
uint8_t buf[4];
uint32_t value;
ft8xxReadBuf(addr, buf, 4);
value = ((uint32_t)buf[0]|((uint32_t)buf[1]<<8)|((uint32_t)buf[2]<<16)|((uint32_t)buf[3]<<24));
return value;
}
类似函数ft8xxWrMem16,ft8xxWrMem8,ft8xxRdMem16,ft8xxRdMem8,分别对应读写2字节数据,1字节数据。
EVE芯片的指令都是32字节的,例如调整alpha值的命令(在文件FT_Gpu.h中有定义)是:
#define COLOR_A(alpha) ((16UL<<24)|(((alpha)&255UL)<<0))
而EVE芯片的指令有2种类型,一种是DL指令,另一种是CMD类型,分别对应Memory空间RAM_DL和RAM_CMD。不过DL指令也可以写入到RAM_CMD执行,而CMD指令不能写入到RAM_DL,所以为了驱动更为简洁,这里设计为所有指令都写入到RAM_CMD,即只使用RAM_CMD空间,不使用RAM_DL,缺点是RAM_CMD只能支持最大1K条指令,而RAM_DL是2K条指令。
另外,当需要写多条指令时,如果采用Addr + Data的方式写每一条指令,那么SPI的通信效率太低,比如写3条指令,SPI上的数据就是3B+4B+3B+4B+3B+4B。所以采用buffer方式存储指令,当buffer满或者写指令结束时才将buffer中的指令写入EVE芯片,这样同样3条指令,SPI上的数据就是3B+4B+4B+4B。
因此,定义3个全局变量记录buffer和地址的变化。
static uint16_t gDispAddrStart = 0;
static uint8_t gCmdBuffer[CMD_BUF_SIZE];
static uint16_t gCmdBufferPoint = 0;
gDispAddrStart记录写入RAM_CMD中的偏移地址,gCmdBuffer保存写入的指令,而gCmdBufferPoint记录写入到gCmdBuffer中的位置。
这样当buffer满时,需将buffer中数据一次写入到RAM_CMD,所以增加API函数ftUpdateBufToCmd:
void ftUpdateBufToCmd(void)
{
if(gCmdBufferPoint > 0)
{
ftWrCoBuf(gCmdBuffer, gCmdBufferPoint, FALSE);
gCmdBufferPoint = 0;
}
}
RAM_CMD有一个特别的特性,即它是Circular Buffer,如下图所示,其中REG_CMD_READ、REG_CMD_WRITE分别记录当前读写RAM_CMD的位置,当相等时表示执行结束。
Circular Buffer的好处在于每次写入数据时不需要考虑越界问题。比如当前写入地址为0x3FB,4KB空间只剩4字节,然后需要写入2个指令8字节,可以一次写入8字节,buffer满后剩下的4字节数据会自动写入到地址0开始。根据此特性增加API函数:
void ftWrCoBuf(uint8_t *buf, uint32_t len, bool_t wait)
{
uint16_t retval;
//FT8xx CMD buffer is circular buffer, FT81X automatically wraps continuous writes from the top address (RAM_CMD + 4095)
//back to the bottom address (RAM_CMD + 0) if the starting address of a write transfer is within RAM_CMD.
while(len > 0)
{
uint16_t wrLen;
retval = (CMDBUF_SIZE - 4);
wrLen = (retval < len) ? retval : len;
ft8xxWrMemBuf(RAM_CMD + (uint32_t)gDispAddrStart, buf, wrLen);
gDispAddrStart += wrLen;
if(wait == TRUE)
ftWaitCoFifoEmpty();
else
{
gDispAddrStart = (gDispAddrStart + 3) & 0xffc;
gDispAddrStart &= (CMDBUF_SIZE - 1);
ft8xxWrMem16(REG_CMD_WRITE, gDispAddrStart);
}
buf += wrLen;
len -= wrLen;
}
}
这里while循环的作用是RAM_CMD最大只能写入4KB,当数据大于4KB时,每写入4KB数据一般需要等待寄存器REG_CMD_READ、REG_CMD_WRITE是否相等(即等待EVE芯片处理完所有数据)再写入下一笔数据。
函数ftWaitCoFifoEmpty的实现如下:
void ftWaitCoFifoEmpty(void)
{
gDispAddrStart &= (CMDBUF_SIZE - 1);
gDispAddrStart = (gDispAddrStart + 3) & 0xffc;
ft8xxWrMem16(REG_CMD_WRITE, gDispAddrStart);
ftWaitPtrEqu(ft8xxRdMem16(REG_CMD_WRITE), REG_CMD_READ);
gDispAddrStart = ft8xxRdMem16(REG_CMD_READ);
}
函数ftWaitPtrEqu就是等待寄存器REG_CMD_READ、REG_CMD_WRITE相等,如果超时或者是非法值则重启系统。下面的代码请根据实际的需求更改,其中函数SystemReset()是根据MCU系统自行修改,函数ft8xxSleep()是delay函数,也是和平台有关。
void ftWaitPtrEqu(uint16_t wrPtr, uint32_t reg)
{
uint16_t timeout = 0;
uint16_t rdPtr = 0;
do{
rdPtr = ft8xxRdMem16(reg);
if(rdPtr >= CMDBUF_SIZE || timeout > 30000)
{//FT8xx is overrun,RESET SYSTEM
gDispAddrStart = 0;
#ifdef SystemReset
SystemReset();
#endif
break;
}
timeout++;
if(timeout % 100 == 0)
{
ft8xxSleep(1);
}
}while (rdPtr != wrPtr);
}
写入一条显示指令的API函数:
void ftWrDispCmd(uint32_t dispCmd)
{
if(gCmdBufferPoint > (CMD_BUF_SIZE - 4))
{
ftUpdateBufToCmd();
}
gCmdBuffer[gCmdBufferPoint] = (uint8_t)(dispCmd & 0xff);
gCmdBuffer[gCmdBufferPoint+1] = (uint8_t)(dispCmd >> 8);
gCmdBuffer[gCmdBufferPoint+2] = (uint8_t)(dispCmd >> 16);
gCmdBuffer[gCmdBufferPoint+3] = (uint8_t)(dispCmd >> 24);
gCmdBufferPoint += FT_BUF_ALIGN;
}
写入多条显示指令的API函数:
void ftWrDispBuf(char *buf, uint16_t len)
{
ftUpdateBufToCmd();
ftWrCoBuf((uint8_t *)buf, len, FALSE);
}
例如要在屏幕坐标(100,200)显示字符串”Hello World!”,写入命令的代码如下:
ftWrDispCmd(CMD_DLSTART);
ftWrDispCmd(CLEAR_COLOR_RGB(0, 0, 0));
ftWrDispCmd(CLEAR(1, 1, 1));
ftWrDispCmd(CMD_TEXT);
ftWrDispCmd((100 << 16) | (200 & 0xffff));
ftWrDispCmd(((0 << 16) | 31));
ftWrDispBuf(“Hello World!”, strlen((char *)”Hello World!”) + 1);
ftWrDispCmd(DISPLAY());
ftWrDispCmd(CMD_SWAP);
注意:显示指令必须在红色部分指令之间。因此增加2个API函数:
void ftStartDisp(void)
{
ftWaitPtrEqu(ft8xxRdMem16(REG_CMD_WRITE), REG_CMD_READ);
ftWrDispCmd(CMD_DLSTART);
}
void ftEnDisp(bool_t wait)
{
ftWrDispCmd(FTDISPLAY());
ftWrDispCmd(CMD_SWAP);
ftUpdateBufToCmd();
if(wait == TRUE)
ftWaitCoFifoEmpty();
else
{
gDispAddrStart = (gDispAddrStart + 3) & 0xffc;
gDispAddrStart &= (CMDBUF_SIZE - 1);
ft8xxWrMem16(REG_CMD_WRITE, gDispAddrStart);
}
}