STM32指针抽象出I2C的数据实例

发布时间:2024-04-03  

1.写在前面

I2C总线是由PHILIPS公司开发的一种简单、「双向二线制同步串行总线」。


关于i2c的使用,并不陌生,STM32、C51、ARM、MSP430等,都基本集成硬件i2c,或者不集成i2c的,可以根据总线时序图使用普通IO口翻转模拟一根i2c总线。

wKgaomU6G5yAH6zwAABnIAjUCo0855.png

对于流行的STM32饱受诟病的硬件I2C,相信很多人都是使用模拟I2C。

模拟i2c的源码比较多,大多都是大同小异,对于各类例程,提供的模拟i2c似乎都不是太规范(个人见解),特别是一根i2c总线挂多个外设、模拟多根i2c总线、以及更换一个i2c外设时,都需要大幅度修改源码、复制源码、重新调试时序等重复的工作。

在阅读过Linux设备驱动框架和RT-Thread的驱动框架,发现在总线分层上处理就特别好,完美解决了上述提及的问题。参考RT-Thread和Linux下的模拟i2c,整理修改在裸机上使用。

2.Linux、RT-Thread设备驱动模型

1)模型分为总线驱动和设备驱动;

2) 总线驱动与外设驱动分离,方便一根总线挂多个外设,方便移植;

3) 底层(与硬件相关)与上层分离,方便添加总线及移植到不同处理器,移植到其他处理器,只需重新实现硬件相关的“寄存器”层即可;

wKgaomU6G5yAcInpAAAqxaqySFY086.jpg

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位较少用;

b)flags:标识,发送、接收、应答、地址位选择等标识;几种标识如下:

#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)

staticinti2c_bitops_send_byte(structops_i2c_dev*i2c_bus,unsignedchardata)

等等,入口参数都是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);

staticunsignedlongi2c_bitops_bus_read(structops_i2c_dev*i2c_bus,structi2c_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

}


a)为例提高速率,上诉代码采用寄存器方式操作,可以用库函数操作io口;


b)延时可以用硬件定时器延时,或者软件延时,具体根据cpu时钟计算;


c)其他源码看附件中“i2c_hw.c”

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初始化;


b)i2c设备实例化,其中“ops_i2c1_dev”和“i2c1_dev”即是我们定义的总线设备,后面使用该总线时主要通过“i2c1_dev”实现对底层的调用。

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;

文章来源于:电子工程世界    原文链接
本站所有转载文章系出于传递更多信息之目的,且明确注明来源,不希望被转载的媒体或个人可与我们联系,我们将立即进行删除处理。

我们与500+贴片厂合作,完美满足客户的定制需求。为品牌提供定制化的推广方案、专属产品特色页,多渠道推广,SEM/SEO精准营销以及与公众号的联合推广...详细>>

利用葫芦芯平台的卓越技术服务和新产品推广能力,原厂代理能轻松打入消费物联网(IOT)、信息与通信(ICT)、汽车及新能源汽车、工业自动化及工业物联网、装备及功率电子...详细>>

充分利用其强大的电子元器件采购流量,创新性地为这些物料提供了一个全新的窗口。我们的高效数字营销技术,不仅可以助你轻松识别与连接到需求方,更能够极大地提高“闲置物料”的处理能力,通过葫芦芯平台...详细>>

我们的目标很明确:构建一个全方位的半导体产业生态系统。成为一家全球领先的半导体互联网生态公司。目前,我们已成功打造了智能汽车、智能家居、大健康医疗、机器人和材料等五大生态领域。更为重要的是...详细>>

我们深知加工与定制类服务商的价值和重要性,因此,我们倾力为您提供最顶尖的营销资源。在我们的平台上,您可以直接接触到100万的研发工程师和采购工程师,以及10万的活跃客户群体...详细>>

凭借我们强大的专业流量和尖端的互联网数字营销技术,我们承诺为原厂提供免费的产品资料推广服务。无论是最新的资讯、技术动态还是创新产品,都可以通过我们的平台迅速传达给目标客户...详细>>

我们不止于将线索转化为潜在客户。葫芦芯平台致力于形成业务闭环,从引流、宣传到最终销售,全程跟进,确保每一个potential lead都得到妥善处理,从而大幅提高转化率。不仅如此...详细>>