STM32串口发送时使用奇偶校验学习感悟——Even(偶校验)

一、相关概念的补充

在有效数据之后,有一个可选的数据校验位。由于数据通信相对更容易受到外部干扰导致传输
数据出现偏差,可以在传输过程加上校验位来解决这个问题。校验方法有奇校验 (odd)、偶校验
(even)、0 校验 (space)、1 校验 (mark) 以及无校验 (noparity),它们介绍如下:
奇校验要求有效数据和校验位中“1”的个数为奇数,比如一个 8 位长的有效数据为:01101001,
此时总共有 4 个“1”,为达到奇校验效果,校验位为“1”,最后传输的数据将是 8 位的有效数据
加上 1 位的校验位总共 9 位

偶校验与奇校验要求刚好相反,要求帧数据和校验位中“1”的个数为偶数,比如数据帧:
11001010,此时数据帧“1”的个数为 4 个,所以偶校验位为“0”。
0 校验是不管有效数据中的内容是什么,校验位总为“0”,1 校验是校验位总为“1”。
在无校验的情况下,数据包中不包含校验位。

当我们加上校验位后,并且使能了相关配置之后,要明确的就是,最后传输的数据是8位有效数据+1位校验位=9位数据,校验位的添加是硬件自动完成的,对于我们来说是透明的,不用关心,只要在CubeMX中配置好即可

二、主要步骤

(1)CubeMX配置

因为我工程里面使用串口空闲终端(IDLE)来接收数据,所以首先我们要在CubeMX中设置一下,数据位为9位(8位数据位和1位校验位),因为这里我们要接收完整的9位数据,然后相关DMA数据的接收对其宽度我们这里都给half a word 也就是两个字节,16位


串口通讯的数据包由发送设备通过自身的 TXD 接口传输到接收设备的 RXD 接口。在串口通讯
的协议层中,规定了数据包的内容,它由启始位、主体数据、校验位以及停止位组成,通讯双方
的数据包格式要约定一致才能正常收发数据

(2)核心代码

1.usart.c

/* USER CODE BEGIN Header */
#include "stdio.h"
#include <string.h>
#include <stdarg.h>
/**
  ******************************************************************************
  * @file    usart.c
  * @brief   This file provides code for the configuration
  *          of the USART instances.
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2025 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "usart.h"

/* USER CODE BEGIN 0 */


// 定义接收缓冲区
uint16_t Rx_buffer[BUFFER_SIZE];
// 定义DMA回显数据发送缓冲区 (这就是我们要发送的“货物”)
uint16_t TxBuffer[BUFFER_SIZE]; 
uint16_t rx_len = 0;

/* USER CODE END 0 */

UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;
DMA_HandleTypeDef hdma_usart1_tx;

/* USART1 init function */

void MX_USART1_UART_Init(void)
{

  /* USER CODE BEGIN USART1_Init 0 */

  /* USER CODE END USART1_Init 0 */

  /* USER CODE BEGIN USART1_Init 1 */

  /* USER CODE END USART1_Init 1 */
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_9B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_EVEN;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN USART1_Init 2 */
	
  // 启动DMA接收,使用循环模式
  HAL_UART_Receive_DMA(&huart1, (uint8_t *)Rx_buffer, BUFFER_SIZE);
  // 使能IDLE中断
  __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
	
  /* USER CODE END USART1_Init 2 */

}

