第三章 rt-thread设备驱动模型-i2c驱动

rt-thread设备驱动模型-i2c驱动

1. 回顾

前面两章分别介绍了rt-thread设备驱动框架的实现原理,以及介绍了一个简单的看门狗驱动程序,用来加深对驱动框架的理解。看门狗驱动程序最终归纳成了下面这一张图:

在这里插入图片描述

rt-thread对看门狗设备进行了抽象,使用rt_watchdog_device结构体进行描述,这个结构体包含一个rt_device的设备对象,用于将该看门狗设备挂载到内核中的设备信息链表上。另外,rt_watchdog_device还包含一套针对看门狗设备进行操作的方法rt_watchdog_ops,这些方法是需要驱动开发者实现。

驱动开发者定义好rt_watchdog_device,并且实现rt_watchdog_ops中的函数,就可以调用接口rt_hw_watchdog_register进行看门狗设备的注册。这个注册函数中会初始化rt_device中的rt_device_ops,然后调用rt_device_register将设备挂载到内核设备信息链表上。

应用层对设备进行操作的标准接口,如rt_device_openrt_device_readrt_device_writert_device中的rt_device_ops的函数一一对应。在看门狗设备框架中,rt_device_ops中的函数会调用驱动开发者实现的rt_watchdog_ops中的函数,从而使整个调用流程形成了一个闭环。

rt-thread的看门狗驱动体现了rt-thread驱动框架的整体流程,其他设备的框架也是采用了相同的架构。下文将会介绍rt-thread中稍微复杂一点的i2c驱动。

2. i2c的使用

i2c是一种半双工同步通信方式,在硬件上包含两条线分别为时钟线SCL和数据线SDA。i2c总线上可以挂载多个从设备,每个从设备都有唯一的地址,主设备通过地址与指定的从设备进行通信。i2c硬件时序主要包含开始信号、从机地址、读写标志位、应答信号和停止信号,关于i2c具体的读写时序有很多资料可以参考,不是本文介绍的重点,在此不做介绍。

要介绍i2c的使用,首先要介绍一下i2c框架的收发数据的组织形式,熟悉linux i2c驱动的开发者对i2c_msg这个结构体应该不会感到陌生,i2c驱动框架会将需要发送的数据或者接收的数据封装成一个message进行发送和接收。rt-thread也采用相同的方法,message的数据结构为:

struct rt_i2c_msg
{
    rt_uint16_t addr;   /* 从机地址 */
    rt_uint16_t flags;  /* 读写标志 */
    rt_uint16_t len;    /* 数据长度 */
    rt_uint8_t  *buf;   /* 读写buffer指针 */
};

addr为i2c设备地址、flags为读写标志、len为读写数据长度、buf为读写buffer指针,使用rt_i2c_msg将需要的读写数据封装起来,然后调用i2c的发送函数即可,rt-thread的发送函数为:

rt_size_t rt_i2c_transfer(struct rt_i2c_bus_device *bus,
                          struct rt_i2c_msg         msgs[],
                          rt_uint32_t               num)

这个函数有三个形参,其中msgs为i2c消息数组的地址,num为消息数组成员的个数。第一个参数busstruct rt_i2c_bus_device的指针,还记得前文说过设备都会备抽象成一个结构体对象来进行描述么,rt_i2c_bus_device就是对i2c控制器设备的抽象。

struct rt_i2c_bus_device
{
    struct rt_device parent;
    const struct rt_i2c_bus_device_ops *ops;
    rt_uint16_t  flags;
    struct rt_mutex lock;
    rt_uint32_t  timeout;
    rt_uint32_t  retries;
    void *priv;
};

在此不对这个结构体进行详细的介绍,后面会进行说明,其实这个结构体与前文说的看门狗结构体rt_watchdog_device等同。举例,i2c读写eeprom使用流程如下:

#define EEPROM_I2CBUS_NAME  "i2c1"
#define EEPROM_ADDR  0x50
struct rt_i2c_bus_device *i2c_bus;

/* 1. 查找i2c设备 */
i2c_bus = (struct rt_i2c_bus_device*)rt_device_find(EEPROM_I2CBUS_NAME);

/* 对eeprm进行读操作 */
rt_err_t eeprom_read(rt_uint8_t addr, rt_uint8_t *buf, rt_uint16_t size) {
	struct rt_i2c_msg msg[2];
	
	msg[0].addr = EEPROM_ADDR;
	msg[0].flags =  RT_I2C_WR;
	msg[0].buf = &addr;
	msg[0].len = 1;
	
	msg[1].addr = EEPROM_ADDR;
	msg[1].flags = RT_I2C_RD;
	msg[1].buf = buf;
	msg[1].len = size;
	
	return rt_i2c_transfer(i2c_bus, msg, 2) == 2 ? RT_EOK : -RT_ERROR;
}

