1.写在前面
I2C总线是由PHILIPS公司开发的一种简单、「双向二线制同步串行总线」。
关于i2c的使用,并不陌生,STM32、C51、ARM、MSP430等,都基本集成硬件i2c,或者不集成i2c的,可以根据总线时序图使用普通IO口翻转模拟一根i2c总线。
对于流行的STM32饱受诟病的硬件I2C,相信很多人都是使用模拟I2C。
模拟i2c的源码比较多,大多都是大同小异,对于各类例程,提供的模拟i2c似乎都不是太规范(个人见解),特别是一根i2c总线挂多个外设、模拟多根i2c总线、以及更换一个i2c外设时,都需要大幅度修改源码、复制源码、重新调试时序等重复的工作。
在阅读过Linux设备驱动框架和RT-Thread的驱动框架,发现在总线分层上处理就特别好,完美解决了上述提及的问题。参考RT-Thread和Linux下的模拟i2c,整理修改在裸机上使用。
2.Linux、RT-Thread设备驱动模型
1)模型分为总线驱动和设备驱动;
2) 总线驱动与外设驱动分离,方便一根总线挂多个外设,方便移植;
3) 底层(与硬件相关)与上层分离,方便添加总线及移植到不同处理器,移植到其他处理器,只需重新实现硬件相关的“寄存器”层即可;
3.MCU下裸机形式i2c总线抽象
此部分实现源码为:i2c_core.c i2c_core.h
1)i2c总线抽象对外接口(API)
“i2c_bus_xfer”为i2c封装对外的API,函数原型如下,提供一个函数模型,具体需要实例化函数指针。
int i2c_bus_xfer(struct i2c_dev_device *dev,struct i2c_dev_message msgs[],unsigned int num) { int size; size = dev->xfer(dev,msgs,num); return size; }
a)此函数即作为驱动外设的对外接口,所有操作通过此函数接口,与底层总线实现分离,如EEPROM、RTC、温度传感器等;
b)一个对外函数已经实现90%的情况使用,对应一些特殊情况,后期再完善或增加API。
c)struct i2c_dev_device *i2c_dev
2)i2c总线抽象API参数
a)i2c_dev:i2c设备指针,类型为“struct i2c_dev_device”,驱动一个i2c外设时,首先要对此指针设备初始化;
b)msgs:i2c一帧数据,发送数据及存放返回数据的缓存;
c)num:数据帧数量。
3)struct i2c_dev_device
该结构体为关键,调用API驱动外设时,首先对此初始化(类似于Linux/RT-Thread注册设备)。完整的设备包括两部分,数据操作函数和i2c相关信息(如硬件i2c或者模拟i2c)。因此“struct i2c_dev_device”的原型为:
struct i2c_dev_device { int (*xfer)(struct i2c_dev_device *dev,struct i2c_dev_message msgs[],unsigned int num); void *i2c_phy; };
a)第一个参数是函数指针,数据收发通过此函数指针调用实体函数实现;
b)第二个参数是一个void指针,初始化时指向我们使用的物理i2c(硬件/模拟),使用时可强制转换为对应的类型。
4)xfer
该函数与i2c总线设备对外接口函数“i2c_bus_xfer”具有相同的参数,形参参数参考此项的第2点,初始化时实例化指向实体函数。
5)struct i2c_dev_message
“struct i2c_dev_message”为i2c总线访问外设的一帧数据信息,包括发送数据、外设从地址、访问标识等。原型如下:
struct i2c_dev_message { unsigned short addr; unsigned short flags; unsigned short size; unsigned char *buff; unsigned char retries; };a)addr:i2c外设从机地址,常用为7位,10位较少用;
#define I2C_BUS_WR 0x0000 #define I2C_BUS_RD (1u << 0) #define I2C_BUS_ADDR_10BIT (1u << 2) #define I2C_BUS_NO_START (1u << 4) #define I2C_BUS_IGNORE_NACK (1u << 5) #define I2C_BUS_NO_READ_ACK (1u << 6)
c)size:发送的数据大小,或者接收的缓存大小;
d)buff:缓存区;
e)retries:i2c启动失败时,重启的次数。
4.模拟i2c抽象
对于模拟i2c,在以往的实现方式中,基本是时序图和外设代码混合在一起,增加外设或者使用新的i2c外设时,需要对模拟i2c代码进行较大工作量的修改,或者以“复制”的方式实现一套新的i2c总线。
但同理,可以把模拟i2c时序部分代码抽象出来,以“复用”代码的形式实现。此部分实现源码为:i2c_bitops.c i2c_bitops.h
1)模拟i2c抽象对外接口
根据上述封装的对外API,使用时,首先需要实现入口参数“i2c_dev”实例化,用模拟i2c即是调用模拟i2c相关接口。
int i2c_bitops_bus_xfer(struct ops_i2c_dev *i2c_bus,struct i2c_dev_message msgs[],unsigned long num) { struct i2c_dev_message *msg; unsigned long i; unsigned short ignore_nack; int ret; ignore_nack = msg->flags & I2C_BUS_IGNORE_NACK; i2c_bitops_start(i2c_bus); for (i = 0; i < num; i++) { msg = &msgs[i]; if (!(msg->flags & I2C_BUS_NO_START)) { if (i) { i2c_bitops_restart(i2c_bus); } ret = i2c_bitops_send_address(i2c_bus,msg); if ((ret != 0) && !ignore_nack) goto out; } if (msg->flags & I2C_BUS_RD) {//read ret = i2c_bitops_bus_read(i2c_bus,msg); if(ret < msg->size) { ret = -1; goto out; } } else {//write ret = i2c_bitops_bus_write(i2c_bus,msg); if(ret < msg->size) { ret = -1; goto out; } } } ret = i; out: i2c_bitops_stop(i2c_bus); return ret; } int ops_i2c_bus_xfer(struct i2c_dev_device *i2c_dev,struct i2c_dev_message msgs[],unsigned int num) { return (i2c_bitops_bus_xfer((struct ops_i2c_dev*)(i2c_dev->i2c_phy),msgs,num)); }
a)模拟一根i2c总线时,对外的操作函数都通过上诉函数;i2c信息帧相关参数由上层调用传递进入,此处主要增加“struct ops_i2c_dev”的封装;
b)该函数使用到的函,其中入口参数为“struct ops_i2c_dev”类型的都是模拟i2c相关;
d)模拟i2c封装实现主要针对“struct ops_i2c_dev”原型的实例化。
2)struct ops_i2c_dev
“struct ops_i2c_dev”原型如下:
struct ops_i2c_dev { void (*set_sda)(int8_t state); void (*set_scl)(int8_t state); int8_t (*get_sda)(void); int8_t (*get_scl)(void); void (*delayus)(uint32_t us); };
a)set_sda:数据线输出;
b)set_scl:时钟线输出;
c)get_sda:数据线输入(捕获);
d)get_scl:时钟线输入(捕获);
e)delayus:延时函数;
要实现一个模拟i2c,只需将上诉函数指针的实体实现即可,具体看后面描述。
3)模拟i2c时序
以产生i2c起始信号函数为例子,简要分析:
static void i2c_bitops_start(struct ops_i2c_dev *i2c_bus) { i2c_bus->set_sda(0); i2c_bus->delayus(3); i2c_bus->set_scl(0); }入口参数为struct ops_i2c_dev * i2c_bus,其实就是i2c_bitops_bus_xfer应用层函数传入的参数,最终是在此调用,底层需要实现的就是io模拟的输入/输出状态函数。
static void i2c_bitops_restart(struct ops_i2c_dev *i2c_bus) static char i2c_bitops_wait_ack(struct ops_i2c_dev *i2c_bus) static int i2c_bitops_send_byte(struct ops_i2c_dev*i2c_bus,unsigned char data)
等等,入口参数都是i2c_bus,时序实现与常规裸机程序设计是一致的,不同的是函数指针的分离调用,具体看附件源码。
4)标识位
在以往的模拟i2c或者硬件i2c中,操作外设时都有各类情况,如读和写方向的切换、连续操作(不需启动i2c总线,如写EEPROM,先写地址再写数据)等。对于这类情况,我们处理办法是选择相关的宏标识即可,具体实现由“中间层”实现,让i2c外设驱动起来更简单!以上述对外函数为例:
a)通过标识位判断是读还是写状态
if (msg->flags & I2C_BUS_RD) {//read ret = i2c_bitops_bus_read(i2c_bus,msg); if(ret < msg->size) { ret = -1; goto out; } }b)应答状态标识
ignore_nack = msg->flags & I2C_BUS_IGNORE_NACK;
5)读写函数
读写函数最终是通过io口1bit的翻转模拟出时序,从而获得数据,这部分与常规模拟i2c一致,通过函数指针方式操作。主要实现接口函数:
static unsigned long i2c_bitops_bus_write(struct ops_i2c_dev *i2c_bus,struct i2c_dev_message *msg); static unsigned long i2c_bitops_bus_read(struct ops_i2c_dev *i2c_bus,struct i2c_dev_message *msg);
5.模拟i2c总线实现
此部分实现源码为:i2c_hw.c i2c_hw.h
以stm32f1为硬件平台,采用上述模拟i2c封装,实现一根模拟i2c总线。
1)实现struct ops_i2c_dev函数实体
除了“delayus”函数外,其余为io翻转,以“set_sda”和“delayus”为例,实现如下:
static void gpio_set_sda(int8_t state) { if (state) I2C1_SDA_PORT->BSRR = I2C1_SDA_PIN; else I2C1_SDA_PORT->BRR = I2C1_SDA_PIN; } static void gpio_delayus(uint32_t us) { #if 0 volatile int32_t i; for (; us > 0; us--) { i = 30; //mini 17 while(i--); } #else Delayus(us); #endif }
2)初始化一根模拟i2c总线
void stm32f1xx_i2c_init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); GPIO_InitStructure.GPIO_Pin = I2C1_SDA_PIN | I2C1_SCL_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(I2C1_SDA_PORT, &GPIO_InitStructure); I2C1_SDA_PORT->BSRR = I2C1_SDA_PIN; I2C1_SCL_PORT->BSRR = I2C1_SCL_PIN; //device init ops_i2c1_dev.set_sda = gpio_set_sda; ops_i2c1_dev.get_sda = gpio_get_sda; ops_i2c1_dev.set_scl = gpio_set_scl; ops_i2c1_dev.get_scl = gpio_get_scl; ops_i2c1_dev.delayus = gpio_delayus; i2c1_dev.i2c_phy = &ops_i2c1_dev; i2c1_dev.xfer = ops_i2c_bus_xfer; }a)i2c io初始化;
6.驱动EEPROM(AT24C16)
此部分实现源码为:24clxx.c 24clxx.h
上面总线完成后,驱动一个i2c外设可以说就是信手拈来的事情了,而且模拟i2c总线抽象出来后,不需在做重复调试时序的工作。
假设初始化的i2c设备为i2c1_dev。
1)写EEPROM
写一个字节,页写算法详细见源码附件(24clxx.c):
char ee_24clxx_writebyte(u16 addr,u8 data) { struct i2c_dev_message ee24_msg[1]; u8 buf[3]; u8 slave_addr; if(EEPROM_MODEL > 16) { slave_addr =EE24CLXX_SLAVE_ADDR; buf[0] = (addr >>8)& 0xff; buf[1] = addr & 0xff; buf[2] = data; ee24_msg[0].size = 3; } else { slave_addr = EE24CLXX_SLAVE_ADDR | (addr>>8); buf[0] = addr & 0xff; buf[1] = data; ee24_msg[0].size = 2; } ee24_msg[0].addr = slave_addr; ee24_msg[0].flags = I2C_BUS_WR; ee24_msg[0].buff = buf; i2c_bus_xfer(&i2c1_dev,ee24_msg,1); return 0; }
2)读EEPROM
voidee_24clxx_readbytes(u16 read_ddr, char* pbuffer, u16 read_size) { struct i2c_dev_message ee24_msg[2]; u8 buf[2]; u8 slave_addr; if(EEPROM_MODEL > 16) { slave_addr =EE24CLXX_SLAVE_ADDR; buf[0] = (read_ddr>>8)& 0xff; buf[1] = read_ddr& 0xff; ee24_msg[0].size = 2; } else { slave_addr =EE24CLXX_SLAVE_ADDR | (read_ddr>>8); buf[0] = read_ddr & 0xff; ee24_msg[0].size = 1; } ee24_msg[0].buff = buf; ee24_msg[0].addr = slave_addr; ee24_msg[0].flags = I2C_BUS_WR; ee24_msg[1].addr = slave_addr; ee24_msg[1].flags = I2C_BUS_RD; ee24_msg[1].buff = (u8*)pbuffer; ee24_msg[1].size = read_size; i2c_bus_xfer(&i2c1_dev,ee24_msg,2); }
3)注意事项
驱动一个外设相对容易了,注意的事项就是标识位部分。
a)此处外设地址(addr),是实际地址,不含读写位(7bit),比如AT24C16外设地址为0x50,可能大家平常用的是0xA0,因为包括读写位;
b)写数据时,如果以2帧i2c_dev_message消息发送,需要注意“I2C_BUS_NO_START”宏,此宏标识意思是不需要再次启动i2c了,一般看i2c外设手册时序图可知道。如写EEPROM是先写地址,然后写数据这个过程是连续的,此时就需用到“I2C_BUS_NO_START”标识。程序可改成这样:
char ee_24clxx_writebyte(u16 addr,u8 data) { struct i2c_dev_message ee24_msg[2]; u8 buf[2]; u8 slave_addr; if(EEPROM_MODEL > 16) { slave_addr =EE24CLXX_SLAVE_ADDR; buf[0] = (addr>>8)& 0xff; buf[1] = addr &0xff; ee24_msg[0].size = 2; } else { slave_addr =EE24CLXX_SLAVE_ADDR | (addr>>8); buf[0] = addr &0xff; ee24_msg[0].size = 1; } ee24_msg[0].addr = slave_addr; ee24_msg[0].flags = I2C_BUS_WR; ee24_msg[0].buff = buf; ee24_msg[1].addr = slave_addr; ee24_msg[1].flags = I2C_BUS_WR |I2C_BUS_NO_START; ee24_msg[1].buff = &data; ee24_msg[1].size = 1; i2c_bus_xfer(&i2c1_dev,ee24_msg,2); return 0; }
4)其他
理解之后,或者使用过Linux、RT-Thread的驱动框架的,再驱动其他i2c外设,就是很容易的事情了,剩下的就是配置寄存器、应用算法的问题了。
7.总结
1)整体思路比较易理解,本质就是函数指针,将与硬件底层无关的部分抽象出来,相关联的地方分层明确,通过函数指针的方式进行调用。
2)事务分离,通用、重复的事情交给总线处理,特殊任务留给外设驱动。
审核编辑:汤梓红
全部0条评论
快来发表一下你的评论吧 !