18.1关于 RS485

RS485是美国电子工业协会(Electronic Industries Association,EIA)于1983年发布的串行通信接口标准,经通讯工业协会(TIA)修订后命名为TIA/EIA-485-A

RS485具有支持多节点(32个节点)、传输距离远(最大1219m)、接收灵敏度高(200mV电压)、连接简单(在构成通信网络时,仅需要一对双绞线作传输线)、能抑制共模干扰(差分传输)、成本低廉等特点,在多站、远距离通信等多种工控环境中获得了广泛应用。

RS485比RS232晚出现20多年,很多RS232的缺点,在RS485上有了改进。

RS232的电平从-15V至+15V,较高的电平值易损坏接口电路的芯片,而RS485采用差分信号后,电平范围为-6V至+6V,相对不易损坏接口电路芯片,同时RS485接口信号电平与TTL信号电平兼容,便于连接TTL电路。

RS232传输速率比较低,传输速率为20Kbps,而RS485最高传输速率达10Mbps。过高的传输速率会降低传输距离,在实际应用中,RS485传输速率往往设置为9600bps或更低。

RS232采用逻辑电平,共地传输容易产生共模干扰,抗噪声干扰性弱,传输距离有限,常用传输距离就几十米左右。而RS485采用平衡发送和差分接收方式,具有抑制共模干扰的能力,加之总线收发器具有高灵敏度,能检测低至200mV的电压,因此RS485的传输距离达到千米以外。

RS232在总线上只允许连接1个收发器,即单站能力,而RS485在总线上允许连接多达128个收发器,即具有多站能力,可以利用单一的RS485方便地建立起设备网络,如图 18.1.1 所示,为RS485通信网络结构。

在RS485通信网络中,通常使用485收发器将TTL电平转换成RS485的差分信号。MCU的串口控制器TxD发送数据,经485收发器转换成差分信号,传输到总线上。接收数据时,485收发器将总线上的差分信号转化成TTL信号由RxD到串口控制器。整个通信网络中,通常只有一个主机,剩下的全部为从机。在RS485总线中,通常还需要在总线起止端分别加上约120Ω的终端匹配电阻,以保证RS485总线的稳定性。
在这里插入图片描述
RS485同样可以使用DB9接口将信号引脚引出,实际工程中通常使用接线端子引出,如图 18.1.2 所示。图中左边的为螺钉式接线端子,适合固定连接的场合,图中右边为插拔式接线端子,适合需要调整的场合。本开发板使用的插拔式接线端子,如上图 3.3.1 中编号10部分所示。

在这里插入图片描述

18.2 硬件设计

如图 18.2.1 为开发板RS485部分的原理图,U16为3.3V低功耗半双工收发器,满足RS-485和RS-422标准。 USART的RX和TX,经过U16转换,变为RS485的A、B。

U16的2脚RE����为接收使能,上划线表示低电平有效,即当U16的2脚为低电平时,U16接收数据。U16的3 脚DE为输出使能,高电平有效,即当U16的3脚为高电平是,U16发送数据。

因此,RS485除了USART,还多了一个收发控制引脚,该引脚使用的PC5。R64为终端匹配电阻,阻值为120Ω。
在这里插入图片描述
结合前面图 17.2.1 电路和表格可知,如果需要将USART2分配给RS485使用,还要将J11(蓝色拨码开关)的1号脚拨到ON位置。

18.3软件设计

18.3.1软件设计思路

实验目的:RS485是差分信号,收发数据时,A、B都在工作。开发板也只提供了一个RS485接口,因此不能自发自收实验,需要至少两个RS485设备进行实验。这里假设两个开发板进行RS485通信,一个做主机,一个做从机,主机发送数据给从机,从机收到数据再发给主机,实现两个设备的收发数据,供读者参考和方便移植。

  1. 初始化USART1、2:设置波特率,收发选择,有效数据位等;
  2. 将所使用的串口引脚初始化:USART使能、GPIO端口时钟使能、GPIO引脚设置为USART复用;
  3. RS485采用中断方式发送,编写中断回调函数;
  4. 主函数编写控制逻辑:按下按键KEY1(KEY_U),主机RS485发送一次数据,从机RS485接收到数据并打印,然后从机RS485发送数据,主机RS485接受到数据并打印;
  5. 在软件方面,RS485的本质跟串口没有差别,不同的地方在于:RS485在发送、接收之前,需要设置收发控制引脚。

