赞
踩
串口是一个泛称,UART、RS232、RS422和RS485都遵循类似的通信时序协议,被通称为串口
UART是STM32的UART外设,由此产生串口时序,产生的电平为CMOS电平
TTL、RS232、RS422、RS485是串行通信接口标准。简单来说,就是逻辑1和0的表示不同
485(一般称作 RS485/EIA-485)隶属于 OSI 模型物理层,是串行通讯的一种。电气特性规定为 2 线,半双工,多点通信的类型。它的电气特性和 RS-232 大不一样。用缆线两端的电压差值来表示传递信号。RS485 仅仅规定了接受端和发送端的电气特性。它没有规定或推荐任何数据协议。
RS485是串行通信标准,使用差分信号传输(两个信号的差值),抗干扰能力强,常用于工控领域
RS485具有强大的组网功能,在串口基础协议之上还制定MODBUS协议
通信接口 | 通信方 式 | 信号线 | 电平标准 | 拓扑结构 | 通信距离 | 通讯速率 | 抗干扰能力 |
---|---|---|---|---|---|---|---|
TTL | 全双工 | TX/RX/GND | 逻辑1 : 2.4~5 V 逻辑0 : 0~0.4 V | 点对点 | 1米 | 100kbps | 弱 |
RS232 | 全双工 | TX/RX/GND | 逻辑1 : -(15~3) V 逻辑0 : +(3~15) V | 点对点 | 100米 | 20kbps | 较弱 |
RS485 | 半双工 | 差分线AB | 逻辑1 : +(2~6)V 逻辑0 : -(2~6)V | 多点双向 | 1200米 | 100kbps | 强 |
RS485 的特点包括:
1, 接口电平低,不易损坏芯片。RS485 的电气特性:逻辑“1”以两线间的电压差为+(2~6)V 表 示;逻辑“0”以两线间的电压差为-(2~6)V 表示。接口信号电平比 RS232 降低了,不易损坏接口电路的芯片,且该电平与 TTL 电平兼容,可方便与 TTL 电路连接。
2, 传输速率高。10 米时,RS485 的数据最高传输速率可达 35Mbps,在 1200m 时,传输速度可达 100Kbps。
3, 抗干扰能力强。RS485 接口是采用平衡驱动器和差分接收器的组合,抗共模干扰能力增强, 即抗噪声干扰性好。
4, 传输距离远,支持节点多。RS485 总线最长可以传输 1200m 左右,更远的距离则需要中继 传输设备支持但这时(速率≤100Kbps)才能稳定传输,一般最大支持 32 个节点,如果使 用特制的 485 芯片,可以达到 128 个或者 256 个节点,最大的可以支持到 400 个节点。
RS485 推荐使用在点对点网络中,比如:线型,总线型网络等,而不能是星型,环型网络。 理想情况下 RS485 需要 2 个终端匹配电阻,其阻值要求等于传输电缆的特性阻抗(一般为 120 Ω)。没有特性阻抗的话,当所有的设备都静止或者没有能量的时候就会产生噪声,而且线移需要双端的电压差。没有终接电阻的话,会使得较快速的发送端产生多个数据信号的边缘,导致数据传输出错。
485 推荐的一主多从连接方式如图所示:
在上面的连接中,如果需要添加匹配电阻,我们一般在总线的起止端加入,也就是主机和设备 4 上面各加一个 120Ω的匹配电阻。
图中,节点1可以是主机(STM32),节点2、3等可以是设备。
由于 RS485 具有传输距离远、传输速度快、支持节点多和抗干扰能力更强等特点,所以 RS485有很广泛的应用。实际多设备时收发器有范围为-7V到+12V的共模电压,为了稳定传输, 也有使用 3 线的布线方式,即在原有的 A、B 两线上多增加一条地线。(4 线制使用全双工通讯方式,这种也叫 RS422,由于布线的难度和通讯局限,相对使用得比较少)。
TP8485E/SP3485/MAX485 可作为 RS485 的收发器(485收发器就是电平转换芯片),该芯片支持 3.3V~5.5V 供电,最大传输速度可达 250Kbps,支持多达 256 个节点(单位负载为 1/8 的条件下),并且支持输出短路保护。该芯片(三个芯片都是pin to pin的)的框图如图所示:
图中 A、B 总线接口,用于连接 485 总线。A: 接收器的输入端/ 驱动器的输出端 B : 接收器的输入端 / 驱动器的输出端
接收器:A、B端口接收到电压,转化为RS485电平,输出到RO端口
驱动器:DI端口接受到电平信号,转化为电压(差值)输出到A、B端口
RO接收器输出端:
DI驱动器输入端:
经过前面的学习我们知道实际的 RS485 仍是串行通讯的一种电平传输方式,那么我们实际通讯时可以使用串口进行实际数据的收发处理,使用 485 转换芯片将串口信号转换为 485 的电 平信号进行传输,本章,我们只需要配置好串口 2,就可以实现正常的 485 通信了,串口 2 的 配置和串口 1 基本类似,只是串口 2 的时钟来自 APB1,最大频率为 36Mhz。
本章将实现这样的功能:通过连接两个精英 STM32F103 的 RS485 接口(485 半双工模式无法自收发,我们需要用两个开发板或者 USB 转 485 调试器+串口助手来帮助我们完成测试),然后由 KEY0 控制发送,当按下一个开发板的 KEY0 的时候,就发送 5 个数据给另外一个开发板,并在两个开发板上分别显示发送的值和接收到的值。
从上图可以看出:开发板的串口 2 和 TP8485 上的引脚连接到 P5 端上的端子,但不直接相 连,所以测试 485 功能时我们需要用跳线帽短接 P5 上的两组排针使之连通。STM32F1 的 PD7控制 RS485 的收发模式:
当 PD7=0 的时候,为接收模式;当 PD7=1 的时候,为发送模式。 另外,图中的 R14 和 R17 是两个偏置电阻,用来保证总线空闲时,A、B 之间的电压差都会大于 200mV(逻辑 1)。从而避免因总线空闲时因 A、B 压差不稳定,可能出现的乱码。 最后,我们用 2 根导线将两个开发板 RS485 端子的 A 和 A,B 和 B 连接起来。这里注意不要接反了(A 接 B),接反了会导致通讯异常!!
1)使能串口和 GPIO 口时钟
本实验用到 USART2 串口,使用 PA2 和 PA3 作为串口的 TX 和 RX 脚,因此需要先使能 USART2 和 GPIOA 时钟。参考代码如下:
__HAL_RCC_USART2_CLK_ENABLE(); /* 使能 USART2 时钟 */
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 使能 GPIOA 时钟 */
2)串口参数初始化(波特率、字长、奇偶校验等)
HAL库通过调用串口初始化函数HAL_UART_Init完成对串口参数初始化,详见例程源码。 该函数通常会调用:HAL_UART_MspInit 函数来完成对串口底层的初始化,包括:串口及 GPIO 时钟使能、GPIO 模式设置、中断设置等。但是本实验避免与 USART1 冲突,所以把串口底层初始化没有放在 HAL_UART_MspInit 函数里。
3)GPIO 模式设置(速度,上下拉,复用功能等)
GPIO 模式设置通过调用 HAL_GPIO_Init 函数实现,详见本例程源码。
4)开启串口相关中断,配置串口中断优先级
本实验我们使用串口中断来接收数据。我们使用__HAL_UART_ENABLE_IT 函数使能接收中断。通过 HAL_NVIC_EnableIRQ 函数使能串口中断,通过 HAL_NVIC_SetPriority 函数设置中断优先级。
5)编写中断服务函数
串口 2 中断服务函数为:USART2_IRQHandler,当发生中断的时候,程序就会执行中断服 务函数,在这里就可以对接收到的数据进行处理,详见本例程源码。
6)串口数据接收和发送
最后我们可以通过读写 USART_DR 寄存器,完成串口数据的接收和发送,HAL 库也给我们提供了:HAL_UART_Receive 和 HAL_UART_Transmit 两个函数用于串口数据的接收和发送。 大家可以根据实际情况选择使用哪种方式来收发串口数据。
驱动函数 | 关联寄存器 | 功能描述 |
---|---|---|
**__****HAL_RCC_USARTx_CLK_ENABLE(…) ** | 使能串口时钟 | |
HAL_UART_Init(…) | USART_CR1/CR2 | 初始化串口 |
__HAL_UART_ENABLE_IT(…) | USART_CR1 | 使能串口相关中断 |
HAL_UART_Receive(…) | USART_DR | 串口接收数据 |
HAL_UART_Transmit(…) | USART_DR | 串口发送数据 |
__HAL_UART_GET_FLAG(…) | USART_SR | 查询当前串口的状态 |
rs485.h
#ifndef __RS485_H #define __RS485_H #include "./SYSTEM/sys/sys.h" /******************************************************************************************/ /* RS485 引脚 和 串口 定义 */ #define RS485_RE_GPIO_PORT GPIOD #define RS485_RE_GPIO_PIN GPIO_PIN_7 #define RS485_RE_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOD_CLK_ENABLE(); }while(0) /* PD口时钟使能 */ #define RS485_TX_GPIO_PORT GPIOA #define RS485_TX_GPIO_PIN GPIO_PIN_2 #define RS485_TX_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PA口时钟使能 */ #define RS485_RX_GPIO_PORT GPIOA #define RS485_RX_GPIO_PIN GPIO_PIN_3 #define RS485_RX_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PA口时钟使能 */ #define RS485_UX USART2 #define RS485_UX_IRQn USART2_IRQn #define RS485_UX_IRQHandler USART2_IRQHandler #define RS485_UX_CLK_ENABLE() do{ __HAL_RCC_USART2_CLK_ENABLE(); }while(0) /* USART2 时钟使能 */ /******************************************************************************************/ /* 控制RS485_RE脚, 控制RS485发送/接收状态 * RS485_RE = 0, 进入接收模式 * RS485_RE = 1, 进入发送模式 */ #define RS485_RE(x) do{ x ? \ HAL_GPIO_WritePin(RS485_RE_GPIO_PORT, RS485_RE_GPIO_PIN, GPIO_PIN_SET) : \ HAL_GPIO_WritePin(RS485_RE_GPIO_PORT, RS485_RE_GPIO_PIN, GPIO_PIN_RESET); \ }while(0) #define RS485_REC_LEN 64 /* 定义最大接收字节数 64 */ #define RS485_EN_RX 1 /* 使能(1)/禁止(0)RS485接收 */ extern uint8_t g_RS485_rx_buf[RS485_REC_LEN]; /* 接收缓冲,最大RS485_REC_LEN个字节 */ extern uint8_t g_RS485_rx_cnt; /* 接收数据长度 */ void rs485_init( uint32_t baudrate); /* RS485初始化 */ void rs485_send_data(uint8_t *buf, uint8_t len); /* RS485发送数据 */ void rs485_receive_data(uint8_t *buf, uint8_t *len);/* RS485接收数据 */ #endif
rs485_init 函数
rs485_init 的配置与串口类似,也需要设置波特率等参数,另外还需要配置收发模式的驱动引脚,我们的程序设计如下:
/** * @brief RS485初始化函数 * @note 该函数主要是初始化串口 * @param baudrate: 波特率, 根据自己需要设置波特率值 * @retval 无 */ void rs485_init(uint32_t baudrate) { /* IO 及 时钟配置 */ RS485_RE_GPIO_CLK_ENABLE(); /* 使能 RS485_RE 脚时钟 */ RS485_TX_GPIO_CLK_ENABLE(); /* 使能 串口TX脚 时钟 */ RS485_RX_GPIO_CLK_ENABLE(); /* 使能 串口RX脚 时钟 */ RS485_UX_CLK_ENABLE(); /* 使能 串口 时钟 */ GPIO_InitTypeDef gpio_initure; gpio_initure.Pin = RS485_TX_GPIO_PIN; gpio_initure.Mode = GPIO_MODE_AF_PP; gpio_initure.Pull = GPIO_PULLUP; gpio_initure.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(RS485_TX_GPIO_PORT, &gpio_initure); /* 串口TX 脚 模式设置 */ gpio_initure.Pin = RS485_RX_GPIO_PIN; gpio_initure.Mode = GPIO_MODE_AF_INPUT; HAL_GPIO_Init(RS485_RX_GPIO_PORT, &gpio_initure); /* 串口RX 脚 必须设置成输入模式 */ gpio_initure.Pin = RS485_RE_GPIO_PIN; gpio_initure.Mode = GPIO_MODE_OUTPUT_PP; gpio_initure.Pull = GPIO_PULLUP; gpio_initure.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(RS485_RE_GPIO_PORT, &gpio_initure); /* RS485_RE 脚 模式设置 */ /* USART 初始化设置 */ g_rs458_handler.Instance = RS485_UX; /* 选择485对应的串口 */ g_rs458_handler.Init.BaudRate = baudrate; /* 波特率 */ g_rs458_handler.Init.WordLength = UART_WORDLENGTH_8B; /* 字长为8位数据格式 */ g_rs458_handler.Init.StopBits = UART_STOPBITS_1; /* 一个停止位 */ g_rs458_handler.Init.Parity = UART_PARITY_NONE; /* 无奇偶校验位 */ g_rs458_handler.Init.HwFlowCtl = UART_HWCONTROL_NONE; /* 无硬件流控 */ g_rs458_handler.Init.Mode = UART_MODE_TX_RX; /* 收发模式 */ HAL_UART_Init(&g_rs458_handler); /* HAL_UART_Init()会使能UART2 */ #if RS485_EN_RX /* 如果使能了接收 */ /* 使能接收中断 */ __HAL_UART_ENABLE_IT(&g_rs458_handler, UART_IT_RXNE); /* 开启接收中断 */ HAL_NVIC_EnableIRQ(RS485_UX_IRQn); /* 使能USART2中断 */ HAL_NVIC_SetPriority(RS485_UX_IRQn, 3, 3); /* 抢占优先级3,子优先级3 */ #endif RS485_RE(0); /* 默认为接收模式 */ }
可以看到代码基本跟串口的配置一样,只是多了收发控制引脚的配置
发送函数
发送函数用于输出 485 信号到 485 总线上,我的默认的 485 方式一般空闲时为接收状态, 只有发送数据时我们才控制 485 芯片进入发送状态,发送完成后马上回到空闲接收状态,这样 可以保证操作过程中 485 的数据丢失最小。我们实现的发送函数如下:
/**
* @brief RS485发送len个字节
* @param buf : 发送区首地址
* @param len : 发送的字节数(为了和本代码的接收匹配,这里建议不要超过 RS485_REC_LEN 个字节)
* @retval 无
*/
void rs485_send_data(uint8_t *buf, uint8_t len)
{
RS485_RE(1); /* 进入发送模式 */
HAL_UART_Transmit(&g_rs458_handler, buf, len, 1000); /* 串口2发送数据 */
g_RS485_rx_cnt = 0;
RS485_RE(0); /* 进入接收模式 */
}
485 接收中断函数
RS485 的接收就与串口中断一样了,不过要注意空闲时要切换回接收状态,否则会收不到 数据。我们定义了一个全局的缓冲区 g_RS485_rx_buf 进行接收测试,通过串口中断接收数据, 编写的接收代码如下:
UART_HandleTypeDef g_rs458_handler; /* RS485控制句柄(串口) */ #ifdef RS485_EN_RX /* 如果使能了接收 */ uint8_t g_RS485_rx_buf[RS485_REC_LEN]; /* 接收缓冲, 最大 RS485_REC_LEN 个字节. */ uint8_t g_RS485_rx_cnt = 0; /* 接收到的数据长度 */ void RS485_UX_IRQHandler(void) { uint8_t res; if ((__HAL_UART_GET_FLAG(&g_rs458_handler, UART_FLAG_RXNE) != RESET)) /* 接收到数据 */ { HAL_UART_Receive(&g_rs458_handler, &res, 1, 1000); if (g_RS485_rx_cnt < RS485_REC_LEN) /* 缓冲区未满 */ { g_RS485_rx_buf[g_RS485_rx_cnt] = res; /* 记录接收到的值 */ g_RS485_rx_cnt++; /* 接收数据增加1 */ } } } #endif
485 查询接收数据函数
该函数用于查询 485 总线上接收到的数据,主要实现的逻辑是:一开始进入函数时,先记录下当前接收计数器的值,再来一个延时去判断接收是否结束(即该期间有无接收到数据),假如说接收计数器的值没有改变,就证明接收结束,我们就可以把当前接收缓冲区传递出去。函数实现如下:
/** * @brief RS485查询接收到的数据 * @param buf : 接收缓冲区首地址 * @param len : 接收到的数据长度 * @arg 0 , 表示没有接收到任何数据 * @arg 其他, 表示接收到的数据长度 * @retval 无 */ void rs485_receive_data(uint8_t *buf, uint8_t *len) { uint8_t rxlen = g_RS485_rx_cnt; uint8_t i = 0; *len = 0; /* 默认为0 */ delay_ms(10); /* 等待10ms,连续超过10ms没有接收到一个数据,则认为接收结束 */ if (rxlen == g_RS485_rx_cnt && rxlen) /* 接收到了数据,且接收完成了 */ { for (i = 0; i < rxlen; i++) { buf[i] = g_RS485_rx_buf[i]; } *len = g_RS485_rx_cnt; /* 记录本次数据长度 */ g_RS485_rx_cnt = 0; /* 清零 */ } }
main.c
#include "./SYSTEM/sys/sys.h" #include "./SYSTEM/usart/usart.h" #include "./SYSTEM/delay/delay.h" #include "./USMART/usmart.h" #include "./BSP/LED/led.h" #include "./BSP/LCD/lcd.h" #include "./BSP/KEY/key.h" #include "./BSP/RS485/rs485.h" int main(void) { uint8_t key; uint8_t i = 0, t = 0; uint8_t cnt = 0; uint8_t rs485buf[5]; HAL_Init(); /* 初始化HAL库 */ sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */ delay_init(72); /* 延时初始化 */ usart_init(115200); /* 串口初始化为115200 */ usmart_dev.init(72); /* 初始化USMART */ led_init(); /* 初始化LED */ lcd_init(); /* 初始化LCD */ key_init(); /* 初始化按键 */ rs485_init(9600); /* 初始化RS485 */ lcd_show_string(30, 50, 200, 16, 16, "STM32", RED); lcd_show_string(30, 70, 200, 16, 16, "RS485 TEST", RED); lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED); lcd_show_string(30, 110, 200, 16, 16, "KEY0:Send", RED); /* 显示提示信息 */ lcd_show_string(30, 130, 200, 16, 16, "Count:", RED); /* 显示当前计数值 */ lcd_show_string(30, 150, 200, 16, 16, "Send Data:", RED); /* 提示发送的数据 */ lcd_show_string(30, 190, 200, 16, 16, "Receive Data:", RED);/* 提示接收到的数据 */ while (1) { key = key_scan(0); if (key == KEY0_PRES) /* KEY0按下,发送一次数据 */ { for (i = 0; i < 5; i++) { rs485buf[i] = cnt + i; /* 填充发送缓冲区 */ lcd_show_xnum(30 + i * 32, 170, rs485buf[i], 3, 16, 0X80, BLUE); /* 显示数据 */ } rs485_send_data(rs485buf, 5); /* 发送5个字节 */ } rs485_receive_data(rs485buf, &key); if (key) /* 接收到有数据 */ { if (key > 5)key = 5; /* 最大是5个数据. */ for (i = 0; i < key; i++) { lcd_show_xnum(30 + i * 32, 210, rs485buf[i], 3, 16, 0X80, BLUE); /* 显示数据 */ } } t++; delay_ms(10); if (t == 20) { LED0_TOGGLE(); /* LED0闪烁, 提示系统正在运行 */ t = 0; cnt++; lcd_show_xnum(30 + 48, 130, cnt, 3, 16, 0X80, BLUE); /* 显示数据 */ } } }
我们是通过按键控制数据的发送。在此部分代码中,cnt 是一个累加数,一旦 KEY0 按下, 就以这个数位基准连续发送 5 个数据。当 485 总线收到数据得时候,就将收到的数据直接显示 在 LCD 屏幕上。
在代码编译成功之后,需要下载代码到正点原子精英 STM32F103 上(注意要 2 个开 发板都下载这个代码)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。