一、stm32串口通信
stm32串口通信一般是指通过UART(Universal Asynchronous Receiver/Transmitter)通用异步收发传输器传输数据,UART 作为异步串口通信协议的一种,工作原理是将传输数据的每个字符一位接一位地传输,其在应用程序开发过程中使用频率最高的数据总线。而MCU的UART 通信实际上是基于GPIO引脚电平信号实现的,因此,在预制UART外设不够时,也会可以采用GPIO引脚自行模拟UART串口通信。
【1】硬件流控
UART 串口的特点是将数据一位一位地顺序传送,只要 2 根传输线就可以实现双向通信,一根线发送数据的同时用另一根线接收数据。但是,数据在两个串口之间进行通讯的时候常常会出现丢失数据的现象,主要原因是数据处理及数据接收效率不一致引起的冲突,为了解决这种问题,引入了流控概念。流控的概念源于 RS232 这个标准,用来解决这个速度匹配的问题,通过新增传输线来控制数据传输数据线的流向、流速等,这就是简单的三线串口的通讯方式,如下图:
在两条数据线基础上,增加两根控制线,一根叫 CTS(Clear To Send 为输入信号,一根叫 RTS(Require To Send 为输出信号),一个是接收控制,一个是发送控制。可以看到,数据线方向与流控线数据方向相反,因为流控的主要是协调接收数据和处理数据一致性,所以需要让发送端等待,接收端发出来的信号叫 RTS 信号,发送端检测管脚叫 CTS。
数据接收与 RTS 信号标记如下图,接收端缓存没数据时,RTS是低电平状态,发送端可以发送数据;当接收端接收到数据后,RTS切换为高电平,就是告诉发送端,数据还没有被拿走,请发送端等待;RTS 信号在数据没有被读取之前都是保持在高电平状态,一旦数据被 DMA 或者 CPU 从 DR 寄存器读取之后,RTS 就释放高电平,变为低电平,这时候发送端又可以发送数据的了。另外,如果 USART 是FIFO 模式,即缓冲模式开启的时候,在 FIFO 满的时候才会去拉高 RTS 信号。
相应的,发送数据和CTS信号类似,发送端在发送数据时,要实时监测 CTS 的电平状态,如果发现是高电平,就不会再发送新的数据,直到 CTS 检测发现已经没有高电平信号才会再次发送。
我们在cubeIDE中,对应上述的就是勾选RS232流控制支持,就可以选择CTS/RTS引脚配置。
如上面所述,由设想的两根数据线传输多出两根流控制线,这无形中增加硬件成本,因此,RS485出现后,RS232的CTS/RT合二为一DE ,STM32 上有一个 DE 管脚和 RS485 的接收器芯片直接相连,控制数据的收发,主要就是数据的方向的控制。因为 RS485 是一个半双工的通讯模式,它的数据收的时候就不能发,发的时候不能收。
在cubeMX中,需要支持DE引脚开始,就需要勾选RS485流控制功能
【2】软件流控
上述的增加流控制线的做法都是属于硬件流控的实现,在实际项目开发过程,我们很多时候为了方便以及节省引脚,往往采用软流控。软件流控是以特殊的字符来代表从机已经不能再接收新的数据了,基本的流程就是从机在接收数据很多的时候或主动给发送端发送一个特殊字符,当发送端接收到这个特殊字符后就不能再发送数据了。
软件流控很方便,不需要增加新的硬件,还是以前的TX、RX两根数据线实现,但是使用了软件流控,它本身的字符也是数据,这个数据只不过是说在软件里把它设置了一个特殊的含义。如果它是一个全双工的通讯,在给另一个串口发送数据的时候如果也包含了这样一个特殊字符,对方就会误以为我让它不要再发送数据了,会有一定的概率出现错误,而硬件流控就不需要考虑这方面,只需要使用 CTS 和 RTS,所有的数据都是由硬件来操作的,因此硬件流控在稳定性上更可靠。
【3】串口通信参数
UART 串口通信有几个重要的参数,分别是起始位、波特率、数据位、停止位和奇偶检验位(流控,按需),对于两个使用 UART 串口通信的端口,这些参数必须匹配,否则通信将无法正常完成。
起始位:表示数据传输的开始,默认电平逻辑为 “0” 。
波特率:串口通信时的速率,它用单位时间内传输的二进制代码的有效位(bit)数来表示,其单位为每秒比特数 bit/s(bps)。常见的波特率值有 4800、9600、14400、38400、115200等,数值越大数据传输的越快,波特率为 115200 表示每秒钟传输 115200 位数据。
数据位:可能值有 5、6、7、8、9,表示传输这几个 bit 位数据。一般取值为 8,因为一个 ASCII 字符值为 8 位。
停止位: 表示一帧数据的结束。默认电平逻辑为 “1”。
奇偶校验位:用于接收方对接收到的数据进行校验,校验 “1” 的位数为偶数(偶校验)或奇数(奇校验),以此来校验数据传送的正确性,使用时不需要此位也可以。
二、新建RS485通信工程
【1】项目实现背景信息
本文采用的STM32L496VGTX-ali开发板,该开发板预留了扩展接口支持USART4通信,本文将USART4与USB转RS485(CH340)连接,进而与电脑通信。
实物图:
扩展接口原理框图如下:
另外本人开发板标注背后扩展口标注的是PA0、PA1,查看其他描述,确定就是PA0、PA1引脚。
现在基于本专栏前面已近实现的串口lpusart通信和及lcd亮屏工程为基础创建新工程,并移植了相关代码。
cubeIDE开发, stm32调试信息串口通信输出显示_py_free的博客-CSDN博客
cubeIDE开发, stm32的OLED点亮及字符显示设计(基于SPI通信)_py_free的博客-CSDN博客
现将在该工程基础上,实现串口Usart4和lpusart通信,并lcd屏打印其发送数据;Usart4发送'A'/'B'可以点亮及熄灭LED0。
【2】项目配置
双击(.ioc)打开cubeMX配置界面,开启USART4串口功能,配置串口参数
并开启usart4的中断支持
点击保存生成输出代码。
【3】在ICore目录下,新建rs485文件夹,并在该文件目录下,创建rs485.h和rs485.c源文件,其实现代码如下:
rs485.h
#ifndef RS485_RS485_H_
#define RS485_RS485_H_
#include "stm32l4xx_hal.h" //HAL库文件声明
extern UART_HandleTypeDef huart4;//声明USART4的HAL库结构体
void RS485_printf (char *fmt, ...); //RS485发送
#endif /* RS485_RS485_H_ */
rs485.c
#include "../usart/usart.h"
#include "main.h"
#include <stdarg.h>
/*
RS485总线通信,使用UART4,这是RS485专用的printf函数
*/
void RS485_printf (char *fmt, ...)
{
char buff[USART4_REC_LEN+1]; //用于存放转换后的数据 [长度]
uint16_t i=0;
va_list arg_ptr;
va_start(arg_ptr,fmt);
vsnprintf(buff, USART4_REC_LEN+1,fmt,arg_ptr);//数据转换
i=strlen(buff);//得出数据长度
if(strlen(buff)>USART4_REC_LEN)i=USART4_REC_LEN;//如果长度大于最大值,则长度等于最大值(多出部分忽略)
HAL_UART_Transmit(&huart4,(uint8_t *)buff,i,0Xffff);//串口发送函数(串口号,内容,数量,溢出时间)
va_end(arg_ptr);
}
//所有USART串口的中断回调函数HAL_UART_RxCpltCallback,统一存放在【USART.C】文件中。
调整usart.h和usart.c文件,重新实现串口USART4的中断回调函数
usart.h
#ifndef _USART_H_
#define _USART_H_
#include "stm32l4xx_hal.h" //HAL库文件声明
#include <string.h>//用于字符串处理的库
#include "../print/print.h"//用于printf函数串口重映射
extern UART_HandleTypeDef hlpuart1;//声明LPUSART的HAL库结构体
extern UART_HandleTypeDef huart4;//声明USART4的HAL库结构体
#define HLPUSART_REC_LEN 256//定义LPUSART最大接收字节数
#define USART4_REC_LEN 256
extern uint8_t HLPUSART_RX_BUF[HLPUSART_REC_LEN];//接收缓冲,最大HLPUSART_REC_LEN个字节.末字节为换行符
extern uint16_t HLPUSART_RX_STA;//接收状态标记
extern uint8_t HLPUSART_NewData;//当前串口中断接收的1个字节数据的缓存
extern uint8_t USART4_RX_BUF[USART4_REC_LEN];
extern uint16_t USART4_RX_STA;
extern uint8_t USART4_NewData;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);//串口中断回调函数声明
#endif /* _USART_H_ */
usart.c
#include "usart.h"
uint8_t HLPUSART_RX_BUF[HLPUSART_REC_LEN];//接收缓冲,最大HLPUSART_REC_LEN个字节.末字节为换行符
/*
* bit15:接收到回车(0x0d)时设置HLPUSART_RX_STA|=0x8000;
* bit14:接收溢出标志,数据超出缓存长度时,设置HLPUSART_RX_STA|=0x4000;
* bit13:预留
* bit12:预留
* bit11~0:接收到的有效字节数目(0~4095)
*/
uint16_t HLPUSART_RX_STA=0;接收状态标记//bit15:接收完成标志,bit14:接收到回车(0x0d),bit13~0:接收到的有效字节数目
uint8_t HLPUSART_NewData;//当前串口中断接收的1个字节数据的缓存
uint8_t USART4_RX_BUF[USART4_REC_LEN];
uint16_t USART4_RX_STA=0;
uint8_t USART4_NewData;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)//串口中断回调函数
{
if(huart ==&hlpuart1)//判断中断来源(串口1:USB转串口)
{
if(HLPUSART_NewData==0x0d){//回车标记
HLPUSART_RX_STA|=0x8000;//标记接到回车
}else{
if((HLPUSART_RX_STA&0X0FFF)<HLPUSART_REC_LEN){
HLPUSART_RX_BUF[HLPUSART_RX_STA&0X0FFF]=HLPUSART_NewData; //将收到的数据放入数组
HLPUSART_RX_STA++; //数据长度计数加1
}else{
HLPUSART_RX_STA|=0x4000;//数据超出缓存长度,标记溢出
}
}
HAL_UART_Receive_IT(&hlpuart1,(uint8_t *)&HLPUSART_NewData,1); //再开启接收中断
}
if(huart ==&huart4)//判断中断来源(串口4:USB转串口)
{
// printf("%c",USART4_NewData);
if(USART4_NewData==0x0d){//回车标记
USART4_RX_STA|=0x8000;//标记接到回车
}else{
if((USART4_RX_STA&0X0FFF)<USART4_REC_LEN){
USART4_RX_BUF[USART4_RX_STA&0X0FFF]=USART4_NewData; //将收到的数据放入数组
USART4_RX_STA++; //数据长度计数加1
}else{
USART4_RX_STA|=0x4000;//数据超出缓存长度,标记溢出
}
}
HAL_UART_Receive_IT(&huart4,(uint8_t *)&USART4_NewData,1); //再开启接收中断
}
}
三、RS485驱动调用及测试
在main.c文件中加入rs485.h头文件支持
/* USER CODE BEGIN Includes */
#include "../../ICore/key/key.h"
#include "../../ICore/led/led.h"
#include "../../ICore/print/print.h"
#include "../../ICore/usart/usart.h"
#include "../../ICore/rs485/rs485.h"
#include "../../ICore/oled/oled.h"
/* USER CODE END Includes */
在主函数初始化中调整加入usart4初始化设定
/* USER CODE BEGIN 2 */
ResetPrintInit(&hlpuart1);
HAL_UART_Receive_IT(&hlpuart1,(uint8_t *)&HLPUSART_NewData, 1); //再开启接收中断
HLPUSART_RX_STA = 0;
HAL_UART_Receive_IT(&huart4,(uint8_t *)&USART4_NewData,1); //开启串口4接收中断
USART4_RX_STA = 0;
//
OLED_init();
//设置OLED蓝色背景显示
BSP_LCD_Clear_DMA(LCD_DISP_BLUE);
printf("OLED_Clear_DMA\r\n");
/* USER CODE END 2 */
在主函数循环体中,修改代码实现对usart4接收数据处理
/* USER CODE BEGIN WHILE */
while (1)
{
if(HLPUSART_RX_STA&0xC000){//溢出或换行,重新开始
RS485_printf("%.*s\r\n",HLPUSART_RX_STA&0X0FFF, HLPUSART_RX_BUF);
OLED_printf(10,10,"%.*s",HLPUSART_RX_STA&0X0FFF, HLPUSART_RX_BUF);
HLPUSART_RX_STA=0;//接收错误,重新开始
HAL_Delay(100);//等待
}
if(USART4_RX_STA&0xC000){//溢出或换行,重新开始
if(1==(USART4_RX_STA&0x0FFF))
{
switch (USART4_RX_BUF[0]){
case 'A':
set_led0_val(1);
break;
case 'B':
set_led0_val(0);
break;
default:
break;
}
}
printf("%.*s\r\n",USART4_RX_STA&0X0FFF, USART4_RX_BUF);
OLED_printf(10,42,"%.*s",USART4_RX_STA&0X0FFF, USART4_RX_BUF);
USART4_RX_STA=0;//接收错误,重新开始
HAL_Delay(100);//等待
}
/* USER CODE END WHILE */
编译及下载及测试
LED0在发送'A'字符是关闭(LED灯是低位有效)