目录
1. 初始化
2. 原理
3. i2cStart
4. i2cStop
5. i2cRecvByte
6. i2cSendByte
7. i2cRead
8. i2cWrite
9. 验证
9.1 初始化i2c
9.2 初始化gpio
9.3 写10个字节到EEPROM
9.4 读回10字节数据
9.5 运行结果
I2C(主)采用2个或3个GPIO模拟的方式实现,如果sdao等于sdai相等,表示SDA是双向的,否则一个GPIO作为SDA输出,一个GPIO作为SDA输入。 如果电平不想转换方向则选择3个GPIO的方式。注意,scl和sda必须在同一组内,比如scl在ACBUS0~7,那么sda也必须是这组内的,不能是ADBUS0~ADBUS7。
typedef struct
{struct ftdi_context *ftdi;uint8_t scl;uint8_t sdao;uint8_t sdai;int freq;uint8_t *pCommand;int iCommand;
}mpsse_i2c_s;
freq表示I2C的频率,0表示最快速度,值越大越慢。
一般I2C可以接多个从设备,所以这里只定义了2组I2C,如果需要更多,修改数组大小即可。
typedef enum
{I2C_PORT_0 = 0,I2C_PORT_1,I2C_PORT_MAX,
}mpsse_i2c_port_e;mpsse_i2c_s i2c[I2C_PORT_MAX];
1. 初始化
void i2cInit(uint8_t port, mpsse_i2c_s init)
{if(port >= I2C_PORT_MAX)return;i2c[port] = init;i2c[port].freq += 1;
}
pCommand和iCommand这里不需要初始化。其他参数按照硬件配置即可。这里freq加1是为了后面循环时直接使用。
2. 原理
从gpio写可知,通过命令0x80或0x81写入3个字节命令,FTDI设备会执行一次IO操作,如果同样的命令写freq次,那么GPIO的操作频率就会降低。比如0x80 level dir 0x80 level dir,写了2次0x80,设备就是连续设置一样GPIO两次,频率就是写一次的一半。
注意设定I2C的GPIO必须是同一组,定义好读写命令
const uint8_t gpioWirteCommand[2] = {0x80, 0x82};
const uint8_t gpioReadCommand[2] = {0x81, 0x83};
然后根据I2C的GPIO中任意一个计算应该使用哪个命令。
#define gpioCommand(port) (i2c[port].scl / 8)
例如scl选择ACBUS0,对应IO编号为8,gpioCommand = 1, 对应的读写命令为0x83和0x82。
将命令更新到命令buffer中,注意,整个I2C的操作命令是最后才真正发送给FTDI设备。
#define i2cCommandWrite(port, n) do{\for (int i = 0; i < (int)i2c[port].freq * n; i++)\{\i2c[port].pCommand[i2c[port].iCommand++] = gpioWriteCommand[gpioCommand(port)];\i2c[port].pCommand[i2c[port].iCommand++] = (gpio.level >> (gpioCommand(port) * 8)) & 0xff;\i2c[port].pCommand[i2c[port].iCommand++] = (gpio.dir >> (gpioCommand(port) * 8)) & 0xff;\}\
}while(0)
定义SDA的输入输出:
#define i2cSDAOut(port) do{\gpio.dir |= ((uint16_t)1 << i2c[port].sdao);\
}while(0)#define i2cSDAIn(port) do{\gpio.dir &= (uint16_t)(~((uint16_t)1 << i2c[port].sdai));\
}while(0)
定义SDA输出高低电平:
#define i2cSDAHigh(port) do{\gpio.level |= ((uint16_t)1 << i2c[port].sdao);\
}while(0)#define i2cSDALow(port) do{\gpio.level &= (uint16_t)(~((uint16_t)1 << i2c[port].sdao));\
}while(0)
定义读SDA的命令:
#define i2cSDARead(port) do{\i2c[port].pCommand[i2c[port].iCommand++] = gpioReadCommand[gpioCommand(port)];\
}while(0)
由于SCL一直为输出,不需要单独设置方向,只需要定义SCL的输出高低电平:
#define i2cSCLHigh(port) do{\gpio.level |= ((uint16_t)1 << i2c[port].scl);\
}while(0)#define i2cSCLLow(port) do{\gpio.level &= (uint16_t)(~((uint16_t)1 << i2c[port].scl));\
}while(0)
接下来根据i2c协议完成各个状态函数即可。
3. i2cStart
SCL为高电平,SDA产生一个下降沿即表示I2C的Start信号
void i2cStart(uint8_t port)
{//Set SDA output//SCL outputs high, SDA outputs highi2cSDAOut(port);i2cSCLHigh(port);i2cSDAHigh(port);i2cCommandWrite(port, 1);//SDA outputs lowi2cSDALow(port);i2cCommandWrite(port, 2);
}
计算command缓存区使用情况:9 * freq
#define I2C_START_CMD_LEN (3 * 3 * i2c[port].freq)
4. i2cStop
SCL为高电平,SDA产生一个下降沿即表示I2C的Stop信号
void i2cStop(uint8_t port)
{//SDA outpus lowi2cSDALow(port);i2cCommandWrite(port, 1);//SCL outputs highi2cSCLHigh(port);i2cCommandWrite(port, 1);//SDA outputs highi2cSDAHigh(port);i2cCommandWrite(port, 1);
}
计算command缓存区使用情况:9 * freq
#define I2C_STOP_CMD_LEN (3 * 3 * i2c[port].freq )
5. i2cRecvByte
SCL产生一个上升沿,然后SDA读入1个位数据,同样的操作连续读入8个位,然后根据参数ack SDA是否输出ack信号,SDA输出低电平且SCL产生一个上升沿即为ack信号,否则SDA输出高电平。ack完成后写入0x87命令,这个命令是将FTDI设备缓冲区中的数据立刻回传到PC。
void i2cRecvByte(uint8_t port, bool ack)
{//SCL outputs low, SDA outputs low//Set SDA as inputi2cSCLLow(port);i2cSDALow(port);i2cSDAIn(port);i2cCommandWrite(port, 1);uint8_t loop = 8;while (loop-- > 0){//SCL output lowi2cSCLLow(port);i2cCommandWrite(port, 1);//SCL output highi2cSCLHigh(port);i2cCommandWrite(port, 1);//Read SDAi2cSDARead(port);}//SCL outputs low, SDA outputs low//Set SDA as outputi2cSCLLow(port);i2cSDALow(port);i2cSDAOut(port);i2cCommandWrite(port, 1);if (ack == true){i2cSDALow(port);}else{i2cSDAHigh(port);}i2cCommandWrite(port, 1);//SCL output highi2cSCLHigh(port);i2cCommandWrite(port, 1);i2c[port].pCommand[i2c[port].iCommand++] = 0x87;//SCL output lowi2cSCLLow(port);i2cCommandWrite(port, 1);
}
注意,读入的数据此时并没有处理,等全部完成再读入数据。
计算command缓存区使用情况:
#define I2C_RECV_CMD_LEN (5 * 3 * i2c[port].freq + 8 * (2 * 3 * i2c[port].freq + 1) + 1)
6. i2cSendByte
SCL上升沿发送SDA的一个位信息,同样的操作连续发送8个位。发送完成后同样一个SCL上升沿读入ack信息。
void i2cSendByte(uint8_t port, uint8_t dat)
{for(uint8_t j = 0; j < 8; j++){//SCL output lowi2cSCLLow(port);i2cCommandWrite(port, 1);//SDA output datif ((dat & 0x80) == 0x80)i2cSDAHigh(port);elsei2cSDALow(port);dat <<= 1;//Set SDA as outputi2cSDAOut(port);i2cCommandWrite(port, 1);//SCL output highi2cSCLHigh(port);i2cCommandWrite(port, 1);}//SCL outputs low, SDA outputs low//Set SDA as inputi2cSCLLow(port);i2cSDALow(port);i2cSDAIn(port);i2cCommandWrite(port, 1);//SCL output low//i2cSCLLow(port);//i2cCommandWrite(port, 1);//SCL output highi2cSCLHigh(port);i2cCommandWrite(port, 10);//Read SDAi2cSDARead(port);//SDA outputs high//Set SDA as outputi2cSDAHigh(port);i2cSDAOut(port);i2cCommandWrite(port, 1);//SCL output lowi2cSCLLow(port);i2cCommandWrite(port, 1);
}
计算command缓存区使用情况:
#define I2C_SEND_CMD_LEN (8 * 3 * (4 * i2c[port].freq) + 12 * 3 * i2c[port].freq + 1)
7. i2cRead
int i2cRead(uint8_t port, uint8_t slaveAddr, uint8_t addrbit, int addr, uint8_t* dat, int len)
首先判断参数合法性
if(port >= I2C_PORT_MAX)return -1;if(len <= 0)return -1;
在读数据前先把USB缓存读空,可以通过变量ftdi->readbuffer_remaining获取当前缓存里面有多少有效数据,大于0的情况就把数据读出来丢弃。
if(i2c[port].ftdi->readbuffer_remaining > 0){uint8_t *pDummy = (uint8_t *)malloc(i2c[port].ftdi->readbuffer_remaining);if(pDummy){ftdi_read_data(i2c[port].ftdi, pDummy, i2c[port].ftdi->readbuffer_remaining);free(pDummy);}}
初始化Command缓存,按照发送的字节数计算需要多少个字节保存命令。
//2 * start + 4 * sendbyte + len * recvbyte + 1 * stop
int commandlength = (I2C_START_CMD_LEN * 2 + I2C_SEND_CMD_LEN * 4+ I2C_RECV_CMD_LEN * len+ I2C_STOP_CMD_LEN);
i2c[port].pCommand = (uint8_t *)malloc(commandlength);
i2c[port].iCommand = 0;
按照I2C设备的协议配置好Command缓存
i2cStart(port);if(addrbit > 0)i2cSendByte(port, slaveAddr);if(addrbit > 8)i2cSendByte(port, (uint8_t)(addr >> 8));if(addrbit > 0){i2cSendByte(port, (uint8_t)addr);i2cStart(port);}i2cSendByte(port, slaveAddr | 0x01);int sendLen = len;while(--sendLen){i2cRecvByte(port, true);}i2cRecvByte(port, false);i2cStop(port);
然后写到FTDI设备
if(i2c[port].iCommand > commandlength){printf("i2cRead error: command buffer is overflow %d:%d\n", i2c[port].iCommand, commandlength);if(i2c[port].pCommand)free(i2c[port].pCommand);return -2;}ftdi_write_data(i2c[port].ftdi, i2c[port].pCommand, i2c[port].iCommand);if(i2c[port].pCommand)free(i2c[port].pCommand);
发送完这些数据后,就是等待FTDI设备执行完数据并把读入的数据更新到USB驱动层。读入的数据是8个位的GPIO数据,要计算一下一共读入了多少个字节数据,每个i2cSendByte读入包含ack的一个字节,最多4个,然后在读入len个8位数据(i2cRecvByte),所以一共读入字节数为
int rdLen = ((addrbit > 0) ? 2 : 0) + ((addrbit > 8) ? 1 : 0) + 1 +len * 8;
uint8_t *pReadBuf = (uint8_t *)malloc(rdLen);
当ftdi_read_data时,实际是有一个超时的问题,如果读入的数据在设定的时间内没读回则返回超时错误,所以在调用前根据数据长度设置一个合适的超时时间:
int readtimeout = i2c[port].ftdi->usb_read_timeout;
i2c[port].ftdi->usb_read_timeout = 2 * i2c[port].iCommand + 2 * rdLen * i2c[port].freq;
int ret = ftdi_read_data(i2c[port].ftdi, pReadBuf, rdLen);
读入数据后如果出错就再试一次,如果再出错就返回错误退出。
if(ret < rdLen){//try againint remain;if(ret < 0){remain = rdLen;ret = ftdi_read_data(i2c[port].ftdi, pReadBuf, rdLen);} else{remain = rdLen - ret;ret = ftdi_read_data(i2c[port].ftdi, pReadBuf + ret, remain);}if(ret + remain < rdLen){i2c[port].ftdi->usb_read_timeout = readtimeout;if(pReadBuf)free(pReadBuf);return -3;}}i2c[port].ftdi->usb_read_timeout = readtimeout;
此时读入的数据中包含了ack信息和I2C读回的数据信息。先判断一下ack是不是正确。
int ackLen = ((addrbit > 0) ? 2 : 0) + ((addrbit > 8) ? 1 : 0) + 1;for(int i = 0; i < ackLen; i++){if ((pReadBuf[i] & ((uint8_t)(1 << (i2c[port].sdai % 8)))) == (uint8_t)(1 << (i2c[port].sdai % 8))) //Check ACK{printf("check ack fail %d, 0x%x\n", i, pReadBuf[i]);if(pReadBuf)free(pReadBuf);return -4;}}
然后把每个位信息组合成字节数据保存在返回指针中。
int j = 0;for(int i = ackLen; i < rdLen; i++){int max = i + 8;uint8_t tmp = 0;for(; i < max; i++){tmp <<= 1;if ((pReadBuf[i] & ((uint8_t)(1 << (i2c[port].sdai % 8)))) == (uint8_t)(1 << (i2c[port].sdai % 8))){tmp |= 0x01;}}dat[j++] = tmp;i--;}
最后释放malloc的数据,且返回0.
if(pReadBuf)free(pReadBuf);printf("i2cRead OK\n");return 0;
8. i2cWrite
int i2cWrite(uint8_t port, uint8_t slaveAddr, uint8_t addrbit, int addr, uint8_t* dat, int len)
类似i2cRead。
首先判断参数合法性
if(port >= I2C_PORT_MAX)return -1;if(len <= 0)return -1;
写数据前也读空一下缓存。
if(i2c[port].ftdi->readbuffer_remaining > 0){uint8_t *pDummy = (uint8_t *)malloc(i2c[port].ftdi->readbuffer_remaining);if(pDummy){ftdi_read_data(i2c[port].ftdi, pDummy, i2c[port].ftdi->readbuffer_remaining);free(pDummy);}}
初始化Command缓存
//1 * start + 3 * sendbyte + len * sendbyte + 1 * stopint commandlength = (I2C_START_CMD_LEN * 1 + I2C_SEND_CMD_LEN * 3+ I2C_SEND_CMD_LEN * len+ I2C_STOP_CMD_LEN);i2c[port].pCommand = (uint8_t *)malloc(commandlength);i2c[port].iCommand = 0;
按照I2C设备的协议配置好Command缓存
i2cStart(port);i2cSendByte(port, slaveAddr);if(addrbit > 8)i2cSendByte(port, (uint8_t)(addr >> 8));if(addrbit > 0)i2cSendByte(port, (uint8_t)addr);int n = 0;int sendLen = len;while(sendLen--){i2cSendByte(port, dat[n++]);}i2cStop(port);
将Command缓存一次发送出去
if(i2c[port].iCommand > commandlength){printf("i2cWrite error: command buffer is overflow %d:%d\n", i2c[port].iCommand, commandlength);if(i2c[port].pCommand)free(i2c[port].pCommand);return -2;}int writetimeout = i2c[port].ftdi->usb_write_timeout;i2c[port].ftdi->usb_write_timeout = i2c[port].iCommand + len * i2c[port].freq;int ret = ftdi_write_data(i2c[port].ftdi, i2c[port].pCommand, i2c[port].iCommand);if(i2c[port].pCommand)free(i2c[port].pCommand);i2c[port].ftdi->usb_write_timeout = writetimeout;if(ret < 0){printf("usb write fail %d\n", ret);return -5;}
i2cWrite只需要读回所有的ack信息
int rdLen = ((addrbit > 0) ? 1 : 0) + ((addrbit > 8) ? 1 : 0) + 1 + len;
同样的方式读回所有数据
int rdLen = ((addrbit > 0) ? 1 : 0) + ((addrbit > 8) ? 1 : 0) + 1 + len;printf("need to read data:%d\n", rdLen);uint8_t *pReadBuf = (uint8_t *)malloc(rdLen);int readtimeout = i2c[port].ftdi->usb_read_timeout;i2c[port].ftdi->usb_read_timeout = i2c[port].iCommand + rdLen * i2c[port].freq;printf("usb read timeout:%d\n", i2c[port].ftdi->usb_read_timeout);ret = ftdi_read_data(i2c[port].ftdi, pReadBuf, rdLen);printf("read data number:%d\n", ret);if(ret < rdLen){//try againint remain;if(ret < 0){remain = rdLen;ret = ftdi_read_data(i2c[port].ftdi, pReadBuf, rdLen);} else{remain = rdLen - ret;ret = ftdi_read_data(i2c[port].ftdi, pReadBuf + ret, remain);}printf("retry read %d\n", ret + remain);if(ret + remain < rdLen){if(pReadBuf)free(pReadBuf);i2c[port].ftdi->usb_read_timeout = readtimeout;return -3;}}i2c[port].ftdi->usb_read_timeout = readtimeout;
判断这些读回来的数据对应SDA位的值是否符合ack正确,注意,最后一个字节并不需要判断。
for(int i = 0; i < rdLen - 1; i++){if ((pReadBuf[i] & ((uint8_t)(1 << (i2c[port].sdai % 8)))) == (uint8_t)(1 << (i2c[port].sdai % 8))) //Check ACK{if(pReadBuf)free(pReadBuf);printf("ack fail:%d, 0x%x\n", i, pReadBuf[i]);return -4;} }
最后释放malloc的数据,且返回0.
if(pReadBuf)free(pReadBuf);printf("i2cWrite OK\n");return 0;
9. 验证
将I2C接口的EEPROM芯片接到FT4232H模块的ADBUS0和ADBUS1口。首先初始化mode为mpsse模式。
int ret;ret = ftdi_set_bitmode(ftdi, 0, BITMODE_MPSSE);if(ret < 0){printf("Set Mode Fail: %d\n", ret);return EXIT_FAILURE;}
9.1 初始化i2c
mpsse_i2c_s i2cSetting;i2cSetting.ftdi = ftdi;i2cSetting.scl = 2;i2cSetting.sdao = 3;i2cSetting.sdai = 3;i2cSetting.freq = 9;i2cInit(0, i2cSetting);
9.2 初始化gpio
将I2C的SCL和SDA初始化为输出高电平的初始状态。
mpsse_gpio_s gpioSetting;gpioSetting.ftdi = ftdi;gpioSetting.dir = 0x0000; //All inputgpioSetting.dir |= ((uint16_t)GPIO_DIR_OUT << i2cSetting.scl) | ((uint16_t)GPIO_DIR_OUT << i2cSetting.sdao); //scl output, sdao outputgpioSetting.level = 0xFFFF;mpsseGpioInit(gpioSetting);
9.3 写10个字节到EEPROM
uint8_t wrBuf[10];printf("i2c write data: ");srand(time(NULL));for(int i = 0; i < sizeof(wrBuf); i++){wrBuf[i] = (uint8_t)rand();printf("0x%2x ", wrBuf[i]);}printf("\n");ret = i2cWrite(0, 0xa0, 16, 0, wrBuf, sizeof(wrBuf));if(ret < 0){printf("write eeprom fail\n");return ret;}
9.4 读回10字节数据
uint8_t rdBuf[10];ret = i2cRead(0, 0xa0, 16, 0, rdBuf, sizeof(rdBuf));if(ret < 0){printf("read eeprom fail\n");return ret;}printf("i2c read data: ");for(int i = 0; i < sizeof(wrBuf); i++){printf("0x%2x ", rdBuf[i]);if(wrBuf[i] != rdBuf[i]){printf("\neeprom verify fail\n");return 0;}}printf("\n");
9.5 运行结果
libftdi-example$ sudo ./libftdi1-example
version:1.5.0, 1.5
Number of FTDI devices found: 1
Manufacturer: FTDI, Description: FT4232H MiniModule, Serial: FT8NZV77Open device OK: 0
i2c write data: 0x8c 0x8d 0xc4 0xf4 0xc2 0x 4 0xd8 0x88 0x26 0xf0
need to read data:13
usb read timeout:7383
read data number:13
i2cWrite OK
read data number:84
i2cRead OK
i2c read data: 0x8c 0x8d 0xc4 0xf4 0xc2 0x 4 0xd8 0x88 0x26 0xf0