[micropython k210] 为 MaixPy 加入软 SPI 接口(移植 MicroPython 的 SPI)
前言
接着上一篇 为 MaixPy 加入软 I2C 接口(移植 MicroPython 的 I2C) 的内容,如果不知道也没关系,我主要也是留一些足迹,方便后来人在其他芯片上按图索骥。
如果你已经看过前一篇的软 I2C 的内容的话,那么我这篇 SPI 只是留做补充章节罢了。
嗯,这次的起因是因为 K210 的 SPI 资源全部用完了,总共 4 个 SPI 硬件资源 spi0 给了 lcd st7789 ,spi1 给了 Sdcard SPI , spi3 给了 Flash w25qxx ,剩下的 spi2 只能做从机(slave mode),如果 MaixPy(MicroPython) 之上想要使用 SPI 去向一些传感器模块(flash、psram、rfid、lora、BME280、scl3300、epaper、lcd)通信就有些捉襟见肘,如在 MaixPy 上莫名其妙的读取不到 SD 卡文件的原因是因为 SPI1 被修改定义了。。
现在 amigo 硬件上存在两个 SPI 口,一个可以留给 wifi 的 spi 或 uart 做网卡用途,另一个则必须提供 spi 功能,而使用方法如下代码。
from machine import SPI
import utime
from Maix import GPIO
from fpioa_manager import fm
fm.register(20, fm.fpioa.GPIO5, force=True)
cs = GPIO(GPIO.GPIO5, GPIO.OUT)
#cs.value(0)
#utime.sleep_ms(2000)
print(os.listdir())
spi = SPI(SPI.SPI1, mode=SPI.MODE_MASTER, baudrate=400*1000, polarity=0, phase=0, bits=8, firstbit=SPI.MSB,
sck=21, mosi=8, miso=15)#使用程序配置了 cs0 则无法读取 W25QXX
print(os.listdir())
print(spi)
while True:
fm.register(21, fm.fpioa.SPI1_SCLK, force=True)
fm.register(8, fm.fpioa.SPI1_D0, force=True)
fm.register(15, fm.fpioa.SPI1_D1, force=True)
cs.value(0)
write_data = bytearray([0x90, 0x00, 0x00, 0x00])
spi.write(write_data)
id_buf = bytearray(2)
spi.readinto(id_buf, write=0xff)
print(id_buf)
print(time.ticks_ms())
cs.value(1)
#cs.value(0)
utime.sleep_ms(200)
#utime.sleep_ms(2200)
fm.register(27, fm.fpioa.SPI1_SCLK, force=True)
fm.register(28, fm.fpioa.SPI1_D0, force=True)
fm.register(26, fm.fpioa.SPI1_D1, force=True)
print(os.listdir())
spi.deinit()
可以看到,想要使用 SPI1 就必须在使用后归还 IO 因为它们不在同一个 IO 上,如果使用软 SPI 则可以极大的改善这个使用的问题。
可以如何实现 MicroPython 中的软 SPI ?
要知道 MicroPython 本来就是为 MCU 跨平台准备了很多软件逻辑的代码,如软 SPI 的实现在 https://github.com/micropython/micropython/blob/master/extmod/machine_spi.c 中,我们要做的就是将硬件的具体功能实现对接起来即可。
在这里我们得知需要的 mp_soft_spi_transfer 关键函数在这里 https://github.com/micropython/micropython/blob/master/drivers/bus/softspi.c 此时我们就拥有了整个软 SPI 需要的核心收发逻辑。
#include "drivers/bus/spi.h"
int mp_soft_spi_ioctl(void *self_in, uint32_t cmd) {
mp_soft_spi_obj_t *self = (mp_soft_spi_obj_t*)self_in;
switch (cmd) {
case MP_SPI_IOCTL_INIT:
mp_hal_pin_write(self->sck, self->polarity);
mp_hal_pin_output(self->sck);
mp_hal_pin_output(self->mosi);
mp_hal_pin_input(self->miso);
break;
case MP_SPI_IOCTL_DEINIT:
break;
}
return 0;
}
void mp_soft_spi_transfer(void *self_in, size_t len, const uint8_t *src, uint8_t *dest) {
mp_soft_spi_obj_t *self = (mp_soft_spi_obj_t*)self_in;
uint32_t delay_half = self->delay_half;
// only MSB transfer is implemented
// If a port defines MICROPY_HW_SOFTSPI_MIN_DELAY, and the configured
// delay_half is equal to this value, then the software SPI implementation
// will run as fast as possible, limited only by CPU speed and GPIO time.
#ifdef MICROPY_HW_SOFTSPI_MIN_DELAY
if (delay_half == MICROPY_HW_SOFTSPI_MIN_DELAY) {
for (size_t i = 0; i < len; ++i) {
uint8_t data_out = src[i];
uint8_t data_in = 0;
for (int j = 0; j < 8; ++j, data_out <<= 1) {
mp_hal_pin_write(self->mosi, (data_out >> 7) & 1);
mp_hal_pin_write(self->sck, 1 - self->polarity);
data_in = (data_in << 1) | mp_hal_pin_read(self->miso);
mp_hal_pin_write(self->sck, self->polarity);
}
if (dest != NULL) {
dest[i] = data_in;
}
}
return;
}
#endif
for (size_t i = 0; i < len; ++i) {
uint8_t data_out = src[i];
uint8_t data_in = 0;
for (int j = 0; j < 8; ++j, data_out <<= 1) {
mp_hal_pin_write(self->mosi, (data_out >> 7) & 1);
if (self->phase == 0) {
mp_hal_delay_us_fast(delay_half);
mp_hal_pin_write(self->sck, 1 - self->polarity);
} else {
mp_hal_pin_write(self->sck, 1 - self->polarity);
mp_hal_delay_us_fast(delay_half);
}
data_in = (data_in << 1) | mp_hal_pin_read(self->miso);
if (self->phase == 0) {
mp_hal_delay_us_fast(delay_half);
mp_hal_pin_write(self->sck, self->polarity);
} else {
mp_hal_pin_write(self->sck, self->polarity);
mp_hal_delay_us_fast(delay_half);
}
}
if (dest != NULL) {
dest[i] = data_in;
}
}
}
const mp_spi_proto_t mp_soft_spi_proto = {
.ioctl = mp_soft_spi_ioctl,
.transfer = mp_soft_spi_transfer,
};
那么要完成的就是硬件 GPIO 模拟 SPI 的操作,有如下需要。
- mp_hal_delay_us_fast
- mp_hal_pin_write
- mp_hal_pin_output
- mp_hal_pin_input
- mp_hal_pin_read
mp_hal_pin_write(self->sck, self->polarity);
mp_hal_pin_output(self->sck);
mp_hal_pin_output(self->mosi);
mp_hal_pin_input(self->miso);
从初始化中可以得知这是标准的 主从 SPI 通信,也就是 sck/mosi/miso/cs ,大部分传感器都支持这种,如果需要 qspi 则在同目录下 https://github.com/micropython/micropython/blob/master/drivers/bus/qspi.c 获得。
typedef struct _mp_soft_qspi_obj_t {
mp_hal_pin_obj_t cs;
mp_hal_pin_obj_t clk;
mp_hal_pin_obj_t io0;
mp_hal_pin_obj_t io1;
mp_hal_pin_obj_t io2;
mp_hal_pin_obj_t io3;
} mp_soft_qspi_obj_t;
可以看到存在 4 line 的数据 IO (D[0:3]) 这种定义,但我们目前不需要去实现它,我们就以常见的 motorola spi 为例吧。
实现流程参考
先对接实现 K210 的 GPIO 功能出来,参考在此 https://github.com/sipeed/MaixPy/blob/f9bb0bb5e807846e6b6e397b9ced2963c5472fab/components/micropython/port/src/standard_lib/machine/machine_spi.c#L114-L204 。
#if MICROPY_PY_MACHINE_SW_SPI
#include "gpiohs.h"
#define mp_hal_delay_us_fast(s) mp_hal_delay_us(s)
#define mp_spi_pin_output(pin) gpiohs_set_drive_mode(pin, GPIO_DM_OUTPUT)
#define mp_spi_pin_input(pin) gpiohs_set_drive_mode(pin, GPIO_DM_INPUT)
#define MICROPY_HW_SOFTSPI_MIN_DELAY 0
#define SPI_SOFTWARE SPI_DEVICE_MAX
STATIC void mp_spi_pin_write(uint8_t pin, uint8_t val)
{
gpiohs_set_pin(pin, val);
mp_spi_pin_output(pin);
mp_spi_pin_input(pin);
}
STATIC int mp_spi_pin_read(uint8_t pin)
{
mp_spi_pin_input(pin);
return gpiohs_get_pin(pin);
}
void mp_soft_spi_transfer(void *self_in, size_t len, const uint8_t *src, uint8_t *dest) {
machine_hw_spi_obj_t *self = (machine_hw_spi_obj_t*)self_in;
uint32_t delay_half = self->delay_half;
// printk("%s %d %d %d\r\n", __func__, self->pin_sck, self->pin_d[0], self->pin_d[1]);
// only MSB transfer is implemented
// If a port defines MICROPY_HW_SOFTSPI_MIN_DELAY, and the configured
// delay_half is equal to this value, then the software SPI implementation
// will run as fast as possible, limited only by CPU speed and GPIO time.
// #ifdef MICROPY_HW_SOFTSPI_MIN_DELAY
if (delay_half == MICROPY_HW_SOFTSPI_MIN_DELAY) {
for (size_t i = 0; i < len; ++i) {
uint8_t data_out = src[i];
uint8_t data_in = 0;
for (int j = 0; j < 8; ++j, data_out <<= 1) {
mp_spi_pin_write(self->pin_d[0], (data_out >> 7) & 1);
mp_spi_pin_write(self->pin_sck, 1 - self->polarity);
if (self->pin_d[1] != -1)
{
data_in = (data_in << 1) | mp_spi_pin_read(self->pin_d[1]);
}
mp_spi_pin_write(self->pin_sck, self->polarity);
}
if (dest != NULL) {
dest[i] = data_in;
}
}
return;
}
// #endif
for (size_t i = 0; i < len; ++i) {
uint8_t data_out = src[i];
uint8_t data_in = 0;
for (int j = 0; j < 8; ++j, data_out <<= 1) {
mp_spi_pin_write(self->pin_d[0], (data_out >> 7) & 1);
if (self->phase == 0) {
mp_hal_delay_us_fast(delay_half);
mp_spi_pin_write(self->pin_sck, 1 - self->polarity);
} else {
mp_spi_pin_write(self->pin_sck, 1 - self->polarity);
mp_hal_delay_us_fast(delay_half);
}
if (self->pin_d[1] != -1)
{
data_in = (data_in << 1) | mp_spi_pin_read(self->pin_d[1]);
}
if (self->phase == 0) {
mp_hal_delay_us_fast(delay_half);
mp_spi_pin_write(self->pin_sck, self->polarity);
} else {
mp_spi_pin_write(self->pin_sck, self->polarity);
mp_hal_delay_us_fast(delay_half);
}
}
if (dest != NULL) {
dest[i] = data_in;
}
}
}
#endif
这样就将收发的逻辑实现起来了,如果你有前一篇的基础,那么这个就是自然而然的事情,非常容易,关于 K210 的 GPIO 的工作差异,在这里就不再重新赘述。
接回 MaixPy 的 SPI 模块
要将其统一到 Python 代码中,我额外添加了 SPI_SOFTWARE 使得不再区分 SPI 编号,从而使得 SPI 设备自由定义。
其中关键的 machine_xxx_spi_transfer 函数整合如下:
STATIC void machine_hw_spi_transfer(mp_obj_base_t *self_in, size_t len, const uint8_t *src, uint8_t *dest, int cs) {
machine_hw_spi_obj_t *self = MP_OBJ_TO_PTR(self_in);
if (self->state == MACHINE_HW_SPI_STATE_DEINIT) {
mp_raise_msg(&mp_type_OSError, "[MAIXPY]SPI: transfer on deinitialized SPI");
return;
}
#if MICROPY_PY_MACHINE_SW_SPI
if(self->id == SPI_SOFTWARE)
{
mp_soft_spi_transfer(self, len, src, dest);
return;
}
#endif
if(dest==NULL)
sipeed_spi_transfer_data_standard(self->id, cs, src, NULL, len, 0);
else
sipeed_spi_transfer_data_standard(self->id, cs, src, dest, len, len);
}
这代码很简单,没有太大的问题,再来就是补充初始化时的设备选择。
https://github.com/sipeed/MaixPy/blob/f9bb0bb5e807846e6b6e397b9ced2963c5472fab/components/micropython/port/src/standard_lib/machine/machine_spi.c#L423-L441
#if MICROPY_PY_MACHINE_SW_SPI
if(self->id == SPI_SOFTWARE)// standard soft-spi mode
{
if (self->baudrate > 1000*1000) {
self->delay_half = 0;
}
fpioa_set_function(self->pin_sck, FUNC_GPIOHS0 + self->pin_sck);
fpioa_set_function(self->pin_d[0], FUNC_GPIOHS0 + self->pin_d[0]);
mp_spi_pin_write(self->pin_sck, self->polarity);
mp_spi_pin_output(self->pin_sck);
mp_spi_pin_output(self->pin_d[0]);
if (self->pin_d[1] != -1)
{
fpioa_set_function(self->pin_d[1], FUNC_GPIOHS0 + self->pin_d[1]);
mp_spi_pin_input(self->pin_d[1]);
}
} else
#endif
最后 Python 代码只需要改变 SPI0 为 SPI.SPI_SOFT 即可。
from machine import SPI
spi1 = SPI(SPI.SPI_SOFT, mode=SPI.MODE_MASTER, baudrate=10000000, polarity=0, phase=0, bits=8, firstbit=SPI.MSB, sck=28, mosi=29, miso=30, cs0=27)
w = b'1234'
r = bytearray(4)
spi1.write(w)
spi1.write(w)
spi1.write_readinto(w, r)
spi1.read(5, write=0x00)
spi1.readinto(r, write=0x00)
最后的总结
可喜可贺,我终于可以有足够的 SPI 模块使用了,但也存在一些不完美,例如没有实现软 QSPI 也没有实现软从机,总得来说,还是有很多可以实现的地方。
但那就留给有需要的时候再实现吧,因为我也只是刚好有需要才实现的。= -=~
本来想写一下实现过程中遇到的一些问题的,但好像也没啥问题,主要也就是和 I2C 实现的时候对 GPIO 注意点使用就好了,我量测的 K210 的 GPIO 软实现的功能都时钟频率大概就在 50khz 附近。
如果没有看过一篇软 I2C 的话,这篇省略了很多内容,不是很好理解的,我自己看完的体会就是这样的。
2020年10月1日 junhuanchen