为 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
", __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 函数整合如下:

https://github.com/sipeed/MaixPy/blob/f9bb0bb5e807846e6b6e397b9ced2963c5472fab/components/micropython/port/src/standard_lib/machine/machine_spi.c#L221-L239

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

原文地址:https://www.cnblogs.com/juwan/p/13758779.html