SPI 简介
本模块中 SPI/I2S 接口复用,默认工作在 SPI 模式,可通过配置切换到 I2S 模式。二者都是同步串行接口通讯协议。串行外设接口(SPI)可工作于主机或从机模式,支持全双工、单工高速通信模式,具有硬件 CRC计算并可配置多主模式。片上音频接口(I2S)在单工通讯中可在主、从两种模式运行,支持飞利浦 I2S 标 准、MSB 对齐标准、LSB 对齐标准和 PCM 四种音频标准。
SPI 和 I2S 主要特征
SPI 特征
全双工和单工同步传输
支持主模式、从模式、多主模式
8 或 16 位数据帧格式
数据位顺序可编程
软件或硬件进行 NSS 管理
时钟极性和相位可编程
发送和接收支持硬件 CRC 计算、校验
支持 DMA
I2S 功能
单工同步传输
支持主模式、从模式
支持四种音频标准:飞利浦 I2S 标准、MSB 对齐标准、LSB 对齐标准和 PCM 标准
可配置 8KHz 到 96kHz 的音频采样频率
支持 16 位,24 位或 32 位数据长度、数据帧格式(根据需求配置)
稳定态时钟极性可编程
数据方向总是 MSB 在先
支持 DMA
SPI 共 4 个引脚与外部器件相连:
SCLK:串口时钟,作为主设备的输出,从设备的输入
MISO:主设备输入/从设备输出管脚。在主模式下接收数据,从模式下发送数据。
MOSI:主设备输出/从设备输入管脚。在主模式下发送数据,从模式下接收数据。
NSS:片选管脚。分为内部管脚和外部管脚,内部管脚检测到高电平时,SPI 工作在主模式;反之,工作在从模式。从设备的 NSS 管脚可以由主设备的一个标准 I/O 引脚来控制。
软件 NSS 模式
SPI_CTRL1 寄存器的 SSMEN=1 启用软件从设备管理(见图 22-2)。
该模式下不使用 NSS 管脚,通过写 SPI_CTRL1 的 SSEL 位驱动内部 NSS 信号电平(主模式 SSEL=1,从模
式 SSEL=0)。
硬件 NSS 模式
SPI_CTRL1 寄存器的 SSMEN=0 禁止软件从设备管理。
输入模式:配置为主模式、NSS 输出禁止(MSEL=1, SSOEN=0),允许操作于多主模式。主设备(从设
备)在整个数据帧传输期间应把 NSS 脚外接到高电平(低电平)。
输出模式:配置为主模式、NSS 输出使能(MSEL=1,SSOEN=1),SPI 作为主设备必须将 NSS 拉低,所有
被设置为 NSS 硬件模式并与之相连的的 SPI 设备会检测到低电平,自动进入从设备状态。若主设备不能拉
低 NSS,则进入从模式,并产生主模式失效错误 MODERR 位置‘1’。
注:软件模式或硬件模式的选择,取决于通讯协议中是否需要 NSS 控制。如果不需要,可以选择软件模式,
释放一个 GPIO 管脚另作他用。
SPI 时序模式
CLKPOL 时钟极性和 CLKPHA 时钟相位的组合选择数据捕捉的时钟边沿。配置 SPI_CTRL1 寄存器的
CLKPOL 和 CLKPHA 位,有以下四种时序关系。
CLKPOL=0,CLKPHA=0:SCLK 引脚在空闲状态保持低电平,数据采样时在第一个边沿,即上升沿;
CLKPOL=0,CLKPHA=1:SCLK 引脚在空闲状态保持低电平,数据采样时在第二个边沿,即下降沿;
CLKPOL=1,CLKPHA=0:SCLK 引脚在空闲状态保持高电平,数据采样时在第一个边沿,即下降沿;
CLKPOL=1,CLKPHA=1:SCLK 引脚在空闲状态保持高电平,数据采样时在第二个边沿,即上升沿。
无论采用何种时序模式,必须保证主从配置相同。
图 22-4 是 SPI_CTRL1 寄存器 LSBFF=0 时,SPI 传输的 4 种 CLKPHA 和 CLKPOL 位组合时序。
数据格式
配置 SPI_CTRL1 寄存器中的 LSBFF 位改变数据顺序,LSBFF=0 时,SPI 先发送高位数据(MSB);LSBFF=1
时,SPI 先发送低位数据(LSB)。
配置 SPI_CTRL1 寄存器的 DATFF 位选择 8 位或 16 位数据帧。
SPI 工作模式
主机全双工模式(MSEL=1,BIDIRMODE=0,RONLY=0)
寄存器SPI_DAT(发送缓冲器)写入数据后,传输过程开始。在发送第一位数据的同时,数据被并行地从
发送缓冲器传送到8位的移位寄存器中,SPI根据LSBFF的配置按顺序将数据串行地移位到MOSI管脚上;
同时,在MISO管脚上接收到的数据,按相同顺序被串行地移位进入8位的移位寄存器中,然后被并行地传
送到SPI_DAT寄存器(接收缓冲器)中。软件操作流程如下(见图22-5主机全双工模式下连续传输时,
TE/RNE/BUSY的变化示意图):
- 使能SPI模块,SPIEN置‘1’;
- 将第一个待发送数据写入SPI_DAT寄存器(该操作将标志位TE清零);
- 等待TE置‘1’,将第二个待发送数据写入SPI_DAT。等待RNE置‘1’,读取SPI_DAT得到第一个
接收到的数据,读SPI_DAT的同时RNE位清零。重复以上操作,发送后续的数据同时接收n-1个数
据; - 等待RNE置‘1’,接收最后一个数据;
- 等待TE置‘1’,然后等待BUSY清零,关闭SPI模块。
数据收发的过程也可以在 RNE 或 TE 标志上升沿产生的中断处理程序中实现。
主机单向只发送模式(MSEL=1,BIDIRMODE=0,RONLY=0)
单向只发送模式的传输原理同全双工模式。不同的是,该模式不会读取接收到的数据,故 SPI_STS 寄存器
中的 OVER 位会置’1’,软件无需理会。软件操作流程如下(见图 22-6 主机单向只发送模式下连续传输时,
TE/BUSY 变化示意图):
- 使能SPI模块,SPIEN置‘1’;
- 将第一个待发送数据写入 SPI_DAT 寄存器(该操作将标志位 TE 清零);
主机单向只接收模式(MSEL=1,BIDIRMODE=0,RONLY=1)
主机双向发送模式(MSEL=1,BIDIRMODE=1,BIDIROEN=1,RONLY=0)
主机双向接收模式(MSEL=1,BIDIRMODE=1,BIDIROEN=0,RONLY=0)
从机全双工模式(MSEL=0,BIDIRMODE=0 并且 RONLY=0)
从机单向只发送模式(MSEL=0,BIDIRMODE=0 并且 RONLY=0)
利用 DMA 的 SPI 通信
SPI 利用 DMA 传输数据,将应用程序从读写收发缓冲区的过程中释放出来,大大提高了系统效率。
当发送缓冲区 DMA 使能(SPI_CTRL2 寄存器 TDMAEN=1),每次 TE 被置’1’时发出 DMA 请求,DMA 自
动将数据写入 SPI_DAT 寄存器,该操作会清除 TE 标志。当接收缓冲区 DMA 使能(SPI_CTRL2 寄存器
RDMAEN =1),每次 RNE 被置’1’时发出 DMA 请求,DMA 自动从 SPI_DAT 寄存器读出数据,该操作会
清除 RNE 标志。
当只使用 SPI 发送数据时,只需使能 SPI 的发送 DMA 通道。此时,因为没有读取收到的数据,OVER 被置
为’1’,此时软件无需处理这个标志。
当只使用 SPI 接收数据时,只需使能 SPI 的接收 DMA 通道。
在发送模式下,当 DMA 已经传输了所有要发送的数据(DMA_INTSTS 寄存器的 TXCF 标志变为’1’)后,
可以通过监视 BUSY 标志以确认 SPI 通信结束,这样可以避免在关闭 SPI 或进入停止模式时,破坏最后一
个数据的传输。因此软件需要先等待 TE=1,然后等待 BUSY=0。
#include <stdio.h>
#include "n32g4fr.h"
#include "n32g4fr_gpio.h"
#include "modules.h"
#include "atcmd.h"
#include "errorno.h"
#include "x_stype.h"
#include "bsp_spi.h"
#define SPI_CS_GPIO_CLK RCC_APB2_PERIPH_GPIOA
#define SPI_MOSI_GPIO_CLK RCC_APB2_PERIPH_GPIOA
#define SPI_MISO_GPIO_CLK RCC_APB2_PERIPH_GPIOA
#define SPI_SCK_GPIO_CLK RCC_APB2_PERIPH_GPIOA
#define SPI1_CLK RCC_APB2_PERIPH_SPI1
#define SPI_CS_PIN GPIO_PIN_4
#define SPI_SCK_PIN GPIO_PIN_5
#define SPI_MISO_PIN GPIO_PIN_6
#define SPI_MOSI_PIN GPIO_PIN_7
#define SPI_SCK_GPIO_PORT GPIOA
#define SPI_MISO_GPIO_PORT GPIOA
#define SPI_MOSI_GPIO_PORT GPIOA
#define SPI_CS_GPIO_PORT GPIOA
#define SPI1_BUS SPI1
#define SPI2_BUS SPI2
#define SPI3_BUS SPI3
#define TRUE 1
#define FALSE 0
typedef enum
{
SPI_CS_LOW = 0,
SPI_CS_HIGH,
} spi_cs_level_t;
static void spi_rcc_cfg(bsp_spi_bus_t bus)
{
if (BSP_SPI_BUS1 == bus)
{
RCC_EnableAPB2PeriphClk(
SPI_CS_GPIO_CLK | SPI_MOSI_GPIO_CLK | SPI_MISO_GPIO_CLK | SPI_SCK_GPIO_CLK | RCC_APB2_PERIPH_GPIOD, ENABLE);
RCC_EnableAPB2PeriphClk(SPI1_CLK, ENABLE);
}
}
static void spi_gpio_cfg(bsp_spi_bus_t bus)
{
GPIO_InitType GPIO_InitStructure;
if (BSP_SPI_BUS1 == bus)
{
GPIO_InitStructure.Pin = SPI_SCK_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitPeripheral(SPI_SCK_GPIO_PORT, &GPIO_InitStructure);
GPIO_InitStructure.Pin = SPI_MOSI_PIN;
GPIO_InitPeripheral(SPI_MOSI_GPIO_PORT, &GPIO_InitStructure);
GPIO_InitStructure.Pin = SPI_MISO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitPeripheral(SPI_MISO_GPIO_PORT, &GPIO_InitStructure);
GPIO_InitStructure.Pin = SPI_CS_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitPeripheral(SPI_CS_GPIO_PORT, &GPIO_InitStructure);
}
}
static void bsp_spi_cs_level(bsp_spi_bus_t bus, spi_cs_level_t level)
{
if (BSP_SPI_BUS1 == bus)
{
if (SPI_CS_LOW == level)
{
GPIO_ResetBits(GPIOA, SPI_CS_PIN);
}
else if (SPI_CS_HIGH == level)
{
GPIO_SetBits(GPIOA, SPI_CS_PIN);
}
}
}
void bsp_spi_init(bsp_spi_bus_t bus)
{
SPI_InitType spi_type;
spi_type.DataDirection = SPI_DIR_DOUBLELINE_FULLDUPLEX,
spi_type.SpiMode = SPI_MODE_MASTER,
spi_type.DataLen = SPI_DATA_SIZE_8BITS,
spi_type.CLKPOL = SPI_CLKPOL_HIGH,
spi_type.CLKPHA = SPI_CLKPHA_SECOND_EDGE,
spi_type.NSS = SPI_NSS_SOFT,
spi_type.BaudRatePres = SPI_BR_PRESCALER_4,
spi_type.FirstBit = SPI_FB_MSB,
spi_type.CRCPoly = 7,
spi_rcc_cfg(bus);
spi_gpio_cfg(bus);
bsp_spi_cs_level(bus, SPI_CS_HIGH);
if (BSP_SPI_BUS1 == bus)
{
SPI_Init(SPI1, &spi_type);
SPI_Enable(SPI1, ENABLE);
}
}
int32_t bsp_spi_transmit_byte(bsp_spi_bus_t spi_bus, uint8_t data)
{
return bsp_spi_transmit_bytes(spi_bus, &data, sizeof(uint8_t));
}
int32_t bsp_spi_transmit_bytes(bsp_spi_bus_t spi_bus, uint8_t *tx_buf, int16_t len)
{
int16_t time_out = 0;
if ((NULL == tx_buf) || (len <= 0))
{
return RETVAL(E_NULL);
}
if (BSP_SPI_BUS1 == spi_bus)
{
bsp_spi_cs_level(BSP_SPI_BUS1, SPI_CS_LOW);
while (len--)
{
while (SPI_I2S_GetStatus(SPI1_BUS, SPI_I2S_TE_FLAG) == RESET)
{
time_out++;
if (time_out > 200)
{
return RETVAL(E_SEND);
}
}
SPI_I2S_TransmitData(SPI1_BUS, *tx_buf);
time_out = 0;
while (SPI_I2S_GetStatus(SPI1_BUS, SPI_I2S_RNE_FLAG) == RESET)
{
time_out++;
if (time_out > 200)
{
return RETVAL(E_RECV);
}
}
SPI_I2S_ReceiveData(SPI1_BUS);
tx_buf++;
time_out = 0;
}
bsp_spi_cs_level(BSP_SPI_BUS1, SPI_CS_HIGH);
}
return RETVAL(E_OK);
}
int32_t bsp_spi_transfer(bsp_spi_bus_t spi_bus, uint8_t *tx_buf, int16_t len, uint8_t *rx_buf)
{
int16_t time_out = 0;
if ((NULL == tx_buf) || (NULL == rx_buf) || (len <= 0))
{
return RETVAL(E_NULL);
}
if (BSP_SPI_BUS1 == spi_bus)
{
bsp_spi_cs_level(BSP_SPI_BUS1, SPI_CS_LOW);
while (len--)
{
while (SPI_I2S_GetStatus(SPI1_BUS, SPI_I2S_TE_FLAG) == RESET)
{
time_out++;
if (time_out > 200)
{
return RETVAL(E_SEND);
}
}
SPI_I2S_TransmitData(SPI1_BUS, *tx_buf);
time_out = 0;
while (SPI_I2S_GetStatus(SPI1_BUS, SPI_I2S_RNE_FLAG) == RESET)
{
time_out++;
if (time_out > 200)
{
return RETVAL(E_RECV);
}
}
*rx_buf = SPI_I2S_ReceiveData(SPI1_BUS);
tx_buf++;
rx_buf++;
time_out = 0;
}
bsp_spi_cs_level(BSP_SPI_BUS1, SPI_CS_HIGH);
}
return RETVAL(E_OK);
}
#ifndef __BSP_SPI_H__
#define __BSP_SPI_H__
typedef enum
{
BSP_SPI_BUS1,
BSP_SPI_BUS_NUM,
} bsp_spi_bus_t;
void bsp_spi_init(bsp_spi_bus_t bus);
int32_t bsp_spi_transfer(bsp_spi_bus_t spi_bus, uint8_t *tx_buf, int16_t len, uint8_t *rx_buf);
int32_t bsp_spi_transmit_byte(bsp_spi_bus_t spi_bus, uint8_t data);
int32_t bsp_spi_transmit_bytes(bsp_spi_bus_t spi_bus, uint8_t *tx_buf, int16_t len);
#endif
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)