本实验配套代码位于“5_程序源码\10_通信—RS485\”。

18.3.2软件设计讲解

  1. GPIO 引脚选择与串口选择
    本实验会用到两个串口,USART1用于调试、USART2用于RS485,在代码框架上,将每个串口都单独放在“.c”文件里,方便修改裁剪。

代码段 18.3.1 调试串口 USART1 相关宏定义(driver_usart1.h)

/*********************
* 引脚宏定义
**********************/
#define DEBUG_USART USART1
#define DEBUG_USART_RX_PIN GPIO_PIN_10
#define DEBUG_USART_TX_PIN GPIO_PIN_9
#define DEBUG_USART_PORT GPIOA
#define DEBUG_USART_GPIO_CLK_EN() __HAL_RCC_GPIOA_CLK_ENABLE()
#define DEBUG_USART_CLK_EN() __HAL_RCC_USART1_CLK_ENABLE()
#define DEBUG_USART_CLK_DIS() __HAL_RCC_USART1_CLK_DISABLE()
#define DEBUG_USART_IRQn USART1_IRQn

代码段 18.3.2 调试串口 USART2 相关宏定义(driver_usart2.h)

/*********************
* 引脚宏定义
**********************/
#define RS485 USART2
#define RS485_RX_PIN GPIO_PIN_3
#define RS485_TX_PIN GPIO_PIN_2
#define RS485_PORT GPIOA
#define RS485_GPIO_CLK_EN() __HAL_RCC_GPIOA_CLK_ENABLE()
#define RE_DE_PIN GPIO_PIN_5
#define RE_DE_PORT GPIOC
#define RE_DE_GPIO_CLK_EN() __HAL_RCC_GPIOC_CLK_ENABLE()
/*********************
* 函数宏定义
**********************/
// 此引脚高电平是发送有效接收无效;低电平时接收有效发送无效
#define RE_DE_TX() HAL_GPIO_WritePin(RE_DE_PORT, RE_DE_PIN, GPIO_PIN_SET)
#define RE_DE_RX() HAL_GPIO_WritePin(RE_DE_PORT, RE_DE_PIN, GPIO_PIN_RESET)
#define RS485_IRQn USART2_IRQn
#define RS485_IRQHandler USART2_IRQHandler
#define RS485_CLK_ENABLE() __HAL_RCC_USART2_CLK_ENABLE()
#define RS485_CLK_DISABLE() __HAL_RCC_USART2_CLK_DISABLE()

分别定义了两个串口、对应GPIO、时钟使能,方便代码复用,同时定义了RS485的收发控制引脚。

  1. 初始化USART
    USART初始化包含两部分:协议部分和硬件部分。
    协议部分放在各自“.c”文件里,硬件部分都是调用“HAL_UART_Init()”,单独创建一个“.c”文件处理。USART1作为调试串口,初始化和前面的实验一样,这里直接跳过。USART2作为RS485,初始化如代码段 18.3.3 所示。

代码段 18.3.3 USART2 初始化(driver_usart2.c)

/*
* 函数名:void RS485_Init(uint32_t baudrate)
* 输入参数:baudrate-串口波特率
* * 输出参数:无
* 返回值:无
* 函数作用:初始化 USART 的波特率,收发选择,有效数据位等
*/
void RS485_Init(uint32_t baudrate)
{
husart2.Instance = RS485; // 选择 USART2
husart2.Init.BaudRate = baudrate; // 配置波特率
husart2.Init.WordLength = USART_WORDLENGTH_8B; // 配置数据有效位为 8bit
husart2.Init.StopBits = USART_STOPBITS_1; // 配置一位停止位
husart2.Init.Parity = USART_PARITY_NONE; // 不设校验位
husart2.Init.Mode = USART_MODE_TX_RX; // 可收可发
husart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
// 使用库函数初始化 USART2 的参数
if (HAL_UART_Init(&husart2) != HAL_OK)
{
Error_Handler(); } }

RS485的本质还是串口,串口的初始化和之前的基本一样。RS485通常也遵循“96-N-8-1”格式,96指波特率9600,N指无校验,8指8bits数据位,1指1bit停止位。

串口协议初始化完后,都调用“HAL_UART_Init()”进行设置,在“HAL_UART_Init()”调用
“HAL_UART_MspInit()”初始化串口硬件部分。

代码段 18.3.4 USART MSP 初始化(driver_msp_usart.c)

