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协议操作函数 | 描述 |
---|---|---|
init | init | |
scan | transfer | 直接调用mp_machine_i2c_writeto() |
start | start | |
stop | stop | |
readinto | read | |
machine_i2c_write | write | |
machine_i2c_readfrom | transfer | 直接调用mp_machine_i2c_readfrom |
machine_i2c_readfrom_into | transfer | 直接调用mp_machine_i2c_readfrom |
machine_i2c_writeto | transfer | 直接调用mp_machine_i2c_writeto |
machine_i2c_writevto | transfer | |
readfrom_mem | transfer | 通过read_mem调用了mp_machine_i2c_writeto、mp_machine_i2c_readfrom |
readfrom_mem_into | transfer | 通过read_mem调用了mp_machine_i2c_writeto、mp_machine_i2c_readfrom |
writeto_mem | transfer | 通过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()。