# MS-RTOS SPI 驱动开发

本章将介绍 MS-RTOS SPI 总线驱动开发及测试。

SPI 是串行外设接口(Serial Peripheral Interface)的缩写,是 Motorola 公司推出的一种同步串行接口技术,是一种高速全双工的通信总线。它被广泛地使用在 EEPROM、FLASH、实时时钟、AD 转换器、数字信号处理器、数字信号解码器和 LCD 等设备与 MCU 间。它以主从方式工作,通常有一个主设备和一个或多个从设备。SPI在芯片的管脚上只占用四根线,分别是MOSI(数据输出),MISO(数据输入),SCLK(时钟),CS(片选)。

# 1. SPI 基础知识

# 1.1 物理层

SPI 通讯设备之间的常用连接方式:

spi_interface

SPI通讯使用3条总线及片选线,3条总线分别为 SCK、MOSI、MISO,片选线为 SSn,它们的作用介绍如下:

  • SSn ( Slave Select):从设备选择信号线,常称为片选信号线,也称为NSS、CS,以下用NSS表示。SPI通讯以NSS线置低电平为开始信号,以NSS线被拉高作为结束信号。
  • SCK (Serial Clock):时钟信号线,用于通讯数据同步。它由通讯主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不一样,如STM32的SPI时钟频率最大为fpclk/2,两个设备之间通讯时,通讯速率受限于低速设备。
  • MOSI (Master Output, Slave Input):主设备输出/从设备输入引脚。主机的数据从这条信号线输出,从机由这条信号线读入主机发送的数据,即这条线上数据的方向为主机到从机。
  • MISO(Master Input, Slave Output):主设备输入/从设备输出引脚。主机从这条信号线读入数据,从机的数据由这条信号线输出到主机,即在这条线上数据的方向为从机到主机。

# 1.2 协议层

SPI 协议定义了通讯的起始和停止信号、数据有效性、时钟同步等环节。

(1)SPI 基本通讯过程

NSS、SCK、MOSI信号都由主机控制产生,而MISO的信号由从机产生,主机通过该信号线读取从机的数据。MOSI与MISO的信号只在NSS为低电平的时候才有效,在SCK的每个时钟周期MOSI和MISO传输一位数据。MOSI及MISO的数据在SCK的上升沿期间变化输出,在SCK的下降沿时被采样。即在SCK的下降沿时刻,MOSI及MISO的数据有效,高电平时表示数据"1",为低电平时表示数据"0"。

spi_timing

(2)I2C 信号和响应

SPI一共有四种通讯模式,它们的主要区别是总线空闲时SCK的时钟状态以及数据采样时刻。为方便说明,在此引入"时钟极性CPOL"和"时钟相位CPHA"的概念。

SPI 模式 CPOL (时钟极性) CPAH(时钟相位) 空闲时 SCK 时钟 数据采样时刻
0 0 0 低 电平 奇数边沿
1 0 1 低 电平 偶数边沿
2 1 0 高 电平 奇数边沿
3 1 1 高 电平 偶数边沿

时钟极性CPOL是指SPI通讯设备处于空闲状态时,SCK信号线的电平信号(即SPI通讯开始前、 NSS线为高电平时SCK的状态)。CPOL=0时, SCK在空闲状态时为低电平,CPOL=1时,则相反。

时钟相位CPHA是指数据的采样的时刻,当CPHA=0时,MOSI或MISO数据线上的信号将会在SCK时钟线的"奇数边沿"被采样。当CPHA=1时,数据线在SCK的"偶数边沿"采样。

spi_communication

# 1.3 STM32 的 SPI 架构

关于 STM32 SPI 的使用,可以自行在网上搜索。

spi_arch

# 2. SPI 驱动框架

# 2.1 模拟 SPI

MS-RTOS 已经实现了通用的 GPIO 模拟 SPI 的库。在构建 MS-RTOS 的 SDK 时,勾选 libmsdriver;或者自己手动从 GitHub 上下载最新的 libmsdriver,并添加到 SDK 中。

(1)相关数据结构

