起因

本文的重心为讲解如何为一款芯片移植和实现 micropython 的通用组件,但会顺带解释不同芯片的工作方式和特性。

国际惯例,先有起因,再谈问题的解决,所以记得上次总结的 关于 K210 MaixPy 的 I2C 读取设备,搜索不到设备,通信失败的一些原因以及解决方案。

而这次终于出现了两个 I2C 从机扫不到的情况,分别是 MLX90640 和 tcs34725 传感器。

可能の问题分析

我们需要注意一个事实就是,无论是在 STM32 / ESP32 / K210 时期都会发生的事情,只要 I2C 主机和从机设计的上拉电阻不合理,经常会出现从机上拉能力不足导致无法向主机应答,虽然说,不应该让软件向硬件妥协,但事实就是,硬件做好了,在不改变电路走线的情况下克服这个问题,也是软件应该做的。(毕竟是硬件)

我们做一下简单分析,如 I2C 扫不到地址,如 I2C 配置后无法连接,关于扫不到地址,我们可以知道 Scan I2C 地址的方法可以为 主机 发生 从机地址 后等待从机 hold 住 SDA 此时主机 read SDA 被拉起 可知 从机做出了 ACK 应答,表示该地址上存在从机,关于这个流程和描述详细可以看看国产芯片对 I2C 主从实现的流程描述,这里我推荐 GD32 / STM32 的中文编程手册,对小白比较友好。

在 MaixPy 中 硬 I2C 使用的是 read 地址查找,软 I2C 则为 write 后 read 。

不过问题往往并非一个 scan 不到的问题,如在初次上电工作正常,配置了 I2C 后就再也得不到数据了。关于这个问题,我做一个简单的示意图,主要原因也和 I2C 的信号衰减,还有从机上拉能力有关,还有从机传感器自身的问题。

如果从硬件上看,这种情况可能是从机开始工作后的与主机的通路上的电平开始衰减,在主机在发送或接收数据的时候,要么上拉能力不足以到达主机与从机识别的电平,要么到达的时间太慢,主从机没能接收到彼此的应答,此时就会出现主从机接收不到数据超时的情况,而关于在 K210 的问题我们在前一次的事件上也给出了解答,所以这次将通过 GPIO 实现的软 I2C 将克服这个问题,关于 GPIO 的内部实现且不讨论,本文将重点介绍软件逻辑的实现过程。

可以如何实现 MaixPy 的 I2C 功能(MicroPython)。

通常来说,实现一个软 I2C 不难,但如何为 MicroPython 实现该功能,并且不影响原有功能,共存使用,所以我们先构建一个 MicroPython 的标准 I2C 示例代码作为参考。

from machine import I2C

i2c = I2C(I2C.I2C0, freq=100000, scl=28, sda=29)
devices = i2c.scan()
print(devices) for device in devices:
i2c.writeto(device, b'123')
i2c.readfrom(device, 3)

事实上从 esp8266 / esp32 之后才开始使用了 machine 模块,早期的 stm32 micropytho 用得所谓的 pyb 就像智障,不为其他芯片做考虑,官方也意识到了这个问题,但已经改不过来了,或许可以额外补充该接口的定义后再迭代到统一,但也不是现在了。

我们知道这份 Python 代码就是我们最终要实现的目标,无论硬软 I2C 都应该可以通过这份代码正常工作。

从这里 https://github.com/micropython/micropython/blob/master/extmod/machine_i2c.c 我们可以获取 MicroPython 官方对软实现功能逻辑的抽象模块,我们可以看到关键的 I2C 操作代码如下。


