学习STM32中的CRC校验算法–详细笔记
进行CRC校验的原因
CRC(循环冗余校验)是一种在数据传输或存储过程中常用的错误检测技术。它通过计算数据的校验值,并将其附加到原始数据中,以便在接收端验证数据的完整性和准确性。
以下是一些进行CRC校验的原因:
1. 数据完整性验证:在数据传输过程中,数据可能会受到干扰、损坏或篡改,而CRC校验可以帮助检测这些错误,确保数据在传输过程中没有发生变化。
2. 错误检测:CRC校验可以有效地检测到数据传输过程中发生的单比特错误、多比特错误或位错。
3. 数据识别:CRC校验码可以帮助识别数据是否在传输过程中发生了意外的变化,从而防止数据被恶意篡改。
4. 数据完整性保护:通过在数据中添加CRC校验码,可以提高数据的完整性和可靠性,确保数据在传输或存储过程中不会受到破坏。
总的来说,CRC校验是一种有效的数据完整性保护机制,可以在数据传输或存储过程中提供额外的安全性和可靠性保障。
一、奇偶校验
奇校验为例,如果数据中1的个数为奇数,则奇校验位0,否则为1。
例如原始数据为:0001 0011,数据中1的个数(或各位相加)为3,所以奇校验位为0。这种校验方法很简单,但这种校验方法有很大的误码率。假设由于传输过程中的干扰,接收端接收到的数据是0010 0011,通过奇校验运算,得到奇校验位的值为0,虽然校验通过,但是数据已经发生了错误。
二、循环冗余校验(CRC)
循环冗余校验的核心数学算法原理基于循环码,在不增加原始数据的信息基础上扩展了信息,以极小的存储代价存储其冗余特征。
1.算法原理及处理过程
多项式
其本质就是多进制的数学表示法,这里是二进制,故X的值为2。
XOR异或操作
模二加法:0+0=0 0+1=1 1+0=1 1+1=0;
模二减法:0-0=0 0-1=1 1-0=1 1-1=0;
算法处理过程
待发送有效数据为二进制多项式M(x),而校验多项式P(x)为收发双方约定好了的,双方已知。
计算过程
2.计算实例
计算冗余码C(X)
验证
余数为0则校验通过,否则校验不通过
三、硬件CRC校验(32位)
以下部分未声明的都基于硬件:STM32F103C8T6 软件:Keil5 函数:HAL库
也有可能会使用其他硬件或者不同的库编程,我会提前注明。
1.硬件CRC校验的定义
硬件CRC校验是一种通过专用的硬件模块来计算数据的循环冗余校验(CRC)值的方法。在微控制器中,通常会集成一个硬件CRC模块,它可以在硬件层面上执行CRC校验,而不需要CPU的直接干预。
硬件CRC校验通常具有以下优点:
1. 高效性:硬件CRC模块通常能够以较高的速度执行CRC校验操作,这是因为它们使用了专用的硬件电路来执行计算,而不需要CPU参与。
2. 低功耗:由于硬件CRC模块可以在不占用CPU资源的情况下执行计算,因此它们通常能够在低功耗模式下执行CRC校验操作。
3. 精确性:硬件CRC模块通常能够以固定的时钟速率执行CRC校验操作,这有助于提高校验结果的准确性。
4. 灵活性:一些硬件CRC模块允许用户配置CRC多项式、初始值和输入/输出数据的位序,从而使得它们适用于不同的应用场景。
在嵌入式系统中,硬件CRC校验通常被用于数据通信、存储器校验、数据完整性验证等方面。通过利用硬件CRC模块,可以加速CRC校验过程,并减少对CPU资源的占用,从而提高系统性能和效率。
2.硬件CRC单次校验计算
实验步骤
本节实验利用硬件CRC计算单元,使用HAL_CRC_Calculate(按字传)函数,对数据进行单次计算,然后在网上找个在线的CRC计算工具(CRC(循环冗余校验)在线计算_ip33.com),二者数据进行对比,看是否一样。
代码
crc.c
#include "stm32f1xx_hal.h"
// 定义一个CRC_HandleTypeDef结构体的实例,用于管理CRC模块
CRC_HandleTypeDef crc;
// CRC初始化函数,用于配置CRC模块的参数并初始化
void CRC_Init(void)
{
// 设置CRC_HandleTypeDef结构体实例的Instance成员为CRC外设的地址
crc.Instance = CRC;
// 调用HAL库提供的函数,初始化CRC模块
HAL_CRC_Init(&crc);
}
// CRC模块的初始化回调函数
void HAL_CRC_MspInit(CRC_HandleTypeDef *hcrc)
{
// 启用CRC外设的时钟,以便可以使用CRC功能
__HAL_RCC_CRC_CLK_ENABLE();
// 这个回调函数会在HAL_CRC_Init()函数中被调用
// 在这里,我们只需要开启CRC的时钟即可,不需要做其他的初始化工作
}
crc.h
#ifndef __CRC_H
#define __CRC_H
void CRC_Init(void); // 声明 CRC 初始化函数
extern CRC_HandleTypeDef crc; // 声明 CRC 外设的句柄变量,该变量在其他文件中定义
#endif
main.c
#include "stm32f1xx_hal.h" // 包含STM32 HAL库的头文件
#include "rcc.h" // 包含RCC模块的头文件
#include "uart.h" // 包含UART模块的头文件
#include "crc.h" // 包含CRC模块的头文件
uint32_t buff[4] = {0x01020304, 0x05060708, 0x090A0B0C, 0x0D0E0F00}; // 定义一个包含4个32位数据的数组
int main(void)
{
HAL_Init(); // 初始化HAL库
RccClock_Init(); // 初始化系统时钟
U1_Init(9600); // 初始化UART1串口,波特率设置为9600
CRC_Init(); // 初始化CRC模块
// 使用CRC模块计算数据缓冲区buff的CRC校验值,并通过UART打印输出
u1_printf("%x\r\n", HAL_CRC_Calculate(&crc, buff, 4));
while (1)
{
// 程序进入主循环,持续运行
}
}
实验结果
每按下一次复位键都会输出一次CRC的校验结果,对比之后发现CRC结果与CRC计算器()一致。
3.硬件CRC连续校验计算
内存量不够的时候分多次连续校验。
实验步骤
本节实验我们在上一节单次计算的基础上,利用HAL_CRC_Accumulate函数进行连续多次计算。
注意:连续多次计算时,第一次用:HAL CRC Calculate函数(初始值就是硬件CRC的固定初始值)后面计算用HAL CRC Accumulate函数(将上一次的计算结果作为本次初始值)。
代码
只需要修改main.c部分。
main.c
#include "stm32f1xx_hal.h" // 包含STM32 HAL库的头文件
#include "rcc.h" // 包含RCC模块的头文件
#include "uart.h" // 包含UART模块的头文件
#include "crc.h" // 包含CRC模块的头文件
uint32_t buff[4] = {0x01020304, 0x05060708, 0x090A0B0C, 0x0D0E0F00}; // 四个字
int main(void)
{
HAL_Init(); // 初始化HAL库
RccClock_Init(); // 初始化系统时钟
U1_Init(9600); // 初始化UART1串口,波特率设置为9600
CRC_Init(); // 初始化CRC模块
// 单次校验
u1_printf("%x\r\n", HAL_CRC_Calculate(&crc, buff, 4));
// 重置CRC模块的状态
crc.Instance->CR |= CRC_CR_RESET;
// 连续校验
HAL_CRC_Calculate(&crc, &buff[0], 1); // 对第一个数据进行单独校验
HAL_CRC_Accumulate(&crc, &buff[1], 1); // 累加第二个数据
HAL_CRC_Accumulate(&crc, &buff[2], 1); // 累加第三个数据
u1_printf("%x\r\n", HAL_CRC_Accumulate(&crc, &buff[3], 1)); // 对第四个数据进行校验并输出结果
while (1)
{
// 主循环,程序会一直停留在这里
}
}
实验结果

