MicroPython中I2C模块的设计与实现(1) - machine_i2c框架的机制

本文介绍MicroPython中I2C模块machine_i2c的设计与实现,重点分析了machine_i2c的框架机制及其实现细节,包括init()和transfer_single()等关键函数的作用和移植方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

MicroPython中I2C模块的设计与实现(1) - machine_i2c框架的机制

苏勇,2022年3月

Introduction

MicroPython在extmod目录下提供了machine_i2c的实现框架,并附带了一个GPIO模拟I2C的实现实例SoftI2C。在本文中,将具体分析machine_i2c的实现框架,以期得到移植machine_i2c的实践方法。在阅读代码的过程中,将专注于machine_i2c的框架,但仍借助于SoftI2C实现的接口描述machine_i2c在具体平台上移植的工作。在后续的文章中,将SoftI2C作为machine_i2c的一个具体实例,与硬件I2C等同,分析SoftI2C的实现,并补完machine_i2c.c文件中需要适配具体硬件平台的部分移植代码。

Algorithm

快速浏览了extmod/machine_i2c.c文件,700多行的代码的源码文件确实比较大。跳过mp_hal_i2c_xxx()和mp_machine_soft_i2c_transfer()函数的部分代码,开始看mp_machine_i2c_xxx()machine_i2c_xxx()系列函数,这部分内容将构成machine_i2c类模块的实现框架。

根据之前分析和设计MicroPython类模块的经验,这里先从实例化类模块的部分代码入手。以machine_i2c.c文件中的SoftI2C为例:

STATIC const mp_rom_map_elem_t machine_i2c_locals_dict_table[] = {
    { MP_ROM_QSTR(MP_QSTR_init), MP_ROM_PTR(&machine_i2c_init_obj) },
    { MP_ROM_QSTR(MP_QSTR_scan), MP_ROM_PTR(&machine_i2c_scan_obj) },

    // primitive I2C operations
    { MP_ROM_QSTR(MP_QSTR_start), MP_ROM_PTR(&machine_i2c_start_obj) },
    { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&machine_i2c_stop_obj) },
    { MP_ROM_QSTR(MP_QSTR_readinto), MP_ROM_PTR(&machine_i2c_readinto_obj) },
    { MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&machine_i2c_write_obj) },

    // standard bus operations
    { MP_ROM_QSTR(MP_QSTR_readfrom), MP_ROM_PTR(&machine_i2c_readfrom_obj) },
    { MP_ROM_QSTR(MP_QSTR_readfrom_into), MP_ROM_PTR(&machine_i2c_readfrom_into_obj) },
    { MP_ROM_QSTR(MP_QSTR_writeto), MP_ROM_PTR(&machine_i2c_writeto_obj) },
    { MP_ROM_QSTR(MP_QSTR_writevto), MP_ROM_PTR(&machine_i2c_writevto_obj) },

    // memory operations
    { MP_ROM_QSTR(MP_QSTR_readfrom_mem), MP_ROM_PTR(&machine_i2c_readfrom_mem_obj) },
    { MP_ROM_QSTR(MP_QSTR_readfrom_mem_into), MP_ROM_PTR(&machine_i2c_readfrom_mem_into_obj) },
    { MP_ROM_QSTR(MP_QSTR_writeto_mem), MP_ROM_PTR(&machine_i2c_writeto_mem_obj) },
};
MP_DEFINE_CONST_DICT(mp_machine_i2c_locals_dict, machine_i2c_locals_dict_table);

STATIC const mp_machine_i2c_p_t mp_machine_soft_i2c_p = {
    .init = mp_machine_soft_i2c_init,
    .start = (int (*)(mp_obj_base_t *))mp_hal_i2c_start,
    .stop = (int (*)(mp_obj_base_t *))mp_hal_i2c_stop,
    .read = mp_machine_soft_i2c_read,
    .write = mp_machine_soft_i2c_write,
    .transfer = mp_machine_soft_i2c_transfer,
};

const mp_obj_type_t mp_machine_soft_i2c_type = {
    { &mp_type_type },
    .name = MP_QSTR_SoftI2C,
    .print = mp_machine_soft_i2c_print,
    .make_new = mp_machine_soft_i2c_make_new,
    .protocol = &mp_machine_soft_i2c_p,
    .locals_dict = (mp_obj_dict_t *)&mp_machine_i2c_locals_dict,
};