/*
* 函数名:void AL_USART_MspInit(USART_HandleTypeDef* husart)
* 输入参数:husart-USART 句柄
* 输出参数:无
* 返回值:无
* 函数作用:使能 USART1、2 的时钟,使能引脚时钟,并配置引脚的复用功能
*/
void HAL_UART_MspInit(UART_HandleTypeDef* husart)
{
// 定义 GPIO 结构体对象
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(husart->Instance==DEBUG_USART) {
// 使能 USART1 的时钟
DEBUG_USART_CLK_EN();
// 使能 USART1 的输入输出引脚的时钟
DEBUG_USART_GPIO_CLK_EN();
/**USART1 GPIO Configuration
PA9 ------> USART1_TX
PA10 ------> USART1_RX
*/
GPIO_InitStruct.Pin = DEBUG_USART_TX_PIN; // 选择 USART1 的 TX 引脚
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 配置为复用推挽功能
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 引脚翻转速率快
HAL_GPIO_Init(DEBUG_USART_PORT, &GPIO_InitStruct); // 初始化 TX 引脚
GPIO_InitStruct.Pin = DEBUG_USART_RX_PIN; // 选择 RX 引脚
GPIO_InitStruct.Mode = GPIO_MODE_AF_INPUT; // 配置为输入
HAL_GPIO_Init(DEBUG_USART_PORT, &GPIO_InitStruct); // 初始化 RX 引脚
}
else if(husart->Instance==RS485) {
// 使能 USART2 的时钟
RS485_CLK_ENABLE();
// 使能 USART2 的输入输出和方向引脚的时钟
RS485_GPIO_CLK_EN();
RE_DE_GPIO_CLK_EN();
/**USART2 GPIO Configuration
PA2 ------> USART2_TX
PA3 ------> USART2_RX
*/
GPIO_InitStruct.Pin = RS485_TX_PIN; // 选择 USART2 的 TX 引脚
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 配置为复用推挽功能
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 引脚翻转速率快
HAL_GPIO_Init(RS485_PORT, &GPIO_InitStruct); // 初始化 TX 引脚
GPIO_InitStruct.Pin = RS485_RX_PIN; // 选择 RX 引脚
GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 配置为输入
GPIO_InitStruct.Pull = GPIO_NOPULL; // 不上拉
HAL_GPIO_Init(RS485_PORT, &GPIO_InitStruct); // 初始化 RX 引脚
GPIO_InitStruct.Pin = RE_DE_PIN; // 选择方向引脚
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 配置为输出
GPIO_InitStruct.Pull = GPIO_NOPULL; // 不上拉
HAL_GPIO_Init(RE_DE_PORT, &GPIO_InitStruct); // 初始化方向引脚
RE_DE_RX(); // 初始化后默认处于接收状态
HAL_NVIC_SetPriority(RS485_IRQn, 1, 1); // 设置 USART2 的中断等级(0-15)(0-15)
// 规则:(0,0)最高,(0,1)次之依次由高到低排序到(15,15)
HAL_NVIC_EnableIRQ(RS485_IRQn); // 使能 USART2 的中断
} }

先后初始化了USART1和USART2的硬件部分,其中USART2设置了中断优先级和使能了中断,便可以使用“HAL_UART_Receive_IT()”和“HAL_UART_Transmit_IT()”收发数据。接着将RS485的收发函数进行封装,如代码段 18.3.5 所示。

代码段 18.3.5 RS485 收发函数(driver_usart2.c)

