STM32进阶回顾(四):RS485串口通信详解

一、RS485通讯协议简介

        RS485(也称为 EIA-485TIA-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__

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32进阶回顾(四):RS485串口通信详解

    发表回复