在基于 libmsdriver 来开发 GPIO 模拟的 SPI 驱动时,只需要实现(填充)好 ms_spi_bitbang_io_t ,然后调用 ms_spi_bitbang_bus_dev_create 即可。在实际开发时,需要包含头文件 "spi/ms_drv_spi_bitdang.h" 并添加链接选项 -lmsdriver

/**
 * @brief Functions for setting and getting the state of the SPI lines.
 *
 * These need to be implemented by the user of this library.
 */
typedef struct {
    /*
     * Delay ns
     */
    void(*delay)(ms_ptr_t io_ctx, ms_uint32_t ns);

    /*
     * Return the state of the MISO line (zero/non-zero value)
     */
    ms_uint8_t (*get_miso)(ms_ptr_t io_ctx);

    /*
     * Set the state of the MOSI line (zero/non-zero value)
     */
    void (*set_mosi)(ms_ptr_t io_ctx, ms_uint8_t state);

    /*
     * Set the state of the SCK line (zero/non-zero value)
     */
    void (*set_sck)(ms_ptr_t io_ctx, ms_uint8_t state);

} const ms_spi_bitbang_io_t;

ms_err_t ms_spi_bitbang_bus_dev_create(const char *bus_name, const char *path, 
                                       ms_spi_bitbang_io_t *io, ms_ptr_t io_ctx);

(2)驱动示例

#include "ms_config.h"
#include "ms_rtos.h"
#include "includes.h"
#include "spi/ms_drv_spi_bitdang.h"

/*
 * Set the state of the SCK line (zero/non-zero value)
 */
static void __spi_io_set_sck(ms_ptr_t io_ctx, ms_uint8_t state)
{
    if (state != 0) {
        gpio_bit_set(SPI_SCK_GPIO_PORT, SPI_SCK_GPIO_PIN);
    } else {
        gpio_bit_reset(SPI_SCK_GPIO_PORT, SPI_SCK_GPIO_PIN);
    }
}

/*
 * Return the state of the MISO line (zero/non-zero value)
 */
static ms_uint8_t __spi_io_get_miso(ms_ptr_t io_ctx)
{
    if (gpio_input_bit_get(SPI_MISO_GPIO_PORT, SPI_MISO_GPIO_PIN) == RESET) {
        return 0;
    } else {
        return 1;
    }
}

/*
 * Set the state of the MOSI line (zero/non-zero value)
 */
static void __spi_io_set_mosi(ms_ptr_t io_ctx, ms_uint8_t state)
{
    if (state != 0) {
        gpio_bit_set(SPI_MOSI_GPIO_PORT, SPI_MOSI_GPIO_PIN);
    } else {
        gpio_bit_reset(SPI_MOSI_GPIO_PORT, SPI_MOSI_GPIO_PIN);
    }
}

/*
 * Delay ns
 */
static void __spi_io_delay(ms_ptr_t io_ctx, ms_uint32_t ns)
{
    bsp_delay_us((ns + (1000 - 1)) / 1000);
}

/*
 * ms_spi_bitbang_io_t
 */
static ms_spi_bitbang_io_t __spi_io = {
    .set_sck  = __spi_io_set_sck,
    .set_mosi = __spi_io_set_mosi,
    .get_miso = __spi_io_get_miso,
    .delay    = __spi_io_delay,
};

/*
 * SPI GPIO initialization function
 */