void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{

  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(uartHandle->Instance==USART1)
  {
  /* USER CODE BEGIN USART1_MspInit 0 */

  /* USER CODE END USART1_MspInit 0 */
    /* USART1 clock enable */
    __HAL_RCC_USART1_CLK_ENABLE();

    __HAL_RCC_GPIOA_CLK_ENABLE();
    /**USART1 GPIO Configuration
    PA9     ------> USART1_TX
    PA10     ------> USART1_RX
    */
    GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    /* USART1 DMA Init */
    /* USART1_RX Init */
    hdma_usart1_rx.Instance = DMA2_Stream2;
    hdma_usart1_rx.Init.Channel = DMA_CHANNEL_4;
    hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
    hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
    hdma_usart1_rx.Init.Mode = DMA_NORMAL;
    hdma_usart1_rx.Init.Priority = DMA_PRIORITY_MEDIUM;
    hdma_usart1_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
    if (HAL_DMA_Init(&hdma_usart1_rx) != HAL_OK)
    {
      Error_Handler();
    }

    __HAL_LINKDMA(uartHandle,hdmarx,hdma_usart1_rx);

    /* USART1_TX Init */
    hdma_usart1_tx.Instance = DMA2_Stream7;
    hdma_usart1_tx.Init.Channel = DMA_CHANNEL_4;
    hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
    hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
    hdma_usart1_tx.Init.Mode = DMA_NORMAL;
    hdma_usart1_tx.Init.Priority = DMA_PRIORITY_MEDIUM;
    hdma_usart1_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
    if (HAL_DMA_Init(&hdma_usart1_tx) != HAL_OK)
    {
      Error_Handler();
    }

    __HAL_LINKDMA(uartHandle,hdmatx,hdma_usart1_tx);

    /* USART1 interrupt Init */
    HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(USART1_IRQn);
  /* USER CODE BEGIN USART1_MspInit 1 */

  /* USER CODE END USART1_MspInit 1 */
  }
}

void HAL_UART_MspDeInit(UART_HandleTypeDef* uartHandle)
{

  if(uartHandle->Instance==USART1)
  {
  /* USER CODE BEGIN USART1_MspDeInit 0 */

  /* USER CODE END USART1_MspDeInit 0 */
    /* Peripheral clock disable */
    __HAL_RCC_USART1_CLK_DISABLE();

    /**USART1 GPIO Configuration
    PA9     ------> USART1_TX
    PA10     ------> USART1_RX
    */
    HAL_GPIO_DeInit(GPIOA, GPIO_PIN_9|GPIO_PIN_10);

    /* USART1 DMA DeInit */
    HAL_DMA_DeInit(uartHandle->hdmarx);
    HAL_DMA_DeInit(uartHandle->hdmatx);

    /* USART1 interrupt Deinit */
    HAL_NVIC_DisableIRQ(USART1_IRQn);
  /* USER CODE BEGIN USART1_MspDeInit 1 */

  /* USER CODE END USART1_MspDeInit 1 */
  }
}

/* USER CODE BEGIN 1 */

// 发送一个字节
void USART1_SendByte(uint8_t Byte)
{
  HAL_UART_Transmit(&huart1, &Byte, 1, HAL_MAX_DELAY);
}

// 发送数组
void USART1_SendArray(uint8_t *Array, uint16_t Length)
{
  HAL_UART_Transmit(&huart1, Array, Length, HAL_MAX_DELAY);
}

// 发送字符串
void USART1_SendString(char *String)
{
  HAL_UART_Transmit(&huart1, (uint8_t*)String, strlen(String), HAL_MAX_DELAY);
}

// 发送数字
void USART1_SendNumber(uint32_t Number, uint8_t Length)
{
  uint8_t i;
  for(i = 0; i < Length; i++)
  {
    USART1_SendByte(Number / (uint32_t)pow(10, Length - i - 1) % 10 + '0');
  }
}

// 重定向printf到串口
void printf_uart1(char *format,...){
    va_list args;
    va_start(args, format);
    static char buffer[1000];
    int len = vsnprintf(buffer, sizeof(buffer), format, args);
    va_end(args);
    if (len > 0) 
		{
			HAL_UART_Transmit(&huart1, (uint8_t *)buffer, len, 100);
		}
}
/* USER CODE END 1 */

2.stm32f4××_it.c (主要是对USART1_IRQHandler的处理)

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file    stm32f4xx_it.c
  * @brief   Interrupt Service Routines.
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2025 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */

/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "stm32f4xx_it.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "usart.h"
#include "string.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN TD */

/* USER CODE END TD */

/* 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 -----------------------------------------------*/
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/* External variables --------------------------------------------------------*/
extern TIM_HandleTypeDef htim2;
extern DMA_HandleTypeDef hdma_usart1_rx;
extern DMA_HandleTypeDef hdma_usart1_tx;
extern UART_HandleTypeDef huart1;
/* USER CODE BEGIN EV */

/* USER CODE END EV */

/******************************************************************************/
/*           Cortex-M4 Processor Interruption and Exception Handlers          */
/******************************************************************************/
/**
  * @brief This function handles Non maskable interrupt.
  */
