1.不定长字节传输的必要性及本文要解决的问题
由于在上一篇文章简单介绍过串口的传输原理并且使用过STM32cubemx对串口的初始化,因此本小章节不再继续赘述相关配置过程,如有需要请跳转https://blog.csdn.net/2401_88485312/article/details/146468025?spm=1001.2014.3001.5501
想必大家在使用串口的时候都有遇到一个问题就是在使用hal库提供的api函数发送或接收数据时都需要填入数据的字节长度,往往在实际应用中我们无法确定每次传输的字节大小具体是多少,因而需要使用一些特殊的编程方式来传输不定长数据,因为我个人在网络上查找相关方法时发现即便方法有许多但是很少有人会做相关讲解,因此我写此篇文章旨在分享个人经验帮助各位和我一样刚接触hal库串口函数的小白能够更好更灵活的使用hal库串口函数。
2.实现不定长字节传输的基本思路
在使用串口函数之前我先向大家讲解数据发送的基本原理(编码),在我们日常生活中因为地域文化原因各个国家使用的语言是各不相同的,为了方便交流大多数时候使用统一语言(英语)进行交流,计算机也是如此,因此数据的交换需要一个统一的编码格式,常见的编码格式有ASCII,UTF-8,GKB等等,然而这些不同的编码格式都有一个统一的特点,就是不管哪种编码格式它最终都会逐个字符的转化成一个个的十六进制组合也就是字节的组合,因此在收发数据时,实际接收的就是一个个字节,然后根据不同的编码格式进行组合翻译成原本的文本,当然还有一种常见的格式就是HEX格式,HEX格式一般用于数值的传输,在文本传输好像比较少(也可能是我浏览的文章不够多)。
由上所述传输连续不定长字节的主要问题就是计算出发送内容的字节大小,本次使用串口中断函数HAL_UART_Receive_IT()接收数据,我们的主要思路是先定义一个变量uint8_t rxbuff来接收PC端上位机发送的数据,然后定义一个缓存数组uint8_t TXarr[256](注意这里必须是uint8_t类型因为每次接收一个字节所以必须是8位的如果是16或32就没法收发了,这一个问题我找了几个小时)
代码如下
//uint8_t rxbuff[2];//使用两个字节传输的时候解开注释并把下面的注释掉
uint8_t rxbuff;//用于存储接收单个字节的变量
uint8_t ncount = 0;//用于指示数组位置的变量
uint8_t TXarr[256];//用于缓存多个字节的变量,当数据接收完毕一次性发送,但字节数不超过256
每次接收一个字节,也就是每收到一个字节进一次中断,然后在中断回调函数HAL_UART_RxCpltCallback()执行溢出判断(因为定义了缓存数组TXarr[256]只能存256个字节,如果超出返回错误标志)并且将收到的数据存入uint8_t TXarr[256],需要注意的是接收到的数据首先是给变量rxbuff,然后再在回调函数中将rxbuff的值传递给数组TXarr[256],当识别出结束标志位后将缓存数组的数据一次性发送到上位机,这里使用网络上大多数使用的回车作为标志位(也就是在输入完数据后加一个回车再发送,在c里面回车对应的ASCII码是0x0D)
附上一张ASCII表(来源于b站江协科技PPT)
,我使用的软件有一个回车加换行,所以最后一个应该是换行。
当程序判断是换行后就将数据发送出去,主要流程图如下
在main函数中while之前开启中断接收函数1.HAL_UART_Receive_IT()当收到数据进入中断2.USART1_IRQHandler()此函数内部调用3.HAL_UART_IRQHandler()---->4.UART_Receive_IT()---->5.HAL_UART_RxCpltCallback()其中只需要在开头调用HAL_UART_Receive_IT()就会自动执行2,3,4步,然后需要自行在5.HAL_UART_RxCpltCallback()中添加接收完成后需要执行的操作。
3.主要代码示例
//uint8_t rxbuff[2];//使用两个字节传输的时候解开注释并把下面的注释掉
uint8_t rxbuff;//用于存储接收单个字节的变量
uint8_t ncount = 0;//用于指示数组位置的变量
uint8_t TXarr[256];//用于缓存多个字节的变量,当数据接收完毕一次性发送,但字节数不超过256
#include <string.h>
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){
if(huart == &huart1 )
{
if(ncount >= 255)
{
ncount = 0;
memset((void *)TXarr,0x00,sizeof(TXarr));
}
else
{
//TXarr[ncount] = rxbuff[0];
//TXarr[ncount+1] = rxbuff[1];
//ncount += 2;//使用两个字节传输的时候解开注释并把下面一行注释掉
TXarr[ncount] = rxbuff;
ncount++;
if(TXarr[ncount-1]=='\n') //因为按下回车会自动换行所以以换行符结尾
{
HAL_UART_Transmit(&huart1,(uint8_t *)&TXarr,sizeof(TXarr),0xffff);
while(HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_TX);//检测UART发送结束
ncount = 0;
memset(TXarr,0x00,sizeof(TXarr)); //清空数组
}
}
}
HAL_UART_Receive_IT(&huart1,(uint8_t *)&rxbuff,1);
//HAL_UART_Receive_IT(&huart1,(uint8_t *)rxbuff,2);//使用两个字节传输的时候解开注释并把上面的注释掉
}
依旧需要注意的是此类方法是使用中断一个字节一个字节的接收,每次接收完一个字节后会清除中断使能,所以在最后依然要重新调用HAL_UART_Receive_IT(&huart1,(uint8_t *)&rxbuff,1),注释部分是用来验证每接收两个字节进一次中断的,但是不太建议大家这么操作,因为如果每两个字节存储一次需要由数组接收值然后又由接收数组传递数组的值到发送缓冲数组,发送缓冲数组的位置每一次都是两个字节两个字节的变动,比较容易出问题,而且只有当发送数据是偶数个的时候才能正确发送,如果是奇数个,前面两个字节两个两个接收完剩下最后一个字节一直等两个字节就接收不到,接收不到也就没有最后的停止字符发送缓冲数组就没法往外发送数据。
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @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 "usart.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "OLED.h"
#include "stm32f1xx_hal.h"
#include <stdio.h>
#include <string.h>
extern UART_HandleTypeDef huart1; //声明串口
/* 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 */
//uint8_t rxbuff[2];//使用两个字节传输的时候解开注释并把下面的注释掉
uint8_t rxbuff;//用于存储接收单个字节的变量
uint8_t ncount = 0;//用于指示数组位置的变量
uint8_t TXarr[256];//用于缓存多个字节的变量,当数据接收完毕一次性发送,但字节数不超过256
#include <string.h>
/* 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 */
/**
* 函数功能: 重定向c库函数printf到DEBUG_USARTx
* 输入参数: 无
* 返 回 值: 无
* 说 明:无
*/
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit_IT(&huart1, (uint8_t *)&ch, 1);
return ch;
}
/**
* 函数功能: 重定向c库函数getchar,scanf到DEBUG_USARTx
* 输入参数: 无
* 返 回 值: 无
* 说 明:无
*/
int fgetc(FILE *f)
{
uint8_t ch = 0;
HAL_UART_Receive_IT(&huart1, &ch, 1);
return ch;
}
/* 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 */
OLED_Init();
HAL_UART_Receive_IT(&huart1, (uint8_t *)&TXarr, 1);
memset(TXarr,0x00,sizeof(TXarr)); //清空数组
//HAL_UART_Receive_IT(&huart1, (uint8_t *)TXarr, 1);//使用两个字节传输的时候解开注释并把上面的注释掉
/* 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 RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
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 buses 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 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){
if(huart == &huart1 )
{
if(ncount >= 255)
{
ncount = 0;
memset((void *)TXarr,0x00,sizeof(TXarr));
}
else
{
//TXarr[ncount] = rxbuff[0];
//TXarr[ncount+1] = rxbuff[1];
//ncount += 2;//使用两个字节传输的时候解开注释并把下面一行注释掉
TXarr[ncount-1] = rxbuff;
ncount++;
if(TXarr[ncount]=='\n') //因为按下回车会自动换行所以以换行符结尾
{
HAL_UART_Transmit(&huart1,(uint8_t *)&TXarr,sizeof(TXarr),0xffff);
while(HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_TX);//检测UART发送结束
ncount = 0;
memset(TXarr,0x00,sizeof(TXarr)); //清空数组
}
}
}
HAL_UART_Receive_IT(&huart1,(uint8_t *)&rxbuff,1);
//HAL_UART_Receive_IT(&huart1,(uint8_t *)rxbuff,2);//使用两个字节传输的时候解开注释并把上面的注释掉
}
/* 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 */
__disable_irq();
while (1)
{
}
/* 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,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
全部代码展示,大家可以自行解开注释测试两字节发送的代码,用的时候记得注释掉单字节发送的代码
错误代码示例主要是我自己用的时候遇到的一些问题
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){
if(huart == &huart1 )
{
if(ncount >= 255)
{
ncount = 0;
memset((void *)TXarr,0x00,sizeof(TXarr));//这个代码是为了防止一次性发送数据的字节大小超过缓冲数组大小
}
else
{
//TXarr[ncount] = rxbuff[0];
//TXarr[ncount+1] = rxbuff[1];
//ncount += 2;//使用两个字节传输的时候解开注释并把下面一行注释掉
TXarr[ncount] = rxbuff;
if(TXarr[ncount]=='\n') //因为按下回车会自动换行所以以换行符结尾 (因为把ncount++放后面去了,所以就不需要减一了)
{
HAL_UART_Transmit(&huart1,(uint8_t *)&TXarr,sizeof(TXarr),0xffff);
while(HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_TX);//检测UART发送结束
ncount = 0;
memset(TXarr,0x00,sizeof(TXarr)); //清空数组
}
}
ncount++;//这里只把这个换了个位置,但是数据会从数组的第二位开始发送
}
HAL_UART_Receive_IT(&huart1,(uint8_t *)&rxbuff,1);
//HAL_UART_Receive_IT(&huart1,(uint8_t *)rxbuff,2);//使用两个字节传输的时候解开注释并把上面的注释掉
}
这是我一开始使用的代码我把ncount++放if后面去了就会导致如下结果
连续字节发送错误示例
出现这种结果的原因是当第一次成功发送后ncount被赋值0,然后又自增导致下一次进中断的时候ncount的值不再是0而是1,所以从数组的第二位开始赋值,至于没有被赋值的部分默认是0。
4.视频结果验证
连续字节发送正确示例,但发送的第一次是乱码我也没找到原因
5.小结
本章节旨在分享单片机编程经验,这次实验看似成功但仍然留有一个大问题,就是发送的第一个数据是错误的但后面发送的数据全部正常,我自己没找着原因,希望有懂得大佬指出错误让我能够改正。
最后提供一些我自己做测试的小经验,当使用非单字节连续传输时要充分考虑到接收数组大小能否容纳,其次就是接收数组的传递过程对应的值要一一对应不然数据一样会出错误,还是建议不要尝试多个字节的连续传输模式,单个字节既省时又省力,如果要发送大量的数据可以尝试dma转运,目前我还在学,等学会之后会发布一篇经验分享文章,关于数据发送建议大家可以找找资料因为数据编码传输的方式并不是串口专有的,其他通信协议一样适用,如果能完全理解,学习其他的通信协议能够事半功倍,以上是本篇文章全部内容,如果有更好的编程建议可在评论区分享供大家学习使用。