这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 嵌入式开发 » 软件与操作系统 » rtthread I2C框架解析

共6条 1/1 1 跳转至

rtthread I2C框架解析

助工
2024-09-18 22:56:15     打赏

I2C驱动框架解析

代码入口

   代码入口其实挺好找,基本上芯片对I2C驱动代码提供会以以下名称编写,drv_i2c.c(硬件I2C驱动)或drv_soft_i2c.c(软件I2C套在硬件I2C驱动接口上的实现,我个人的观点,如果不支持硬件I2C,其实根本没必要专门写一套软件I2C的驱动,原因是,软件I2C本质上就是gpio操作加延时处理,完全就是业务逻辑的事情,不应该放在驱动层实现)。这里我们主要看硬件I2C,因此直接查看硬件I2C驱动文件。在驱动文件中,我们找到么一个入口:

struct i2c_handle
{
    struct rt_i2c_bus_device bus;
    char bus_name[RT_NAME_MAX];
};

static struct i2c_handle ra_i2cs[] =
{
    ...
}

static const struct rt_i2c_bus_device_ops i2c_ops =
{
    .master_xfer        = ra_i2c_mst_xfer,
    .slave_xfer         = RT_NULL,
    .i2c_bus_control    = RT_NULL
};

int xxxx(void)
{
    fsp_err_t err     = FSP_SUCCESS;
    for (rt_uint32_t i = 0; i < sizeof(ra_i2cs) / sizeof(ra_i2cs[0]); i++)
    {
        // I2C接口初始化
        i2cs[i].bus.ops = &i2c_ops;
        i2cs[i].bus.priv = 0;

        // 注册I2C设备
        rt_i2c_bus_device_register(&i2cs[i].bus, i2cs[i].bus_name);
    }

    return 0;
}


INIT_DEVICE_EXPORT(xxxx);

其中:rt_i2c_bus_device_register即为我们所需要找到的I2C框架的入口位置。

I2C驱动注册函数解析

   找到了设备级的I2C设备注册入口,我们便开始逐层分析注册接口。

#ifdef RT_USING_DEVICE_OPS
const static struct rt_device_ops i2c_ops =
{
    RT_NULL,
    RT_NULL,
    RT_NULL,
    i2c_bus_device_read,
    i2c_bus_device_write,
    i2c_bus_device_control
};
#endif

rt_err_t rt_i2c_bus_device_device_init(struct rt_i2c_bus_device *bus,
                                       const char               *name)
{
    struct rt_device *device;
    RT_ASSERT(bus != RT_NULL);

    device = &bus->parent;

    device->user_data = bus;

    /* set device type */
    device->type    = RT_Device_Class_I2CBUS;
    /* initialize device interface */
#ifdef RT_USING_DEVICE_OPS
    device->ops     = &i2c_ops;
#else
    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;
#endif

    /* register to device manager */
    rt_device_register(device, name, RT_DEVICE_FLAG_RDWR);

    return RT_EOK;
}

rt_err_t rt_i2c_bus_device_register(struct rt_i2c_bus_device *bus,
                                    const char               *bus_name)
{
    rt_err_t res = RT_EOK;

    // 注册设备锁,用于不同线程调用I2C设备时的互斥操作
    rt_mutex_init(&bus->lock, "i2c_bus_lock", RT_IPC_FLAG_PRIO);

    if (bus->timeout == 0) bus->timeout = RT_TICK_PER_SECOND;

    // 注册I2C设备
    res = rt_i2c_bus_device_device_init(bus, bus_name);

    LOG_I("I2C bus [%s] registered", bus_name);

#ifdef RT_USING_DM
    if (!res)
    {
        i2c_bus_scan_clients(bus);
    }
#endif

    return res;
}

     完整代码一贴出来,我们便可以发现,所谓的I2C总线设备,实际上只是在posix标准框架上标准化了一套I2C接口框架,这个框架没有init,open和close。只有read,write和control。而这个注册最核心的部分,恰恰是rtt设备框架中的驱动注册入口rt_device_register。

应用获取I2C入口

    既然I2C设备是通过rt_device_register注册的,因此应用端寻找I2C设备,也只需要调用rt_device_find即可,若存在,则返回设备操作句柄,若不存在,则返回null。

       但是呢,实际上,rtt还封装了一层接口。

