梳理STM32F429之通信传输部分---NO.6 RS485 通讯
目录
一、RS-485 通讯协议简介
RS-485 的物理层
二、RS-485—双机通讯实验
1、硬件设计
2、软件设计
3、下载验证
一、RS-485 通讯协议简介
与 CAN 类似, RS-485 是一种工业控制环境中常用的通讯协议,它具有抗干扰能力强、传输距离远的特点。 RS-485 通讯协议由 RS-232 协议改进而来,协议层不变,只是改进了物理层,因而保留了串口通讯协议应用简单的特点。
RS-485 的物理层
从《CAN—通讯实验》章节中了解到,差分信号线具有很强的干扰能力,特别适合应用于电磁环境复杂的工业控制环境中, RS-485 协议主要是把 RS-232 的信号改进成差分信号,从而大大提高了抗干扰特性,它的通讯网络示意图见下图。
对比 CAN 通讯网络,可发现它们的网络结构组成是类似的,每个节点都是由一个通讯控制器和一个收发器组成,在 RS-485 通讯网络中,节点中的串口控制器使用 RX 与 TX信号线连接到收发器上,而收发器通过差分线连接到网络总线,串口控制器与收发器之间一般使用 TTL 信号传输,收发器与总线则使用差分信号来传输。发送数据时,串口控制器的 TX 信号经过收发器转换成差分信号传输到总线上,而接收数据时,收发器把总线上的差分信号转化成 TTL 信号通过 RX 引脚传输到串口控制器中。
RS-485 通讯网络的最大传输距离可达 1200 米,总线上可挂载 128 个通讯节点,而由于 RS-485 网络只有一对差分信号线,它使用差分信号来表达逻辑,当 AB 两线间的电压差为-6V~-2V 时表示逻辑 1,当电压差为+2V~+6V 表示逻辑 0,在同一时刻只能表达一个信号,所以它的通讯是半双工形式的,它与 RS-232 通讯协议的特性对比见下图。
RS-485 与 RS-232 的差异只体现在物理层上,它们的协议层是相同的,也是使用串口数据包的形式传输数据。而由于 RS-485 具有强大的组网功能,人们在基础协议之上还制定了 MODBUS 协议,被广泛应用在工业控制网络中。此处说的基础协议是指前面串口章节中讲解的,仅封装了基本数据包格式的协议(基于数据位),而 MODBUS 协议是使用基本数据包组合成通讯帧格式的高层应用协议(基于数据包或字节)。感兴趣的读者可查找MODBUS 协议的相关资料了解。
由于 RS-485 与 RS-232 的协议层没有区别,进行通讯时,我们同样是使用 STM32 的USART 外设作为通讯节点中的串口控制器,再外接一个 RS-485 收发器芯片把 USART 外设的 TTL 电平信号转化成 RS-485 的差分信号即可。
二、RS-485—双机通讯实验
本小节演示如何使用 STM32 的 USART 控制器与 MAX485 收发器,在两个设备之间使用 RS-485 协议进行通讯,本实验中使用了两个实验板,无法像 CAN 实验那样使用回环测试(把 STM32 USART 外设的 TXD 引脚使用杜邦线连接到 RXD 引脚可进行自收发测试,不过这样的通讯不经过 RS-485 收发器,跟普通 TTL 串口实验没有区别),本教程主要以“ USART—485 通讯”工程进行讲解。
1、硬件设计
上图中的是两个实验板的硬件连接。在单个实验板中,作为串口控制器的 STM32从 USART 外设引出 TX 和 RX 两个引脚与 RS-485 收发器 MAX485 相连,收发器使用它的A 和 B 引脚连接到 RS-485 总线网络中。为了方便使用,我们每个实验板引出的 A 和 B 之间都连接了 1 个 120 欧的电阻作为 RS-485 总线的端电阻,所以要注意如果您要把实验板作为一个普通节点连接到现有的 RS-485 总线时,是不应添加该电阻的!
由于 485 只能以半双工的形式工作,所以需要切换状态, MAX485 芯片中有“ RE”和“ DE”两个引脚,用于控制 485 芯片的收发工作状态的,当 RE 引脚为低电平时, 485 芯片处于接收状态,当 DE 引脚为高电平时芯片处于发送状态。实验板中使用了 STM32 的PB8 直接连接到这两个引脚上,所以通过控制 PB8 的输出电平即可控制 485 的收发状态。
2、软件设计
(1)编程要点
- 初始化 485 通讯使用的 USART 外设及相关引脚;
- 编写控制 MAX485 芯片进行收发数据的函数;
- 编写测试程序,收发数据。
(2)代码分析
NO.1 485 硬件相关宏定义我们把 485 硬件相关的配置都以宏的形式定义到 “ bsp_485.h”文件中,见下面的代码清单。
/*USART 号、时钟、波特率*/
#define RS485_USART USART2
#define RS485_USART_CLK RCC_APB1Periph_USART2
#define RS485_USART_BAUDRATE 115200
/*RX 引脚*/
#define RS485_USART_RX_GPIO_PORT GPIOD
#define RS485_USART_RX_GPIO_CLK RCC_AHB1Periph_GPIOD
#define RS485_USART_RX_PIN GPIO_Pin_6
#define RS485_USART_RX_AF GPIO_AF_USART2
#define RS485_USART_RX_SOURCE GPIO_PinSource6
/*TX 引脚*/
#define RS485_USART_TX_GPIO_PORT GPIOD
#define RS485_USART_TX_GPIO_CLK RCC_AHB1Periph_GPIOD
#define RS485_USART_TX_PIN GPIO_Pin_5
#define RS485_USART_TX_AF GPIO_AF_USART2#define RS485_USART_TX_SOURCE GPIO_PinSource5
/*485 收发控制引脚*/
#define RS485_RE_GPIO_PORT GPIOB
#define RS485_RE_GPIO_CLK RCC_AHB1Periph_GPIOB
#define RS485_RE_PIN GPIO_Pin_8
/*中断相关*/
#define RS485_INT_IRQ USART2_IRQn
#define RS485_IRQHandler USART2_IRQHandler
以上代码根据硬件连接, 把与 485 通讯使用的 USART 外设号 、引脚号、引脚源以及复用功能映射都以宏封装起来,并且定义了接收中断的中断向量和中断服务函数,我们通过中断来获知接收数据。
NO.2 初始化 485 的 USART 配置
利用上面的宏,编写 485 的 USART 初始化函数,见下面的代码清单。
/*
* 函数名: RS485_Config
* 描述 : USART GPIO 配置,工作模式配置
* 输入 :无
* 输出 : 无
* 调用 :外部调用
*/
void RS485_Config(void)
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
/* 配置 USART 时钟 */
RCC_AHB1PeriphClockCmd(RS485_USART_RX_GPIO_CLK|
RS485_USART_TX_GPIO_CLK|
RS485_RE_GPIO_CLK, ENABLE);
RCC_APB1PeriphClockCmd(RS485_USART_CLK, ENABLE);
/* TX 引脚源*/
GPIO_PinAFConfig(RS485_USART_RX_GPIO_PORT,RS485_USART_RX_SOURCE, RS485_USART_RX_AF);
/* RX 引脚源*/
GPIO_PinAFConfig(RS485_USART_TX_GPIO_PORT,RS485_USART_TX_SOURCE,RS485_USART_TX_AF);
/* USART GPIO 配置 */
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
/*TX*/
GPIO_InitStructure.GPIO_Pin = RS485_USART_TX_PIN;
GPIO_Init(RS485_USART_TX_GPIO_PORT, &GPIO_InitStructure);
/*RX */
GPIO_InitStructure.GPIO_Pin = RS485_USART_RX_PIN;
GPIO_Init(RS485_USART_RX_GPIO_PORT, &GPIO_InitStructure);
/* 485 收发控制管脚 */
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStructure.GPIO_Pin = RS485_RE_PIN ;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(RS485_RE_GPIO_PORT, &GPIO_InitStructure);
/* USART 模式配置*/
USART_InitStructure.USART_BaudRate = RS485_USART_BAUDRATE;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No ;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(RS485_USART, &USART_InitStructure);
/*使能 USART*/
USART_Cmd(RS485_USART, ENABLE);
/*配置中断优先级*/
NVIC_Configuration();
/* 使能串口接收中断 */
USART_ITConfig(RS485_USART, USART_IT_RXNE, ENABLE);
/*控制 485 芯片进入接收模式*/
GPIO_ResetBits(RS485_RE_GPIO_PORT,RS485_RE_PIN);
}
与所有使用到 GPIO 的外设一样,都要先把使用到的 GPIO 引脚模式初始化,配置好复用功能,其中用于控制 MAX485 芯片的收发状态的引脚被初始化成普通推挽输出模式,以便手动控制它的电平输出,切换状态。 485 使用到的 USART 也需要配置好波特率、有效字长、停止位及校验位等基本参数,在通讯中,两个 485 节点的串口参数应一致,否则会导致通讯解包错误。在实验中还使能了串口的接收中断功能,当检测到新的数据时,进入中断服务函数中获取数据。
NO.3 使用中断接收数据
接下来我们编写在 USART 中断服务函数中接收数据的相关过程,见代码清单,其中的 bsp_RS485_IRQHandler 函数直接被 bsp_stm32f4xx_it.c 文件的 USART 中断服务函数调用,不在此列出。
//中断缓存串口数据
#define UART_BUFF_SIZE 1024
volatile uint16_t uart_p = 0;
uint8_t uart_buff[UART_BUFF_SIZE];
void bsp_RS485_IRQHandler(void)
{
if (uart_p<UART_BUFF_SIZE) {
if (USART_GetITStatus(RS485_USART, USART_IT_RXNE) != RESET) {
uart_buff[uart_p] = USART_ReceiveData(RS485_USART);
uart_p++;
USART_ClearITPendingBit(RS485_USART, USART_IT_RXNE);
}
} else {
USART_ClearITPendingBit(RS485_USART, USART_IT_RXNE);
}
}//获取接收到的数据和长度
char *get_rebuff(uint16_t *len)
{
*len = uart_p;
return (char *)&uart_buff;
}
//清空缓冲区
void clean_rebuff(void)
{
uint16_t i=UART_BUFF_SIZE+1;
uart_p = 0;
while (i)
uart_buff[--i]=0;
}
这个数据接收过程主要思路是使用了接收缓冲区,当 USART 有新的数据引起中断时,调用库函数 USART_ReceiveData 把新数据读取到缓冲区数组 uart_buff 中,其中 get_rebuff函数可以用于获缓冲区中有效数据的长度,而 clean_rebuff 函数可以用于对缓冲区整体清 0,这些函数配合使用,实现了简单的串口接收缓冲机制。这部分串口数据接收的过程跟 485收发器无关,是串口协议通用的。
NO.4 切换收发状态
在前面我们了解到 RS-485 是半双工通讯协议,发送数据和接收数据需要分时进行,所以需要经常切换收发状态。而 MAX485 收发器根据其“ RE”和“ DE”引脚的外部电平信号切换收发状态,所以控制与其相连的 STM32 普通 IO 电平即可控制收尾,为简便起见,我们把收发状态切换定义成了宏。
// 简单的延时
static void RS485_delay(__IO u32 nCount)
{
for (; nCount != 0; nCount--);
}/*控制收发引脚*/
//进入接收模式,必须要有延时等待 485 处理完数据
#define RS485_RX_EN() RS485_delay(1000);\
GPIO_ResetBits(RS485_RE_GPIO_PORT,RS485_RE_PIN); \
RS485_delay(1000);//进入发送模式,必须要有延时等待 485 处理完数据
#define RS485_TX_EN() RS485_delay(1000); \
GPIO_SetBits(RS485_RE_GPIO_PORT,RS485_RE_PIN);\
RS485_delay(1000);
这两个宏中,主要是在控制电平输出前后加了一小段时间延时,这是为了给 MAX485芯片预留响应时间,因为 STM32 的引脚状态电平变换后, MAX485 芯片可能存在响应延时。例如,当 STM32 控制自己的引脚电平输出高电平(控制成发送状态),然后立即通过 TX 信号线发送数据给 MAX485 芯片,而 MAX485 芯片由于状态不能马上切换,会导致丢失了部分 STM32 传送过来的数据,造成错误。
NO.5 发送数据
STM32 使用 485 发送数据的过程也与普通的 USART 发送数据过程差不多,我们定义了一个 RS485_SendByte 函数来发送一个字节的数据内容,见代码清单。
/***************** 发送一个字符 **********************/
//使用单字节数据发送前要使能发送引脚,发送后要使能接收引脚。
void RS485_SendByte( uint8_t ch )
{
/* 发送一个字节数据到 USART1 */
USART_SendData(RS485_USART,ch);
/* 等待发送完毕 */
while (USART_GetFlagStatus(RS485_USART, USART_FLAG_TXE) == RESET);
}
上述代码中就是直接调用了 STM32 库函数 USART_SendData 把要发送的数据写入到USART 的数据寄存器,然后检查标志位等待发送完成。在调用 RS485_SendByte 函数前,需要先使用前面提到的切换收发状态宏,把MAX485 切换到发送模式, STM32 发出的数据才能正常传输到 485 网络总线上,当发送完数据的时候,应重新把 MAX485 切换回接收模式,以便获取网络总线上的数据。
(3)main 函数
最后我们来阅读 main 函数,了解整个通讯过程,见代码清单。这个 main 函数的整体设计思路是,实验板检测自身的按键状态,若按键被按下,则通过 485 发送 256 个测试数据到网络总线上,若自身接收到总线上的 256 个数据,则把这些数据作为调试信息打印到电脑端。所以,如果把这样的程序分别应用到 485 总线上的两个通讯节点时,就可以通过按键控制互相发送数据了。
/**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
char *pbuf;
uint16_t len;
LED_GPIO_Config();
/*初始化 USART1*/
Debug_USART_Config();
/*初始化 485 使用的串口,使用中断模式接收*/
RS485_Config();
LED_BLUE;
Key_GPIO_Config();
printf("\r\n 欢迎使用 STM32 F429 开发板。 \r\n");
printf("\r\n F429 485 通讯实验例程\r\n");
printf("\r\n 实验步骤: \r\n");
printf("\r\n 1.使用导线连接好两个 485 通讯设备\r\n");
printf("\r\n 2.使用跳线帽连接好: 3V3<--->CAN/485_3V3,485-RX--PD5,485-TX--PD6 \r\n");
printf("\r\n 3.若使用两个开发板进行实验,给两个开发板都下载本程序即可。 \r\n");
printf("\r\n 4.准备好后,按下其中一个开发板的 KEY1 键,会使用 485 向外发送 0-255 的数字 \r\n");
printf("\r\n 5.若开发板的 485 接收到 256 个字节数据,会把数据以 16 进制形式打印出来。 \r\n");
while (1) {
/*按一次按键发送一次数据*/
if ( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON) {
uint16_t i;
LED_BLUE;
//切换到发送状态
RS485_TX_EN();
for (i=0; i<=0xff; i++) {
RS485_SendByte(i); //发送数据
}
/*加短暂延时,保证 485 发送数据完毕*/
Delay(0xFFF);
RS485_RX_EN();//切换回接收状态
LED_GREEN;
printf("\r\n 发送数据成功! \r\n"); //使用调试串口打印调试信息到终端
} else {
LED_BLUE;
pbuf = get_rebuff(&len);
if (len>=256) {
LED_GREEN;
printf("\r\n 接收到长度为%d 的数据\r\n",len);
RS485_DEBUG_ARRAY((uint8_t*)pbuf,len);
clean_rebuff();
}
}
}
}
在 main 函数中,首先初始化了 LED、按键以及调试使用的串口,再调用前面分析的RS485_Config 函数初始化了 RS-485 通讯使用的串口工作模式。
初始化后 485 就进入了接收模式,当接收到数据的时候会进入中断并把数据存储到接收缓冲数组中,我们在 main 函数的 while 循环中(else 部分)调用 get_rebuff 来查看该缓冲区的状态,若接收到 256 个数据就把这些数据通过调试串口打印到电脑端,然后清空缓冲区。
在 while 循环中,还检测了按键的状态,若按键被按下,就把 MAX485 芯片切换到发送状态并调用 RS485_SendByte 函数发送测试数据 0x00-0xFF,发送完毕后切换回接收状态以检测总线的数据。
3、下载验证
下载验证这个 485 通讯实验需要您有两个实验板,操作步骤如下:
- 按照“硬件设计”小节中的图例连接两个板子的 485 总线;
- 使用跳线帽连接 : 485_TX<--->PD6、 485_RX<--->PD5、 3V3<--->CAN/485_3V3 ;
- 用 USB 线使实验板“ USB TO UART”接口跟电脑连接起来,在电脑端打开串口调试助手,编译本章配套的程序,并给两个板子都下载该程序,然后复位。
- 复位后在串口调试助手应看到 485 测试的调试信息,按一下其中一个实验板上的KEY1 按键,另一个实验板会接收到报文,在串口调试助手可以看到相应的发送和接收的信息。