这里mp_machine_soft_i2c_type即定义了一个SoftI2C模块的类型,其中.name中指定了SoftI2C作为这个新模块的名字,.print对应打印类对象实例时将要调用的函数,.make_new对应实例化对象时调用的函数,.protocol指定了一组类模块内部定义的一堆函数,.locals_dict中指定该模块的属性关键字和属性方法。除了protocol,其余的字段都是定义一个新类常用的、一般的方法。protocol是专属于machine_i2c类的。在extmod/machine_i2c.h文件中可以找到mp_machine_i2c_p_t类型的定义:

// I2C protocol
// - init must be non-NULL
// - start/stop/read/write can be NULL, meaning operation is not supported
// - transfer must be non-NULL
// - transfer_single only needs to be set if transfer=mp_machine_i2c_transfer_adaptor
typedef struct _mp_machine_i2c_p_t {
    void (*init)(mp_obj_base_t *obj, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args);
    int (*start)(mp_obj_base_t *obj);
    int (*stop)(mp_obj_base_t *obj);
    int (*read)(mp_obj_base_t *obj, uint8_t *dest, size_t len, bool nack);
    int (*write)(mp_obj_base_t *obj, const uint8_t *src, size_t len);
    int (*transfer)(mp_obj_base_t *obj, uint16_t addr, size_t n, mp_machine_i2c_buf_t *bufs, unsigned int flags);
    int (*transfer_single)(mp_obj_base_t *obj, uint16_t addr, size_t len, uint8_t *buf, unsigned int flags);
} mp_machine_i2c_p_t;

这里定义的是实现硬件I2C通信协议的函数,init、start、stop、read、write等函数都好理解,但是transfer和transfer_single是做什么的?根据代码注释中的说明:init函数是必须的实现的,start/stop/read/write是可选实现的,如果不实现,只是对应的类方法不再支持而已,transfer函数是必须实现的,但machine_i2c中提供了一种实现的范例,可以使用mp_machine_i2c_transfer_adaptor对接transfer。当使用mp_machine_i2c_transfer_adaptor对接transfer时,就需要再实现另一个跟transfer函数相关的函数接口transfer_single。从接口上可以看出,transfer接口函数中,使用了size_t n, mp_machine_i2c_buf_t * bufs作为参数,这个传参不够直观,但transfer_single函数接口看起来就比较正常了。实际上,在machine_i2c.c文件中实现的mp_machine_i2c_transfer_adaptor,就是将transfer_single接口中的函数打了包,重新封装以适配transfer接口。

// Generic helper functions

// For use by ports that require a single buffer of data for a read/write transfer
int mp_machine_i2c_transfer_adaptor(mp_obj_base_t *self, uint16_t addr, size_t n, mp_machine_i2c_buf_t *bufs, unsigned int flags) {
    size_t len;
    uint8_t *buf;
    if (n == 1) {
        // Use given single buffer
        len = bufs[0].len;
        buf = bufs[0].buf;
    } else {
        // Combine buffers into a single one
        len = 0;
        for (size_t i = 0; i < n; ++i) {
            len += bufs[i].len;
        }
        buf = m_new(uint8_t, len);
        if (!(flags & MP_MACHINE_I2C_FLAG_READ)) {
            len = 0;
            for (size_t i = 0; i < n; ++i) {
                memcpy(buf + len, bufs[i].buf, bufs[i].len);
                len += bufs[i].len;
            }
        }
    }

    mp_machine_i2c_p_t *i2c_p = (mp_machine_i2c_p_t *)self->type->protocol;
    int ret = i2c_p->transfer_single(self, addr, len, buf, flags);

    if (n > 1) {
        if (flags & MP_MACHINE_I2C_FLAG_READ) {
            // Copy data from single buffer to individual ones
            len = 0;
            for (size_t i = 0; i < n; ++i) {
                memcpy(bufs[i].buf, buf + len, bufs[i].len);
                len += bufs[i].len;
            }
        }
        m_del(uint8_t, buf, len);
    }

    return ret;
}