4.硬件CRC校验(接收数据校验后再发送)
试验步骤
在程序中添加函数,我们手动发送数据,串口接收之后进行CRC硬件校验,然后再将其校验结果发送出来。
代码
只需要修改main.c部分即可
main.c
#include "stm32f1xx_hal.h" // 包含STM32 HAL库的头文件
#include "rcc.h" // 包含RCC模块的头文件
#include "uart.h" // 包含UART模块的头文件
#include "crc.h" // 包含CRC模块的头文件
#define BUFFER_SIZE 4 // 定义数据缓冲区大小为4字节
uint32_t buff[BUFFER_SIZE]; // 数据缓冲区,存放接收到的数据
// 将32位数据的字节顺序调整为小端序,并打印出来
void print_little_endian(uint32_t data) {
uint8_t *ptr = (uint8_t *)&data;
u1_printf("%02X%02X%02X%02X ", ptr[0], ptr[1], ptr[2], ptr[3]);
}
int main(void)
{
HAL_Init(); // 初始化HAL库
RccClock_Init(); // 初始化系统时钟
U1_Init(9600); // 初始化UART1串口,波特率设置为9600
CRC_Init(); // 初始化CRC模块
while (1)
{
// 等待接收数据
while (HAL_UART_Receive(&huart1, (uint8_t *)buff, sizeof(buff), HAL_MAX_DELAY) != HAL_OK);
// 打印接收到的数据(按照小端序)
for (int i = 0; i < BUFFER_SIZE; i++) {
print_little_endian(buff[i]);
}
u1_printf("\r\n");
// 将小端序数据转换为大端序
for (int i = 0; i < BUFFER_SIZE; i++) {
buff[i] = __REV(buff[i]);
}
// 计算CRC校验值并通过UART发送
u1_printf("CRC: %x\r\n", HAL_CRC_Calculate(&crc, (uint32_t *)buff, BUFFER_SIZE));
}
}
实验结果
接收数据和CRC校验结果都正确。
数据的处理过程
-
初始化:
- 调用
HAL_Init()
初始化 HAL 库。 - 调用
RccClock_Init()
初始化系统时钟。 - 调用
U1_Init(9600)
初始化 UART1 串口,设置波特率为 9600。 - 调用
CRC_Init()
初始化 CRC 模块。 -
接收数据:
- 进入
while(1)
主循环后,通过HAL_UART_Receive()
函数等待接收数据。一旦接收到数据,将其存储到名为buff
的数据缓冲区中。 - 接收的数据长度为
BUFFER_SIZE
个 32 位字(4字节),这是通过sizeof(buff)
的方式得到的。 - 此循环使用
HAL_MAX_DELAY
作为超时参数,表示无限等待,直到成功接收到数据为止。 -
打印接收到的数据:
- 使用
print_little_endian()
函数按照小端序打印接收到的数据。在这个函数中,将每个 32 位字节数据拆分为 4 个字节,并按照小端序顺序打印。 - 每个字节打印为两位十六进制数,并以空格分隔,形成一行数据。
- 在每次接收到完整的数据后,会打印一行数据,然后换行。
-
进行 CRC 校验:
- 使用
HAL_CRC_Calculate()
函数对buff
数组中的数据进行 CRC 校验。 - 将计算得到的 CRC 校验值通过 UART 发送出去。
5.CubeMax配置硬件CRC校验
在此只需要配置好时钟、串口和CRC即可。
创建工程