struct rt_i2c_bus_device *rt_i2c_bus_device_find(const char *bus_name)
{
    struct rt_i2c_bus_device *bus;
    rt_device_t dev = rt_device_find(bus_name);
    if (dev == RT_NULL || dev->type != RT_Device_Class_I2CBUS)
    {
        LOG_E("I2C bus %s not exist", bus_name);

        return RT_NULL;
    }

    bus = (struct rt_i2c_bus_device *)dev->user_data;

    return bus;
}

      从接口实现上看,其实可以看出来了,加了个判断,判断获取到的设备的设备类类型是否是I2C设备

I2C读写函数解析

    由RTT官方的I2C总线设备使用手册可知,I2C读写入口可以分为I2C读写入口,I2C读入口和I2C写入口

I2C 读写入口

rt_ssize_t rt_i2c_transfer(struct rt_i2c_bus_device *bus,
                          struct rt_i2c_msg         msgs[],
                          rt_uint32_t               num)
{
    rt_ssize_t ret;
    rt_err_t err;

    if (bus->ops->master_xfer)
    {
#ifdef RT_I2C_DEBUG
        for (ret = 0; ret < num; ret++)
        {
            LOG_D("msgs[%d] %c, addr=0x%02x, len=%d", ret,
                  (msgs[ret].flags & RT_I2C_RD) ? 'R' : 'W',
                  msgs[ret].addr, msgs[ret].len);
        }
#endif
        err = rt_mutex_take(&bus->lock, RT_WAITING_FOREVER);
        if (err != RT_EOK)
        {
            return (rt_ssize_t)err;
        }
        ret = bus->ops->master_xfer(bus, msgs, num);
        err = rt_mutex_release(&bus->lock);
        if (err != RT_EOK)
        {
            return (rt_ssize_t)err;
        }
        return ret;
    }
    else
    {
        LOG_E("I2C bus operation not supported");
        return -RT_EINVAL;
    }
}

   看到这,我们可以大致知道这些信息,驱动需要实现xfer函数,而xfer函数的作用需要实现读写操作。因此我们拿一份简化驱动实现来看。

static rt_ssize_t i2c_mst_xfer(struct rt_i2c_bus_device *bus,
                                  struct rt_i2c_msg msgs[],
                                  rt_uint32_t num)
{
    rt_size_t i;
    RT_ASSERT(bus != RT_NULL);

    for (i = 0; i < num; i++)
    {
        if (msg[i].flags & RT_I2C_NO_START)
        {
            // 设置是否需要起始位
        }
        if (msg[i].flags & RT_I2C_ADDR_10BIT)
        {
            // 设置成10bit地址工作模式
        }
        else
        {
            // 设置成8bit地址工作模式
        }

        if (msg[i].flags & RT_I2C_RD)
        {
            // 读操作,读失败跳出
        }
        else
        {
            // 写操作,写失败跳出
        }
    }
    return (rt_ssize_t)i;
}

    实际上驱动的实现也是按照此思路实现的,根据flags的标记做对应功能的实现,根据传入的数据量决定读写多少数据。

I2C 写入口

rt_ssize_t rt_i2c_master_send(struct rt_i2c_bus_device *bus,
                             rt_uint16_t               addr,
                             rt_uint16_t               flags,
                             const rt_uint8_t         *buf,
                             rt_uint32_t               count)
{
    rt_ssize_t ret;
    struct rt_i2c_msg msg;

    msg.addr  = addr;
    msg.flags = flags;
    msg.len   = count;
    msg.buf   = (rt_uint8_t *)buf;

    ret = rt_i2c_transfer(bus, &msg, 1);

    return (ret == 1) ? count : ret;
}

   从数据上结构上看,写入口仅仅是多开放了一种实现方式,将struct rt_i2c_msg放在函数内部实现,应用是现实不再需要维护这么个结构体,仅仅需要将读写参数以变量的形式传递至rt_i2c_master_send即可

I2C读入口

