SPI 总线
SPI(Serial Peripheral Interface),串行外设接口,是 Motorola 提出的一种高速的、全双工、同步的通信总线。SPI 接口主要用于 MCU 与外围设备的通信,外围设备包括 EEPROM、FLASH、实时时钟、AD 转换器、数字信号处理器和数字信号解码器等。
它以主从方式工作,通常有一个主设备和一个或多个从设备。
SPI 在芯片的管脚上只占用四根线,分别是 MOSI(数据输出),MISO(数据输入),SCLK(时钟),CS(片选)。
MOSI:主机数据输出,从机数据输入;
MISO:主机数据输入,从机数据输出;
SCLK:时钟信号,由主机产生;
CS:从机使能信号,由主机控制。
SPI 总线原理
SPI 协议规定一个 SPI 设备不能在数据通信过程中仅仅只充当一个“发送者(Transmitter)”或者“接收者(Receiver)”。实质上每次 SPI 的数据传输都是主从设备在交换数据。SPI 总线的传输中,CS 信号是低电平有效的,当我们要与某外设通信的时候,需要将该外设上的 CS 线置低。在数据传输的过程中,每次接收到的数据必须在下一次数据传输之前被采样。如果之前接收到的数据没有被读取,那么这些已经接收完成的数据将有可能会被丢弃,导致 SPI 物理模块最终失效。因此,SPI 在传输完数据后,必须读取 SPI 设备里的数据,即使这些数据在程序里是无用的。
SPI 模块为了和外设进行数据交换,根据外设工作要求,其输出串行同步时钟极性 CPOL (Clock Polarity)和相位 CPHA(Clock Phase)可以配置。CPOL,表示 SCLK 空闲的时候,其电平值是低电平还是高电平;CPHA,表示数据采样在第一个跳变沿还是第二个跳变沿。
CPHA=0,表示第一个边沿:
对于 CPOL=0,SCLK 空闲时是低电平,第一个跳变沿就是从低变到高,所以是上升沿;
对于 CPOL=1,SCLK 空闲时是高电平,第一个跳变沿就是从高变到低,所以是下降沿;
CPHA=1,表示第二个边沿:
对于 CPOL=0,SCLK 空闲时是低电平,第二个跳变沿就是从高变到低,所以是下降沿;
对于 CPOL=1,SCLK 空闲时是高电平,第一个跳变沿就是从低变到高,所以是上升沿;
SylixOS SPI 总线框架分析
SylixOS 的 SPI 体系结构和 I2C 类似,也分为 3 个组成部分。
- SPI 核心
SPI 核心提供了 SPI 总线驱动和设备驱动的注册、注销方法,SPI 通信方法,适配器无关的代码等。每个 SPI 从设备驱动都能找到和它相连的 SPI 总线适配器。
- SPI 总线驱动
SPI 总线驱动主要包括 SPI 适配器结构 lw_spi_adapter 和 SPI 适配器的通信方法数据结构。
- SPI 设备驱动
SPI 设备驱动是对 SPI 设备端的实现,设备一般挂接在受CPU控制的 SPI 适配器上,通过 SPI 适配器与 CPU 交换数据。每一条 SPI 总线对应一个 SPI 适配器。在 内核中,每一个适配器提供了一个描述的结构,也定义了 Adapter 支持的操作。再通过 SPI 核心层将 SPI 设备与 SPI 适配器关联起来。
SPI 总线适配器相关信息位于“libsylixos/SylixOS/system/device/spi”下,其适配器创建原型如下:
#include <SylixOS.h>
INT API_SpiAdapterCreate (CPCHAR pcName,
PLW_SPI_FUNCS pspifunc)
函数 API_SpiAdapterCreate 原型分析:
此函数成功返回 ERROR_NONE ,失败返回 PX_ERROR ;
- 参数 pcName 是 SPI 适配器的名称,即 shell 命令 buss 显示的名称。
- 参数 pspifunc 是 SPI 总线传输函数的指针。
函数 API_SpiAdapterCreate 使用结构体 PLW_ SPI _FUNCS 来向内核提供传输函数集合,其详细描述如下:
#include <SylixOS.h>
typedef struct lw_spi_funcs {
INT (*SPIFUNC_pfuncMasterXfer)(PLW_SPI_ADAPTER pspiadapter,
PLW_SPI_MESSAGE pspimsg,
INT iNum);
INT (*SPIFUNC_pfuncMasterCtl)(PLW_SPI_ADAPTER pspiadapter,
INT iCmd,
LONG lArg);
} LW_SPI_FUNCS;
typedef LW_SPI_FUNCS *PLW_SPI_FUNCS;
- SPIFUNC_pfuncMasterXfer :SPI 传输函数,SPI 设备会直接调用此函数实现消息发送。第一个参数 pspiadapter 为 SPI 总线适配器指针,第二个参数 pspimsg 为 SPI 设备需要传输的消息结构体首地址指针,第三个参数 iNum 为需要传输的消息个数,以上三个参数即可告知 SPI 设备如何调用此函数实现消息传输。
- SPIFUNC_pfuncMasterCtl :SPI 适配器控制函数,用来实现与硬件控制器相关的控制。第一个参数 pspiadapter 为 SPI 总线适配器指针,第二个参数 iCmd 为控制命令,第三个参数 lArg 与 iCmd 相关。
注册到内核的传输函数集合中要用到多种结构体, PLW_SPI_ADAPTER 总线适配器结构体指针主要包含当前总线适配器节点信息, PLW_SPI_DEVICE 总线设备结构体指针主要包含当前SPI设备的相关信息, PLW_SPI_MESSAGE 消息请求结构体指针作用是指向需要发送的消息缓冲区,提供以上三种结构体后控制器即可知道如何进行发送,各种结构体的详细描述如下:
首先介绍 SPI 总线适配器结构体,该结构体详细描述如下:
#include <SylixOS.h>
typedef struct lw_spi_adapter {
LW_BUS_ADAPTER SPIADAPTER_pbusadapter; /* 总线节点 */
struct lw_spi_funcs *SPIADAPTER_pspifunc; /* 总线适配器操作函数 */
LW_OBJECT_HANDLE SPIADAPTER_hBusLock; /* 总线操作锁 */
LW_LIST_LINE_HEADER SPIADAPTER_plineDevHeader; /* 设备链表 */
} LW_SPI_ADAPTER;
typedef LW_SPI_ADAPTER *PLW_SPI_ADAPTER;
- SPIADAPTER_pbusadapter :系统总线节点结构体。
- SPIADAPTER_pi2cfunc :指向总线适配器的操作函数,即 API_SpiAdapterCreate 函数注册到核心层的操作函数集指针。
- SPIADAPTER_hBusLock :SPI 总线锁,不需要手动处理。
- SPIADAPTER_plineDevHeader :指向此适配器下挂载的设备链表,不需要手动处理。
SPI 设备结构体详细描述如下:
#include <SylixOS.h>
typedef struct lw_spi_device {
PLW_SPI_ADAPTER SPIDEV_pspiadapter; /* 挂载的适配器 */
LW_LIST_LINE SPIDEV_lineManage; /* 设备挂载链 */
atomic_t SPIDEV_atomicUsageCnt; /* 设备使用计数 */
CHAR SPIDEV_cName[LW_CFG_OBJECT_NAME_SIZE]; /* 设备的名称 */
} LW_SPI_DEVICE;
typedef LW_SPI_DEVICE *PLW_SPI_DEVICE;
- SPIDEV_pi2cadapter :设备挂载的 SPI 总线适配器。
- SPIDEV_lineManage :设备挂载的链表。
- SPIDEV_atomicUsageCnt :设备使用的计数。
- SPIDEV_cName :设备名称。
SPI 消息结构体是 SPI 主机和从机通信的消息格式,该结构体的详细描述如下:
#include <SylixOS.h>
typedef struct lw_spi_message {
UINT16 SPIMSG_usBitsPerOp; /* 操作单位bits数 */
UINT16 SPIMSG_usFlag; /* 传输控制 */
UINT32 SPIMSG_uiLen; /* 长度(缓冲区大小) 长度为0, 只设置传输控制参数 */
UINT8 *SPIMSG_pucWrBuffer; /* 发送缓冲区 */
UINT8 *SPIMSG_pucRdBuffer; /* 接收缓冲区 */
VOIDFUNCPTR SPIMSG_pfuncComplete; /* 传输结束后的回调函数 */
PVOID SPIMSG_pvContext; /* 回调函数参数 */
} LW_SPI_MESSAGE;
typedef LW_SPI_MESSAGE *PLW_SPI_MESSAGE;
- SPIMSG_usBitsPerOp :操作单位 bits 数。
- SPIMSG_usFlag :传输控制参数,其取值见下表。
- SPIMSG_usLen :存放消息内容的缓存区长度,若长度为 0,只设置传输控制参数。
- SPIMSG_pucWrBuffer :发送缓存区。
- SPIMSG_pucusRdBuffer :接收缓存区。
- SPIMSG_pfuncComplete :传输结束后的回调函数。
- SPIMSG_pvContext :回调函数参数。
传输控制参数取值 | 含义 |
---|---|
LW_SPI_M_CPOL_0 | CPOL 配置 0 |
LW_SPI_M_CPOL_1 | CPOL 配置 1 |
LW_SPI_M_CPHA_0 | CPHA 配置 0 |
LW_SPI_M_CPHA_1 | CPHA 配置 1 |
LW_SPI_M_CPOL_EN | 是否设置新的 CPOL 配置 |
LW_SPI_M_CPHA_EN | 是否设置新的 CPHA 配置 |
LW_SPI_M_WRBUF_FIX | 发送缓存区仅发送第一个字节 |
LW_SPI_M_RDBUF_FIX | 接收缓存区仅接收第一个字节 |
LW_SPI_M_MSB | 从高位到低位 |
LW_SPI_M_LSB | 从低位到高位 |
SPI flash 代码分析
一个具体的 SPI 设备驱动以 lw_spi_device 结构体的形式进行组织,用于将设备挂接于 SPI 总线,组织好了后,再完成设备本身所属类型的驱动。下面以 SPI flash 为例介绍 SPI 设备驱动的实现。
SPI flash 是一个挂载在 SPI 总线上的设备,它依靠 SPI 总线的传输函数传递数据。所以在 SPI flash 设备创建时,要调用 API_SpiDeviceCreate 函数将其挂载在一个指定的 SPI 总线适配器上,具体实现如下程序清单所示。函数 API_SpiDeviceCreate 原型如下:
#include <SylixOS.h>
PLW_SPI_DEVICE API_SpiDeviceCreate (CPCHAR pcAdapterName,
CPCHAR pcDeviceName)
函数 API_I2cDeviceTransfer 原型分析:
- 此函数成功返回 pspidevice (SPI 设备结构体类型),失败返回 LW_NULL 。
- 参数 pcAdapterName 是设备挂载的 SPI 适配器名称。
- 参数 pcDeviceName 是设备名称。
函数 API_SpiDeviceBusRequest 的作用是获得指定 SPI 设备的总线使用权,原型如下:
#include <SylixOS.h>
INT API_SpiDeviceBusRequest (PLW_SPI_DEVICE pspidevice)
函数 API_SpiDeviceBusRequest 原型分析:
- 此函数成功返回 ERROR_NONE ,失败返回 PX_ERROR 。
- 参数 pspidevice 是 SPI 设备结构体。
函数 API_SpiDeviceBusRelease 的作用是释放指定 SPI 设备的总线使用权,原型如下:
#include <SylixOS.h>
INT API_SpiDeviceBusRelease (PLW_SPI_DEVICE pspidevice)
函数 API_SpiDeviceBusRelease 原型分析:
- 此函数成功返回 ERROR_NONE ,失败返回 PX_ERROR 。
- 参数 pspidevice 是 SPI 设备结构体。
函数 API_SpiDeviceCtl 的作用是指定 SPI 设备处理指定命令,原型如下:
#include <SylixOS.h>
INT API_SpiDeviceCtl (PLW_SPI_DEVICE pspidevice, INT iCmd, LONG lArg)
函数 API_SpiDeviceCtl 原型分析:
- 此函数成功返回 ERROR_NONE ,失败返回 PX_ERROR 。
- 参数 pspidevice 是 SPI 设备结构体。
- 参数 iCmd 是 SPI 传输控制命令。
- 参数 lArg 是 SPI 传输控制命令的参数。
#include <SylixOS.h>
INT norDevCreate (PCHAR pSpiBusName, UINT uiSSPin)
{
INT iError;
__PSPI_NOR_OBJ pSpi_Nor = &_G_spiNorObj;
if (pSpi_Nor->pSpiNorDev == NULL) {
pSpi_Nor->pSpiNorDev = API_SpiDeviceCreate(pSpiBusName, SPI_NOR_DEVNAME);
}
__ssGpioInit(uiSSPin);
iError = API_SpiDeviceBusRequest(pSpi_Nor->pSpiNorDev);
if (iError == PX_ERROR) {
NOR_DEBUG("Spi Request Bus error\n\r");
return (PX_ERROR);
}
API_SpiDeviceCtl(pSpi_Nor->pSpiNorDev, SPI_MODE_SET, SPI_MODE_0);
/*
* 设置SPI模式为MODE 0
*/
API_SpiDeviceCtl(pSpi_Nor->pSpiNorDev, BAUDRATE_SET, BAUDRATE_MAX / 4); /* 设置波特率 */
API_SpiDeviceCtl(pSpi_Nor->pSpiNorDev, SPI_QUADM_CANCLE, 0);
/*
* 取消4线模式设置
*/
SPI_ENABLE_SS();
__flashNorCmd1Byte(EQPI);
SPI_DISABLE_SS();
API_SpiDeviceCtl(pSpi_Nor->pSpiNorDev, SPI_QUADM_SET, 0);
/*
* 设置4线模式
*/
API_SpiDeviceBusRelease(pSpi_Nor->pSpiNorDev);
/*
* 释放SPI总线
*/
return (ERROR_NONE);
}
如下所示,SPI flash 需要调用 SPI 的传输函数 API_SpiDeviceTransfer 来完成数据的传输,函数 API_SpiDeviceTransfer 的原型如下:
#include <SylixOS.h>
INT API_SpiDeviceTransfer (PLW_SPI_DEVICE pspidevice,
PLW_SPI_MESSAGE pspimsg,
INT iNum)
函数 API_SpiDeviceTransfer 原型分析:
- 此函数成功返回 ERROR_NONE ,失败返回 PX_ERROR 。
- 参数 pspidevice 是 SPI 设备结构体。
- 参数 pspimsg 是 SPI 传输消息结构体组。
- 参数 iNum 是 SPI 传输消息数量。
#include <SylixOS.h>
static INT __flashNorCmd1Byte (UINT8 ucCmd)
{
UINT8 ucTxCmd[1] = {ucCmd};
LW_SPI_MESSAGE spiCmdMessage = {
.SPIMSG_uiLen = 1,
.SPIMSG_pucWrBuffer = ucTxCmd,
.SPIMSG_pucRdBuffer = NULL,
};
API_SpiDeviceTransfer(_G_spiNorObj.pSpiNorDev, &spiCmdMessage, 1);
/*
* 发起传输
*/
return (ERROR_NONE);
}