本章节将与大家一起使用CRC模块进行数据校验。
在数据传输过程中,无论传输系统的设计再怎么完美,差错总会存在,这种差错可能会导致在链路上传输的一个或者多个帧被破坏(出现比特差错,0变为1,或者1变为0),从而接受方接收到错误的数据。为尽量提高接受方收到数据的正确率,在接受方接收数据之前需要对数据进行差错检测,当且仅当检测的结果为正确时接收方才真正收下数据。检测的方式有多种,常见的有奇偶校验、因特网校验和循环冗余校验等。
其中循环冗余校验(CRC)原理实际上就是在一个p位二进制数据序列之后附加一个r位二进制检验码(序列),从而构成一个总长为n=p+r位的二进制序列;附加在数据序列之后的这个检验码与数据序列的内容之间存在着某种特定的关系。如果因干扰等原因使数据序列中的某一位或某些位发生错误,这种特定关系就会被破坏。因此,通过检查这一关系,就可以实现对数据正确性的检验。只要经过严格的挑选,并使用位数足够多的除数 P,那么出现检测不到的差错的概率就很小很小。CRC是一种常用的检错码,无法检测出错误在哪里,因此并不能用于自动纠错,一般的做法是丢弃接收的数据。
从上面的程序框图,我们可以发现,多项式的幂次越高,校验效果越好,但所花费的时间也越长。因此常用查表法或专用的硬件CRC模块来提高效率。
其中查表法,是事先根据特定的校验多项式,算出1字节数据范围所对应的256个余数,将其作为表格,编程写到程序存储器中查询而避免在线运算,已是非常通用的做法。但如果是CRC16校验,存储表格要占512字节(CRC32则需要1 KB),对于有限的单片机ROM资源来说所占比例不小,往往只因为多装了此表,就不得不升级单片机的型号。
在SPIN2x系列MCU中,加入了一个CRC计算单元,使用1个32位数据寄存器作为输入和输出,在执行写操作时输入CRC计算的新数据,在执行读操作时返回上一次CRC计算的结果。每一次写入数据寄存器,都会对整个32位字进行CRC计算,而不是逐字节地计算,从而节省大量的时间。
• 使用CRC-32(以太网)多项式: 0x4C11DB7
• 每次CRC计算需要4个AHB时钟周期
• 在 CRC 计算期间会暂停写操作,因此可以对寄存器 CRC_DR 进行背靠背写入或者连续地写-读操作。
• 可以通过设置寄存器CRC_CTRL的RESET位来重置寄存器 CRC_DR 为 0xFFFFFFFF,该操作不影响8位独立数据寄存器CRC_IDR内的数据。
下面我们来看一下在程序中软件CRC与硬件CRC的配置。
CRC模块的配置步骤如下:
• CRC模块时钟使能
• CRC_CR的第一位RESET位复位(可选)
• 将数据写入CRC_DR寄存器
• 从CRC_DR寄存器中读出计算结果
程序中配置如下:
uint32_t Hardware_CRC(u32*addr, int num) { CRC->CR|=1; //复位 for (; num > 0; num--) CRC->DR = (*addr++); return CRC->DR; }
我们可以使用软件算法来检验计算结果,对比两种方式花费的时间。
软件算法如下:
u32 Software_CRC (u32 *ptr,u32 len) { u32 xbit; u32 data; u32 CRC32 = 0xFFFFFFFF; u32 bits; const u32 dwPolynomial =0x04C11DB7 ; u32 i; for(i = 0;i < len;i++) { xbit = (unsigned)1 <<31; data = ptr[i]; for (bits = 0; bits < 32;bits++) { if (CRC32 & 0x80000000) { CRC32 <<= 1; CRC32 ^= dwPolynomial; } else CRC32 <<= 1; if (data & xbit) CRC32 ^= dwPolynomial; xbit >>= 1; } } return CRC32; }
下面我们用两个简单的数组,一个数组从0x00递增到0x7F,另一个数组从0x7F递减到0x00,分别使用硬件CRC和软件CRC计算,同时使用TIM1进行计时,最后通过UART输出得到的校验码和花费的时间。
计算和输出程序:
void CRCTest() { unsigned int i; u32 CRCtime,CRCresault; u32 crc1[128]; for(i=0;i<128;i++) crc1[i] = i; printf("CRC_test\r\n"); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_CRC,ENABLE);//使能CRC时钟 CRC->CR|=1; //复位 printf("\r\nCRC_DR=%x\t\r\n",CRC->DR);//输出复位值 TIM1->CNT &= 0; CRCresault=Hardware_CRC(crc1,128); CRCtime =TIM1->CNT; printf("Hardware_CRC1:resault=%08x\ttime=%d\r\n",CRCresault,CRCtime); TIM1->CNT &= 0; CRCresault=Software_CRC(crc1,128); CRCtime =TIM1->CNT; printf("Software_CRC1:resault=%08x\ttime=%d\r\n",CRCresault,CRCtime); for(i=0;i<256;i++) crc1[i] = 0xFF-i; TIM1->CNT &= 0; CRCresault=Hardware_CRC(crc1,128); CRCtime =TIM1->CNT; printf("Hardware_CRC2:resault=%08x\ttime=%d\r\n",CRCresault,CRCtime); TIM1->CNT &= 0; CRCresault=Software_CRC(crc1,128); CRCtime =TIM1->CNT; printf("Software_CRC2:resault=%08x\ttime=%d\r\n",CRCresault,CRCtime); }
定时器配置程序:
void Tim1_UPCount_test(u16Prescaler,u16 Period) { TIM_TimeBaseInitTypeDefTIM_StructInit; /*使能TIM1时钟,默认时钟源为PCLK2(PCLK2未分频时不倍频,否则由PCLK2倍频输出),可选其它时钟源*/ RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1,ENABLE); TIM_StructInit.TIM_Period=Period; //ARR寄存器值 TIM_StructInit.TIM_Prescaler=Prescaler; //预分频值 /*数字滤波器采样频率,不影响定时器时钟*/ TIM_StructInit.TIM_ClockDivision=TIM_CKD_DIV1; //采样分频值 TIM_StructInit.TIM_CounterMode=TIM_CounterMode_Up; //计数模式 TIM_StructInit.TIM_RepetitionCounter=0; TIM_TimeBaseInit(TIM1,&TIM_StructInit); TIM_Cmd(TIM1, ENABLE); /*更新定时器时会产生更新时间,清除标志位*/ TIM_ClearFlag(TIM1,TIM_FLAG_Update); }
Main函数:
int main(void) { Tim1_UPCount_test(48-1,0xffff); //APB2时钟为48M,48分频后TIM1时钟为1MHz delay_init(); uart_initwBaudRate(9600); //初始化UART CRCTest(); while(1) { } }
从结果中,我们可以看到,对于这两个数组,硬件CRC与软件CRC所得到的结果一致,但是硬件CRC每个数组只花费了52us,远小于软件CRC的2.5ms,由此可见,在进行大量数据处理的时候,使用硬件CRC模块可以节省大量的时间,同时保证了计算结果的正确。
来源:灵动MM32MCU