rt_ssize_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_ssize_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 == 1) ? count : ret;
}

   读入口,从功能上看,和写入口一致,仅仅是将struct rt_i2c_msg封装,另外,把读标记的操作方式固化下来。

   看完这几个入口,其实我们已经可以看出,RTT的I2C框架,目前仅支持主模式工作的I2C,并不支持I2C从模式。另外,还有一个疑问,I2C设备不是注册了几个标准事件接口(i2c_bus_device_read, i2c_bus_device_write, i2c_bus_device_control)吗,怎么感觉完全没用上?实际上,i2c_bus_device_read 和 i2c_bus_device_write这两个接口,从使用上来说,就是调用rtt文档中暴露的 rt_i2c_master_recv 和 rt_i2c_master_send接口,其实现如下:

static rt_ssize_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;

    RT_ASSERT(bus != RT_NULL);
    RT_ASSERT(buffer != RT_NULL);

    LOG_D("I2C bus dev [%s] reading %u bytes.", dev->parent.name, count);

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

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

static rt_ssize_t i2c_bus_device_write(rt_device_t dev,
                                      rt_off_t    pos,
                                      const 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;

    RT_ASSERT(bus != RT_NULL);
    RT_ASSERT(buffer != RT_NULL);

    LOG_D("I2C bus dev [%s] writing %u bytes.", dev->parent.name, count);

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

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

I2C控制接口

   至于i2c_bus_device_control接口,我们可以通过代码层级来看

rt_err_t rt_i2c_control(struct rt_i2c_bus_device *bus,
                        int                       cmd,
                        void                      *args)
{
    rt_err_t ret;

    if(bus->ops->i2c_bus_control)
    {
        ret = bus->ops->i2c_bus_control(bus, cmd, args);
        return ret;
    }
    else
    {
        LOG_E("I2C bus operation not supported");
        return -RT_EINVAL;
    }
}

static rt_err_t i2c_bus_device_control(rt_device_t dev,
                                       int         cmd,
                                       void       *args)
{
    rt_err_t ret;
    struct rt_i2c_priv_data *priv_data;
    struct rt_i2c_bus_device *bus = (struct rt_i2c_bus_device *)dev->user_data;

    RT_ASSERT(bus != RT_NULL);

    switch (cmd)
    {
    /* set 10-bit addr mode */
    case RT_I2C_DEV_CTRL_10BIT:
        bus->flags |= RT_I2C_ADDR_10BIT;
        break;
    case RT_I2C_DEV_CTRL_TIMEOUT:
        bus->timeout = *(rt_uint32_t *)args;
        break;
    case RT_I2C_DEV_CTRL_RW:
        priv_data = (struct rt_i2c_priv_data *)args;
        ret = rt_i2c_transfer(bus, priv_data->msgs, priv_data->number);
        if (ret < 0)
        {
            return -RT_EIO;
        }
        break;
    default:
        return rt_i2c_control(bus, cmd, args);
    }

    return RT_EOK;
}

    可以看出,在i2c_dev.c这一层,标准化了I2C常用格式的控制逻辑,并将这部分数据在读写I2C时传递至驱动实现。另外,该接口也预留了驱动自定义实现接口的部分,方便各家在实际应用中自定义一些内部参数控制入口。

总结

   至此,我们基本上了理清了RTT的硬件I2C框架结构,其设计上的分层逻辑如下:

2.jpg

对于用于应用来说,最好的使用方式是直接调用设备驱动暴露的接口或者是i2s_dev.c中暴露的接口。而对于板载驱动来说,最好的方式是在驱动中调用i2c_core.c中暴露的接口来实现功能。这样便可实现代码的逻辑分层,便于后期代码迭代维护。





关键词: rtthread     框架     I2C     设备    

专家
2024-09-19 08:04:37     打赏
2楼

厉害啊



专家
2024-09-19 11:05:42     打赏
3楼

在RTT中加载某些外设驱动,我看到有好多文章说是利用应用工具自动设置的。楼主这种分析,或帮助理解驱动的处理过程,谢谢分享!


院士
2024-09-19 14:12:59     打赏
4楼

话说I2C不是要使用中断的方式来实现吗?

这个要怎么处理等待时间啊


高工
2024-09-19 20:32:42     打赏
5楼

感谢分享


专家
2024-09-19 21:37:42     打赏
6楼

感谢分享


共6条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]