void NMI_Handler(void)
{
  /* USER CODE BEGIN NonMaskableInt_IRQn 0 */

  /* USER CODE END NonMaskableInt_IRQn 0 */
  /* USER CODE BEGIN NonMaskableInt_IRQn 1 */
   while (1)
  {
  }
  /* USER CODE END NonMaskableInt_IRQn 1 */
}

/**
  * @brief This function handles Hard fault interrupt.
  */
void HardFault_Handler(void)
{
  /* USER CODE BEGIN HardFault_IRQn 0 */

  /* USER CODE END HardFault_IRQn 0 */
  while (1)
  {
    /* USER CODE BEGIN W1_HardFault_IRQn 0 */
    /* USER CODE END W1_HardFault_IRQn 0 */
  }
}

/**
  * @brief This function handles Memory management fault.
  */
void MemManage_Handler(void)
{
  /* USER CODE BEGIN MemoryManagement_IRQn 0 */

  /* USER CODE END MemoryManagement_IRQn 0 */
  while (1)
  {
    /* USER CODE BEGIN W1_MemoryManagement_IRQn 0 */
    /* USER CODE END W1_MemoryManagement_IRQn 0 */
  }
}

/**
  * @brief This function handles Pre-fetch fault, memory access fault.
  */
void BusFault_Handler(void)
{
  /* USER CODE BEGIN BusFault_IRQn 0 */

  /* USER CODE END BusFault_IRQn 0 */
  while (1)
  {
    /* USER CODE BEGIN W1_BusFault_IRQn 0 */
    /* USER CODE END W1_BusFault_IRQn 0 */
  }
}

/**
  * @brief This function handles Undefined instruction or illegal state.
  */
void UsageFault_Handler(void)
{
  /* USER CODE BEGIN UsageFault_IRQn 0 */

  /* USER CODE END UsageFault_IRQn 0 */
  while (1)
  {
    /* USER CODE BEGIN W1_UsageFault_IRQn 0 */
    /* USER CODE END W1_UsageFault_IRQn 0 */
  }
}

/**
  * @brief This function handles System service call via SWI instruction.
  */
void SVC_Handler(void)
{
  /* USER CODE BEGIN SVCall_IRQn 0 */

  /* USER CODE END SVCall_IRQn 0 */
  /* USER CODE BEGIN SVCall_IRQn 1 */

  /* USER CODE END SVCall_IRQn 1 */
}

/**
  * @brief This function handles Debug monitor.
  */
void DebugMon_Handler(void)
{
  /* USER CODE BEGIN DebugMonitor_IRQn 0 */

  /* USER CODE END DebugMonitor_IRQn 0 */
  /* USER CODE BEGIN DebugMonitor_IRQn 1 */

  /* USER CODE END DebugMonitor_IRQn 1 */
}

/**
  * @brief This function handles Pendable request for system service.
  */
void PendSV_Handler(void)
{
  /* USER CODE BEGIN PendSV_IRQn 0 */

  /* USER CODE END PendSV_IRQn 0 */
  /* USER CODE BEGIN PendSV_IRQn 1 */

  /* USER CODE END PendSV_IRQn 1 */
}

/**
  * @brief This function handles System tick timer.
  */
void SysTick_Handler(void)
{
  /* USER CODE BEGIN SysTick_IRQn 0 */

  /* USER CODE END SysTick_IRQn 0 */
  HAL_IncTick();
  /* USER CODE BEGIN SysTick_IRQn 1 */

  /* USER CODE END SysTick_IRQn 1 */
}

/******************************************************************************/
/* STM32F4xx Peripheral Interrupt Handlers                                    */
/* Add here the Interrupt Handlers for the used peripherals.                  */
/* For the available peripheral interrupt handler names,                      */
/* please refer to the startup file (startup_stm32f4xx.s).                    */
/******************************************************************************/

/**
  * @brief This function handles EXTI line3 interrupt.
  */
void EXTI3_IRQHandler(void)
{
  /* USER CODE BEGIN EXTI3_IRQn 0 */

  /* USER CODE END EXTI3_IRQn 0 */
  HAL_GPIO_EXTI_IRQHandler(KEY1_Pin);
  /* USER CODE BEGIN EXTI3_IRQn 1 */

  /* USER CODE END EXTI3_IRQn 1 */
}