从代码中可以看出,mp_machine_i2c_transfer_adaptor()函数是将多个缓冲区合并成一个,然后调用transfer_single函数实际执行I2C的通信过程。若是指定读操作,则还需要将收到的数拆分到原来的多个缓冲区中。machine_i2c中还将transfer函数进一步打包成readfrom和writeto函数:

STATIC int mp_machine_i2c_readfrom(mp_obj_base_t *self, uint16_t addr, uint8_t *dest, size_t len, bool stop) {
    mp_machine_i2c_p_t *i2c_p = (mp_machine_i2c_p_t *)self->type->protocol;
    mp_machine_i2c_buf_t buf = {.len = len, .buf = dest};
    unsigned int flags = MP_MACHINE_I2C_FLAG_READ | (stop ? MP_MACHINE_I2C_FLAG_STOP : 0);
    return i2c_p->transfer(self, addr, 1, &buf, flags);
}

STATIC int mp_machine_i2c_writeto(mp_obj_base_t *self, uint16_t addr, const uint8_t *src, size_t len, bool stop) {
    mp_machine_i2c_p_t *i2c_p = (mp_machine_i2c_p_t *)self->type->protocol;
    mp_machine_i2c_buf_t buf = {.len = len, .buf = (uint8_t *)src};
    unsigned int flags = stop ? MP_MACHINE_I2C_FLAG_STOP : 0;
    return i2c_p->transfer(self, addr, 1, &buf, flags);
}

至于mp_machine_i2c_locals_dict中定义的其它属性方法的实现,大多是调用了protocol中的与硬件相关的函数,总结如下表所示。

属性方法protocol中的I2C协议操作函数描述
initinit
scantransfer直接调用mp_machine_i2c_writeto()
startstart
stopstop
readintoread
machine_i2c_writewrite
machine_i2c_readfromtransfer直接调用mp_machine_i2c_readfrom
machine_i2c_readfrom_intotransfer直接调用mp_machine_i2c_readfrom
machine_i2c_writetotransfer直接调用mp_machine_i2c_writeto
machine_i2c_writevtotransfer
readfrom_memtransfer通过read_mem调用了mp_machine_i2c_writeto、mp_machine_i2c_readfrom
readfrom_mem_intotransfer通过read_mem调用了mp_machine_i2c_writeto、mp_machine_i2c_readfrom
writeto_memtransfer通过write_mem直接调用了transfer

对于在注释中说明为“primitive I2C operations”的函数,有start、stop、readinto、write,以start()的实现为例:

STATIC mp_obj_t machine_i2c_start(mp_obj_t self_in) {
    mp_obj_base_t *self = (mp_obj_base_t *)MP_OBJ_TO_PTR(self_in);
    mp_machine_i2c_p_t *i2c_p = (mp_machine_i2c_p_t *)self->type->protocol;
    if (i2c_p->start == NULL) {
        mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("I2C operation not supported"));
    }
    int ret = i2c_p->start(self);
    if (ret != 0) {
        mp_raise_OSError(-ret);
    }
    return mp_const_none;
}
MP_DEFINE_CONST_FUN_OBJ_1(machine_i2c_start_obj, machine_i2c_start);

从代码中可以看到,start()函数是一个可以不提供的函数,当未指定有效的start()的函数时,可以声明异常并提示“I2C operation not supported”。若是指定了有效的start()函数,就调用装入的函数。

这样看下来,还是transfer_single最接地气。

Implementation

通过上文的分析,若实现machine_i2c模块,就需要提供与底层硬件相关的关键函数,只有init和transfer_signle。本节继续通过阅读代码,以SoftI2C为例,追溯这两个与移植紧密相关的函数,详细分析它们对应需要的传入传出参数和执行行为,以便在具体的移植中对应试它们。

init()

在mp_machine_soft_i2c_p中,接入init函数的是mp_machine_soft_i2c_init,这个函数在对接make_new的mp_machine_soft_i2c_make_new() 中也被调用。machine_i2c.c文件中mp_machine_soft_i2c_init() 函数的实现代码如下:

STATIC void mp_machine_soft_i2c_init(mp_obj_base_t *self_in, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
    enum { ARG_scl, ARG_sda, ARG_freq, ARG_timeout };
    static const mp_arg_t allowed_args[] = {
        { MP_QSTR_scl, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} },
        { MP_QSTR_sda, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} },
        { MP_QSTR_freq, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 400000} },
        { MP_QSTR_timeout, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 255} },
    };

    mp_machine_soft_i2c_obj_t *self = (mp_machine_soft_i2c_obj_t *)self_in;
    mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
    mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);

    self->scl = mp_hal_get_pin_obj(args[ARG_scl].u_obj);
    self->sda = mp_hal_get_pin_obj(args[ARG_sda].u_obj);
    self->us_timeout = args[ARG_timeout].u_int;
    mp_hal_i2c_init(self, args[ARG_freq].u_int);
}

从代码中可以看出,init() 函数接收参数列表包含scl和sda引脚的对象,还有通信频率freq和超时等待时间两个配置参数,函数中对传入参数进行解析后,将解析出的freq频率传入mp_hal_i2c_init()完成对同硬件相关的初始化配置工作。

transfer_single()

好吧,extmod/machine_i2c.c 文件中自带的SoftI2C的实现没有直接包含transfer_single() 的实现样例,但可以通过mp_machine_i2c_transfer_adaptor()函数,从transfer函数反推transfer_single()的参数清单和执行行为。当然,还有更简单的做法,就是从现有的其它平台的移植中查阅这个函数的实现。但此处我仍通过SoftI2C的transfer函数反推,因为通过GPIO模拟的I2C行为更加直观,而现有平台对接硬件I2C的驱动行为,同具体硬件I2C外设绑定,如果不看芯片手册,还是搞不清楚实际是做了什么。

transfer函数同transfer_single相比,只是表述缓冲区的方式不同而已,至于addr、flags等参数,都是一样的。

// return value:
//  >=0 - success; for read it's 0, for write it's number of acks received
//   <0 - error, with errno being the negative of the return value
int mp_machine_soft_i2c_transfer(mp_obj_base_t *self_in, uint16_t addr, size_t n, mp_machine_i2c_buf_t *bufs, unsigned int flags) {
    machine_i2c_obj_t *self = (machine_i2c_obj_t *)self_in;

    // start the I2C transaction
    int ret = mp_hal_i2c_start(self);
    if (ret != 0) {
        return ret;
    }

    // write the slave address
    ret = mp_hal_i2c_write_byte(self, (addr << 1) | (flags & MP_MACHINE_I2C_FLAG_READ));
    if (ret < 0) {
        return ret;
    } else if (ret != 0) {
        // nack received, release the bus cleanly
        mp_hal_i2c_stop(self);
        return -MP_ENODEV;
    }

    int transfer_ret = 0;
    for (; n--; ++bufs) {
        size_t len = bufs->len;
        uint8_t *buf = bufs->buf;
        if (flags & MP_MACHINE_I2C_FLAG_READ) {
            // read bytes from the slave into the given buffer(s)
            while (len--) {
                ret = mp_hal_i2c_read_byte(self, buf++, (n | len) == 0);
                if (ret != 0) {
                    return ret;
                }
            }
        } else {
            // write bytes from the given buffer(s) to the slave
            while (len--) {
                ret = mp_hal_i2c_write_byte(self, *buf++);
                if (ret < 0) {
                    return ret;
                } else if (ret != 0) {
                    // nack received, stop sending
                    n = 0;
                    break;
                }
                ++transfer_ret; // count the number of acks
            }
        }
    }

    // finish the I2C transaction
    if (flags & MP_MACHINE_I2C_FLAG_STOP) {
        ret = mp_hal_i2c_stop(self);
        if (ret != 0) {
            return ret;
        }
    }

    return transfer_ret;
}

这里要注意i2c驱动程序的写法,transfer函数在某种程度上实现了i2c通信总线的物理层通信帧,即纯粹的发数和收数,不是协议层面上的写通信过程和读通信过程,每次发送和接收,都是以start开始,stop结尾。当然,对于读通信过程,可能有先写再读的过程,并且前面的读过程不能发送stop,这些都可以通过flag控制。

在machine_i2c.h中,定义了flags的可选项目:

  • MP_MACHINE_I2C_FLAG_READ /* by default, write. */
  • MP_MACHINE_I2C_FLAG_STOP

Conclusion

对于machine_i2c模块的移植,主要搞定init()和transfer_single()。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值