void bsp_spi_emulator_init(const char *bus_name, const char *path)
{
    /* Enable all GPIO clock */
    SPI_ALL_GPIO_EN();

    /* Configure SCK (PB3) pin as push-pull output with pull-up */
    gpio_mode_set(SPI_SCK_GPIO_PORT, GPIO_MODE_OUTPUT, 
                  GPIO_PUPD_NONE, SPI_SCK_GPIO_PIN);
    gpio_output_options_set(SPI_SCK_GPIO_PORT, GPIO_OTYPE_PP, 
                            GPIO_OSPEED_50MHZ, SPI_SCK_GPIO_PIN);
    gpio_bit_set(SPI_SCK_GPIO_PORT, SPI_SCK_GPIO_PIN);

    /* Configure MISO (PB4) pin as push-pull output with pull-up */
    gpio_mode_set(SPI_MISO_GPIO_PORT, GPIO_MODE_OUTPUT, 
                  GPIO_PUPD_NONE, SPI_MISO_GPIO_PIN);
    gpio_output_options_set(SPI_MISO_GPIO_PORT, GPIO_OTYPE_PP, 
                            GPIO_OSPEED_50MHZ, SPI_MISO_GPIO_PIN);
    gpio_bit_set(SPI_MISO_GPIO_PORT, SPI_MISO_GPIO_PIN);

    /* Configure MOSI (PB5) pin as push-pull output with pull-up */
    gpio_mode_set(SPI_MOSI_GPIO_PORT, GPIO_MODE_OUTPUT, 
                  GPIO_PUPD_NONE, SPI_MOSI_GPIO_PIN);
    gpio_output_options_set(SPI_MOSI_GPIO_PORT, GPIO_OTYPE_PP, 
                            GPIO_OSPEED_50MHZ, SPI_MOSI_GPIO_PIN);
    gpio_bit_set(SPI_MOSI_GPIO_PORT, SPI_MOSI_GPIO_PIN);
	
    ms_spi_bitbang_bus_dev_create(bus_name, path, &__spi_io, MS_NULL);
}

# 2.2 硬件 SPI

MS-RTOS 为 SPI 总线驱动封装了一层简单易用的驱动框架。同时,MS-RTOS 也已经支持了部分系列的 NorFlash 驱动,这部分驱动放到了 libmsdriver 中。

(1)相关数据结构

MS-RTOS SPI 驱动框架相关的数据结构和接口可以在头文件 sdk/src/driver/ms_drv_spi.h 中找到。

  • ms_spi_bus_t

    该结构体用于描述一条 I2C 总线,并包含操作总线控制器的接口。

    /*
     * Chip select function
     */
    typedef ms_err_t (*ms_spi_cs_func_t)(ms_bool_t select);
    
    /*
     * ms_spi_bus_ops_t
     */
    typedef struct {
        ms_ssize_t  (*trans)(ms_ptr_t bus_ctx, ms_spi_cs_func_t cs, const ms_spi_msg_t *msg, ms_size_t n_msg);
        int         (*ioctl)(ms_ptr_t bus_ctx, int cmd, ms_ptr_t arg);
    } const ms_spi_bus_ops_t;
    
    /*
     * ms_spi_bus_t
     */
    typedef struct {
        ms_io_name_node_t   nnode;
        ms_spi_bus_ops_t   *ops;
        ms_handle_t         lockid;
        ms_list_head_t      dev_list;
        ms_ptr_t            ctx;
    } ms_spi_bus_t;
    
  • ms_spi_device_t

    该结构体用于描述一个 SPI 总线上的设备。通常实际 SPI 设备需要包含一个该类型的成员,在进行 SPI 通信时,需要将 ms_spi_device_t attach 到一条 ms_spi_bus_t ,然后调用相应的接口进行通信,如:ms_spi_device_trans

    /*
     * ms_spi_device_t
     */
    typedef struct {
        ms_io_name_node_t   nnode;
        ms_spi_bus_t       *bus;
        ms_ptr_t            ctx;
        ms_spi_cs_func_t    cs;
    } ms_spi_device_t;
    

    SPI 设备可以使用 API 接口包括:

    | 接口 | 说明 | | ------------------------ | ------------------------------------------------------------ | | ms_spi_device_attach | 将 ms_spi_device_t 附加/绑定 到 一条 SPI Bus 上 (bus_name) | | ms_spi_device_detach | 取消 ms_spi_device_t 和 SPI Bus 的 绑定 | | ms_spi_device_trans | 传输消息 | | ms_spi_device_ioctl | 通过 ioctl 发送总线控制命令 | | ms_spi_device_lock_bus | 锁住 SPI Bus (非硬件上的锁定) | | ms_spi_device_unlock_bus | 解锁 SPI Bus (非硬件上的解锁) |