/**
  * @brief This function handles TIM2 global interrupt.
  */
void TIM2_IRQHandler(void)
{
  /* USER CODE BEGIN TIM2_IRQn 0 */

  /* USER CODE END TIM2_IRQn 0 */
  HAL_TIM_IRQHandler(&htim2);
  /* USER CODE BEGIN TIM2_IRQn 1 */

  /* USER CODE END TIM2_IRQn 1 */
}

/**
  * @brief This function handles USART1 global interrupt.
  */
void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */

  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
	/* USER CODE BEGIN USART1_IRQn 0 */
		static uint8_t parity_error_flag = 0;  // 奇偶错误标志

		// 检查奇偶校验错误标志(优先级高于IDLE中断,先处理错误)
		if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_PE) != RESET) {
			parity_error_flag = 1;  // 标记发生奇偶错误
			__HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_PE);  // 清除标志
		}
		
	// 检查是否是IDLE中断
    if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET)
    {
      // 清除IDLE标志位
      __HAL_UART_CLEAR_IDLEFLAG(&huart1);
      // 停止DMA接收
      HAL_UART_DMAStop(&huart1);
      
      // 计算接收长度
      rx_len = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
      
      if(rx_len > 0)
      {
				// 若发生奇偶错误,丢弃数据不进行回显并提示
      if (parity_error_flag) 
				{
					memset(Rx_buffer,0,sizeof(Rx_buffer));
					printf_uart1("接收数据包含奇偶校验错误,已丢弃\r\n");
					parity_error_flag = 0;  // 重置标志
				} 
			else 
				{
					printf_uart1("接收数据不包含奇偶校验错误,下面进行数据回显\r\n");
					// 提取低8位数据位,过滤校验位
					for(uint16_t i=0; i<rx_len; i++){
							TxBuffer[i] = Rx_buffer[i] & 0xFF; // 只取低8位数据
					}
					// 发送纯数据位,长度为rx_len
					HAL_UART_Transmit_DMA(&huart1, (uint8_t*)TxBuffer, rx_len);
				}
      }
      
      // 重新启动DMA接收
      HAL_UART_Receive_DMA(&huart1,(uint8_t*)Rx_buffer, BUFFER_SIZE);
    }
  /* USER CODE END USART1_IRQn 1 */
}

/**
  * @brief This function handles DMA2 stream2 global interrupt.
  */
void DMA2_Stream2_IRQHandler(void)
{
  /* USER CODE BEGIN DMA2_Stream2_IRQn 0 */

  /* USER CODE END DMA2_Stream2_IRQn 0 */
  HAL_DMA_IRQHandler(&hdma_usart1_rx);
  /* USER CODE BEGIN DMA2_Stream2_IRQn 1 */

  /* USER CODE END DMA2_Stream2_IRQn 1 */
}

/**
  * @brief This function handles DMA2 stream7 global interrupt.
  */
void DMA2_Stream7_IRQHandler(void)
{
  /* USER CODE BEGIN DMA2_Stream7_IRQn 0 */

  /* USER CODE END DMA2_Stream7_IRQn 0 */
  HAL_DMA_IRQHandler(&hdma_usart1_tx);
  /* USER CODE BEGIN DMA2_Stream7_IRQn 1 */

  /* USER CODE END DMA2_Stream7_IRQn 1 */
}

/* USER CODE BEGIN 1 */

/* USER CODE END 1 */

上述代码中实现了对接收到的数据中校验位的剥离,若奇偶校验无错误,UART_FLAG_PE为RESET,相关的parity_error_flag不会被设置,进入正常的数据回显阶段,而在回显之前,程序已经帮我们将校验位剥离出去,剩下完整的有效数据给TxBuffer,也就是说Rx_buffer包含了完整的九位数据(包括了一位校验位),TxBuffer里面只有8位有效数据

在这段代码中,从 9 位数据中剥离校验位的操作,核心思路是区分 “有效数据位” 和 “校验位”,通过位运算保留低 8 位有效数据,屏蔽第 9 位的校验位,具体实现逻辑如下:

一、核心思路

当串口配置为 “9 位字长(含校验位)” 时,每帧实际传输的数据由两部分组成:

  • 低 8 位(bit0~bit7):用户发送的有效数据(如0x010x02等);
  • 第 9 位(bit8):硬件自动添加的校验位(用于奇偶校验验证,不属于有效数据)。