/*
* 函数名:void RS485_Tx(uint8_t *pdata, uint16_t sz)
* 输入参数:pdata->指向发送数据所存储的首地址
sz->发送数据个数
* 输出参数:无
* 返回值:无
* 函数作用:USART2 的发送函数
*/
void RS485_Tx(uint8_t *pdata, uint16_t sz)
{
usart2_tx_finish = 0;
RE_DE_TX();
HAL_UART_Transmit_IT(&husart2, pdata, sz); }
/*
* 函数名:void RS485_Rx(uint8_t *pdata, uint16_t sz)
* 输入参数:pdata->指向接收数据所存储的首地址
sz->接收数据个数
* 输出参数:无
* 返回值:无
* 函数作用:USART2 的接收函数
*/
void RS485_Rx(uint8_t *pdata, uint16_t sz)
{
usart2_rx_finish = 0;
HAL_UART_Receive_IT(&husart2, pdata, sz); }
  • 11行:usart2_tx_finish为一个全局变量,用来标记USART2是否发送完成。这里将其设置为0,USART2发送完成后,在中断函数将其置为1,通过该标记便可得知USART2是否发送完成;
  • 13行:RS485设备通常默认为接收状态,以方便接收数据。这里发送数据,需要手动临时改为发送状态;
  • 14行:调用串口中断函数发送数据;
  • 27行:usart2_rx_finish为一个全局变量,用来标记USART2是否接收完成。这里将其设置为0,USART2接收数据完成后,在中断函数将其置为1,通过该标记便可得知USART2是否接收到数据;
  • 29行:RS485设备通常默认为接收状态,这里无需其它设置,直接调用串口中断函数接收数据;
  1. 中断回调函数
    当USART2发生中断时,将自动调用“USART2_IRQHandler()”,“USART2_IRQHandler()”又调用“HAL_UART_IRQHandler()”,最后调用“HAL_UART_TxCpltCallback()”或“HAL_UART_RxCpltCallback()”, 在这两个回调函数里修改USART2接收/发送完成标志,以便后面查询是否收发成功。

代码段 18.3.6 USART2 中断回调函数(driver_msp_usart.c)

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == RS485) {
usart2_tx_finish = 1;
RE_DE_RX(); } }
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == RS485) {
usart2_rx_finish = 1; } }

在发送完成回调函数里,需要将RS485设置为默认的接收模式,以方便随时接收数据。

  1. 按键中断函数
    因为是通过按键来控制RS485主机发送数据,这里还需要编写按键中断函数。参考前面的按键中断实验,首先初始化按键引脚、设置中断优先级、使能中断,便可在发生按键事件时,自动调用中断回调函数“HAL_GPIO_EXTI_Callback()”,在中断回调函数里,修改按键标志,以便随时查询是否该按键按下,按键中断回调函数如代码段 18.3.7 所示。

代码段 18.3.7 按键中断回调函数(driver_key.c)