STATIC void mp_hal_i2c_delay(machine_i2c_obj_t *self) {
// We need to use an accurate delay to get acceptable I2C
// speeds (eg 1us should be not much more than 1us).
mp_hal_delay_us_fast(self->us_delay);
} STATIC void mp_hal_i2c_scl_low(machine_i2c_obj_t *self) {
mp_hal_pin_od_low(self->scl);
} STATIC int mp_hal_i2c_scl_release(machine_i2c_obj_t *self) {
uint32_t count = self->us_timeout; mp_hal_pin_od_high(self->scl);
mp_hal_i2c_delay(self);
// For clock stretching, wait for the SCL pin to be released, with timeout.
for (; mp_hal_pin_read(self->scl) == 0 && count; --count) {
mp_hal_delay_us_fast(1);
}
if (count == 0) {
return -MP_ETIMEDOUT;
}
return 0; // success
} STATIC void mp_hal_i2c_sda_low(machine_i2c_obj_t *self) {
mp_hal_pin_od_low(self->sda);
} STATIC void mp_hal_i2c_sda_release(machine_i2c_obj_t *self) {
mp_hal_pin_od_high(self->sda);
} STATIC int mp_hal_i2c_sda_read(machine_i2c_obj_t *self) {
return mp_hal_pin_read(self->sda);
}

也就是说,其他芯片只需要提供如下操作即可将软 I2C 实现,实现后我们再来说说如何硬软功能结合。

  • mp_hal_i2c_delay

    • mp_hal_delay_us_fast
  • mp_hal_i2c_scl_low
    • mp_hal_pin_od_low
  • mp_hal_i2c_scl_release
    • mp_hal_pin_od_high
    • mp_hal_pin_read
  • mp_hal_i2c_sda_low
    • mp_hal_pin_od_low
  • mp_hal_i2c_sda_release
    • mp_hal_pin_od_high
  • mp_hal_i2c_sda_read
    • mp_hal_pin_read

不仅要实现 I2C 的 SCL 和 SDA 的 release 和 low 以及 sda 的 read ,还要实现 GPIO 的 od 开漏的 high 和 low 就可以将其对接到最终的工作流程中,这样你就可以实现了软 I2C 功能,是不是很简单?我相信你也可以的。

MicroPython 软 I2C 移植后出现的问题

我认为移植逻辑是很容易的一件事情,难的反而是要结合硬件的实际情况来判断问题,所以在 K210 MaixPy 上实现 I2C 后就当场去世了,嗯,根本不能用。

最初从机不应答的时候,量测数据后发现输出结果不一样,所以我怀疑 I2C 的逻辑有问题,但经过调试后发现,实际上是 GPIO 的工作机制存在一些误差或者说差异,这里拿一张我很久之前记录下来的图,现在拿这张图出来解释解释。

在 K210 中 I2C 的引脚配置由内部硬件完成,现在单独拿到 GPIO 模拟实现,我们需要注意的就是 GPIO 的配置,通常我们的软件逻辑都是先配置后再设置电平输出,但 K210 的函数封装中存在配置的时候 GPIO 输出会被打开,这就导致了上一次的 GPIO 状态被输出,所以我们的逻辑要改成 先配置电平,再配置输出,否正它会像下图一样出现。

这跟 GPIO 的实现也有很大的关系,但从逻辑上来看,或许 K210 这种才是正确的逻辑,以往的可能是因为内部逻辑设计的比较好,所以在 esp32 上没有存在这种问题。


STATIC void mp_hal_i2c_sda_low(machine_hard_i2c_obj_t *self) {
// mp_hal_pin_od_low(self->pin_sda);
gpiohs_set_pin(self->pin_sda, 0);
gpiohs_set_drive_mode(self->pin_sda, GPIO_DM_OUTPUT);
}

接着遇到的问题是 K210 主机的 SDA GPIO 配置了开漏输出还是会导致从机无法拉低信号做出应答,所以要求主机的 GPIO 在输出后立刻转回输入,这事实上有一些不合常理,可能这就是 K210 吧。


STATIC void mp_hal_i2c_sda_release(machine_hard_i2c_obj_t *self) {
// mp_hal_pin_od_high(self->pin_sda);
gpiohs_set_pin(self->pin_sda, 1);
gpiohs_set_drive_mode(self->pin_sda, GPIO_DM_OUTPUT);
gpiohs_set_drive_mode(self->pin_sda, GPIO_DM_INPUT);
}

至此 K210 的 MaixPy 的软 I2C 就完成拉,相信在知道了这些细节后,在其他芯片的移植上面可以多一些经验和理解。

软 I2C 代码实现参考和关键函数

最后,我们开始整合到 硬 I2C 中,整理的过程很简单,唯独需要注意的是,如何区分定义硬软的工作方式和定义。