(2)SPI 框架

下图主要描绘了 SPI 总线驱动、SPI 设备驱动 和 应用层操作 SPI 总线 的过程。

spi_bus_arch

上图由上到下分别为:应用层、IO 驱动层、子系统框架层、驱动适配层。不一定所有类型的驱动都有驱动框架。在注册 SPI 总线驱动时,ms_spi_bus_t 将作为 SPI 总线控制器设备的 privinfo_t 的一员;在应用程序操作 SPI 总线时,将调用到 ms_spi_bus_drv_ops ,实际上将调用 privinfo_tms_spi_bus_t 的操作函数;在注册 SPI 设备驱动时,需要指定 ms_nor_port_t (包含总线信息和挂载点信息),在创建 nor_dev 时将绑定 spi_dev 到对应的 spi_bus , 文件系统的读写操作将通过 SPI 来完成,而使用 spi_dev 就可以调用到 SPI 相关的 API。

(3)驱动示例

SPI 驱动示例,仅作为参考。

#define __MS_IO
#include "ms_config.h"
#include "ms_rtos.h"
#include "ms_io_core.h"
#include <string.h>
#include "includes.h"

/*
 * Private info
 */
typedef struct {
    ms_uint8_t      channel;
    ms_spi_param_t  param;

    ms_handle_t     rx_complete_sem;
    char            rx_sem_name[__SPI_MAX_NAME_LEN];
    ms_handle_t     tx_complete_sem;
    char            tx_sem_name[__SPI_MAX_NAME_LEN];
} privinfo_t;

/*
 * @brief  transmit and receive
 */
static ms_ssize_t __sccb9001cxx_spi_bus_trans(ms_ptr_t bus_ctx, ms_spi_cs_func_t cs,
                                              const ms_spi_msg_t *msg, ms_size_t n_msg)
{
    int         i;
    privinfo_t *priv = bus_ctx;
    ms_err_t    err;

    for (i = 0; i < n_msg; i++, msg++) {

        /*
         * Fix 1 line TRANSMODE to receive data or transmit data
         */
        if (priv->param.direction == MS_SPI_DIRECTION_1LINE) {
            if (!!(msg->flags & MS_SPI_M_WRITE)) {
                priv->native_param.trans_mode = SPI_TRANSMODE_BDTRANSMIT;
            } else {
                priv->native_param.trans_mode = SPI_TRANSMODE_BDRECEIVE;
            }
            __sccb9001cxx_spi_bus_configure(priv, &priv->native_param);
        }

        /*
         * Assert CS before transfer
         */
        if ((msg->flags & MS_SPI_M_BEGIN) && (priv->param.nss == MS_SPI_NSS_SOFT)) {
            if (cs) {
                cs(MS_TRUE);
            }
        }

        /*
         * SPI transmit data with dma or polling
         */
        if (priv->hw_dma_en) {
            err = __sccb9001cxx_spi_transmit_receive_dma(priv, msg, MS_TIMEOUT_FOREVER);
        } else {
            err = __sccb9001cxx_spi_transmit_receive(priv, msg, MS_TIMEOUT_FOREVER);
        }

        /*
         * Deassert CS after transfer
         */
        if ((msg->flags & MS_SPI_M_END) && (priv->param.nss == MS_SPI_NSS_SOFT)) {
            if (cs) {
                cs(MS_FALSE);
            }
        }

        if (err != MS_ERR_NONE) {
            break;
        }
    }

    return  i;
}

/*
 * __sccb9001cxx_spi_bus_ioctl
 */