校验位的作用仅为 “验证数据传输是否正确”,一旦完成校验(通过或失败),就无需保留。因此,需要从 9 位完整数据中剥离第 9 位校验位,只保留低 8 位有效数据,避免校验位干扰后续的数据处理或回显。

二、具体实现(代码解析)

代码中通过按位与运算(&0xFF) 实现校验位的剥离,关键语句如下:

for(uint16_t i=0; i<rx_len; i++){
    TxBuffer[i] = Rx_buffer[i] & 0xFF; // 只取低8位数据
}
原理说明:
  1. 数据存储形式:由于 9 位数据无法用 8 位变量(uint8_t)完整存储,Rx_buffer实际应为uint16_t类型(16 位变量,足够容纳 9 位数据)。此时,接收的 9 位数据在内存中存储为:

    • bit0~bit7:有效数据(8 位);
    • bit8:校验位(1 位);
    • bit9~bit15:无意义(因仅需 9 位,高位补 0)。
  2. 按位与运算(&0xFF)的作用0xFF是十六进制常量,对应二进制为00000000 11111111(16 位表示)。当Rx_buffer[i](9 位数据,存储在 16 位变量中)与0xFF进行按位与时:

    • 低 8 位(bit0~bit7):与11111111运算后,保留原始有效数据;
    • 第 9 位(bit8)及更高位(bit9~bit15):与0运算后,被强制清零(即剥离校验位)。

    例如:

    • 接收的 9 位数据为1 00000001(第 9 位校验位为 1,低 8 位有效数据为0x01),存储在 16 位变量中为00000001 00000001
    • 0xFF00000000 11111111)运算后,结果为00000000 00000001,即仅保留低 8 位的0x01,校验位被剥离。

稍后我会将完整的工程上传,需要的可以自行获取,或者评论区联系我

三.整个实践过程中的雷点(踩到的雷)

(1)Rx_buffer 和Tx_buffer 不使用uint16_t类型

未修改前 Rx_buffer 是 uint8_t 类型,接收时确实会自动截断 9 位数据,但这是 “被动丢弃校验位”,而非 “正确的校验位与数据位分离”—— 看似 “分离” 了,实则会导致校验逻辑异常和数据混乱,具体分析如下:

1、uint8_t 接收时的 “自动截断” 原理

当串口配置为 “9 位字长(含校验位)”,但接收缓冲区是 uint8_t(仅 8 位)时:

  • STM32 的 UART 数据寄存器(DR)是 16 位,接收 9 位数据后,会将 低 8 位(数据位)存入 uint8_t 变量第 9 位(校验位)被硬件自动丢弃(因为 uint8_t 没有第 9 位的存储空间)。
  • 这个过程确实是 “自动截断”,客观上实现了 “数据位保留、校验位丢弃”,但本质是 “被动丢弃”,而非 “主动分离”。

2、为什么 “自动截断” 不能替代 “主动分离”?(核心问题)

虽然 uint8_t 会截断校验位,但这种方式存在两个致命问题,导致之前的通信异常:

  1. 硬件校验逻辑仍依赖完整 9 位数据,截断不影响校验结果串口的奇偶校验是由硬件在接收完整 9 位数据时完成的 —— 不管缓冲区是 uint8_t 还是 uint16_t,硬件都会先检查 “8 位数据 + 1 位校验位” 是否符合奇偶规则,再决定是否置位 PE 错误标志。你之前用 uint8_t 时,偶校验发送 0x01 正常,是因为硬件校验通过后,截断保留了正确的低 8 位数据

  2. 截断会导致数据存储和回显的混乱当 uint8_t 截断第 9 位时,若串口配置或通信参数有偏差(比如数据位长度误设为 9 位无校验),会导致 “校验位被当作数据位截断”,或 “数据位被当作校验位丢弃”:

    • 例如:若串口误配置为 “9 位数据位 + 无校验”,发送 0x101(9 位数据),uint8_t 会截断为 0x01,丢失高 1 位数据;
    • 之前你看到的 “奇怪正方形符号”,部分原因就是截断后的数据包含了校验位被误判的控制字符(比如截断后的低 8 位恰好是 0x80 等不可见字符)。

