更多交流欢迎关注作者抖音号:81849645041
目的
了解STM32F4 RS485,RS422通讯原理,掌握RS485,RS422通讯协议实现设备间通讯。
原理
与 CAN 类似,RS-485 是一种工业控制环境中常用的通讯协议,它具有抗干扰能力强、 传输距离远的特点。RS-485 通讯协议由 RS-232 协议改进而来,协议层不变,只是改进了 物理层,因而保留了串口通讯协议应用简单的特点。
RS-485 的物理层
差分信号线具有很强的干扰能力,特别适合应用于电磁环境复杂的工业控制环境中,RS-485 协议主要是把 RS-232 的信号改进成差分信号,从而大大提高了抗干扰特性,它的通讯网络示意图见图:
每个节点都是由一个通讯控制器和一个收发器组成,在 RS-485 通讯网络中,节点中的串口控制器使用 RX 与 TX信号线连接到收发器上,而收发器通过差分线连接到网络总线,串口控制器与收发器之间一般使用 TTL 信号传输,收发器与总线则使用差分信号来传输。发送数据时,串口控制器的 TX 信号经过收发器转换成差分信号传输到总线上,而接收数据时,收发器把总线上的差分信号转化成 TTL 信号通过 RX 引脚传输到串口控制器中。
RS-485 通讯网络的最大传输距离可达 1200 米,总线上可挂载 128 个通讯节点,而由 于 RS-485 网络只有一对差分信号线,它使用差分信号来表达逻辑,当 AB 两线间的电压差 为-6V~-2V 时表示逻辑 0,当电压差为+2V~+6V 表示逻辑 1,在同一时刻只能表达一个信 号,所以它的通讯是半双工形式的。
485(一般称作 RS485/EIA-485)是隶属于 OSI 模型物理层的电气特性规定为 2 线,半双工,多点通信的标准。它的电气特性和 RS-232 大不一样。用缆线两端的电压差值来表示传递信号。RS485 仅仅规定了接受端和发送端的电气特性。它没有规定或推荐任何数据协议。
RS485 的特点包括:
① 接口电平低,不易损坏芯片。RS485 的电气特性:逻辑“1”以两线间的电压差为+(2~6)V表示;逻辑“0”以两线间的电压差为-(2~6)V 表示。接口信号电平比 RS232 降低了,不易损坏接口电路的芯片,且该电平与 TTL 电平兼容,可方便与 TTL 电路连接。
② 传输速率高。10 米时,RS485 的数据最高传输速率可达 35Mbps,在 1200m 时,传输速度可达 100Kbps。
③ 抗干扰能力强。RS485 接口是采用平衡驱动器和差分接收器的组合,抗共模干扰能力 增强,即抗噪声干扰性好。传输距离远,支持节点多。RS485 总线最长可以传输 1200m以上(速率≤100Kbps)
④ 一般最大支持 32 个节点,如果使用特制的 485 芯片,可以达到 128 个或者 256 个节点,最大的可以支持到 400 个节点。
RS485 推荐使用在点对点网络中,线型,总线型,不能是星型,环型网络。理想情况下 RS485需要 2 个终端匹配电阻,其阻值要求等于传输电缆的特性阻抗(一般为 120Ω)。没有特性阻抗的话,当所有的设备都静止或者没有能量的时候就会产生噪声,而且线移需要双端的电压差。没有终接电阻的话,会使得较快速的发送端产生多个数据信号的边缘,导致数据传输出错。
在上面的连接中,如果需要添加匹配电阻,我们一般在总线的起止端加入,也就是主机和设备 4 上面各加一个 120Ω的匹配电阻。
由于 RS485 具有传输距离远、传输速度快、支持节点多和抗干扰能力更强等特点,所以 RS485 有很广泛的应用。
我们的STM32F407开发板使用的是RS422,使用MAX3490作为接受器。下面介绍RS422特性。
RS422概述
RS-422标准全称是“平衡电压数字接口电路的电气特性”,它定义了接口电路的特性。实际上还有一根信号地线,共5根线。由于接收器采用高输入阻抗和发送驱动器比RS232更强的驱动能力,故允许在相同传输线上连接多个接收节点,最多可接10个节点。一个主设备(Master),其余为从设备(Slave),从设备之间不能通信,所以RS-422支持点对多的双向通信。接收器输入阻抗为4k,故发端最大负载能力是10*4k+100Ω(终接电阻)。
RS-422和RS-485电路原理基本相同,都是以差动方式发送和接受,不需要数字地线。差动工作是同速率条件下传输距离远的根本原因,这正是二者与RS232的根本区别,因为RS232是单端输入输出,双工工作时至少需要数字地线。发送线和接受线三条线(异步传输),还可以加其它控制线完成同步等功能。
RS-422通过两对双绞线可以全双工工作收发互不影响,而RS485只能半双工工作,发收不能同时进行,但它只需要一对双绞线。RS422和RS485在19kpbs下能传输1200米。用新型收发器线路上可连接多台设备。
RS-422的电气性能与RS-485完全一样。主要的区别在于:RS-422有4根信号线:两根发送(Y、Z)、两根接收(A、B)。由于RS-422的收与发是分开的所以可以同时收和发(全双工);RS-485有2根信号线:发送和接收。
开发板采用的是MAX3490作为接收器,引脚图:
RS422引脚配置和典型工作电路:
准备
MDK5 开发环境。
STM32F4xx HAL库。
STM32F407 开发板。
STM32F4xx 参考手册。
STM32F4xx 数据手册。
STM32F407 开发板电路原理图。
步骤
- 查看STM32F407电路原理图,找到RS422接线引脚。
对照开发板,可以看到RS422输入输出引脚和串口2共用引脚,连接的是PA2和PA3。所以需要对PA2,PA3引脚复用。开发板上也需要跳线帽连接RS422和串口2。
最后,通过导线或双绞线把两个开发板A,B,Z,Y连接起来。对应的A连接Y,B连接Z,Z连接B,Y连接A,可以参考上面工作电路连接图。连接错误会导致通讯异常。
- 新建两个文件bsp_rs422.h和bsp_rs422.c,在bsp_rs422.h定义宏和函数。
#ifndef __BSP_RS422_H
#define __BSP_RS422_H
#include "stm32f4xx.h"
#define EN_USART2_RX 1 //接收中断使能
void RS422_Init(uint32_t bound); // RS422初始化
void RS422_Send_Data(uint8_t *buf, uint8_t len); // 发送数据
void RS422_Receive_Data(uint8_t *buf, uint8_t *len); // 接收数据
#endif
- 在bsp_rs422.c中实现串口2初始化,接收数据和发送数据函数。
第一步:RS422_Init(u32 bound)初始化函数中,复用PA2和PA3引脚,初始化串口2,RS422和USART2共用,调用的也是串口初始化函数。开启接收中断。
第二步:RS422_Send_Data(u8 *buf,u8 len)数据发送函数,调用HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)发送指定长度数据;
第三步:USART2_IRQHandler(void) 中断函数中,判断接收状态,调用HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)接收一个字节放到数组中;
第四步:RS422_Receive_Data(u8 *buf,u8 *len)接收函数中,判断数据是否接收完,返回数据和长度。
#include "bsp_rs422.h"
UART_HandleTypeDef RS422_huart; // USART2 句柄(用于 RS422)
#if EN_USART2_RX // 如果使能了接收
uint8_t RS422_RX_BUF[64]; // 接收缓存区 接收缓冲,最大 64 个字节.
uint8_t rxCount=0; // 接收到的数据长度
/**
* 函数名:USART2_IRQHandler
* 描述:接收中断 连续接收一个字节 直到接收完
* 输入:无
* 输出:无
*/
void USART2_IRQHandler(void) // 接收中断 连续接收一个字节 直到接收完
{
uint8_t rec;
if(__HAL_UART_GET_FLAG(&RS422_huart, UART_FLAG_RXNE) != RESET) // 接收中断
{
HAL_UART_Receive(&RS422_huart, &rec, 1, 1000);
if(rxCount < 64)
{
RS422_RX_BUF[rxCount] = rec; // 记录接收到的值
rxCount++; // 接收数据增加 1
}
}
}
#endif
/**
* 函数名:RS422_Init
* 描述:初始化
* 输入:bound 波特率
* 输出:无
*/
void RS422_Init(uint32_t bound) // 初始化
{
GPIO_InitTypeDef GPIO_Init;
__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能 GPIOA 时钟
__HAL_RCC_USART2_CLK_ENABLE(); // 使能 USART2 时钟
GPIO_Init.Pin = GPIO_PIN_2 | GPIO_PIN_3;
GPIO_Init.Mode = GPIO_MODE_AF_PP;
GPIO_Init.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_Init.Pull = GPIO_PULLUP;
GPIO_Init.Alternate = GPIO_AF7_USART2; // 复用为 USART2
HAL_GPIO_Init(GPIOA, &GPIO_Init);
RS422_huart.Instance = USART2; // USART2
RS422_huart.Init.BaudRate = bound; // 波特率
RS422_huart.Init.Mode = UART_MODE_TX_RX; // 收发模式
RS422_huart.Init.WordLength = UART_WORDLENGTH_8B; // 字长 8 位数据
RS422_huart.Init.StopBits = UART_STOPBITS_1; // 一个停止位
RS422_huart.Init.Parity = UART_PARITY_NONE; // 无奇偶校验位
RS422_huart.Init.HwFlowCtl = UART_HWCONTROL_NONE; // 无硬件流控
HAL_UART_Init(&RS422_huart);
//__HAL_UART_DISABLE_IT(&RS422_huart, UART_IT_TC); // 失能
#if EN_USART2_RX
__HAL_UART_ENABLE_IT(&RS422_huart, UART_IT_RXNE); // 开启接收中断
HAL_NVIC_EnableIRQ(USART2_IRQn); // 使能 USART1 中断
HAL_NVIC_SetPriority(USART2_IRQn, 3, 3); // 抢占优先级 3,子优先级 3
#endif
}
/**
* 函数名:RS422_Send_Data
* 描述:发送 len 个字节
* 输入:buf:发送区首地址 len:发送的字节数(为了和本代码的接收匹配,这里建议不要超过 64 个字节)
* 输出:无
*/
void RS422_Send_Data(uint8_t *buf, uint8_t len)
{
HAL_UART_Transmit(&RS422_huart, buf, len, 10000);
}
/**
* 函数名:RS422_Receive_Data
* 描述:RS422 查询接收到的数据
* 输入:buf:接收缓存首地址 len:读到的数据长度
* 输出:
*/
void RS422_Receive_Data(uint8_t *buf, uint8_t *len)
{
*len = 0; // 默认为 0
HAL_Delay(10); // 等待 10ms,连续超过 10ms 没有接收到一个数据,则认为接收结束
if(rxCount)
{ // 接收到了数据,且接收完成了
for(uint8_t i = 0; i < rxCount; i++)
{
buf[i] = RS422_RX_BUF[i];
}
*len = rxCount; // 记录本次数据长度
rxCount = 0; // 清零
}
}
- main.c中程序如下:
第一步:定义5元素数组作为发送的数据。
第二步:初始化系统时钟,串口打印,按键,LED,RS422。
第三步:KEY1按下就发送数据。循环接收数据,有数据就打印接收的数据。
#include "bsp_clock.h"
#include "bsp_uart.h"
#include "bsp_key.h"
#include "bsp_rs422.h"
#include "bsp_led.h"
int main(void)
{
uint8_t Txbuf[5] = {1,2,3,4,5}; // 发送的数据
uint8_t Rxbuf[5]; // 接收数组
uint8_t i,len;
CLOCLK_Init(); // 初始化系统时钟
UART_Init(); // 串口初始化
KEY_Init(); // 按键初始化
LED_Init(); // LED初始化
RS422_Init(115200); // RS422初始化
while(1)
{
if(KEY_Scan(0) == 1) // KEY1按下
{
LED1_Toggle; // 点亮LED1
printf("Sand data ");
for(i = 0; i < 5; i++)
{
printf("%d ", Txbuf[i]); // 打印发送的数据
}
RS422_Send_Data(Txbuf, 5); // 调用发送函数发送数据
}
RS422_Receive_Data(Rxbuf, &len); // 循环接收数据 得到数据和长度
if(len) // 长度不为0
{
LED2_Toggle; // 点亮LED2
printf("Recv data ");
for(i = 0; i < len; i++) // 打印接收的数据
{
printf("%d ", Rxbuf[i]);
}
}
HAL_Delay(50);
}
}
现象
将程序下载到两个开发板中,保证端口A,B,Z,Y连接正确。A连接Y,B连接Z,Z连接B,Y连接A。按下开发板1的KEY1发送数据,打印发送的数据,点亮LED1。按下开发板2的KEY1发送数据,开发板1接收到数据,打印接收的数据并点亮LED2。
按下板1 KEY1,板2接收到数据LED2会点亮。
按下板2的KEY1,板1接收到数据打印并点亮LED2。