/*
* 函数名:void HAL_GPIO_EXTI_Callback(void)
* 输入参数:无
* 输出参数:无
* 返回值:无
* 函数作用:外部中断处理函数的回调函数,用以处理不同引脚触发的中断服务最终函数
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == KEY_UP_GPIO_PIN) {
step = 0; }
  1. 主函数控制逻辑
    RS485主机和RS485从机,通常是两套独立代码工程。但本实验中,除了主函数,其它的驱动代码都一样,因此将两个套代码公共部分共用,读者通过如图 18.3.1 所示下拉选择对应工程,即可切换RS485主机工程和RS485从机工程。
    在这里插入图片描述
    在RS485主机的主函数如代码段 18.3.8 所示。
    代码段 18.3.8 RS485 主机主函数(master_main.c)
// 初始化 USART1,设置波特率为 115200 bps
DEBUG_USART_Init(115200);
// 初始化 USART2,设置波特率为 9600 bps
RS485_Init(9600);
// 初始化按键
KeyInit();
// 在 windows 下字符串\n\r 表示回车
// 如果工程在编译下面这句中文的时候报错,请在“Option for target”->"C/C++"->"Misc Controls"添加“ --locale=english”
printf("百问科技 www.100ask.net\n\r");
printf("RS485 收发实验\n\r");
printf("当前设备:主机\n\r");
printf("\n\r");
// 初始化 RS485 CAN 的发送信息
RS485_Msg.ID = 0x305;
RS485_Msg.length = 8;
for(i=0; i<8; i++) {
RS485_Msg.tx_data[i] = i;
}
RS485_Rx((uint8_t*)&RS485_Msg.rx_data[0], RS485_Msg.length);
while(1) {
if(step == 0) // KEY1 按下
{
RS485_Tx((uint8_t*)&RS485_Msg.tx_data[0], RS485_Msg.length);
printf("主机 ----> 从机 数据:\n\r");
for(i=0; i<8; i++) {
printf("0x%x ", RS485_Msg.tx_data[i]); }
printf("\n\r");
step = 0xFF; }
if(usart2_rx_finish == 0x01) {
usart2_rx_finish = 0;
printf("主机 <---- 从机 数据:\n\r");
for(i=0; i<8; i++) {
printf("0x%x ", RS485_Msg.rx_data[i]); }
printf("\n\r");
RS485_Rx((uint8_t*)&RS485_Msg.rx_data[0], RS485_Msg.length); } }
  • 1~8行:初始化调试串口、RS485和按键;
  • 17~23行:创建要发送的数据,RS485_Msg为自己创建的数据结构,包含ID、数据长度、数据内容,其中ID可用于设备识别,整个数据数据结构,可方便数据传输;
  • 25行:先接收数据,将设备设置为默认的接收模式;
  • 29~40行:如果按键按下,则按键标志step变为0,此时调用“RS485_Tx()”发送数据;
  • 42~53行:如果接到数据,则接收标志usart2_rx_finish变为1,此时将接收数据打印处理,并再次调用“RS485_Rx()”接收数据;

在RS485从机的主函数如代码段 18.3.9 所示。
代码段 18.3.9 RS485 从机主函数(slave_main.c)

// 初始化 USART1,设置波特率为 115200 bps
DEBUG_USART_Init(115200);
// 初始化 USART2,设置波特率为 9600 bps
RS485_Init(9600);
// 初始化按键
KeyInit();
// 在 windows 下字符串\n\r 表示回车
// 如果工程在编译下面这句中文的时候报错,请在“Option for target”->"C/C++"->"Misc Controls"添加“ --locale=english”
printf("百问科技 www.100ask.net\n\r");
printf("RS485 收发实验\n\r");
printf("当前设备:从机\n\r");
printf("\n\r");
// 初始化 RS485 CAN 的发送信息
RS485_Msg.ID = 0x305;
RS485_Msg.length = 8;
for(i=0; i<8; i++) {
RS485_Msg.tx_data[i] = i ^ 0xAB; }
RS485_Rx((uint8_t*)&RS485_Msg.rx_data[0], RS485_Msg.length);
while(1) {
if(usart2_rx_finish == 0x01) {
usart2_rx_finish = 0;
printf("从机 <---- 主机 数据:\n\r");
for(i=0; i<8; i++) {
printf("0x%x ", RS485_Msg.rx_data[i]); }
printf("\n\r");
RS485_Tx((uint8_t*)&RS485_Msg.tx_data[0], RS485_Msg.length);
printf("从机 ----> 主机 数据:\n\r");
for(i=0; i<8; i++) {
printf("0x%x ", RS485_Msg.tx_data[i]); }
printf("\n\r");
RS485_Rx((uint8_t*)&RS485_Msg.rx_data[0], RS485_Msg.length); } }
  • 1~8行:初始化调试串口、RS485和按键;
  • 17~23行:创建要发送的数据;
  • 25行:先接收数据,将设备设置为默认的接收模式;
  • 29~49行:如果接收到数据,先打印接收的数据,然后调用“RS485_Tx()”发送数据,最后再调用“RS485_Rx()”接收数据;

18.4实验效果

本实验对应配套资料的“5_程序源码\ 10_通信—RS485\”。准备两个开发板,连接好下载器。在Keil中,分别切换到RS485主机工程和RS485从机工程,编译,分别给两个开发板下载RS485主机程序和RS485从机程序。

将两个开发板的J11(蓝色拨码开关)的1脚拨为ON,使用配套的插拔式接线端子将两个板子的RS485接口连接,注意RS485不需要交叉,即两个开发板RS485的A对A,B对B,最后连接好两个开发板串口和电源,如图 18.4.1 所示。
在这里插入图片描述

启动电源后,串口会有打印当前设备是主机还是从机。按下主机的KEY1_U,主机将数据发送给从机,从机接收到数据后,发送新数据给主机,如图 18.4.2 所示。

在这里插入图片描述

【总结】
调试串口、RS232、RS485本质都是一样的,不同的部分由转换芯片实现,用户几乎不用关心转换实现。因此,用户只需要控制串口收发数据即可。

串口的编程,可分为三步:

  1. 初始化串口,包含串口协议的设置和硬件的初始化(引脚、中断等);
  2. 使用对应模式(超时管理模式、中断模式、DMA模式)的串口收发函数,进行收发数据;
  3. 主函数控制收发逻辑;

百问网技术论坛:
http://bbs.100ask.net/

百问网嵌入式视频官网:
https://www.100ask.net/index

百问网开发板:
淘宝:https://100ask.taobao.com/
天猫:https://weidongshan.tmall.com/

技术交流群(鸿蒙开发/Linux/嵌入式/驱动/资料下载)
QQ群:752871361

单片机-嵌入式Linux交流群:
QQ群:536785813

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