关于这个的实现,可以参考这份代码的实现 https://github.com/sipeed/MaixPy/blob/master/components/micropython/port/src/standard_lib/machine/machine_i2c.c

只需要注意两个地方,初始化时设置为 软设备的 标记 MACHINE_I2C_MODE_MASTER_SOFT 。

    if(self->i2c == (i2c_device_number_t)I2C_DEVICE_3
|| self->i2c == (i2c_device_number_t)I2C_DEVICE_4
|| self->i2c == (i2c_device_number_t)I2C_DEVICE_5) {
self->mode = MACHINE_I2C_MODE_MASTER_SOFT;
}

从而让 I2C 的逻辑函数可以判断使用的函数。

STATIC int machine_hard_i2c_writeto(mp_obj_base_t *self_in, uint16_t addr, const uint8_t *src, size_t len, bool stop) {
// mp_obj_base_t *self = (mp_obj_base_t*)MP_OBJ_TO_PTR(self_in);
machine_hard_i2c_obj_t *self = MP_OBJ_TO_PTR(self_in); #if MICROPY_PY_MACHINE_SW_I2C
if (self->mode == MACHINE_I2C_MODE_MASTER_SOFT) {
return mp_machine_i2c_writeto(self, addr, src, len, stop);
}
#endif
//TODO: stop not implement
//TODO: send 0 byte date support( only send start, slave address, and wait ack, stop at last)
int ret = maix_i2c_send_data(self->i2c, addr, src, len, 20);
if(ret != 0)
ret = -EIO;
else
ret = len;//TODO: get sent length actually
return ret;
}

不过我实现的手段是让软设备的操作截断后续的逻辑,这个看个人的实现心情决定。

关于 MicroPython 的软设备的实现函数只需要注意关键的回调函数,如:

STATIC const mp_machine_i2c_p_t machine_hard_i2c_p = {
.readfrom = machine_hard_i2c_readfrom,
.writeto = machine_hard_i2c_writeto,
};

本来按预期来说,正确的实现手段是传递该结构体所需要的接口进去完成正确的接口替换,但我没有这么做,所以只提及到这里。

最后的问题总结和后续问题的改善

实现 I2C 的过程中,保证了接口一致,并在 MaixPy 中测试为 50khz 的时钟频率,同时解决了 I2C 通信不稳定以及通信不到的情况,但我们需要注意的是这绝非上策,只是因为 GPIO 的驱动能力强,能够保证信号的及时罢了,如果可以,还是要多多检讨硬件的设计,这样才能真正解决这个问题。

在这不久后我实现了 SPI ,然后发现定义方面出了一些问题,我在 I2C 的时候惯性思维设计为 I2C3+ 以后的设备资源,但事实上只需要将其定义为 I2C_SOFT 即可,根本不需要在意有几个设备资源,因为它只与 pin 脚有关,这或许也是先入为主导致的问题吧,所以今后在实现接口的时候,要多参考其他人的接口设计来实现。

那么,快快试试吧!

就酱紫!

2020年10月1日 junhuanchen

