STM32_HAL:UART通信

0x00.章索引

STM32_HAL:点亮第一个LED

STM32_HAL:按键输入检测

STM32_HAL:简单定时器的使用

STM32_HAL:PWM输出

STM32_HAL:UART通信

0x01.目录

0x02.背景/声明

得益于STM32系列MCU的外设功能丰富、软件驱动库规范与参考资料多等特性,该系列MCU被广泛运用于各种电子/电气设备中,本文所使用的软件驱动库以官方的HAL(硬件抽象层)库为基础,以结构体指针的方式将各种外设功能进行进一步的封装,让程序具有面对对象编程的部分特性。
本文所使用的驱动库均为自行编写,受限于自身的水平,代码中难免会有不规范的地方,各位多指正~

0x03.开始前…

0X04.UART串口类和它的成员们

在单片机系统中,运用场景最为广泛的通信方式便是串行通信,而在包含I2C\SPI\RS232\RS485等通信方式的多种串行通信协议中,应用最为广泛(至少在我近年接触到的通信方式中)的即为通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),通常称作UART。
UART是一种异步全双工的通信方式,可进行起始位、数据位、奇偶检验位与停止位等报文配置,在单片机系统设计中,单片机往往作为下位机与PC上的上位机进行UART通信,或是作为主机与系统中的其他支持UART通信的从机设备(ESP无线模块、蓝牙透传模块等)进行UART通信,所以在设计UART模块时需要优先实现以下功能:

  • 发送数据包
  • 多种方式接收数据包
    • 定长接收数据包
    • 帧尾分包
    • 定时器分包
    • IDLE+DMA分包
  • 可变波特率初始化

针对以上功能需求,定义出串口类与new函数的具体实现:

#define MAX_RX_LENGTH 128		//最大接收长度
#define MAX_TX_LENGTH 128		//最大发送长度
// #define UART_USE_DMA

typedef struct UART_COMMON{
	UART_HandleTypeDef uart_handle;
	u8 rx_flag;
	u16 rx_len;
	u8 rx_buf[MAX_RX_LENGTH];
	u8 tx_buf[MAX_TX_LENGTH];
	void (*UClearRec)(struct UART_COMMON *this);
	void (*UPrintf)(struct UART_COMMON *this,char *fmt, ...);
}UART_COMMON;

UART_COMMON* new_Uart(struct UART_COMMON *this, USART_TypeDef *port, u32 baud)//串口的初始化与相关函数注册
{
	this = (struct UART_COMMON*) calloc(1, sizeof(struct UART_COMMON));
	this->uart_handle.Instance = port;
	this->uart_handle.Init.BaudRate = baud;
	this->uart_handle.Init.WordLength = UART_WORDLENGTH_8B;
	this->uart_handle.Init.StopBits = UART_STOPBITS_1;
	this->uart_handle.Init.Parity = UART_PARITY_NONE;
	this->uart_handle.Init.Mode = UART_MODE_TX_RX;
	this->uart_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE;
	this->uart_handle.Init.OverSampling = UART_OVERSAMPLING_16;
	if (HAL_UART_Init(&this->uart_handle) != HAL_OK) {
		Error_Handler();
	}

#ifdef UART_USE_DMA
	__HAL_UART_ENABLE_IT(&this->uart_handle, UART_IT_RXNE); //使能RXNE中断
	__HAL_UART_ENABLE_IT(&this->uart_handle, UART_IT_IDLE); //使能IDLE中断

	//DMA接收函数,此句一定要加,不加接收不到第一次传进来的实数据,是空的,且此时接收到的数据长度为缓存器的数据长度
	HAL_UART_Receive_DMA(&this->uart_handle, this->rx_buf,MAX_RX_LENGTH);
#else
	HAL_UART_Receive_IT(&this->uart_handle, (u8*) aRxBuffer, 1);
#endif
	this->UClearRec = uart_clear_rec;
	this->UPrintf = uart_printf;
	return this;
}

定义了串口类后,在源文件中加上串口相关函数的具体实现函数:

void uart_clear_rec(struct UART_COMMON *this) {
	memset(this->rx_buf, 0, this->rx_len);
	this->rx_flag = 0;
	this->rx_len = 0;
}

void uart_printf(struct UART_COMMON *this, char *fmt, ...) {	//通过任意串口发送字符串
	u16 i;
	va_list ap;
	va_start(ap, fmt);
	vsprintf((char*) this->tx_buf, fmt, ap);
	va_end(ap);
	i = strlen((const char*) this->tx_buf);		//此次发送数据的长度
#ifdef UART_USE_DMA
	HAL_UART_Transmit_DMA(&this->uart_handle, (uint8_t*) this->tx_buf, i);
#else
	HAL_UART_Transmit(&this->uart_handle, (uint8_t*) this->tx_buf, i, 0xffff);
#endif
}

接下来,通过帧尾分包与IDLE+DMA分包两种方式来进行串口回环程序的示例。

0x05.帧尾分包

使用帧尾分包进行数据的接收时,串口每接收到1个Byte,RXNE标志位被拉起并同时进入一次串口中断,再由串口中断拉起接收回调函数,最后在回调函数中进行数据包分包的标记,串口中断与回调函数的实例如下所示:

void USART1_IRQHandler(void) {
	u32 timeout = 0;
	u32 maxDelay = 0x1FFFF;

	HAL_UART_IRQHandler(&huart1);	//调用HAL库中断处理公用函数

	timeout = 0;
	while (HAL_UART_GetState(&huart1) != HAL_UART_STATE_READY)	//等待就绪
	{
		timeout++;	超时处理
		if (timeout > maxDelay)
			break;
	}

	timeout = 0;
	while (HAL_UART_Receive_IT(&huart1, (u8*) aRxBuffer, 1) != HAL_OK)//一次处理完成之后,重新开启中断并设置RxXferCount为1
	{
		timeout++; //超时处理
		if (timeout > maxDelay)
			break;
	}
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
	if (huart->Instance == USART1) //如果是串口1
	{
		if (((uart1_common->rx_flag) & 0x80) == 0) //接收未完成
				{
			if ((uart1_common->rx_flag) & 0x40) //接收到了0x0d
					{
				if (aRxBuffer[0] != 0x0a) {
					uart_clear_rec(uart1_common); //接收错误,重新开始
				} else
					uart1_common->rx_flag |= 0x80;	//接收完成了
			} else //还没收到0X0D
			{
				if (aRxBuffer[0] == 0x0d)
					uart1_common->rx_flag |= 0x40;
				else {
					uart1_common->rx_buf[uart1_common->rx_len++] = aRxBuffer[0];
					if (uart1_common->rx_len > (MAX_RX_LENGTH - 1)) {
						uart_clear_rec(uart1_common);	//接收错误,重新开始
					}
				}
			}
		}
	}
}

数据包分包的标志位被标记后,在主函数中进行数据包的回环与标志位的清除:

int main(void) {
	u8 uart_rec_str[128] = { 0 };
	HAL_Init();
	/* Configure the system clock */
	SystemClock_Config();
	uart1_common = new_Uart(uart1_common, USART1, 115200);	//初始化串口一 注册相关成员函数
	uart1_common->UPrintf(uart1_common, "UART ECHO EXAMPLE!\r\n");
	while (1) {
		if (uart1_common->rx_flag)		//接收完成
		{
			uart1_common->rx_buf[uart1_common->rx_len] = 0;
			sprintf((char*) uart_rec_str, "%s", uart1_common->rx_buf);
			uart1_common->UPrintf(uart1_common, (char*) uart_rec_str);		//回环
			uart1_common->UClearRec(uart1_common);	//重新开始下次接收
		}
	}
}

0x06.IDLE+DMA分包

使用IDLE+DMA分包的方式,不仅能够减少MCU的运算负担,还能更为简单明了的对一整帧数据包进行直接的接收:当UART接收到一帧数据后,IDLE标志位被拉起并进入串口中断,在串口中断中直接对数据包的分包标志位进行标记:

void USART1_IRQHandler(void) {
	uint32_t temp;
	if (__HAL_UART_GET_FLAG(&uart1_common->uart_handle,UART_FLAG_IDLE) != RESET)    //如果接受到了一帧数据
			{
		__HAL_UART_CLEAR_IDLEFLAG(&uart1_common->uart_handle);    //清除标志位
		HAL_UART_DMAStop(&uart1_common->uart_handle); //
		temp = __HAL_DMA_GET_COUNTER(&hdma_usart1_rx); // 获取DMA中未传输的数据个数
		uart1_common->rx_len = MAX_RX_LENGTH - temp; //总计数减去未传输的数据个数,得到已经接收的数据个数
		uart1_common->rx_flag = 1;	// 接受完成标志位置1
	}
	HAL_UART_IRQHandler(&uart1_common->uart_handle);
}

最后,同样在主函数中进行数据包的回环、标志位的清除并开启下一次的DMA接收:

int main(void) {
	u8 uart_rec_str[128] = { 0 };
	HAL_Init();
	/* Configure the system clock */
	SystemClock_Config();
	uart1_common = new_Uart(uart1_common, USART1, 115200);	//初始化串口一 注册相关成员函数
	uart1_common->UPrintf(uart1_common, "UART ECHO EXAMPLE!\r\n");
	while (1) {
		if (uart1_common->rx_flag)		//接收完成
		{
			uart1_common->rx_buf[uart1_common->rx_len] = 0;
			sprintf((char*) uart_rec_str, "%s", uart1_common->rx_buf);
			uart1_common->UPrintf(uart1_common, (char*) uart_rec_str);		//回环
			uart1_common->UClearRec(uart1_common);	//重新开始下次接收
			HAL_UART_Receive_DMA(&uart1_common->uart_handle, uart1_common->rx_buf,
					MAX_RX_LENGTH);	//重新打开DMA接收
		}
	}
}

0x07.小结

UART的软件驱动总的来说并不复杂,个人认为串口通信难的部分是:当串口通信运用于实际项目或情景时,应该如果商定一份让系统能够保持长时间稳定运行的报文协议,得到报文协议后,如果编写程序能够保证数据包被连续且稳定的接收并处理。
其实串口通信还是有很多其它的难点,在实际项目中经常需要开发者对某种特定的串口通信报文协议再封装一层更高层的类,供上层模块调用(例如给GPS模块用的TinyGPS++)。在串口通信的接收部分同样也有许多不同的处理方式,上回也有看到大牛们使用链表之类的结构来进行数据包的存储,实现了不定大小的数据缓冲区,同样十分值得学习。

附录

Gitee:https://gitee.com/hyjjjjjjjj/STM32_HAL_MODULES
Github:https://github.com/HYJJJJJJJJ/STM32_HAL_MODULES(不怎么更新

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值