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.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位置。
实验目的:RS485是差分信号,收发数据时,A、B都在工作。开发板也只提供了一个RS485接口,因此不能自发自收实验,需要至少两个RS485设备进行实验。这里假设两个开发板进行RS485通信,一个做主机,一个做从机,主机发送数据给从机,从机收到数据再发给主机,实现两个设备的收发数据,供读者参考和方便移植。
本实验配套代码位于“5_程序源码\10_通信—RS485\”。
代码段 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的收发控制引脚。
代码段 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); }
代码段 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设置为默认的接收模式,以方便随时接收数据。
代码段 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; }
// 初始化 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); } }
在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); } }
本实验对应配套资料的“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本质都是一样的,不同的部分由转换芯片实现,用户几乎不用关心转换实现。因此,用户只需要控制串口收发数据即可。
串口的编程,可分为三步:
百问网技术论坛:
http://bbs.100ask.net/
百问网嵌入式视频官网:
https://www.100ask.net/index
百问网开发板:
淘宝:https://100ask.taobao.com/
天猫:https://weidongshan.tmall.com/
技术交流群(鸿蒙开发/Linux/嵌入式/驱动/资料下载)
QQ群:752871361
单片机-嵌入式Linux交流群:
QQ群:536785813