3、总结:“被动截断” vs “主动分离”

方式本质核心问题
uint8_t 自动截断被动丢弃第 9 位(校验位)1. 校验逻辑依赖硬件,截断不解决校验匹配问题;2. 数据存储混乱,易出现显示异常。
uint16_t + &0xFF主动保留低 8 位(数据位)1. 可控分离,明确保留有效数据;2. 不影响硬件校验逻辑,数据处理更可靠。
  • uint16_t 缓冲区 + 位运算是 “主动可控的分离方案”,既能完整接收 9 位数据供硬件校验,又能通过位运算精准提取有效数据,适合需要校验位验证且数据处理严谨的场景(如你的需求),所以这种方式更严谨,对我们来说更好一些

(2)Rx_buffer 和Tx_buffer 使用了uint16_t类型,但是DMA数据对齐DataLength没有使用halfword的时候

我刚开始DMA 接收的配置为:

hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;  // 外设端按字节对齐
hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;     // 内存端按字节对齐
  • 问题点:9 位数据必须用 16 位(半字)存储(因为 1 个字节只能存 8 位,放不下 9 位),但 DMA 被配置为按字节(8 位)对齐传输
  • 后果:DMA 会将 16 位的DR寄存器数据拆成两个 8 位字节传输到内存,而 STM32 内存默认是小端模式(低地址存低字节,高地址存高字节),导致数据存储顺序颠倒。

大小端模式相关概念补充:【数据存储】大端存储||小端存储(超详细解析,小白一看就懂!!!)https://blog.csdn.net/weixin_45031801/article/details/136471384?ops_request_misc=%257B%2522request%255Fid%2522%253A%25221c34ef43c63f0195930ea552f6e066d7%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=1c34ef43c63f0195930ea552f6e066d7&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-136471384-null-null.142^v102^pc_search_result_base2&utm_term=%E5%A4%A7%E7%AB%AF%E5%AD%98%E5%82%A8%E5%92%8C%E5%B0%8F%E7%AB%AF%E5%AD%98%E5%82%A8&spm=1018.2226.3001.4187

现象解释

串口通信的 “串行传输” 特性决定的:串口发送多字节数据时,会按照 “先发送低地址字节,后发送高地址字节” 的顺序逐字节传输

(1)发送0x01(单字节)时,Rx_buffer[0] 显示0x0001
  • 串口助手发送的0x01是 8 位有效数据,STM32 会自动添加 1 位偶校验位(0x01有 1 个 1,偶校验位为 1,使总 1 的个数为偶数)。
  • 实际接收的 9 位数据为:0b100000001(二进制),对应 16 位寄存器值为0x0101(低 9 位有效)。
  • DMA 按字节传输:先传低字节0x01,再传高字节0x01(高 7 位无效,实际可能为 0),最终存储到uint16_t类型的Rx_buffer中,表现为0x0001(高字节被截断或清零)。
(2)发送0x0001(两字节)时,Rx_buffer[0]  显示0x0100
  • 串口助手发送两个字节0x000x01,STM32 分别为每个字节添加校验位后接收。
  • DMA 按字节传输:先接收0x00的低字节存到内存低地址,再接收0x01的低字节存到内存高地址(这里其实并不和大小端存储矛盾,通过串口助手发送的 “0x0001”,本质是手动输入的两个独立字节(而非一个 16 位数值),串口助手会按照你输入的顺序逐字节发送,所以这里是先发送的00,00就被放到了低地址,然后01就被放到了高地址)
  • 由于Rx_bufferuint16_t(16 位)数组,小端模式下,低地址字节(0x00)作为低 8 位,高地址字节(0x01)作为高 8 位,最终表现为0x0100(高低字节颠倒)。
解决方案

将 DMA 的数据对齐方式修改为半字(16 位),匹配 9 位数据的存储需求:

// 在HAL_UART_MspInit函数中,修改DMA接收配置
hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;  // 外设按半字(16位)对齐
hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;     // 内存按半字(16位)对齐

修改后,DMA 会按 16 位为单位传输数据,直接匹配 USART 的 16 位DR寄存器和uint16_t类型的Rx_buffer,避免字节拆分导致的顺序颠倒问题,解决这一问题的

本质原因如下:
1. 字节对齐(8 位)的问题:强制拆分 16 位寄存器数据

