打印
[RISC-V MCU 应用开发]

第五十二章、CH32V103应用教程——SPI-CRC校验

[复制链接]
2110|5
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
spi, CRC, IO, ni, ST
本帖最后由 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下载验证
将编译好的程序分别在主机模式和从机模式下下载到两个开发版,并将主机的引脚与从机的引脚一一对应进行连接,开发板上电后,串口打印如下:
主机打印:
从机打印:


  

51、SPI-CRC.rar

467.53 KB

使用特权

评论回复

相关帖子

沙发
gejigeji521| | 2021-2-22 12:01 | 只看该作者
工作以来,还没用过CRC,看看怎么用。

使用特权

评论回复
板凳
单片小菜| | 2021-2-23 15:26 | 只看该作者
CRC是8还是CRC16的?

使用特权

评论回复
地板
真爱吴迪迪| | 2021-2-23 15:44 | 只看该作者
说实话,第一次看见SPI的CRC校验的。

使用特权

评论回复
5
RISCVLAR|  楼主 | 2021-2-23 15:56 | 只看该作者

SPI接口提供了两种CRC计算方法,取决于所选的发送或接收的数据帧格式:8位数据帧采用CRC8;16位数据帧采用CRC16。

使用特权

评论回复
6
hezzz| | 2023-2-6 11:42 | 只看该作者
很少见spi crc feature 测试的,学习一下。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

132

主题

293

帖子

41

粉丝