目的
SPI是单片机中比较常用的一个功能。这篇文章将对CH32V307中相关内容进行说明。
本文使用沁恒官方的开发板 (CH32V307-EVT-R1沁恒RISC-V模块MCU赤兔评估板) 进行演示。
基础说明
SPI的基础概念见下面文章:
SPI基础概念:https://blog.csdn.net/Naisu_kun/article/details/118102844
从上面文章中可以看到SPI主要涉及两条通讯线(MOSI、MISO)、一条时钟线(SCLK)、一条片选线(CS或NSS,非必需)。
除了接线,SPI使用上还需要了解极性和相位的概念,在此之上理解读写时序逻辑等内容。
另在在SPI读数据时需要注意,这时候虽然数据是从机发送的,但时钟还是主机给出的,所以往往读数据时主机还需要通过写数据(任意均可)来给出时钟信号。
使用演示
下面是个简单的使用SPI发送数据的示例:
#include "debug.h"
void SPI1_Master_Init(void) {
// 初始化时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE);
// 初始化GPIO
GPIO_InitTypeDef GPIO_InitStructure = { 0 };
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; // CS
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 这里使用普通推挽输出模式,而不是复用模式,所以下面需要手动控制
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init( GPIOA, &GPIO_InitStructure);
GPIO_SetBits( GPIOA, GPIO_Pin_4); // 拉高不工作
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; // SCK
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init( GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; // MISO
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init( GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; // MOSI
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init( GPIOA, &GPIO_InitStructure);
// 初始化SPI1
SPI_InitTypeDef SPI_InitStructure = { 0 };
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 全双工模式
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 作为主机
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 8bit数据
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 时钟不通讯时低电平
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 在第一个边缘进行采样
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 片选引脚手动控制
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_32; // SPI外设时钟源32分频
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 传输时高比特在前
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStructure); // 初始化SPI1设置
SPI_Cmd(SPI1, ENABLE); // 启动SPI1
}
int main(void) {
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
SystemCoreClockUpdate();
Delay_Init();
USART_Printf_Init(115200);
SPI1_Master_Init(); // 初始SPI1
while(1) {
GPIO_ResetBits( GPIOA, GPIO_Pin_4); // 拉低片选信号,开始SPI通讯
u8 data[4] = {0x00, 0xff, 0x22, 0x33};
for (int i = 0; i < 4; i++) {
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET) { } // 等待可以写数据到发送缓冲
SPI_I2S_SendData(SPI1, data[i]); // 写数据
}
Delay_Us(6); // 写数据并不代表发送完成,发送完成前后不能拉高片选信号
// 目前设置下SPI时钟为 96/32 = 3MHz, 每发送一个字节时间为 8/3 = 2.667us
// 通常为了保险最好等待两个字节以上时间
GPIO_SetBits( GPIOA, GPIO_Pin_4); // 拉高片选信号,结束SPI通讯
Delay_Us(20);
}
}
使用逻辑分析仪抓取上面代码实际运行中的一帧数据如下:
需要注意的是逻辑分析仪的采样速度要远远大于SPI时钟速度,不然解析数据时可能出现一个bit的异常。
下面代码是使用SPI1作为主机,SPI2作为从机的实验,除了上面的发送数据外还进行了数据读取操作:
#include "debug.h"
void SPI1_Master_Init(void) {
// 初始化时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE);
// 初始化GPIO
GPIO_InitTypeDef GPIO_InitStructure = { 0 };
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; // CS
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 这里使用普通推挽输出模式,而不是复用模式,所以下面需要手动控制
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init( GPIOA, &GPIO_InitStructure);
GPIO_SetBits( GPIOA, GPIO_Pin_4); // 拉高不工作
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; // SCK
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init( GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; // MISO
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init( GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; // MOSI
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init( GPIOA, &GPIO_InitStructure);
// 初始化SPI1
SPI_InitTypeDef SPI_InitStructure = { 0 };
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 全双工模式
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 作为主机
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 8bit数据
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 时钟不通讯时低电平
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 在第一个边缘进行采样
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 片选引脚手动控制
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_64; // SPI外设时钟源64分频
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 传输时高比特在前
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStructure); // 初始化SPI1设置
SPI_Cmd(SPI1, ENABLE); // 启动SPI1
}
void SPI2_Slave_Init(void) {
// 初始化时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);
// 初始化GPIO
GPIO_InitTypeDef GPIO_InitStructure = { 0 };
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12; // CS
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; // 片选信号为低才工作
GPIO_Init( GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; // SCK
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init( GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14; // MISO
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init( GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15; // MOSI
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init( GPIOB, &GPIO_InitStructure);
// 初始化SPI2
SPI_InitTypeDef SPI_InitStructure = { 0 };
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 全双工模式
SPI_InitStructure.SPI_Mode = SPI_Mode_Slave; // 作为从机
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 8bit数据
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 时钟不通讯时低电平(和主机保持一致)
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 在第一个边缘进行采样(和主机保持一致)
SPI_InitStructure.SPI_NSS = SPI_NSS_Hard; // 片选信号依赖外部信号
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_64; // 从模式下其实不用关心这个
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 传输时高比特在前
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI2, &SPI_InitStructure); // 初始化SPI2设置
SPI_Cmd(SPI2, ENABLE); // 启动SPI2
}
int main(void) {
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
SystemCoreClockUpdate();
Delay_Init();
USART_Printf_Init(115200);
printf("SystemClk:%d\r\n", SystemCoreClock);
// 接线方式: PA4-PB12 PA5-PB13 PA6-PB14 PA7-PB15
SPI1_Master_Init(); // 初始化SPI1作为主机
SPI2_Slave_Init(); // 初始化SPI2作为从机
while(1) {
u8 data1 = 0;
u8 data2 = 0;
GPIO_ResetBits( GPIOA, GPIO_Pin_4); // 拉低片选信号,开始SPI通讯
while(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET) { }
SPI_I2S_SendData(SPI2, 0x33); // 从机先写一个数据到发送区,这样当主机开始发送消息产生时钟信号时,该数据就会被传输
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET) { }
SPI_I2S_SendData(SPI1, 0x22); // 主机发送数据给从机
while(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET) { } // 等待可以读取接收到的数据
data2 = SPI_I2S_ReceiveData(SPI2); // 读取接收到的数据
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET) { }
data1 = SPI_I2S_ReceiveData(SPI1);
GPIO_SetBits( GPIOA, GPIO_Pin_4); // 拉高片选信号,结束SPI通讯
printf("rx1 = %d, rx2 = %d \r\n", data1, data2);
Delay_Ms(500);
}
}
其它补充
SPI除了最基础的上面查询的方式使用外,本身也可以使用中断方式或者DMA方式来使用。不过一般来说只是作为主机使用的话,因为SPI的速度很快,所以很多时候上面方式就够用了。如果有需要的话中断和DMA的方式使用起来也不难。
总结
单片机本身的SPI外设使用比较简单,真正工作时进行SPI通讯更多的是要能够理解SPI的时序逻辑概念,能够读懂芯片手册上的时序逻辑图。