本帖最后由 RISCVLAR 于 2021-1-13 17:21 编辑
CH32V103应用教程——SPI-CRC校验
本章教程主要在SPI通信方式下使用CRC校验以保证通信的可靠性。
1、SPI简介及相关函数介绍 关于SPI CRC校验,其用于保证全双工通信的可靠性。数据的发送和接收分别使用单独的CRC计算器。通过对每一个接收位进行可编程的多项式运算来计算CRC。CRC的计算是在由SPI控制寄存器1(SPIx_CTLR1)中的CPHA和CPOL位定义的采样时钟边沿进行的。 注意:该SPI接口提供了两种CRC计算方法,取决于所选的发送和/或接收的数据帧格式:8位数据帧采用CR8;16位数据帧采用CRC16。 CRC计算是通过设置SPI控制寄存器1(SPIx_CTLR1)中的CRCEN位启动的。设置CRCEN位时同时复位SPI接收CRC寄存器和SPI发送CRC寄存器(SPIx_RCRCR和SPI_TCRCR)。当设置了SPI控制寄存器1(SPIx_CTLR1)的CRCNEXT位, SPI_TCRCR的内容将在当前字节发送之后发出。 在传输SPI_TCRCR的内容时,如果在移位寄存器中收到的数值与SPI_RCRCR的内容不匹配,则SPI_SR寄存器的CRCERR标志位被置1。 如果在TX缓冲器中还有数据, CRC的数值仅在数据字节传输结束后传送。在传输CRC期间,CRC计算器关闭,寄存器的数值保持不变。
SPI通信可以通过以下步骤使用CRC:
● 设置CPOL、CPHA、LSBFIRST、BR、SSM、SSI和MSTR的值; ● 在SPI_CRCR寄存器输入多项式; ● 通过设置SPIx_CTLR1寄存器CRCEN位使能CRC计算,该操作也会清除寄存器SPI_RCRCR和SPI_TCRCR; ● 设置SPIx_CTLR1寄存器的SPE位启动SPI功能; ● 启动通信并且维持通信,直到只剩最后一个字节或者半字; ● 在把最后一个字节或半字写进发送缓冲器时,设置SPIx_CTLR1的CRCNEST位,指示硬件在发送完成最后一个数据之后,发送CRC的数值。在发送CRC数值期间,停止CRC计算; ● 当最后一个字节或半字被发送后,SPI发送CRC数值, CRCNEST位被清除。同样,接收到的CRC与SPI_RCRCR值进行比较,如果比较不相配,则设置SPI_SR上的CRCERR标志位,当设置了SPIx_CTLR2寄存器的ERRIE时,则产生中断。 注意:当SPI模块处于从设备模式时,请注意在时钟稳定之后再使能CRC计算,否则可能会得到错误的CRC计算结果。事实上,只要设置了CRCEN位,只要在SCK引脚上有输入时钟,不管SPE位的状态,都会进行CRC的计算。 当SPI时钟频率较高时,用户在发送CRC时必须小心。在CRC传输期间,使用CPU的时间应尽可能少;为了避免在接收最后的数据和CRC时出错,在发送CRC过程中应禁止函数调用。必须在发送/接收最后一个数据之前完成设置CRCNEXT位的操作。 当SPI时钟频率较高时,因为CPU的操作会影响SPI的带宽,建议采用DMA模式以避免SPI降低的速度。 当CH32V103配置为从模式并且使用了NSS硬件模式,NSS引脚应该在数据传输和CRC传输期间保持为低。 当配置SPI为从模式并且使用CRC的功能,即使NSS引脚为高时仍然会执行CRC的计算(译注:当NSS信号为高时,如果SCK引脚上有时钟脉冲,则CRC计算会继续执行)。例如:当主设备交替与多个从设备进行通信时,将会出现这种情况(译注:此时要想办法避免CRC的误操作)。 在不选中一个从设备(NSS信号为高)转换到选中一个新的从设备(NSS信号为低)的时候,为了保持主从设备端下次CRC计算结果的同步,应该清除主从两端的CRC数值。 按照下述步骤清除CRC数值: 1. 关闭SPI模块(SPE=0); 2. 清除CRCEN位为’0’; 3. 设置CRCEN位为’1’; 4. 使能SPI模块(SPE=1)。
2、硬件设计 本章教程主要在第46章基础上进行,需用到两个开发板,主设备使用MOSI引脚,从设备使用MISO引脚进行通讯。此处使用外设为SPI1,主设备MOSI对应引脚为PA7引脚,从设备MISO对应引脚为PA6引脚,将主设备PA7引脚与从设备PA6引脚连接起来,此外还需将两个开发板SPI1对应的SCK引脚PA5连接起来。 此外,由于两个开发板需要同时进行上电传输,因此将两个开发板的3.3V引脚和GND引脚进行连接。
3、软件设计 本章教程主要在第46章基础上进行,增加了CRC校验步骤,具体程序如下: spi.h文件 #ifndef __SPI_H
#define __SPI_H
#include "ch32v10x_conf.h"
/* SPI Mode Definition */
#define HOST_MODE 0
#define SLAVE_MODE 1
/* SPI Communication Mode Selection */
#define SPI_MODE HOST_MODE
//#define SPI_MODE SLAVE_MODE
#define Size 4
extern u16 TxData[Size];
extern u16 RxData[Size];
void SPI_1Lines_HalfDuplex_Init(void);
#endif
spi.c文件 #include "spi.h"
/* Global Variable */
u16 TxData[Size] = { 0x01, 0x02, 0x03, 0x04};
u16 RxData[Size];
/*******************************************************************************
* Function Name : SPI_1Lines_HalfDuplex_Init
* Description : Configuring the SPI for half-duplex communication.
* Input : None
* Return : None
*******************************************************************************/
void SPI_1Lines_HalfDuplex_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE );
#if (SPI_MODE == HOST_MODE)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
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_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init( GPIOA, &GPIO_InitStructure );
#elif (SPI_MODE == SLAVE_MODE)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init( GPIOA, &GPIO_InitStructure );
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init( GPIOA, &GPIO_InitStructure );
#endif
#if (SPI_MODE == HOST_MODE)
SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
#elif (SPI_MODE == SLAVE_MODE)
SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Rx;
SPI_InitStructure.SPI_Mode = SPI_Mode_Slave;
#endif
SPI_InitStructure.SPI_DataSize = SPI_DataSize_16b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_64;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init( SPI1, &SPI_InitStructure );
SPI_CalculateCRC( SPI1, ENABLE ); //开启CRC计算
SPI_Cmd( SPI1, ENABLE );
}
spi.c文件主要包括1个函数:SPI_1Lines_HalfDuplex_Init函数。SPI_1Lines_HalfDuplex_Init函数主要进行主机发送从机接收配置。在这种配置下,主机需要用到SCK引脚和MOSI引脚,从机需要用到SCK引脚和MISO引脚,此处,由于用到SPI1,对应SCK引脚为PA5,MOSI引脚为PA7,MISO引脚为PA6,因此,在SPI_1Lines_HalfDuplex_Init函数中首先对主机和从机对应GPIO引脚进行初始化配置,此处需要注意,由于是主机发送从机接收,需要将PA7设置为复用推挽输出模式,PA6设置为浮空输入模式。此外,由于主机作为发送,从机作为接收,此处在SPI_1Lines_HalfDuplex_Init函数中需要对SPI进行主机发送和从机接收初始化配置,此配置可根据CH32V103应用手册主模式和从模式配置步骤进行,主要对SPI通信的通信方向、主从模式、数据帧大小、时钟极性、时钟相位、NSS引脚使用方式、波特率等进行配置,可对照手册参考标准库函数ch32v10x_spi.c文件中SPI_Init函数进行配置。 main.c文件 /********************************** (C) COPYRIGHT *******************************
* File Name : main.c
* Author : WCH
* Version : V1.0.0
* Date : 2020/04/30
* Description : Main program body.
*******************************************************************************/
#include "debug.h"
#include "spi.h"
/*
*@Note
使用CRC错误校验,Master/Slave 模式收发例程:
Master:SPI1_SCK(PA5)、SPI1_MOSI(PA7)。
Slave :SPI1_SCK(PA5)、SPI1_MISO(PA6)。
本例程演示使用CRC错误校验,Master 发,Slave 收。
注:两块板子分别下载 Master 和 Slave 程序,同时上电。
硬件连线:PA5 —— PA5
PA7 —— PA6
*/
/*******************************************************************************
* Function Name : main
* Description : Main program.
* Input : None
* Return : None
*******************************************************************************/
int main(void)
{
u8 i=0,crcval;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
Delay_Init();
USART_Printf_Init(115200);
printf("SystemClk:%d\r\n",SystemCoreClock);
SPI_1Lines_HalfDuplex_Init();
#if (SPI_MODE == SLAVE_MODE)
printf("SLAVE Mode\r\n");
Delay_Ms(1000);
#endif
#if (SPI_MODE == HOST_MODE)
printf("HOST Mode\r\n");
Delay_Ms(2000);
#endif
while(1)
{
#if (SPI_MODE == HOST_MODE)
while( i<4 )
{
if( i<3 )
{
if( SPI_I2S_GetFlagStatus( SPI1, SPI_I2S_FLAG_TXE ) != RESET ) //发送缓冲区非空
{
SPI_I2S_SendData( SPI1, TxData[i] ); //发送数据
i++;
}
}
else
{
if( SPI_I2S_GetFlagStatus( SPI1, SPI_I2S_FLAG_TXE ) != RESET ) //发送缓冲区非空
{
//SPI_TransmitCRC函数通过控制SPIx_CTLR1寄存器的CRCNEXT位实现,当将CRCNEXT位置1,SPIx_TCRCR寄存器的内容将在当前字节发送之后发出。
SPI_TransmitCRC( SPI1 );
SPI_I2S_SendData( SPI1, TxData[i] ); //发送数据
i++;
}
}
}
while( SPI_I2S_GetFlagStatus( SPI1, SPI_I2S_FLAG_TXE ) != RESET ) //发送缓冲区非空
{
if(SPI_I2S_GetFlagStatus( SPI1, SPI_I2S_FLAG_BSY ) == RESET) //SPI不在通讯状态
{
SPI_Cmd( SPI1, DISABLE );
crcval = SPI_GetCRC( SPI1, SPI_CRC_Tx ); //获取SPI1发送CRC寄存器的值
printf( "CRC:%02x\r\n", crcval );
while(1);
}
}
#elif (SPI_MODE == SLAVE_MODE)
while( i<4 )
{
if( i<3 )
{
if( SPI_I2S_GetFlagStatus( SPI1, SPI_I2S_FLAG_RXNE ) != RESET ) //接收缓冲区为空
{
RxData[i] = SPI_I2S_ReceiveData( SPI1 ); //接收数据
i++;
}
}
else if( i == 3)
{
//在发送或者接收最后一个数据之前需将SPI控制寄存器1(SPIx_CTLR1)CRCNEXT位置1
SPI_TransmitCRC( SPI1 );
if( SPI_I2S_GetFlagStatus( SPI1, SPI_I2S_FLAG_RXNE ) != RESET )
{
RxData[i] = SPI_I2S_ReceiveData( SPI1 );
i++;
}
}
else
{
if( SPI_I2S_GetFlagStatus( SPI1, SPI_I2S_FLAG_RXNE ) != RESET )
{
RxData[i] = SPI_I2S_ReceiveData( SPI1 );
i++;
}
}
}
crcval = SPI_GetCRC( SPI1, SPI_CRC_Rx ); //获取SPI1接收CRC寄存器的值
printf( "CRC:%02x\r\n", crcval );
for( i=0; i<4; i++ )
{
printf( "Rxdata:%02x\r\n", RxData[i] );
}
while(1);
#endif
}
}
main.c文件主要进行主机和从机下的数据发送和接收。并将接收数据与发送数据进行对比,当发送数据与接收数据相同,输出same,若不同,输出different。
4、下载验证 将编译好的程序分别在主机模式和从机模式下下载到两个开发版,并将主机的引脚与从机的引脚一一对应进行连接,开发板上电后,串口打印如下: 主机打印: 从机打印:
|