(1)实验平台:
普中STM32F103 朱雀、玄武开发板
https://item.taobao.com/item.htm?id=620302685024(2)资料下载:普中科技-各型号产品资料下载链接
在前面章节中, 我们介绍了使用 STM32F1 的串口实现 RS232 通信, 它是一种全双工通信。 这一章我们来学习如何使用 STM32F1 的串口实现 RS485(半双工)通信。 STM32F1 的串口非常多, 本章就利用 STM32F1 的串口 2 与 PC 机进行 RS485通信。 本章要实现的功能是: 通过 STM32F1 的串口 2 将 PC 机发送过来的数据原封不动的返回给 PC 机串口, 同时 DS0 指示灯不断闪烁, 提示系统正常运行。 学习本章可以参考前面"串口通信实验" 章节内容。 若结合视频学习效果更佳。 本章分为如下几部分内容:
[38.1 RS485 通信介绍](#38.1 RS485 通信介绍)
[38.2 硬件设计](#38.2 硬件设计)
[38.3 软件设计](#38.3 软件设计)
[38.3.1 RS485(串口 2) 初始化函数](#38.3.1 RS485(串口 2) 初始化函数)
[38.3.2 串口 2 中断函数](#38.3.2 串口 2 中断函数)
[38.3.3 RS485 发送与接收函数](#38.3.3 RS485 发送与接收函数)
[38.3.4 主函数](#38.3.4 主函数)
[38.4 实验现象](#38.4 实验现象)
38.1 RS485 通信介绍
典型的串口通信标准有 RS232 和 RS485, RS232 是全双工点对点的通信, 而RS485 是半双工通信(2 线制) , 可以一点对多点进行组网, 而且 RS485 是用缆线两端的电压差值来表示传递信号, 这与 RS232 电气特性大不一样。 RS485 仅仅规定了接受端和发送端的电气特性, 并没有规定或推荐任何数据协议, 因此
RS485 的协议层可以和 RS232 一样。下面我们看下 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Ω ) 。 没有特性阻抗的话, 当所有的设备都静止或者没有能量的时候就会产生噪声, 而且线移需要双端的电压差。 没有接电阻的话, 会使得较快速的发送端产生多个数据信号的边缘, 导致数据传输出错。 RS485 推荐的连接方式如下图所示:

在 RS485 通信网络中, 通常会使用 485 收发器来转换 TTL 电平和 RS485 电平。 节点中的串口控制器使用 RX 与 TX 信号线连接到 485 收发器上, 而收发器通过差分线连接到网络总线, 串口控制器与收发器之间一般使用 TTL 信号传输,收发器与总线则使用差分信号来传输。 发送数据时, 串口控制器的 TX 信号经过收发器转换成差分信号传输到总线上, 而接收数据时, 收发器把总线上的差分信号转化成 TTL 信号通过 RX 引脚传输到串口控制器中。 通常在这些节点中只能有一个主机, 剩下的全为从机。 在总线的起止端分别加了一个 120 欧的匹配电阻。
我们开发板采用 MAX3485 作为收发器, 该芯片支持 3.3V 供电, 最大传输速度可达 10Mbps, 支持多达 32 个节点, 并且有输出短路保护。 该芯片的框图如下图所示:

图中 A、 B 为 RS485 总线接口, 用于连接 485 总线。 RO 是接收输出端, DI是发送数据输入端, RE 是接收使能信号(低电平有效) , DE 是发送使能信号(高电平有效) 。 因为 RS485 为半双工通信, 通过 RE 和 DE 就能控制发送与接收。
本章我们使用 STM32F1 的串口 2 跟 PC 机进行 RS485 通信, 这里要特别注意,因为 PC 机不具有 RS485 接口, 所以我们需要使用一个 RS232/RS485 的转换器, DB9 接口连接 PC 机, RS485 总线接口连接我们开发板上 RS485 模块的 A 和 B。RS232/RS485 转换器如下图所示:

只要配置好串口 2 功能就可以实现 RS485 通信, 串口 2 的配置与串口 1 是类似的, 只不过串口 2 是挂接在 APB1 总线上, 因此使能串口 2 时钟时要注意。
38.2 硬件设计
本实验使用到硬件资源如下:
(1) DS0 指示灯
(2) 串口 2
(3) RS485 收发器: MAX3485
DS0 指示灯电路在前面章节都介绍过, 这里就不多说, 下面我们来看下开发板上 RS485 模块电路, 如下图所示:



从电路图中可以看到, SP3485(MAX3485) 芯片的 RO 和 DI 管脚连接 P8 端子的 1、 2 脚, 该端子的作用在串口通信章节硬件设计中介绍过, 用来切换 RS232和 RS485 通信的, 如果要做 RS485 实验, 即使用短接片把 P6 端子的 1、 3 管脚短接, 2、 4 管脚短接。 在 STM32F1 芯片的串口 2 管脚 PA3 和 PA2 上, SP3485 芯片的 DE 与 RE 短接在一起连接在 STM32F1 芯片的 PD7 上, 通过 PD7 管脚就可以控制SP3485 的收发, 当 PD7=0 时, 为接收模式, 当 PD7=1 时, 为发送模式。 要注意以太网模块的 DM9000_RST 也使用了 PD7 管脚, 所以它们不能同时使用, 只能分时复用。 图中的 R35 电阻为匹配电阻, 大小为 120 欧。 图中另外 2 个电阻 R34和 R30 为偏置电阻, 用来保证总线空闲时, A、 B 之间的电压差都会大于 2V(逻辑 1) , 从而避免因总线空闲时, A、 B 压差不定, 引起逻辑错乱导致出现乱码。
要实现本章功能, 我们需要使用 2 根导线将开发板上 485 模块的 A 和 B 与RS232/RS485 转换器的 A 和 B 对应连接, 这里一定要注意不能交叉, 否则通信异常。 开发板上 485 模块的 A 和 B 如下图所示:

38.3 软件设计
本章所要实现的功能是: 通过操作 KEY_UP 键, STM32F1 的串口 2 将 PC 机发送过来的数据原封不动的返回给 PC 机串口, 同时 DS0 指示灯不断闪烁, 提示系统正常运行。 程序框架如下:
(1) 初始化串口 2, 并使能串口接收中断等
(2) 编写串口 2 中断函数(将接收到的数据返回出去)
(3) 编写主函数
串口初始化配置在"串口通信实验" 中就介绍过。 下面我们打开"\4--实验程序\1--基础实验\30-RS485 通信实验" 工程, 在 APP 工程组中可以看到添加了rs485.c 文件(里面包含了 RS485(串口 2) 驱动程序) , 在 StdPeriph_Driver工程组中添加了 stm32f10x_usart.c 库文件。 USART 操作的库函数都放在stm32f10x_usart.c 和 stm32f10x_usart.h 文件中, 所以使用到串口就必须加入stm32f10x_usart.c 文件, 同时还要包含对应的头文件路径。
这里我们分析几个重要函数, 其他部分程序大家可以打开工程查看。
38.3.1 RS485(串口 2) 初始化函数
因为 485 收发器是连接 STM32F1 串口 2, 要实现 RS485 通信, 我们就需要初始化串口 2, 初始化代码如下:
cpp
//初始化IO 串口2
//pclk1:PCLK1时钟频率(Mhz)
//bound:波特率
void RS485_Init(u32 bound)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOD, ENABLE);//使能GPIOA,D时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);//使能USART2时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; //PD7端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOD, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; //PA2
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;//PA3
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
RCC_APB1PeriphResetCmd(RCC_APB1Periph_USART2,ENABLE);//复位串口2
RCC_APB1PeriphResetCmd(RCC_APB1Periph_USART2,DISABLE);//停止复位
USART_InitStructure.USART_BaudRate = bound;//波特率设置
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//8位数据长度
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(USART2, &USART_InitStructure); ; //初始化串口
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn; //使能串口2中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3; //先占优先级2级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //从优先级2级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);//开启中断
USART_Cmd(USART2, ENABLE); //使能串口
RS485_TX_EN=0; //默认为接收模式
}
38.3.2 串口 2 中断函数
初始化串口 2 后, 中断就已经开启了, 当 PC 机发送数据过来后会触发串口接收中断, 在中断内将串口 2 接收到的数据存储到我们定义的缓冲数组内, 具体代码如下:
cpp
//接收缓存区
u8 RS485_RX_BUF[64]; //接收缓冲,最大64个字节.
//接收到的数据长度
u8 RS485_RX_CNT=0;
void USART2_IRQHandler(void)
{
u8 res;
if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) //接收到数据
{
res =USART_ReceiveData(USART2); //读取接收到的数据
if(RS485_RX_CNT<64)
{
RS485_RX_BUF[RS485_RX_CNT]=res; //记录接收到的值
RS485_RX_CNT++; //接收数据增加1
}
}
}
为了确认串口中断是否发生接收中断, 调用了读取串口中断状态标志方法USART_GetITStatus(USART2, USART_IT_RXNE), 如果确实产生接收中断, 那么就会执行 if 内的语句, 调用 USART_ReceiveData 函数将接收到的数据存放在变量res 中, 然后再将 res 变量存放到缓存数组 RS485_RX_BUF, 该数组是我们定义的一个全局数组变量, 可最多存储 64 字节, 每接收一个字节就让数组脚标增加 1。
38.3.3 RS485 发送与接收函数
在串口 2 中断函数内我们已经将接收的数据存储到了缓存数组中, 为了能够实现程序的通用性, 我们再次将接收部分封装成独立函数, 同样也对发送封装成一个独立函数, 代码如下:
cpp
//RS485发送len个字节.
//buf:发送区首地址
//len:发送的字节数(为了和本代码的接收匹配,这里建议不要超过64个字节)
void RS485_Send_Data(u8 *buf,u8 len)
{
u8 t;
RS485_TX_EN=1; //设置为发送模式
for(t=0;t<len;t++) //循环发送数据
{
while(USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET);
USART_SendData(USART2,buf[t]);
}
while(USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET);
RS485_RX_CNT=0;
RS485_TX_EN=0; //设置为接收模式
}
//RS485查询接收到的数据
//buf:接收缓存首地址
//len:读到的数据长度
void RS485_Receive_Data(u8 *buf,u8 *len)
{
u8 rxlen=RS485_RX_CNT;
u8 i=0;
*len=0; //默认为0
delay_ms(10); //等待10ms,连续超过10ms没有接收到一个数据,则认为接收结束
if(rxlen==RS485_RX_CNT&&rxlen)//接收到了数据,且接收完成了
{
for(i=0;i<rxlen;i++)
{
buf[i]=RS485_RX_BUF[i];
}
*len=RS485_RX_CNT; //记录本次数据长度
RS485_RX_CNT=0; //清零
}
}
这里要注意, 在发送数据之前, 需要将 RS485_TX_EN(PD7) 置 1, 即选择发送模式, 数据发送完成后需要将 RS485_TX_EN(PD7) 置 0, 即选择接收模式, 等待下一次数据的接收。 最后要记得将全部变量 RS485_RX_CNT 清零。
发送和接收函数均有 2 个入口参数, 第一个参数是发送数据区首地址, 第二个参数指定所要发送或接收的数据长度。
这里我们重点介绍一下接收数据的流程(超时法) : 首先令rxlen=RS485_RX_CNT, 记录当前接收到的字节数, 随后, 等待 10ms, 如果在这个 10ms 里面, 没有接收到任何数据(RS485_RX_CNT 的值未增加) , 那么就说明接收完成了。 如果有接收到其他数据( RS485_RX_CNT 变大了) , 那么说明还在继续接收数据, 需等到下一个循环再处理;
38.3.4 主函数
编写好串口 2 初始化、 中断服务函数及数据发送和接收函数后, 接下来就可以编写主函数了, 代码如下:
cpp
#include "system.h"
#include "SysTick.h"
#include "led.h"
#include "usart.h"
#include "key.h"
#include "rs485.h"
int main()
{
u8 i=0;
u8 rs485buf[5];
u8 len=0;
u8 key=0;
SysTick_Init(72);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中断优先级分组 分2组
LED_Init();
USART1_Init(115200);
KEY_Init();
RS485_Init(9600);
while(1)
{
key=KEY_Scan(0);
if(key==KEY_UP_PRESS)
{
RS485_Send_Data(rs485buf,5);
}
RS485_Receive_Data(rs485buf,&len);
i++;
if(i%20==0)
{
LED1=!LED1;
}
delay_ms(10);
}
}
主函数实现的功能很简单, 首先调用之前编写好的硬件初始化函数, 包括SysTick 系统时钟, LED 初始化等。 然后调用我们前面编写的 RS485_Init 函数初始化串口 2, 我们将串口 2 的波特率设置为 9600, 所以再与 PC 机进行通信的时候也要设置这个波特率。 最后进入 while 循环, 循环检测按键 KEY_UP 键是否按下, 如按下则将 RS485 接收的数据发送给 PC 机串口助手。 同时控制 DS0 指示灯闪烁, 提示系统正常运行。
需要注意的是: 程序中定义的发送与接收存储的数组 rs485buf 只能存放 5个字节, 也就是说我们使用串口助手一次只能发送 5 个字节数据, 不能多也不能少。
38.4 实验现象
将工程程序编译后下载到开发板内, 可以看到 DS0 指示灯不断闪烁, 表示程序正常运行。 使用 RS232/RS485 转换器将 PC 机与我们开发板 485 模块的 A 和 B连接, 打开串口调试助手, 选择好转换器的串口(不是开发板上的 CH340 串口) ,将波特率设置为 9600, 在字符串输入框内输入数据, 点击发送后, 然后按下开发板上 KEY_UP 键, 在串口助手显示窗口就会显示发送的数据(程序中定义的发送与接收存储的数组 rs485buf 只能存放 5 个字节, 也就是说我们使用串口助手一次只能发送 5 个字节数据, 不能多也不能少) 。 要想看到现象可以打开"\5--开发工具\4-常用辅助开发软件\串口调试助手\串口调试助手(丁丁) " 内串口调试助手, 实验现象如下: (前提一定要连接好线路, USB 线一端连接电脑, 另一端连接开发板"USB 转串口模块" 上的 USB 下载口, 并且在"USB 转 TTL&电源"模块上 P4 端子短接片已插上)

注意: 做 RS485 实验, 必须把 P6 端子短接片调到 485 端, 如下:

课后作业
(1) 通过 PC 机发送的数据控制开发板上的 LED 和蜂鸣器。 (温馨提示: 将串口 2 接收到的数据进行处理判断后控制)