为 MaixPy 加入软 I2C 接口(移植 MicroPython 的 I2C)的更多相关文章

  1. (6)s3c2440用I2C接口访问EEPROM

    在前面阅读理解了I2C的官方协议文档后,就拿s3c2440和EEPROM来验证一下. 本来是想用s3c2440的SDA和SCL管脚复用为GPIO来模拟的,但在没有示波器的情况下搞了一周,怎么都出不来, ...

  2. 解决STM32 I2C接口死锁在BUSY状态的方法讨论

    关于STM32的I2C接口死锁在BUSY状态无法恢复的现象,网上已有很多讨论,看早几年比较老的贴子,有人提到复位MCU也无法恢复.只有断电才行的状况,那可是相当严重的问题.类似复位也无法恢复的情况是存 ...

  3. JZ2440 裸机驱动 第12章 I2C接口

    本章目标: 了解I2C总线协议: 掌握S3C2410/S3C2440中I2C接口的使用方法: 12.1 I2C总线协议及硬件介绍 12.1.1 I2C总线协议 1 I2C总线的概念 2 I2C总线的信 ...

  4. 树莓派配置RTC时钟(DS3231,I2C接口)

    1.购买基于DS3231的RTC时钟模块,并且支持3.3V的那种 2.配置树莓派 a.打开树莓派的i2c接口 sudo raspi-config -->Interfacing Options - ...

  5. LWIP network interface 即 LWIP 的 硬件 数据 接口 移植 首先 详解 STM32 以太网数据 到达 的第一站: ETH DMA 中断函数

    要 运行  LWIP  不光 要实现  OS  的 一些 接口  ,还要 有 硬件 数据 接口 移植 ,即 网线上 来的 数据 怎么个形式 传递给  LWIP ,去解析 做出相应的 应答  ,2017 ...

  6. linux 标准i2c接口(一)

    一:I2C设备操作方式: 1.  应用程序操作法:i2c的设备的驱动可以直接利用linux内核提供的i2c-dev.c文件提供的ioctl函数接口在应用层实现对i2c设备的读写,但是在应用层使用ioc ...

  7. EEPROM的操作---SPI接口和I2C接口

    参考:http://blog.csdn.net/yuanlulu/article/details/6163106 ROM最初不能编程,出厂什么内容就永远什么内容,不灵活.后来出现了PROM,可以自己写 ...

  8. i2c接口笔记

    一. i2c基础知识 1. NACK信号:当在第9个时钟脉冲的时候SDA线保持高电平,就被定义为NACK信号.Master要么产生STOP条件来放弃这次传输,或者重复START条件来发起一个新的开始. ...

  9. I2C总线和S5PV210的I2C总线控制器

    一.什么是I2C通信协议? 1.物理接口:SCL + SDA (1)SCL(serial clock):时钟线,传输CLK信号,一般是I2C主设备向从设备提供时钟的通道. (2)SDA(serial ...

随机推荐

  1. webpack(从上篇博客中拿出来的)

    插件配置: emmet: vscode内置了这个,但是没有开启,要在设置里面"emmet.triggerExpansionOnTab": true, vscoed-icons插件: ...

  2. Autoit 使用

    一.Autoit 上传文件. 1.常用语法 - WinActivate("title")         聚焦到指定活动窗口 - ControlFocus ( "titl ...

  3. idea如何安装插件

    原文地址:https://jingyan.baidu.com/article/215817f742a61c1eda142329.html 1.首先打开idea界面,然后 按住快捷键ctrl+shift ...

  4. Labview学习之路(十三)数组函数

    本文章介绍一下函数选版中的数组函数,一眼看懂没什么特殊地方的就不说了 (一)数组大小 返回I32类型的数或者数组. 什么是I32类型?就是32位有符号整型, 有符号什么意思?就是在32位数据中最高做符 ...

  5. Pandoanload涅槃重生,小白羊重出江湖?

    Pandoanload涅槃重生,小白羊重出江湖? 科技是把双刃剑,一方面能够砸烂愚昧和落后,另一方面也可能带给人类无尽的灾难. 原子物理理论的发展是的人类掌握了核能技术但是也带来了广岛和长崎的核灾难, ...

  6. python3之range()

    python range() 函数可创建一个整数列表,一般用在 for 循环中. 函数语法(左闭右开) Python3 range() 函数返回的是一个可迭代对象(类型是对象),而不是列表类型, 所以 ...

  7. 20190923-04Linux用户管理命令 000 012

     useradd 添加新用户 1.基本语法 useradd 用户名 (功能描述:添加新用户) useradd -g 组名 用户名 (功能描述:添加新用户到某个组) 2.案例实操 (1)添加一个用户 [ ...

  8. leetcode刷题-94二叉树的中序遍历

    题目 给定一个二叉树,返回它的中序 遍历. 实现 # def __init__(self, x): # self.val = x # self.left = None # self.right = N ...

  9. SpringBoot打Jar包在命令行运行

    首先写一个测试文件 然后点击IDEA右侧的maven,然后选择package,之后点击上面运行或者直接双击即可, 等下方控制台构建成功即可: 然后找到项目目录下target下即可看到打的jar包 然后 ...

  10. JavaScript五中迭代方法小解

    ECMAScript 为数组定义了五个迭代方法.每个方法都接收两个参数:要在每一项上运行的函数和(可选的)运行该函数的作用域对象——影响this的值.传入这些方法中的函数会接收三个参数:数组项的值.该 ...