STM32进阶回顾(四):RS485串口通信详解
一、RS485通讯协议简介
RS485(也称为 EIA-485 或 TIA-485)是一种常用于工业和通信领域的串行通信标准。它定义了一个电气特性规范,用于在多点网络中进行数据传输,特别适用于需要长距离、高速传输和抗干扰的应用场景。RS485标准常用于工业自动化、传感器、智能仪表等系统中。
特点:
1.差分信号传输
RS485使用差分信号进行数据传输。它由两根信号线(A和B)组成:
A线(+) 和 B线(-) 之间的电压差传输数据。 当电流在这两根线之间的方向发生变化时,接收端会读取这种变化并解码为数据位。 相比单端信号,差分信号对噪声的抗干扰能力更强,因为噪声通常影响两条线的相同程度,因此它们之间的电压差不会受到噪声的显著影响。
2.多点通信
RS485支持多达32个设备(发送器和接收器总和)连接在同一总线上。这使得它非常适合于多节点的网络应用。3.长距离传输
RS485的通信距离可以达到1200米(约4000英尺),这取决于传输速率和电缆质量。在低速率下(如9600bps),可以实现更长的传输距离。4.高传输速率
RS485支持较高的传输速率(如10 Mbps),尽管速度和距离通常是相互制约的。5.抗干扰能力强
由于使用差分信号,RS485能在电磁干扰(EMI)较强的环境下稳定工作,因此常用于工厂、自动化设备和恶劣环境下的通信。
RS-485与RS-232通讯协议的特性对比:
通讯标准 | 信号线 | 通讯方向 | 电平标准 | 通讯距离 | 通讯节点数 |
RS232 | TXD,RXD,GND | 全双工 |
逻辑1:+3V ~ +15V 逻辑0:-15V ~ -3V |
100米以内 | 只有两个节点 |
RS485 | 差分线AB | 半双工 |
逻辑1:+2V ~ +6V 逻辑0:-6V ~ -2V |
1200米 | 支持多个节点,支持多个主设备,任意节点间可以相互通讯 |
差分信号线具有很强的干扰能力,特别适合应用于电磁环境复杂的工业控制环境中,RS-485协议主要是把RS-232的信号改进成差分信号,从而大大提高了抗干扰特性
二、 RS485通讯实验
本实验我们采用土壤传感器,RS485转TTL模块以及STM32F103C8T6最小系统板来实现。
1.土壤传感器
以下是土壤传感器的部分参数:
这个协议内容用到了ModBus-Rtu,感兴趣的小伙伴可以看一下这个https://blog.csdn.net/as480133937/article/details/123197782
2.RS485转TTL模块
实验原理:
485通讯实质上就是软件层的串口通讯,再加上物理层上的485芯片,将TTL信号转化为差分信号进行传输,对待上就按照串口通讯就行。
编程思想:
DE: 1 发送使能;0发送禁止
RE: 0 接收使能;1接收禁止
一般将DE和RE连在一起,这样就是高电平发送,低电平接收
通过单片机串口2的TXD(PA2)将数据发送出去,RXD(PA3)进行接收数据,然后通过串口1打印接收到的数据 。
由于是通过485协议发送数据,每次发送前要对485传输方式设置为发送模式,完成后要改为接收模式,由于是半双工,发送完要有一定的延时,确保数据不会丢失;
3.接线图
4、程序代码
Usart.h
#ifndef __USART_H__
#define __USART_H__
extern uint8_t Serial_TxPacket[];
extern uint8_t Serial_RxPacket[];
void my_Usart_Init2(void);
void my_Usart_Init(void);
void My_Usart_Send_Byte(USART_TypeDef* USARTx, uint16_t Data); //发送一个字节
void My_Usart_SendArray(uint8_t *Array,uint16_t Length); //发送一个数组(用串口2)
void My_Usart_SendArray2(uint8_t *Array,uint16_t Length); //发送一个数组(用串口1)
void My_Usart_Send_String(USART_TypeDef* USARTx, char * str); //发送字符串
uint8_t Serial_GetRxFlag(void); //实现读后自动清除的功能
#endif
Usart.c
#include "stm32f10x.h"
#include "Usart.h"
#include "stdio.h"
#include "RS485.h"
uint8_t Serial_TxPacket[8];
uint8_t Serial_RxPacket[12];
uint8_t Serial_RxFlag;
void my_Usart_Init2(void) //初始化串口2
{
GPIO_InitTypeDef gpio_initstruct;
USART_InitTypeDef usart_initstruct;
NVIC_InitTypeDef nvic_initstruct;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);
//Tx PA2
gpio_initstruct.GPIO_Mode = GPIO_Mode_AF_PP;//发送数据固定为复用推挽输出
gpio_initstruct.GPIO_Pin = GPIO_Pin_2;
gpio_initstruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&gpio_initstruct);
//Rx PA3
gpio_initstruct.GPIO_Mode = GPIO_Mode_IPU;//接收数据为浮空输入或上拉输入
gpio_initstruct.GPIO_Pin = GPIO_Pin_3;
GPIO_Init(GPIOA,&gpio_initstruct);
usart_initstruct.USART_BaudRate = 9600;
usart_initstruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//不使用流控
usart_initstruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
usart_initstruct.USART_Parity = USART_Parity_No;//不使用校验位
usart_initstruct.USART_StopBits = USART_StopBits_1;//一位停止位
usart_initstruct.USART_WordLength = USART_WordLength_8b;//因为不使用校验位所以字节长度为八个
USART_Init(USART2,&usart_initstruct);
USART_ITConfig(USART2,USART_IT_RXNE,ENABLE);
nvic_initstruct.NVIC_IRQChannel = USART2_IRQn;
nvic_initstruct.NVIC_IRQChannelPreemptionPriority = 2;
nvic_initstruct.NVIC_IRQChannelSubPriority = 2;
nvic_initstruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&nvic_initstruct);
USART_Cmd(USART2,ENABLE);//一定记得配置完后使能USART2外设
}
void my_Usart_Init(void) //初始化串口1
{
GPIO_InitTypeDef gpio_initstruct;
USART_InitTypeDef usart_initstruct;
NVIC_InitTypeDef nvic_initstruct;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1,ENABLE);
//Tx PA9
gpio_initstruct.GPIO_Mode = GPIO_Mode_AF_PP;//发送数据固定为复用推挽输出
gpio_initstruct.GPIO_Pin = GPIO_Pin_9;
gpio_initstruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&gpio_initstruct);
//Rx PA10
gpio_initstruct.GPIO_Mode = GPIO_Mode_IPU;//接收数据为浮空输入或上拉输入
gpio_initstruct.GPIO_Pin = GPIO_Pin_10;
gpio_initstruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&gpio_initstruct);
usart_initstruct.USART_BaudRate = 115200;
usart_initstruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//不使用流控
usart_initstruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
usart_initstruct.USART_Parity = USART_Parity_No;//不使用校验位
usart_initstruct.USART_StopBits = USART_StopBits_1;//一位停止位
usart_initstruct.USART_WordLength = USART_WordLength_8b;//因为不使用校验位所以字节长度为八个
USART_Init(USART1,&usart_initstruct);
USART_Cmd(USART1,ENABLE);//一定记得配置完后使能USART1外设
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
nvic_initstruct.NVIC_IRQChannel = USART1_IRQn;
nvic_initstruct.NVIC_IRQChannelPreemptionPriority = 1;
nvic_initstruct.NVIC_IRQChannelSubPriority = 1;
nvic_initstruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&nvic_initstruct);
}
//发送一个字节
void My_Usart_Send_Byte(USART_TypeDef* USARTx, uint16_t Data)
{
USART_SendData(USARTx, Data);
while(USART_GetFlagStatus(USARTx,USART_FLAG_TXE) == RESET);//‘发送数据寄存器空’等于0,即说明寄存器中还有数据,因此需要留在while循环中
}
//发送一个数组
void My_Usart_SendArray(uint8_t *Array,uint16_t Length)
{
uint16_t i;
for(i=0;i<Length;i++)
{
My_Usart_Send_Byte(USART2,Array[i]);
}
}
//发送一个数组
void My_Usart_SendArray2(uint8_t *Array,uint16_t Length)
{
uint16_t i;
for(i=0;i<Length;i++)
{
My_Usart_Send_Byte(USART1,Array[i]);
}
}
//发送一个字符串
void My_Usart_Send_String(USART_TypeDef* USARTx, char * str)
{
uint16_t i=0;
do
{
My_Usart_Send_Byte(USARTx,*(str+i));//逐个字符发送
i++;
}while(*(str+i) != '\0');// 直到遇到字符串结束符
while(USART_GetFlagStatus(USARTx,USART_FLAG_TC) == RESET);
}
uint8_t Serial_GetRxFlag(void)//实现读后自动清除的功能
{
if(Serial_RxFlag == 1)
{
Serial_RxFlag=0;
return 1;
}
return 0;
}
int fputc(int ch,FILE*f)//对printf函数重定向
{
My_Usart_Send_Byte(USART1,ch);
return ch;
}
void USART1_IRQHandler(void)//中断函数名称看开始文件
{
if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET)
{
USART_ClearITPendingBit(USART1,USART_IT_RXNE);//清除标志位,如果读取DR是可以自动清除的
}
}
void USART2_IRQHandler()
{
if(USART_GetITStatus(USART2,USART_IT_RXNE) == SET)
{
static uint8_t RxState = 0;
static uint8_t pRxState = 0;
uint8_t RxData = USART_ReceiveData(USART2);
if(RxState == 0)
{
if(RxData==0x01) //如果地址是0x01再接收其他返回的数据并存在Serial_RxPacket数组里
{
RxState=1;
pRxState=0;
}
}
else if(RxState == 1)
{
Serial_RxPacket[pRxState]=RxData;
pRxState ++;
if(pRxState>=12)
{
RxState=0;
Serial_RxFlag=1;
}
}
USART_ClearITPendingBit(USART2,USART_IT_RXNE);//清除标志位,如果读取DR是可以自动清除的
}
}
这样我们就把发送接收数据的串口2和打印数据的串口1配置好了,接下来配置控制RS485发送还是接收的PA7
RS485.h
#ifndef __RS485_H__
#define __RS485_H__
void RS485_Init(void);
#endif
RS485.c
#include "stm32f10x.h"
#include "RS485.h"
void RS485_Init(void)
{
GPIO_InitTypeDef gpio_initstruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
gpio_initstruct.GPIO_Mode = GPIO_Mode_Out_PP;
gpio_initstruct.GPIO_Pin = GPIO_Pin_7;
gpio_initstruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&gpio_initstruct);
}
main.c
#include "stm32f10x.h" // Device header
#include "stdio.h"
#include "dht11.h"
#include "delay.h"
#include "Usart.h"
#include "RS485.h"
#include "relay.h"
#include "OLED.h"
int main(void)
{
Relay_Init();
my_Usart_Init();
my_Usart_Init2();
RS485_Init();
Serial_TxPacket[0]=0x01;
Serial_TxPacket[1]=0x03;
Serial_TxPacket[2]=0x00;
Serial_TxPacket[3]=0x00;
Serial_TxPacket[4]=0x00;
Serial_TxPacket[5]=0x04;
Serial_TxPacket[6]=0x44;
Serial_TxPacket[7]=0x09;
u8 tem1=0;
u8 hum1=0;
float temp=0;
float humi=0;
u16 EC=0;
float PH=0;
while(1)
{
delay_s(5); //5s采集一次数据
GPIO_SetBits(GPIOA,GPIO_Pin_7); //给高电平,设置为发送模式
My_Usart_SendArray(Serial_TxPacket,8); //将询问帧发给土壤传感器
delay_ms(1); //发送完延时1ms保证数据不会丢失
GPIO_ResetBits(GPIOA,GPIO_Pin_7); //给低电平,设置为接收模式,接收土壤传感器返回的数据
if(Serial_GetRxFlag() == 1) //如果返回的数据满足中断函数的要求将会返回1
{
temp = ((float)(Serial_RxPacket[4]*256 + Serial_RxPacket[5]))/10.0;
humi = ((float)(Serial_RxPacket[2]*256 + Serial_RxPacket[3]))/10.0;
EC = Serial_RxPacket[6]*256 + Serial_RxPacket[7];
PH = ((float)(Serial_RxPacket[8]*256 + Serial_RxPacket[9]))/10.0;
printf("\r\ntemp: %.1f \r\nhumi: %.1f \r\nEC: %d \r\nPH: %.1f \r\n",temp,humi,EC,PH);
} //打印收到的各个数据
if(humi <= 30)
{
GPIO_ResetBits(GPIOA,GPIO_Pin_0);
}
else
{
GPIO_SetBits(GPIOA,GPIO_Pin_0);
}
//这个后续可以加一个继电器来控制水泵的开关,如果湿度过低就打开水泵,这里就先不加了
}
}
三、串口助手显示
这个Airtemp和Airhumi是我测试的空气温度和湿度,可以不看
作者:do_while__