static int __sccb9001cxx_spi_bus_ioctl(ms_ptr_t bus_ctx, int cmd, ms_ptr_t arg)
{
    privinfo_t *priv = bus_ctx;
    ms_err_t    err;
    int         ret;

    switch (cmd) {
    case MS_SPI_CMD_GET_PARAM:
        if (ms_access_ok(arg, sizeof(ms_spi_param_t), MS_ACCESS_W)) {
            *(ms_spi_param_t *)arg = priv->param;
            ret = 0;
        } else {
            ms_thread_set_errno(EFAULT);
            ret = -1;
        }
        break;

    case MS_SPI_CMD_SET_PARAM:
        if (ms_access_ok(arg, sizeof(ms_spi_param_t), MS_ACCESS_R)) {
            priv->param = *(ms_spi_param_t *)arg;

            err = __sccb9001cxx_spi_bus_param_convert(&priv->param, &priv->native_param);
            if (err == MS_ERR_NONE) {
                __sccb9001cxx_spi_bus_configure(priv, &priv->native_param);
                ret = 0;

            } else {
                ms_thread_set_errno(EINVAL);
                ret = -1;
            }

        } else {
            ms_thread_set_errno(EFAULT);
            ret = -1;
        }
        break;

    default:
        ms_thread_set_errno(EINVAL);
        ret = -1;
        break;
    }

    return ret;
}

static ms_spi_bus_ops_t sccb9001cxx_spi_bus_ops = {
        .trans = __sccb9001cxx_spi_bus_trans,
        .ioctl = __sccb9001cxx_spi_bus_ioctl,
};

static privinfo_t spi_bus_privinfo[__SPI_MAX_CHANNEL_NUM] = {
    {
            .channel = 0,
            .base    = SPI0,
            .irq     = SPI0_IRQn,
    },
};

static ms_spi_bus_t spi_bus_info[__SPI_MAX_CHANNEL_NUM] = {
    {
        .nnode = {
            .name = "spi0",
        },
        .ops = &sccb9001cxx_spi_bus_ops,
        .ctx = &spi_bus_privinfo[0],
    },
};

/*
 * Create SPI device file
 */
ms_err_t sccb9001cxx_spi_bus_dev_create(const char *path, ms_uint32_t channel)
{
    bsp_dma_logic_id_t      tx_dma_id;
    bsp_dma_logic_id_t      rx_dma_id;
    sccb9001_spi_hw_info_t  hw_info;
    ms_spi_bus_t           *spi_bus;
    privinfo_t             *priv;
    ms_err_t                err;

    if (path == MS_NULL || channel >= __SPI_MAX_CHANNEL_NUM) {
        return MS_ERR;
    }

    spi_bus       = &spi_bus_info[channel];
    priv          = spi_bus->ctx;
    priv->channel = channel;

    sprintf(priv->rx_sem_name, "spi%d-rxsem", channel);
    if (ms_semb_create(priv->rx_sem_name, MS_FALSE, 
                       MS_WAIT_TYPE_PRIO, &priv->rx_complete_sem) != MS_ERR_NONE) {
        goto abort_exit;
    }

    sprintf(priv->tx_sem_name, "spi%d-txsem", channel);
    if (ms_semb_create(priv->tx_sem_name, MS_FALSE, 
                       MS_WAIT_TYPE_PRIO, &priv->tx_complete_sem) != MS_ERR_NONE) {
        goto abort_exit;
    }

    err = bsp_spi_low_level_init(channel, &hw_info);
    if (err == MS_ERR_NONE) {
        priv->base           = hw_info.spi_base;
        priv->irq            = hw_info.spi_irq;
        priv->hw_cs_en       = hw_info.hw_cs_en;
        priv->hw_dma_en      = hw_info.hw_dma_en;
        priv->tx_dma_base    = hw_info.tx_dma_base;
        priv->tx_dma_channel = hw_info.tx_dma_channel;
        priv->tx_dma_subperi = hw_info.tx_dma_subperi;
        priv->rx_dma_base    = hw_info.rx_dma_base;
        priv->rx_dma_channel = hw_info.rx_dma_channel;
        priv->rx_dma_subperi = hw_info.rx_dma_subperi;

        if (hw_info.hw_dma_en) {
            err = __SPI_TX_DMA_ID(&hw_info, &tx_dma_id);
            if (err == MS_ERR_NONE) {
                err = __SPI_RX_DMA_ID(&hw_info, &rx_dma_id);
            }

            if (err == MS_ERR_NONE) {
                priv->tx_dma_id         = tx_dma_id.dma_id;
                priv->tx_dma_channel_id = tx_dma_id.channel_id;
                priv->rx_dma_id         = rx_dma_id.dma_id;
                priv->rx_dma_channel_id = rx_dma_id.channel_id;
            } else {
                goto abort_exit;
            }
        }

        priv->param.baud_rate      = 400000;
        priv->param.mode           = MS_SPI_MODE_MASTER;
        priv->param.direction      = MS_SPI_DIRECTION_2LINES;
        priv->param.data_size      = MS_SPI_DATA_SIZE_8BIT;
        priv->param.frame_mode     = MS_SPI_CLK_POLARITY_HIGH | 
                                     MS_SPI_CLK_PHASE_2EDGE | MS_SPI_FIRST_BIT_MSB;
        priv->param.nss            = MS_SPI_NSS_SOFT;
        priv->param.crc_polynomial = 7;

        err = __sccb9001cxx_spi_bus_param_convert(&priv->param, &priv->native_param);
        if (err == MS_ERR_NONE) {
            __sccb9001cxx_spi_bus_configure(priv, &priv->native_param);

            err =  ms_spi_bus_register(spi_bus);
            if (err == MS_ERR_NONE) {
                err = ms_spi_bus_dev_create(path, spi_bus);
            }
        }
    }

    return err;

abort_exit:
    return MS_ERR;
}