配置时钟RCC

SYS
UART


CRC

文件配置



代码
在程序中我只修改了main.c部分,把打印函数插入生成的程序中
注意:自己写的程序写入在有begin和end的之间,否则再次用Cubemax时会删除我们所写的程序。
main.c
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* <h2><center>© Copyright (c) 2024 STMicroelectronics.
* All rights reserved.</center></h2>
*
* This software component is licensed by ST under BSD 3-Clause license,
* the "License"; You may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
* opensource.org/licenses/BSD-3-Clause
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "usart.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
uint32_t buff[4] = {0x01020304, 0x05060708, 0x090A0B0C, 0x0D0E0F00}; // 四个字
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
u1_printf("%x\r\n", HAL_CRC_Calculate(&crc, buff, 4));
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Initializes the CPU, AHB and APB busses clocks
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB busses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
tex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/
实验结果

注意:串口波特率这次设置的是115200,把串口助手的波特率设置的与程序一致才可以试验成功。
四、软件CRC校验
软件CRC校验的原因
在本文章中我们使用的硬件为STM32F103C8T6,在这个芯片中,硬件CRC校验只可以实现32位的CRC校验,如果工程的通讯要求是16位或者8位的,就只能用软件CRC校验了,以下我也做了16位和8位的程序烧录,在过程中会改变多项式、初始值、异或值或者输入输出的数据反转。
当然如果选用的芯片本就可以实现16位或者8位的CRC校验来满足要求,那就可以直接配置硬件CRC来完成,毕竟硬件CRC的数据校验要更快。
1.软件CRC单次校验计算
实验步骤
按字节计算,利用while循环一个字节一个字节计算。(硬件CRC校验是按字来校验)
软件CRC校验计算多项式初值可以自由设定,灵活性高。
代码步骤:
(1)取一个字节数据,因为是CRC32,将该字节左移到最高处。
(2)左移到最高处后和初值异或,结果变为新初值。
(3)利用for循环,循环8次,处理新初值中每个二进制位,如果是1,新初值左移1位后和多项式异或,结果是再新一次的初值,如果是0,仅左移,不同多项式异或。
(4)再去取下一个字节重复操作,直到while循环处理完所有数据,将最终的新初值最为校验结果值返回。
代码
crc.c
#include "stm32f1xx_hal.h"
// 初始化CRC
CRC_HandleTypeDef crc;
void CRC_Init(void)
{
crc.Instance = CRC;
HAL_CRC_Init(&crc);
}
// CRC模块的初始化回调函数
void HAL_CRC_MspInit(CRC_HandleTypeDef *hcrc)
{
__HAL_RCC_CRC_CLK_ENABLE(); // 开启CRC模块的时钟
}
// 计算CRC32校验值
// 参数:
// - data: 待计算CRC的数据指针
// - len: 数据长度
// - init: CRC初始值
// 返回值:
// - uint32_t类型的CRC32校验值
uint32_t CRC32(uint8_t *data, uint16_t len, uint32_t init)
{
uint32_t poly = 0x04C11DB7; // CRC32多项式
uint8_t i;
// 遍历数据字节,计算CRC32校验值
while (len--)
{
init = init ^ (*data << 24);
for (i = 0; i < 8; i++)
{
if (init & 0x80000000) // 如果当前最高位是1
{
init = (init << 1) ^ poly; // 左移一位并与多项式异或
}
else // 如果当前最高位是0
{
init = (init << 1); // 左移一位
}
}
data++; // 指向下一个数据字节
}
return init; // 返回计算得到的CRC32校验值
}
crc.h
#ifndef __CRC_H
#define __CRC_H
#include "stm32f1xx_hal_crc.h"
extern CRC_HandleTypeDef crc; // CRC模块句柄声明
// CRC模块初始化函数声明
void CRC_Init(void);
// CRC32校验函数声明
// 参数:
// - data: 待计算CRC的数据指针
// - len: 数据长度
// - init: CRC初始值
// 返回值:
// - uint32_t类型的CRC32校验值
uint32_t CRC32(uint8_t *, uint16_t, uint32_t);
#endif
main.c
#include "stm32f1xx_hal.h" // 包含STM32 HAL库的头文件
#include "rcc.h" // 包含RCC模块的头文件
#include "uart.h" // 包含UART模块的头文件
#include "crc.h" // 包含CRC模块的头文件
uint32_t buff[4] = {0x01020304, 0x05060708, 0x090A0B0C, 0x0D0E0F00}; // 定义一个包含4个32位数据的数组
uint8_t buff2[16] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,0}; // 定义一个包含16个字节数据的数组
int main(void)
{
HAL_Init(); // 初始化HAL库
RccClock_Init(); // 初始化系统时钟
U1_Init(9600); // 初始化UART1串口,波特率设置为9600
CRC_Init(); // 初始化CRC模块
// 使用CRC模块计算数据缓冲区buff的CRC校验值,并通过UART打印输出
u1_printf("单次校验结果:%x\r\n", HAL_CRC_Calculate(&crc, buff, 4));
// 对数据缓冲区buff的每个元素逐个进行CRC校验累加
HAL_CRC_Calculate(&crc, &buff[0], 1); // 对第1个元素进行CRC校验
HAL_CRC_Accumulate(&crc, &buff[1], 1); // 对第2个元素进行CRC校验累加
HAL_CRC_Accumulate(&crc, &buff[2], 1); // 对第3个元素进行CRC校验累加
u1_printf("连续校验结果:%x\r\n", HAL_CRC_Accumulate(&crc, &buff[3], 1)); // 对第4个元素进行CRC校验累加并打印输出
// 使用CRC32函数计算数据缓冲区buff2的CRC32校验值,并通过UART打印输出
u1_printf("CRC32校验结果:%x\r\n", CRC32(buff2, 16, 0xFFFFFFFF));
while (1)
{
// 程序进入主循环,持续运行
}
}
实验结果
在软件校验的初始值和生成多项式和硬件CRC相同时,软件CRC校验的结果和硬件CRC校验以及CRC计算器校验结果相同。
2.软件CRC连续校验计算
代码
只需要修改main.c部分即可。
main.c
#include "stm32f1xx_hal.h" // 包含STM32 HAL库的头文件
#include "rcc.h" // 包含RCC模块的头文件
#include "uart.h" // 包含UART模块的头文件
#include "crc.h" // 包含CRC模块的头文件
uint32_t buff[4] = {0x01020304, 0x05060708, 0x090A0B0C, 0x0D0E0F00}; // 定义一个包含4个32位数据的数组
uint8_t buff2[16] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0}; // 定义一个包含16个字节数据的数组
uint32_t res; // 存储CRC32计算结果
int main(void)
{
HAL_Init(); // 初始化HAL库
RccClock_Init(); // 初始化系统时钟
U1_Init(9600); // 初始化UART1串口,波特率设置为9600
CRC_Init(); // 初始化CRC模块
// 使用CRC模块计算数据缓冲区buff的CRC校验值,并通过UART打印输出
u1_printf("单次校验结果:%x\r\n", HAL_CRC_Calculate(&crc, buff, 4));
// 对数据缓冲区buff的每个元素逐个进行CRC校验累加
HAL_CRC_Calculate(&crc, &buff[0], 1); // 对第1个元素进行CRC校验
HAL_CRC_Accumulate(&crc, &buff[1], 1); // 对第2个元素进行CRC校验累加
HAL_CRC_Accumulate(&crc, &buff[2], 1); // 对第3个元素进行CRC校验累加
u1_printf("连续校验结果:%x\r\n", HAL_CRC_Accumulate(&crc, &buff[3], 1)); // 对第4个元素进行CRC校验累加并打印输出
// 使用CRC32函数计算数据缓冲区buff2的CRC32校验值,并通过UART打印输出
u1_printf("软件CRC32单次校验结果:%x\r\n", CRC32(buff2, 16, 0xFFFFFFFF));
// 对数据缓冲区buff2进行分块CRC32校验
res = CRC32(&buff2[0], 4, 0xFFFFFFFF); // 对前4个字节进行CRC32校验
res = CRC32(&buff2[4], 4, res); // 对接下来的4个字节进行CRC32校验并累加到前面的结果中
res = CRC32(&buff2[8], 4, res); // 对接下来的4个字节进行CRC32校验并累加到前面的结果中
u1_printf("软件CRC连续校验结果:%x\r\n", CRC32(&buff2[12], 4, res)); // 对最后的4个字节进行CRC32校验并打印输出
while (1)
{
// 程序进入主循环,持续运行
}
}
实验结果
3.使用软件CRC校验16位CRC
实验步骤
修改生成多项式和初始值,即可进行16位CRC的校验
代码
crc.c
#include "stm32f1xx_hal.h"
CRC_HandleTypeDef crc;
// CRC初始化函数
void CRC_Init(void)
{
crc.Instance = CRC;
HAL_CRC_Init(&crc);
}
// CRC模块的初始化回调函数
void HAL_CRC_MspInit(CRC_HandleTypeDef *hcrc)
{
__HAL_RCC_CRC_CLK_ENABLE(); // 开启CRC模块的时钟
}
// 计算CRC16校验值
// 参数:
// - data: 待计算CRC的数据指针
// - len: 数据长度
// - init: CRC初始值
// 返回值:
// - uint16_t类型的16位CRC16校验值
uint16_t CRC16(uint8_t *data, uint16_t len, uint16_t init)
{
uint16_t poly = 0x8005; // CRC16多项式
uint8_t i;
// 遍历数据字节,计算CRC16校验值
while (len--)
{
init ^= (*data << 8);
for (i = 0; i < 8; i++)
{
if (init & 0x8000) // 如果当前最高位是1
{
init = (init << 1) ^ poly; // 左移一位并与多项式异或
}
else // 如果当前最高位是0
{
init = (init << 1); // 左移一位
}
}
data++; // 指向下一个数据字节
}
return init; // 返回计算得到的CRC16校验值
}
crc.h
#ifndef __CRC_H
#define __CRC_H
#include "stm32f1xx_hal_crc.h"
extern CRC_HandleTypeDef crc;
void CRC_Init(void);
uint16_t CRC16(uint8_t *data, uint16_t len, uint16_t init);
#endif
main.c
#include "stm32f1xx_hal.h"
#include "rcc.h"
#include "uart.h"
#include "crc.h"
// 声明变量buff2
uint8_t buff2[16] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0};
int main(void)
{
HAL_Init();
RccClock_Init();
U1_Init(9600);
CRC_Init();
u1_printf("%x\r\n", CRC16(buff2, 16, 0xFFFF));
while(1)
{
}
}
实验结果
在上程序中未添加输入数据输出数据反转的功能,故在CRC计算器中我先自定义了计算方法如下图发现校验结果一致,因此代码实验正确。
4.使用软件CRC校验8位CRC
实验步骤
修改生成多项式和初始值,即可进行8位CRC的校验
代码
crc.c
#include "stm32f1xx_hal.h"
CRC_HandleTypeDef crc;
// CRC初始化函数
void CRC_Init(void)
{
crc.Instance = CRC;
HAL_CRC_Init(&crc);
}
// CRC模块的初始化回调函数
void HAL_CRC_MspInit(CRC_HandleTypeDef *hcrc)
{
__HAL_RCC_CRC_CLK_ENABLE(); // 开启CRC模块的时钟
}
// 计算CRC-8校验值
// 参数:
// - data: 待计算CRC的数据指针
// - len: 数据长度
// - init: CRC初始值
// 返回值:
// - uint8_t类型的8位CRC-8校验值
uint8_t CRC8(uint8_t *data, uint16_t len, uint8_t init)
{
uint8_t poly = 0x07; // CRC-8多项式
uint8_t i;
// 遍历数据字节,计算CRC-8校验值
while (len--)
{
init ^= *data++;
for (i = 0; i < 8; i++)
{
if (init & 0x80) // 如果当前最高位是1
{
init = (init << 1) ^ poly; // 左移一位并与多项式异或
}
else // 如果当前最高位是0
{
init <<= 1; // 左移一位
}
}
}
return init; // 返回计算得到的CRC-8校验值
}
crc.h
#ifndef __CRC_H
#define __CRC_H
#include "stm32f1xx_hal_crc.h"
extern CRC_HandleTypeDef crc;
void CRC_Init(void);
uint8_t CRC8(uint8_t *data, uint16_t len, uint8_t init);
#endif
main.c
#include "stm32f1xx_hal.h"
#include "rcc.h"
#include "uart.h"
#include "crc.h"
// 声明变量buff2
uint8_t buff2[16] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0};
int main(void)
{
HAL_Init();
RccClock_Init();
U1_Init(9600);
CRC_Init();
u1_printf("%x\r\n", CRC8(buff2, 16, 0));
while(1)
{
}
}
实验结果