当 DMA 按字节对齐时,USART 的 16 位 DR 寄存器(存储 9 位数据:8 位有效数据 + 1 位校验位)会被 DMA 拆成两个 8 位字节传输到内存:

  • 比如一帧 9 位数据是 1 0000 0001(校验位 1 + 数据 0x01),对应 DR 寄存器的 16 位值是 0x0101(低 9 位有效)。
  • 字节对齐的 DMA 会先传低 8 位 0x01 到内存低地址,再传高 8 位 0x01 到内存高地址。
  • 但如果是连续两帧数据(比如 0x00 和 0x01),两帧的低 8 位会被依次存入连续的内存地址,小端模式下 16 位变量读取时就会出现顺序颠倒(如前所述的0x0100)。
2. 半字对齐(16 位)的解决:完整传输一帧数据

改为半字对齐后,DMA 会将 USART 的 16 位 DR 寄存器作为一个整体(16 位)传输,直接存入uint16_t类型的Rx_buffer数组元素中:

  • 每帧 9 位数据(8 位 + 1 位校验)对应 DR 寄存器的 16 位值(如0x0101),会被完整存入Rx_buffer[i],不拆分。
  • 连续发送两帧数据(0x00 和 0x01)时:
    • 第一帧(0x00 + 校验位)的 16 位值存入Rx_buffer[0]
    • 第二帧(0x01 + 校验位)的 16 位值存入Rx_buffer[1]
    • 两个元素独立存储,彼此没有字节交叉,自然不会出现 “高低字节颠倒”。

(3)知识补充:开启校验后,1 个字节(8 位)→ 加 1 位校验位 → 变成 9 位 → 用 16 位变量存

现象:当我发送数据0x01的时候,Rx_buffer[0]为0x0101,但是当我通过串口发送数据0x0001的时候,Rx_buffer[1]为0x0101?上述现象是什么情况?

当时懵逼的地方在于,发送0x0001时候,Rx_buffer[0]不应该为0x0101吗?怎么是Rx_buffer[1]为0x0101呢?

这种现象的核心原因是9 位数据格式(8 位有效数据 + 1 位校验位)的存储特性与 DMA 半字对齐(16 位)传输的结合,导致每个接收的 8 位数据会被扩展为包含校验位的 16 位值存储在Rx_buffer

1. 9 位数据的组成规则(关键背景)

你的串口配置为 UART_WORDLENGTH_9B + UART_PARITY_EVEN(偶校验),这意味着:

  • 每帧数据包含 8 位有效数据 + 1 位偶校验位(共 9 位)。
  • 偶校验位的规则:确保 8 位数据中 “1” 的个数为偶数。如果数据中 “1” 的个数是奇数,校验位为 1;如果是偶数,校验位为 0。

2. 发送0x01时,Rx_buffer[0] = 0x0101的原因

  • 发送的0x01是 8 位有效数据,二进制为 0000 0001
  • 该数据中 “1” 的个数为 1(奇数),根据偶校验规则,校验位必须为 1(使总 “1” 的个数为 2,偶数)。
  • 因此,实际接收的 9 位数据为:1 0000 0001(前 1 位是校验位,后 8 位是有效数据)。
  • STM32 的 USART 接收 9 位数据时,会将这 9 位存入 16 位的DR寄存器(低 9 位有效,高 7 位无效),因此 16 位值为 0000 0001 0000 0001(十六进制0x0101)。
  • DMA 按半字(16 位)对齐传输,直接将DR寄存器的 16 位值存入Rx_buffer[0],因此Rx_buffer[0] = 0x0101

3. 发送0x0001时,Rx_buffer[1] = 0x0101的原因

这里需要明确:你发送的0x0001实际是两个连续的 8 位数据0x000x01),串口会按帧依次接收:

  • 第一个字节0x00:8 位数据为 0000 0000,其中 “1” 的个数为 0(偶数),因此校验位为 0。9 位数据为 0 0000 0000,存入 16 位寄存器后为 0000 0000 0000 00000x0000),对应Rx_buffer[0] = 0x0000

  • 第二个字节0x01:与单独发送0x01的情况完全一致,9 位数据为 1 0000 0001,16 位值为0x0101,存入Rx_buffer[1],因此Rx_buffer[1] = 0x0101

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值