/* 对eeprm进行写操作 */
rt_err_t eeprom_write(rt_uint8_t addr, rt_uint8_t *buf, rt_uint16_t size) {
	struct rt_i2c_msg msg[2];
	
	msg[0].addr = EEPROM_ADDR;
	msg[0].flags =  RT_I2C_WR;
	msg[0].buf = &addr;
	msg[0].len = 1;
	
	msg[1].addr = EEPROM_ADDR;
	msg[1].flags = RT_I2C_WR | RT_I2C_NO_START;
	msg[1].buf = buf;
	msg[1].len = size;
	
	return rt_i2c_transfer(i2c_bus, msg, 2) == 2 ? RT_EOK : -RT_ERROR;
}

对i2c设备进行操作的步骤与看门狗设备没有什么差别,第一步就是调用通用接口rt_device_find根据设备名查找rt_i2c_bus_device,然后用rt_i2c_msg封装要发送或者接收的数据,最后调用rt_i2c_transfer进行数据收发即可。所以,i2c驱动框架在应用层开发还是挺简单的,就是填充数据然后调用收发接口,下面将介绍i2c框架的具体实现。

3. i2c驱动框架

i2c驱动一般分为两个部分,一部分为i2c控制器驱动,另一部分为挂载在i2c总线上设备的驱动。在Linux i2c驱动框架中将i2c控制器抽象为i2c_adapter结构体,在这个结构体中包含i2c的收发函数,将挂载在i2c总线上的设备驱动抽象为i2c_driver,并且将i2c设备信息抽象为i2c_clienti2c_driver使用i2c_adapter的收发函数与i2c设备进行数据交互。在rt-thread的i2c驱动框架中,并没有使用这么多的结构体进行抽象,其框架相较Linux的i2c框架更为简单一些,使用rt_i2c_bus_device对i2c控制器进行抽象,并没有使用专门的数据结构对i2c设备进行抽象。

struct rt_i2c_bus_device
{
    struct rt_device parent;
    const struct rt_i2c_bus_device_ops *ops;
    rt_uint16_t  flags;
    struct rt_mutex lock;
    rt_uint32_t  timeout;
    rt_uint32_t  retries;
    void *priv;
};

rt_i2c_bus_device是rt-thread用来描述i2c控制器的结构体,对i2c驱动框架的理解只需关注rt_device parentrt_i2c_bus_device_ops ops成员即可。parent的作用相信有了前面几章的介绍应该不会感到陌生了,就是将i2c控制器设备挂载到内核的设备信息链表中进行统一的管理。ops成员就是i2c控制器的设备操作函数集合,实际就是i2c的数据收发函数:

struct rt_i2c_bus_device_ops
{
    rt_size_t (*master_xfer)(struct rt_i2c_bus_device *bus,
                             struct rt_i2c_msg msgs[],
                             rt_uint32_t num);
    rt_size_t (*slave_xfer)(struct rt_i2c_bus_device *bus,
                            struct rt_i2c_msg msgs[],
                            rt_uint32_t num);
    rt_err_t (*i2c_bus_control)(struct rt_i2c_bus_device *bus,
                                rt_uint32_t,
                                rt_uint32_t);
};

上面结构体就是i2c控制器操作函数,本文只关心master_xfer,该函数就是对i2c设备进行数据交互的核心函数。上面两个数据结构对i2c控制器进行了抽象,根据以往的经验,会提供一个注册函数向rt-thread内核注册设备对象,函数rt_i2c_bit_add_bus的作用就是这个。

rt_err_t rt_i2c_bit_add_bus(struct rt_i2c_bus_device *bus,
                            const char               *bus_name)
{
    bus->ops = &i2c_bit_bus_ops;
    return rt_i2c_bus_device_register(bus, bus_name);
}

该函数的作用就是向rt-thread内核注册i2c设备内核对象,然后进行统一的管理。在进行注册之前会对rt_i2c_bit_add_busrt_device中的函数进行初始化,如上rt_i2c_bus_device中的rt_i2c_bus_device_ops就被赋值成i2c_bit_bus_ops

static const struct rt_i2c_bus_device_ops i2c_bit_bus_ops =
{
    i2c_bit_xfer,
    RT_NULL,
    RT_NULL
};

i2c_bit_xfer就是实际的i2c控制器与i2c设备进行数据交互的函数,在此先不讲这函数怎么实现的。接着看rt_i2c_bit_add_bus的后面过程,rt_i2c_bit_add_bus最后会调用rt_i2c_bus_device_register,该函数主要调用流程为:

rt_i2c_bus_device_register
	rt_i2c_bus_device_device_init

就是调用了函数rt_i2c_bus_device_device_initrt_i2c_bus_device_device_init的调用流程为:

rt_i2c_bus_device_device_init
    1. device = &bus->parent;
    2. device->user_data = bus;
    /* set device type */
    3. device->type    = RT_Device_Class_I2CBUS;
	4. device->init    = RT_NULL;
       device->open    = RT_NULL;
       device->close   = RT_NULL;
       device->read    = i2c_bus_device_read;
       device->write   = i2c_bus_device_write;
       device->control = i2c_bus_device_control;
	5. rt_device_register(device, name, RT_DEVICE_FLAG_RDWR);