5.32位软件CRC校验的数据反转
原因
在工程中我想使用CRC-16/MODBUS进行485通讯,很明显在第三节我的初始值、多项式以及异或值都和CRC-16/MODBUS一致,但是CRC-16/MODBUS有输入数据输出数据反转,故此节来写输入数据输出数据反转的功能。
实验步骤
本节实验我们在上一节软件CRC32校验计算程序基础上,增加数据反转函数,适用于更多种情形下的CRC32校验方式。
注意:数据翻转包含:
(1)待校验的数据,先反转后再参与计算
(2)校验后的结果数据,先反转后再return返回
注意:反转指的是低位变高位,高位变低位,比如按字节翻转,原来的位0成为了位7位1成为位6,原来高位的位7成为位0,位6变成位1,以此类推。
代码
crr.c
#include "stm32f1xx_hal.h"
#include "crc.h"
// 初始化CRC
CRC_HandleTypeDef crc;
void CRC_Init(void)
{
crc.Instance = CRC;
HAL_CRC_Init(&crc);
}
// CRC模块的初始化回调函数
void HAL_CRC_MspInit(CRC_HandleTypeDef *hcrc)
{
__HAL_RCC_CRC_CLK_ENABLE(); // 开启CRC模块的时钟
}
// 计算CRC32校验值
// 参数:
// - data: 待计算CRC的数据指针
// - len: 数据长度
// - init: CRC初始值
// 返回值:
// - uint32_t类型的CRC32校验值
uint32_t CRC32(uint8_t *data, uint16_t len, uint32_t init)
{
uint32_t poly = 0x04C11DB7; // CRC32多项式
uint8_t i;
while(len--)
{
//init = init ^ (*data<<24); // 用这个输入数据不反转
init = init ^ (InverUint8(*data)<<24); // 初始值异或输入数据,同时进行输入反转,用这个输入数据反转
for(i=0;i<8;i++)
{
if(init&0x80000000) // 如果当前最高位是1
{
init =(init<<1)^poly; // 左移一位并与多项式异或
}
else // 如果当前最高位是0
{
init =(init<<1); // 左移一位
}
}
data++; // 指向下一个数据字节
}
// return init;//用这个输出数据不反转
return InverUint32(init); // 返回计算得到的CRC32校验值,同时进行输出反转,用这个输出数据反转
}
// 反转8位数据
uint8_t InverUint8(uint8_t data)
{
uint8_t i;
uint8_t temp;
temp = 0;
for(i = 0; i < 8; i++)
{
if(data & (1 << i))
{
temp |= 1 << (7 - i);
}
}
return temp ;
}
// 反转32位数据
uint32_t InverUint32(uint32_t data)
{
uint8_t i;
uint32_t temp;
temp = 0;
for(i = 0; i < 32; i++)
{
if(data & (1 << i))
{
temp |= 1 << (31 - i);
}
}
return temp ;
}
crc.h
#ifndef __CRC_H
#define __CRC_H
#include "stm32f1xx_hal_crc.h"
extern CRC_HandleTypeDef crc;
void CRC_Init(void);
uint32_t CRC32(uint8_t *,uint16_t ,uint32_t );
uint8_t InverUint8(uint8_t data);
uint32_t InverUint32(uint32_t data);
#endif
main.c
#include "stm32f1xx_hal.h"
#include "rcc.h"
#include "uart.h"
#include "crc.h"
uint8_t buff2[16]={1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,0};
int main(void)
{
HAL_Init();
RccClock_Init();
U1_Init(9600);
CRC_Init();
u1_printf("%x\r\n",CRC32(buff2,16,0xFFFFFFFF));
while(1)
{
}
}
实验结果
分别进行了输入数据和输出数据反转的实验,结果都一致。
输入数据和输出数据都不反转
输入数据反转
输出数据反转
输入数据和输出数据都反转
6.16位软件CRC校验的数据反转
实验步骤
对第三节的16位CRC校验程序改写
代码
crc.c
#include "stm32f1xx_hal.h"
#include "crc.h"
CRC_HandleTypeDef crc;
// CRC初始化函数
void CRC_Init(void)
{
crc.Instance = CRC;
HAL_CRC_Init(&crc);
}
// CRC模块的初始化回调函数
void HAL_CRC_MspInit(CRC_HandleTypeDef *hcrc)
{
__HAL_RCC_CRC_CLK_ENABLE(); // 开启CRC模块的时钟
}
// 计算CRC16校验值
// 参数:
// - data: 待计算CRC的数据指针
// - len: 数据长度
// - init: CRC初始值
// 返回值:
// - uint16_t类型的16位CRC16校验值
uint16_t CRC16(uint8_t *data, uint16_t len, uint16_t init)
{
uint16_t poly = 0x8005; // CRC16多项式
uint8_t i;
// 遍历数据字节,计算CRC16校验值
while (len--)
{
// init ^= (*data << 8);//不反转
init ^= (InverUint8(*data) << 8);//反转
for (i = 0; i < 8; i++)
{
if (init & 0x8000) // 如果当前最高位是1
{
init = (init << 1) ^ poly; // 左移一位并与多项式异或
}
else // 如果当前最高位是0
{
init = (init << 1); // 左移一位
}
}
data++; // 指向下一个数据字节
}
// return init; // 返回计算得到的CRC16校验值,不反转
return InverUint16(init);//反转
}
// 反转8位数据
uint8_t InverUint8(uint8_t data)
{
uint8_t i;
uint8_t temp;
temp = 0;
for(i = 0; i < 8; i++)
{
if(data & (1 << i))
{
temp |= 1 << (7 - i);
}
}
return temp ;
}
// 反转32位数据
uint16_t InverUint16(uint16_t data)
{
uint8_t i;
uint16_t temp;
temp = 0;
for(i = 0; i < 16; i++)
{
if(data & (1 << i))
{
temp |= 1 << (15 - i);
}
}
return temp ;
}
crc.h
#ifndef __CRC_H
#define __CRC_H
#include "stm32f1xx_hal_crc.h"
extern CRC_HandleTypeDef crc;
void CRC_Init(void);
uint16_t CRC16(uint8_t *data, uint16_t len, uint16_t init);
uint8_t InverUint8(uint8_t data);
uint16_t InverUint16(uint16_t data);
#endif
main.c
#include "stm32f1xx_hal.h"
#include "rcc.h"
#include "uart.h"
#include "crc.h"
// 声明变量buff2
uint8_t buff2[16] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0};
int main(void)
{
HAL_Init();
RccClock_Init();
U1_Init(9600);
CRC_Init();
u1_printf("%x\r\n", CRC16(buff2, 16, 0xFFFF));
while(1)
{
}
}
实验结果
实验结果一致。
7.8位软件CRC校验的数据反转
实验步骤
同CRC16
代码
crc.c
#include "stm32f1xx_hal.h"
#include "crc.h"
CRC_HandleTypeDef crc;
// CRC初始化函数
void CRC_Init(void)
{
crc.Instance = CRC;
HAL_CRC_Init(&crc);
}
// CRC模块的初始化回调函数
void HAL_CRC_MspInit(CRC_HandleTypeDef *hcrc)
{
__HAL_RCC_CRC_CLK_ENABLE(); // 开启CRC模块的时钟
}
// 计算CRC-8校验值
// 参数:
// - data: 待计算CRC的数据指针
// - len: 数据长度
// - init: CRC初始值
// 返回值:
// - uint8_t类型的8位CRC-8校验值
uint8_t CRC8(uint8_t *data, uint16_t len, uint8_t init)
{
uint8_t poly = 0x07; // CRC-8多项式
uint8_t i;
// 遍历数据字节,计算CRC-8校验值
while (len--)
{
// init ^= *data++;
init ^= InverUint8(*data++);
for (i = 0;i < 8; i++)
{
if (init & 0x80) // 如果当前最高位是1
{
init = (init << 1) ^ poly; // 左移一位并与多项式异或
}
else // 如果当前最高位是0
{
init <<= 1; // 左移一位
}
}
}
// return init; // 返回计算得到的CRC-8校验值
return InverUint8(init); // 返回计算得到的CRC32校验值,同时进行输出反转,用这个输出数据反转
}
// 反转8位数据
uint8_t InverUint8(uint8_t data)
{
uint8_t i;
uint8_t temp;
temp = 0;
for(i = 0; i < 8; i++)
{
if(data & (1 << i))
{
temp |= 1 << (7 - i);
}
}
return temp ;
}
crc.h
#ifndef __CRC_H
#define __CRC_H
#include "stm32f1xx_hal_crc.h"
extern CRC_HandleTypeDef crc;
void CRC_Init(void);
uint8_t CRC8(uint8_t *data, uint16_t len, uint8_t init);
uint8_t InverUint8(uint8_t data);
#endif
main.c
#include "stm32f1xx_hal.h"
#include "rcc.h"
#include "uart.h"
#include "crc.h"
// 声明变量buff2
uint8_t buff2[16] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0};
int main(void)
{
HAL_Init();
RccClock_Init();
U1_Init(9600);
CRC_Init();
u1_printf("%x\r\n", CRC8(buff2, 16, 0));
while(1)
{
}
}
实验结果
校验结果对照一致。
8.软件CRC校验数据反转(添加接收数据校验后再发送)
实验步骤
添加手动发送数据,接收数据之后把原始数据和CRC校验值都发送的功能。
代码
只修改main.c。
main.c
#include "stm32f1xx_hal.h"
#include "rcc.h"
#include "uart.h"
#include "crc.h"
#define BUFFER_SIZE 16 // 定义数据缓冲区大小为16字节
uint8_t buff[BUFFER_SIZE]; // 数据缓冲区,存放接收到的数据
// 打印16位数据的字节顺序为小端序
void print_little_endian(uint16_t data) {
uint8_t *ptr = (uint8_t *)&data;
u1_printf("%02X%02X ", ptr[0], ptr[1]);
}
int main(void)
{
HAL_Init();
RccClock_Init();
U1_Init(9600);
CRC_Init();
while(1)
{
// 等待接收数据
while (HAL_UART_Receive(&huart1, buff, BUFFER_SIZE, HAL_MAX_DELAY) != HAL_OK);
// 打印接收到的数据(按照小端序)
u1_printf("Received data: ");
for (int i = 0; i < BUFFER_SIZE; i+=2) {
print_little_endian(*(uint16_t*)&buff[i]);
}
u1_printf("\r\n");
// 计算CRC16校验值并打印
u1_printf("CRC16 result: %x\r\n", CRC16(buff, BUFFER_SIZE, 0xFFFF));
}
}
实验结果
结果一致。
五、处理过程
若要求某位的CRC校验(8bits\16bits\32bits)
从硬件出发:直接选用具有要求CRC校验功能的芯片(如CRC校验位数、多项式值、初始值、异或值等)。
从软件出发:若硬件配置有限,没有能够满足各项条件的CRC硬件校验,则可以选用软件校验,通过软件校验可以设置所要求的CRC校验位数、多项式值、初始值、异或值等。
这篇笔记是我基于这个视频来学习记录的,特注明!
STM32 CRC校验篇 章节大纲 HAL库精讲教程_哔哩哔哩_bilibili基础篇教程将完成700+例程源码的编写(HAL库程序),30+思维导图,力求测试HAL库中的每一个API函数以及每一个STM32单片机IO口上的各种复用功能。首先进行的基础篇教程大纲如下:【第 01 章】时钟篇【第 02 章】GPIO基本输入输出篇【第 03 章】串口异步通信篇(含双机+多机通信)【第 04 章】定时器篇【第 05 章】ADC模数转换(含双ADC模式)【第 06 章】I2C 通信(, 视频播放量 1475、弹幕量 0、点赞数 40、投硬币枚数 24、收藏人数 58、转发人数 5, 视频作者 超子说物联网, 作者简介 LoRa和4G旧的分享群满了,可加新分享群8群795476070,相关视频:STM32单片机 硬件CRC32 单次校验计算 HAL库精讲教程,STM32单片机 C语言 软件CRC16 CRC8校验 HAL库精讲教程,stm32 Keil程序源码 BootLoader程序思路流程讲解,STM32单片机 C语言 软件CRC32校验计算 HAL库精讲教程,【STM32+LoRa网关】Keil工程源码,Lora模块参数配置以及参数读取程序讲解,STM32单片机 C语言 软件CRC32 输入输出数据反转 HAL库精讲教程,手把手写【STM32/GD32 入门教程】OTA升级Boot程序 软件实现Xmodem协议CRC16校验,【开源群】【STM32+LoRa(E32)+WiFi+4G 物联网OTA升级】视频教程(1)应用简介,【开源群】STM32上手控制LoRa模块的第一步,STM32 HAL库汇 如何使用外部 HSE 晶振设置系统时钟https://www.bilibili.com/video/BV1cU411F7T9/?spm_id_from=333.788&vd_source=a721d1e848b89861d89632a0232e63c4
作者:小齿轮lsl