# 2.3 ioctl 命令

以下仅列出几个最基本的命令,可以在 sdk/src/driver/ms_drv_spi.h 文件中找到所有命令的定义。

命令 描述 参数
MS_SPI_CMD_SET_PARAM 设置 SPI 控制器的工作模式 ms_spi_param_t 指针
MS_SPI_CMD_GET_PARAM 获取 SPI 控制器的工作模式 ms_spi_param_t 指针

(1)ms_spi_param_t

typedef struct {
    ms_uint32_t     baud_rate;  	// Specifies the clock frequency.    
    ms_uint8_t      mode;       	// Specifies the SPI operating mode.        
    ms_uint8_t      direction;   	// Specifies the SPI Directional mode.      
    ms_uint8_t      data_size;    	// Specifies the SPI data size.            
    ms_uint16_t     frame_mode; 	// Specifies the frame mode.
    ms_uint8_t      nss;        	// whether the NSS signal is managed by hardware.
    ms_uint32_t     crc_polynomial; // the polynomial used for the CRC calculation. 
} ms_spi_param_t;
  • SPI 工作模式 (mode)
可选配置 描述
MS_SPI_MODE_SLAVE SPI 从机模式
MS_SPI_MODE_MASTER SPI 主机模式
  • SPI 数据线数(direction)
可选配置 描述
MS_SPI_DIR_MODE_2LINES SPI 使用两线模式
MS_SPI_DIR_MODE_2LINES_RXONLY SPI 使用两线模式,只使能接收
MS_SPI_DIR_MODE_1LINE SPI 使用一线模式
  • SPI 数据帧大小(data_size)
可选配置 描述
MS_SPI_DATA_SIZE_8BIT SPI 数据帧大小为 8bit
MS_SPI_DATA_SIZE_16BIT SPI 数据帧大小为 16bit
  • SPI 时钟/相位/大小端(frame_mode)
可选配置 描述
MS_SPI_CLK_POLARITY_LOW SPI 时钟极性,总线空闲时SCLK引脚为低电平
MS_SPI_CLK_POLARITY_HIGH SPI 时钟极性,总线空闲时SCLK引脚为高电平
MS_SPI_CLK_PHASE_1EDGE SPI 时钟相位,在SCLK第一个变化沿捕获数据
MS_SPI_CLK_PHASE_2EDGE SPI 时钟相位,在SCLK第二个变化沿捕获数据
MS_SPI_FIRST_BIT_MSB SPI 数据端序,总线上先发送 MSB
MS_SPI_FIRST_BIT_LSB SPI 数据端序,总线上先发送 LSB
MS_SPI_TI_MODE_DISABLE 禁能 TI 兼容模式
MS_SPI_TI_MODE_ENABLE 使能 TI 兼容模式
MS_SPI_CRC_CALC_DISABLE 禁能 CRC 计算和校验
MS_SPI_CRC_CALC_ENABLE 使能 CRC 计算和校验
  • SPI 硬件片选模式(nss)