在函数rt_i2c_bus_device_device_init中,首先根据传入的rt_i2c_bus_device结构体获取到rt_device,然后将rt_device的用户私有数据设置为传入的rt_i2c_bus_device。最后,就是初始化rt_device中设备标准操作函数了,还记得前几篇章节文章中说过,设备标准操作函数与rt_device中的函数是一一对应的么,从这里就能看出i2c设备操作函数的对应关系为:

rt_device_read     --->   i2c_bus_device_read
rt_device_write    --->   i2c_bus_device_write
rt_device_control  --->   i2c_bus_device_control

最后就调用了熟悉的设备对象注册函数rt_device_register,这个函数前面文章已经讲了多次,在此不再进行赘述了。

看到此处,可以引出一个问题:

  • rt_i2c_bus_devicert_i2c_bus_device_ops函数集是被谁调用?

在前文讲i2c的使用的时候,我们使用函数rt_i2c_transfer与设备进行交互,该函数定义为:

rt_size_t rt_i2c_transfer(struct rt_i2c_bus_device *bus,
                          struct rt_i2c_msg         msgs[],
                          rt_uint32_t               num)
{
    rt_size_t ret;
    if (bus->ops->master_xfer)
    {
        rt_mutex_take(&bus->lock, RT_WAITING_FOREVER);
        ret = bus->ops->master_xfer(bus, msgs, num);
        rt_mutex_release(&bus->lock);

        return ret;
    }
}

上述函数对无用的部分进行了删减,可见rt_i2c_transfer会调用rt_i2c_bus_device_ops中的master_xfer函数,对i2c设备进行数据收发。使用函数rt_i2c_transfer是一种与设备进行交互的方式,其实还有另一种方式与i2c设备进行数据的通信。rt_i2c_bus_device_device_init中初始化了rt_devicereadwrite等函数,也就意味这可以使用rt_device_readrt_device_write等函数与i2c设备进行通信,通过分析源代码来验证猜想。

static rt_size_t i2c_bus_device_read(rt_device_t dev,
                                     rt_off_t    pos,
                                     void       *buffer,
                                     rt_size_t   count)
{
    rt_uint16_t addr;
    rt_uint16_t flags;
    struct rt_i2c_bus_device *bus = (struct rt_i2c_bus_device *)dev->user_data;

    addr = pos & 0xffff;
    flags = (pos >> 16) & 0xffff;

    return rt_i2c_master_recv(bus, addr, flags, (rt_uint8_t *)buffer, count);
}

上述函数进行了部分删减,rt_device_read函数最终会调用到i2c_bus_device_read,从这个函数中可以看出i2c_bus_device_read的第二个参数,即读写偏移量作为了i2c设备的地址,然后调用rt_i2c_master_recv

rt_size_t rt_i2c_master_recv(struct rt_i2c_bus_device *bus,
                             rt_uint16_t               addr,
                             rt_uint16_t               flags,
                             rt_uint8_t               *buf,
                             rt_uint32_t               count)
{
    rt_err_t ret;
    struct rt_i2c_msg msg;
    RT_ASSERT(bus != RT_NULL);

    msg.addr   = addr;
    msg.flags  = flags | RT_I2C_RD;
    msg.len    = count;
    msg.buf    = buf;

    ret = rt_i2c_transfer(bus, &msg, 1);
    return (ret > 0) ? count : ret;
}

看到这儿是不是一切都清楚了,使用rt_device_readrt_device_write函数与i2c设备进行通信的时候,会将收发的数据封装成rt_i2c_msg消息,最终通过函数rt_i2c_transfer进行数据的交互。所以,上面问题的答案就有两个:

  • rt_i2c_transfer会调用rt_i2c_bus_device_ops中的master_xfer函数
  • rt_device_readrt_device_write等函数会调用rt_i2c_transfer

可见,master_xfer函数就是i2c控制器与i2c设备进行数据交互的最底层函数了,在rt_i2c_bus_device注册中,该函数已经被初始化为i2c_bit_xfer,函数声明为:

static rt_size_t i2c_bit_xfer(struct rt_i2c_bus_device *bus,
                              struct rt_i2c_msg         msgs[],
                              rt_uint32_t               num)

对于这个函数需要实现的内容,在此应该能够大致猜测出来,就是根据i2c的协议实现相应的读写时序,这些时序包括开始信号、从机地址、读写标志位、应答信号和停止信号。至此,rt-thread的i2c驱动框架就已经介绍完了,驱动开发者需要根据不同的soc平台实现i2c_bit_xfer的i2c时序。stm32的bsp采用的是模拟i2c,i2c_bit_xfer就是模拟i2c实现源码,可以自行分析。最后,以一张图进行总结,如下:

在这里插入图片描述

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340