RS485+DMA+空闲中断+HAL库收发数据
一、概念讲解
1.1 RS485
485(一般称作 RS485/EIA-485)是隶属于 OSI 模型物理层的电气特性规定为 2 线,半双工,多点通信的标准。它的电气特性和 RS-232 大不一样。用缆线两端的电压差值来表示传递信号。RS485 仅仅规定了接受端和发送端的电气特性。它没有规定或推荐任何数据协议。
RS485 的特点包括:
1) 接口电平低,不易损坏芯片。RS485 的电气特性:逻辑“1”以两线间的电压差为+(2~6)V表示;逻辑“0”以两线间的电压差为-(2~6)V 表示。接口信号电平比 RS232 降低了,不易损坏接口电路的芯片,且该电平与 TTL 电平兼容,可方便与 TTL 电路连接。
2) 传输速率高。10 米时,RS485 的数据最高传输速率可达 35Mbps,在 1200m 时,传输速度可达 100Kbps。
3) 抗干扰能力强。RS485 接口是采用平衡驱动器和差分接收器的组合,抗共模干扰能力增强,即抗噪声干扰性好。
4) 传输距离远,支持节点多。RS485 总线最长可以传输 1200m 以上(速率≤100Kbps)一般最大支持 32 个节点,如果使用特制的 485 芯片,可以达到 128 个或者 256 个节点,最大的可以支持到 400 个节点。
1.2 RS485电气特性
差分信号逻辑1(正)电压为+2~+6V,而逻辑0(负)电压为-2~-6V.接口信号电平比RS-232-C降低了,就不易损坏接口电路的芯片,且该电平与TTL电平兼容,可方便与TTL电路连接。
SP3485 作为收发器,该芯片支持 3.3V 供电,最大传输速度可达10Mbps,支持多达 32 个节点,并且有输出短路保护。图中 A、B 总线接口,用于连接 485 总线。RO 是接收输出端,DI 是发送数据收入端,RE是接收使能信号(低电平有效),DE 是发送使能信号(高电平有效)。可以将RE和DE用同一个线连接,然后控制该脚的电平信号来确定是发送还是接收模式
1.3 RS485切换模式需要时间
因为RS485通信是采用半双工通信,有一个引脚作用是使能接收还是发送,但是MCU切换引脚电平需要一定的时间,在这段时间里面MCU的引脚是高阻态。
RS485芯片从接收模式切换到发送模式需要经过3.5us才有驱动能力输出
1.4 DMA介绍
(1)DMA是单片机集成在芯片内部的一个数据搬运工,它可以代替单片机对数据进行传输、存储,节约CPU资源。一般应用场景,ADC多通道采集,串口收发(频繁进入接收中断),SPI和IIC通信等
(2)STM32F2系列的DMA控制器最多有2个,每个控制器有8个数据流,每个数据流可以映射到不同的通道。例如,DMA2的数据流7可能用于某个特定外设,比如USART1的TX。(每个数据流同一时间只能服务一个外设。例如,若USART1_TX占用了DMA2_Stream7,则该流不可用于其他外设)
(3)DMA配置参数:
DMA请求源与通道选择:DMA1通道1
数据传输方向:外设 ↔ 存储器,存储器 ↔ 存储器
地址递增配置:外设地址递增和存储器地址递增(如果外设是ADC数据寄存器,关闭递增,如果存储器地址是接收数组,则开启递增)
数据宽度与对齐:外设/存储器数据宽度(串口一般是8位,ADC一般是16位)
传输模式:单次模式(串口接收)和循环模式(ADC采集数据)
(4)配置DMA发送必须要使能DMA中断,否则会出现发送一次成功后,后续串口始终处于“busy”状态(DMA传输完成后需通过中断通知CPU,否则无法判断数据是否发送完毕)
更详细的可以查看下面的博客
DMA伟大的数据搬运工_lpc1768定时器匹配触发dma-CSDN博客
二、配置过程
1、配置时钟,烧入方式,配置RS485的接收使能脚
2、配置串口,DMA
查看芯片参考手册
STMCU中文官网
3、串口收发缓存结构体
#define USART_TX_MAX_LEN 100
#define USART_RX_MAX_LEN 100
typedef struct
{unsigned char tx_buf[USART_TX_MAX_LEN]; unsigned char rx_buf[USART_RX_MAX_LEN];unsigned char rx_flag;unsigned char rx_len;
}usart_data_t;extern usart_data_t stUsart1Data;
4、 串口初始化代码加入 开启IDLE中断、开始DMA接收
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); //使能IDLE中断
HAL_UART_Receive_DMA(&huart1,stUsart1Data.rx_buf,USART_RX_MAX_LEN);
5、串口中断函数中加入 接收数据代码
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 1 */if((__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE) != RESET)) {__HAL_UART_CLEAR_IDLEFLAG(&huart1);//清除标志位HAL_UART_AbortReceive(&huart1); //已经接收完一帧数据,所以这里要停止接收,然后再重新接收 uint32_t temp = __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);// 获取DMA中未传输的数据个数 ,stUsart1Data.rx_len = USART_RX_MAX_LEN - temp; //总计数减去未传输的数据个数,得到已经接收的数据个数stUsart1Data.rx_flag = 1; // 接受完成标志位置1 HAL_UART_Receive_DMA(&huart1,stUsart1Data.rx_buf,USART_RX_MAX_LEN); //开始DMA接收}/* USER CODE END USART1_IRQn 1 */
}
6、配置串口printf打印函数
void Usart1Printf(const char *format,...)
{uint16_t len;va_list args; va_start(args,format);len = vsnprintf((char*)stUsart1Data.tx_buf,sizeof(stUsart1Data.tx_buf)+1,(char*)format,args);va_end(args);if(HAL_UART_Transmit_DMA(&huart1, stUsart1Data.tx_buf, len)!= HAL_OK) //判断是否发送正常,如果出现异常则进入异常中断函数{Error_Handler();}}
7、RS485通信加入使能发送和接收
/* USER CODE BEGIN 1 */
void USART1_RS485_Send_Enable(void)
{HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);
}void USART1_RS485_Receive_Enable(void)
{HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
}
8、测试代码
USART1_RS485_Receive_Enable(); //默认为接收使能while (1){/* USER CODE END WHILE */if(stUsart1Data.rx_flag == 1) //接收完成标志{USART1_RS485_Send_Enable();Usart1Printf("%s\r\n",stUsart1Data.rx_buf);HAL_Delay(100);//等待串口DMA发送完成,如果不加入此行代码,会出现发送失败,因为还没有发送完就切换成接收模式,导致不能发送数据 USART1_RS485_Receive_Enable();stUsart1Data.rx_len = 0;//清除计数stUsart1Data.rx_flag = 0;//清除接收结束标志位memset(stUsart1Data.rx_buf,0,USART_RX_MAX_LEN);}}
9、测试结果
三、优化RS485发送
当发送数据后,我们会等待发送完成后在开启串口接收,一般要么是延时一定时间,要么就是判断发送完成标志位,如果没有这个过程,会造成发送数据不完全,因此资源遭到了一定的浪费
优化思路:等待触发串口发送完成中断后,使能串口接收中断
void USART1_IRQHandler(void)
{HAL_UART_IRQHandler(&huart1);if((__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE) != RESET)) {__HAL_UART_CLEAR_IDLEFLAG(&huart1);//清除标志位HAL_UART_AbortReceive(&huart1); //已经接收完一帧数据,所以这里要停止接收,然后再重新接收 uint32_t temp = __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);// 获取DMA中未传输的数据个数 ,stUsart1Data.rx_len = USART_RX_MAX_LEN - temp; //总计数减去未传输的数据个数,得到已经接收的数据个数stUsart1Data.rx_flag = 1; // 接受完成标志位置1 HAL_UART_Receive_DMA(&huart1,stUsart1Data.rx_buf,USART_RX_MAX_LEN); //开始DMA接收}if(hdma_usart1_tx.State==HAL_DMA_STATE_READY){USART1_RS485_Receive_Enable();//传输完成,在此开启下一次传输}
}USART1_RS485_Receive_Enable();while (1){/* USER CODE END WHILE */if(stUsart1Data.rx_flag == 1) //接收完成标志{USART1_RS485_Send_Enable();Usart1Printf("%s\r\n",stUsart1Data.rx_buf);
// HAL_Delay(100);//等待串口DMA发送完成
// USART1_RS485_Receive_Enable();stUsart1Data.rx_len = 0;//清除计数stUsart1Data.rx_flag = 0;//清除接收结束标志位memset(stUsart1Data.rx_buf,0,USART_RX_MAX_LEN);}}
四、调试问题记录
第一步:控制串口调试助手和单片机的波特率相同,可以正常接收数据
第二步:控制串口调试助手和单片机的波特率不同,单片机接收数据不正确,乱码,但此时还是正常现象
第三步:控制串口调试助手切换正确波特率后,单片机接收数据还是不正确,并且DMA状态会变为HAL_DMA_STATE_ABORT,此时说明DMA配置存在Bug
上述问题根本原因是 波特率不匹配导致DMA传输超时或硬件错误,进而触发DMA中止,但切换回正确波特率后DMA未正确恢复
解决办法一:当DMA状态变为 HAL_DMA_STATE_ABORT
时,需重新初始化DMA
//重新初始化串口if (hdma_usart1_rx.State == HAL_DMA_STATE_ABORT){MX_USART1_UART_Init();LOG("DMA Abort\r\n");}
解决办法二:(玄学解决)
hdma_usart1_rx.Instance = DMA2_Stream2;
//DMA2_Stream5改为DMA2_Stream2