可选配置 描述
MS_SPI_NSS_SOFT 使用软件片选,关闭控制器的硬件片选
MS_SPI_NSS_HARD_INPUT 配置控制器接收片选信号(用于从机模式下)
MS_SPI_NSS_HARD_OUTPUT 配置控制器输出片选信号(用于主机模式下)

(2)ms_spi_msg_t

typedef struct {
    ms_uint16_t     flags;		// 传输控制参数
    ms_ptr_t        tx_buf;		// 发送缓冲区
    ms_ptr_t        rx_buf;		// 接收缓冲区
    ms_size_t       len;		// 缓冲区大小
} ms_spi_msg_t;
  • SPI 传输控制标记(flags)
可选配置 描述
MS_SPI_M_BEGIN Assert CS before transfer
MS_SPI_M_END Deassert CS after transfer
MS_SPI_M_ONCE Assert CS before transfer and deassert CS after transfer
MS_SPI_M_TX_FIX Only transmit tx_buf[0] in this transfer
MS_SPI_M_RX_FIX Only store recieve data to rx_buf[0] in this transfer
MS_SPI_M_READ MSG use read operation
MS_SPI_M_WRITE MSG use write operation

# 3. SPI 的应用

# 3.1 使用 SPI 读写 NorFlash

//NOTE: 请自行参考 libmsdriver 库中的 nor/ms_drv_nor_spi.c 

# 3.2 使用 SPI 驱动传感器

//NOTE: 请自行参考 libmsdriver 库中 sensor 目录下相关驱动

# 附录(Appendix)

1. Reference

STM32 SPI:https://blog.csdn.net/flyleaf91/article/details/52325513

2. FAQ

(1)Stand SPI、Dual SPI 和 Quad SPI?

对于 SPI Flash,有标准 spi flash、dual spi、qual spi 三种类型,分别对应 3-wire、4-wire、6-wire,在相同clock下,线数越多,传输速率越高。

  • Stand SPI:标准 SPI 通常就称 SPI,它是一种串行外设接口规范,有4根引脚信号:clk、cs、mosi、miso 这是全双工模式,输入和输出可以同时进行(mosi、miso分别对应的是DI和DO)。

  • Dual SPI:它只是针对SPI Flash而言,不是针对所有SPI外设。对于SPI Flash,全双工并不常用,那么就有人想着让MOSI、MISO同时向一个方向传输数据,变成半双工通信, 每一个时钟传输两个位,这样传输速度不就加倍了吗?因此扩展了mosi和miso的用法,让它们工作在半双工,用以加倍数据传输。也就是对于Dual SPI Flash,可以发送一个命令字节进入dual mode,这样mosi变成SIO0(serial io 0),mosi变成SIO1(serial io 1),这样一个时钟周期内就能传输2个bit数据,加倍了数据传输,如果传输八个位,那么MOSI传输偶数位0 2 4 6, MISO传输奇数位1 3 5 7。6根引脚信号: CLK, /CS, IO0, IO1, /WP, /Hold。这是半双工模式,mosi和miso对应的是IO0和IO1,这样单次SPI的传输就可以传输2个Bit。

  • Qual SPI:与Dual SPI类似,也是针对SPI Flash,Qual SPI Flash增加了两根I/O线(SIO2,SIO3),目的是一个时钟内传输4个bit,如果传输八个位,那么MOSI传输0 4, MISO传输1 5。SIO2传输2 6,SIO2传输3 7。引脚信号:CLK, /CS, IO0, IO1, /WP(IO2), /Hold(IO3)。同样是半双工模式,Quad SPI增加了2个IO口(WP , HOLD),增加的目的是将SPI的单次传输数据量加大到4个Bit。