起因
本文的重心為講解如何為一款芯片移植和實現 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