HAL库条件下,串口+DMA收发相对容易,串口收发有各自通道,收发互不影响。而RS485通信是半双工通信模式,同一时刻,RS485网络只能有一个网络设备处于发送状态,其他网络设备必须处于接收状态,所以,要保证RS485网络正常工作,必须协调好网络设备的收发时机。设备收发控制的本质就是对RS485设备接口芯片的收发控制引脚的电平进行控制,本文将重点介绍RS485接口芯片的收发控制引脚电平的控制时机。
1 RS485接口芯片电路
如图1所示,U7是典型的RS485接口芯片,其接收输出引脚RO连接到MCU串口2的串口输入端RXD2;驱动输入DI引脚连接到MCU串口2的串口输出端TXD2;接收输出使能RE#引脚和驱动输出使能引脚DE短接作为DE2,连接到MCU的GPIO引脚,该GPIO引脚配置为推挽输出引脚。根据MAX3075的真值表,DE2高电平时,接口芯片处于发送状态;DE2低电平,接口芯片处于接收状态(A,B高阻态,不影响总线网络)。所以,DE2通常默认状态为低电平,使RS485接口芯片处于接收状态。该电路没有考虑远距离互联,仅适用于小型设备各部分互联使用,如果要实现较远距离(数十米甚至数百米以上)的设备之间互联,485A、485B网络标号部分要加上保护电路,请参见相关资料,这里不再赘述。
图1 RS485接口芯片电路图
可将MCU的PA1作为DE2。
2 串口2扩展RS485+DMA的收发控制
2.1 CUBEMAX 配置要点
下面以STM32F103RBT6为例介绍
串口2 参数配置
串口DMA配置串口中断
串口的端口配置
为了提高串口输入抗干扰性能,通常应将RX引脚配置为输入上拉模式,参见
《STM32系列MCU串口RX引脚上拉的必要性》。RS485的收发控制端口配置
例如,将PA1作为DE2——RS485芯片的收发控制引脚,默认低电平。
工程管理按如下配置
时钟配置
CUBEMAX的其他配置从略。
然后生成MDK-ARM工程框架代码。
2.2 在工程中添加的代码
在GPIO.h中添加如下代码
#define RS485_2_TX HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); //RS485_串口2发
#define RS485_2_RX HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET);//RS485_串口2收
在USART.c文件中添加如下代码:
uint8_t RX2_BUF[RX2_BUFFER_SIZE]; //串口2接收缓冲区
uint8_t TX2_BUF[TX2_BUFFER_SIZE]; //串口2发送缓冲区
volatile uint8_t RX2_LEN; //串口2接收的数据长度实际大小
volatile uint8_t RECV2_END_FLG; //串口2接收完成标志
volatile uint8_t TRANS2_END_FLG; //串口2发送完成标志
/
//函数名称:void RS485_Usart3_DMA_Send(uint8_t *buf,uint8_t len)
//功能描述:串口2采用RS485总线实现DMA发送
//参数说明:*buf 数据帧的指针,len数据帧的长度
//返 回 值: 无
void RS485_Usart2_DMA_Send(uint8_t *buf,uint8_t len)
{
uint8_t i;
if(TRANS_END_FLG==0) //如果串口发送处于完成状态
{
RS485_2_TX; //发送控制置位到发送状态——DE2高电平正在发送
TRANS_END_FLG=1; //发送完成标识置位———有发送
for( i=0;i<10;i++); //短暂延时,使DE2进入高电平稳定状态
if(HAL_UART_Transmit_DMA(&huart2, buf,len)!= HAL_OK) //判断是否发送正常,如果出现异常则进入异常中断函数
{
Error_Handler();
}
}
}
在USART.c的void MX_USART3_UART_Init(void)函数中添加:
/* USER CODE BEGIN USART3_Init 2 */
__HAL_UART_ENABLE_IT(&huart3,UART_IT_IDLE); //使能串口2空闲中断
/* USER CODE END USART3_Init 2 */
开启串口2的空闲中断接收。
在USART.h文件中添加如下代码:
/* USER CODE BEGIN Private defines */
#define RX3_BUFFER_SIZE 100
#define TX3_BUFFER_SIZE 100
/* USER CODE END Private defines */
/* USER CODE BEGIN Prototypes */
void RS485_Usart2_DMA_Send(uint8_t *buf,uint8_t len);
/* USER CODE END Prototypes */
在main.c中添加如下代码:
/* USER CODE BEGIN PV */
extern uint8_t RX2_BUF[RX3_BUFFER_SIZE]; //串口2接收缓冲区
extern uint8_t TX2_BUF[TX3_BUFFER_SIZE]; //串口2发送缓冲区
extern volatile uint8_t RX2_LEN; //串口2接收的数据长度实际大小
extern volatile uint8_t RECV2_END_FLG; //串口2接收完成标志
extern volatile uint8_t TRANS2_END_FLG; //串口2接收完成标志
/* USER CODE END PV */
/* USER CODE BEGIN PFP */
//
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart == &huart2)
{
RS485_2_RX; // RS485 接收模式
HAL_UART_Receive_DMA(&huart2,RX2_BUF,RX2_BUFFER_SIZE);//重新打开DMA接收
TRANS_END_FLG=1; //发送完成标志置位
}
}
/* USER CODE END PFP */
在stm32f1xx_it.c文件中添加如下代码:
/* USER CODE BEGIN Includes */
#include "usart.h"
#include "GPIO.h"
/* USER CODE END Includes */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
uint8_t RX2_BUF[RX2_BUFFER_SIZE]; //串口2接收缓冲区
uint8_t TX2_BUF[TX2_BUFFER_SIZE]; //串口2发送缓冲区
volatile uint8_t RX2_LEN; //串口2接收的数据长度实际大小
volatile uint8_t RECV2_END_FLG; //串口2接收完成标志
volatile uint8_t TRANS2_END_FLG; //串口2接收完成标志
/* USER CODE END PV */
/**
* @brief This function handles USART2 global interrupt.
*/
void USART2_IRQHandler(void)
{
/* USER CODE BEGIN USART2_IRQn 0 */
uint32_t tmp_flag = 0;
uint32_t temp;
tmp_flag =__HAL_UART_GET_FLAG(&huart2,UART_FLAG_IDLE); //获取IDLE标志位
if((tmp_flag != RESET)) //idle标志被置位
{
__HAL_UART_CLEAR_IDLEFLAG(&huart2); //清除标志位
HAL_UART_DMAStop(&huart2); //停止DMA传输,防止串行总线其他帧的干扰
temp = __HAL_DMA_GET_COUNTER(&hdma_usart2_rx); //获取DMA中未传输的数据个数
RX2_LEN = RX2_BUFFER_SIZE - temp; //总计数减去未传输的数据个数,得到实际接收的数据个数
RECV2_END_FLG=1; // 接收完成标志位置1
//如果是从设备,此处可以用条件语句判断是否应该应答主机的呼叫(从设备地址与呼叫地址一致),如果是就发送(应答)。下面是直接发送收到的信息
RS485_Usart3_DMA_Send(RX3_BUF,RX3_LEN); //RS485发送数据
TRANS_END_FLG=1; //串口3发送完成标志置1,表示正要发送
//下面的语句通常在发送完成回调函数内调用,在此处调用也可以
//HAL_UART_Receive_DMA(&huart3,RX3_BUF,RX3_BUFFER_SIZE);//重新打开DMA接收
}
/* USER CODE END USART3_IRQn 0 */
HAL_UART_IRQHandler(&huart3);
/* USER CODE BEGIN USART3_IRQn 1 */
/* USER CODE END USART3_IRQn 1 */
}
在有多个RS485设备的网络中,通常有一个主设备发起通信,网络中每个设备通常有一个唯一标识,也可称为地址,主设备发起通信,有点像点名,被点到的设备响应,向网络发送数据。不论是主设备还是从设备,仅在发数据时才拉高RS485接口芯片的发送数据控制引脚DE,其他时间DE一律为低电平。如图2所示,主设备呼叫帧通常包括帧头、设备地址、命令、数据、校验和帧尾等字节。通常从设备应答帧的长度也会根据主设备的命令不同而有变化,例如图中主设备两次呼叫从设备1,从设备1给出的应答长度是不一样的。帧长度不一样,DE的脉冲宽度也应随之变化。
在RS485网络中,只要本地设备不发送,均可接收网络中任何设备的发送的信息。从上面的代码可见,接收是通过串口空闲中断实现的,它可接收不定长的数据帧。RS485通信的关键是发送,即发送的时机控制,控制发送时机是为了防止通信中的冲突,主要是发送控制DE电平拉高和拉低的时机掌握,也就是何时拉高,何时拉低。
作为主设备,具有发起通信的主导权,网络中,主设备不发送,任何设备不能发送。主设备下一次发送是在确认不会有从设备发送时,才能进行再次发送。
作为从设备,通常是不被呼叫不应答,即不发送。
下面介绍DE的电平控制,对主设备和从设备是一样的,见代码:
void RS485_Usart2_DMA_Send(uint8_t *buf,uint8_t len)
{
uint8_t i;
if(TRANS_END_FLG==0) //如果串口有发送且已完成,或发送处于完成状态
{
RS485_2_TX; //发送控制置位到发送状态——DE2高电平
for( i=0;i<10;i++); //短暂延时,使DE2进入高电平稳定状态
if(HAL_UART_Transmit_DMA(&huart2, buf,len)!= HAL_OK) //判断是否发送正常,如果出现异常则进入异常中断函数
{
Error_Handler();
}
}
}
在发送数据前执行RS485_2_TX; 拉高DE引脚,其后有一个小的延时,保证DE进入高电平的稳定状态,再执行DMA发送。当数据发送完成要及时将DE拉低,让出总线使用权,DE拉低在发送完成中断回调函数中实现:
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart == &huart2)
{
RS485_2_RX; // RS485 接收模式
HAL_UART_Receive_DMA(&huart2,RX2_BUF,RX2_BUFFER_SIZE);//重新打开DMA接收
}
}
该回调函数一方面执行RS485_2_RX; 将DE拉低,另一方面,发送完成重新开启DMA接收。
如图3是PC串口调试器(+USB转485)和STM32F103扩展的RS485芯片,实现通信的波形图,即PC定时发送数据,MCU收到数据直接再发给PC的波形图。
图4为串口调试器收发实况。