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

苏勇,2022年3月

文章目录

  • MicroPython中I2C模块的设计与实现(1) - machine_i2c框架的机制
    • Introduction
    • Algorithm
    • Implementation
      • init()
      • transfer_single()
    • Conclusion

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 bufferlen = bufs[0].len;buf = bufs[0].buf;} else {// Combine buffers into a single onelen = 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 oneslen = 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 transactionint ret = mp_hal_i2c_start(self);if (ret != 0) {return ret;}// write the slave addressret = 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 cleanlymp_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 slavewhile (len--) {ret = mp_hal_i2c_write_byte(self, *buf++);if (ret < 0) {return ret;} else if (ret != 0) {// nack received, stop sendingn = 0;break;}++transfer_ret; // count the number of acks}}}// finish the I2C transactionif (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()。

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

  1. 页面中查询模块的设计与实现思路

    页面中查询模块的设计与实现思路 一 智能查询 1.1 界面设计 1.2 技术实现 二 万能查询 2.1 界面设计 2.2 技术实现 三 便捷查询 3.1 界面设计 3.2 技术实现 总结   在做一个 ...

  2. 通过mem函数在MicroPython中访问模块寄存器

    简 介: 通过mem函数直接访问MCU内部的寄存器,可以完成一些在原来的MicroPython中内核没有实现的模块.通过测试可以看到,通过mem访问GPIO并没有明显增加访问的速度.使用mem访问CR ...

  3. IM系统中聊天记录模块的设计与实现

    看到很多开发IM系统的朋友都想实现聊天记录存储和查询这一不可或缺的功能,这里我就把自己前段时间为傲瑞通(OrayTalk)开发聊天记录模块的经验分享出来,供需要的朋友参考下. 一.总体设计 1.存储位 ...

  4. IPTV系统中EPG模块的设计与实现

    1.引 言 IPTV即网络电视,是目前一种新兴的网络应用,它利用宽带互联网的基础设施,以家用电视机作为主要终端,通过互联网协议(IP)来提供包括电视节目在内的多种数字媒体服务及其增值业务的技术.IPT ...

  5. 利用mem数组完成MM32 的 MicroPython中UART1的(REPL)的交互

    简 介: 利用了UART中的CSR的RXVAL标志位,可以判断有新的byte从REPL中获得,通过查询该标志位,可以实现通过REPL(UART1)上位机发送的信息.进而可以提高软件调试效率.在程序中添 ...

  6. 利用mem数组在MM32 MicroPython中实现COMP的功能

    简 介: 通过MicroPython中的数组mem对于MM32F3277内部的模拟比较器进行初步测试,验证了它的最基本的设置和工作关系.测试过程中也发现了MM32F3277的数据手册与对应的内部功能方 ...

  7. 电子商务(电销)平台中用户模块(User)数据库设计明细

    原文:电子商务(电销)平台中用户模块(User)数据库设计明细 以下是自己在电子商务系统设计中的订单模块的数据库设计经验总结,而今发表出来一起分享,如有不当,欢迎跟帖讨论~ 用户基础表(user_ba ...

  8. 【论文写作】客户端设计与实现中各模块设计如何写

    4 FTP软件详细设计与实现 4.1软件总体分析与设计 根据需求分析,按照系统开发的基本观点对功能进行分解,从功能上可对模块作如下划分: 1.连接管理模块:主要完成主机与服务器之间的连接与关闭操作. ...

  9. 电子商务(电销)平台中订单模块(Order)数据库设计明细

    电子商务(电销)平台中订单模块(Order)数据库设计明细 以下是自己在电子商务系统设计中的订单模块的数据库设计经验总结,而今发表出来一起分享,如有不当,欢迎跟帖讨论~ 订单表 (order) |-- ...

最新文章

  1. 1016 Phone Bills
  2. 简单使用DESeq2/EdgeR做差异分析
  3. 树的先序遍历的栈实现
  4. SharePoint2013开发环境搭建(完整版:图文并茂)
  5. java ReentrantLock 使用
  6. mmap函数_Linux内存映射mmap原理分析
  7. 实现后台高级查询(基础版)
  8. c语言学习-在一个三行三列的矩阵中求出数值最大的元素及其行/列下标并打印输出
  9. Linux学习总结(41)——运维不仅仅是Linux
  10. CMake test目录和项目同名错误
  11. ztree 指定节点清空_zTree节点文字过多的处理方法
  12. java使用DES加密方式,实现对数据的加密解密
  13. mysql root远程访问权限_解决Navicat连接MySQL数据库报错问题
  14. 运行JProfiler:ERROR: Invalid license key. Aborting
  15. python: pandas 、dataframe 与hdf5
  16. Chromium OS Developer Guide
  17. .net core | donet core IIS 文件路径问题
  18. 目标文件(.obj)的COFF文件结构
  19. Mybatis入门学习
  20. bootable_noemulation.img linux,Syslinux使用

热门文章

  1. C++ Programming Basic acknowledge
  2. babel-预设和插件
  3. 几种抽奖方式之轮盘抽奖
  4. 此beta版已额满_日志MIUI 11 第439周开发版内测日志补充
  5. Linux 磁盘管理(RAID)--第五章
  6. 题目 1904: 蓝桥杯算法提高VIP-求arccos值
  7. Tensorflow2训练Fer2013数据集
  8. matlab中在同一图形窗口中绘制出一个周期内的正弦曲线和余弦曲线,多选(3分) 在一个图形窗口同时绘制[0,2π]的正弦曲线、余弦曲线,可以使用命令( )。...
  9. 云原生时代,OAM模型加持下的应用交付与管理实践
  10. 写英文论文的一些心得