一、前言
我们的单片机串口一般常用RS232、RS485、TTL这几种通讯方式,日常调试可能RS232、TTL比较多,真正和其它厂家数据交互的时候,还是RS485用的比较多,因为它是差分信号等电气属性,所以比较稳定,传输距离长,常与modbus协议同用交互数据,今天这篇文章就讲一下如何利用RS485进行数据收发。
二、RS485特性
RS485(或EIA-485、过去称为RS-485)是隶属于OSI模型物理层的电气特性规定为2线、半双工、平衡传输线多点通信的标准。这是由电信行业协会(TIA)及电子工业联盟(EIA)联合发布的标准。RS485的主要特性和应用如下:
-
电气特性:
- RS-485采用差分信号负逻辑,逻辑“0”以两线间的电压差为+(2~6)V表示;逻辑“1”以两线间的电压差为-(2~6)V表示。
- 接口信号电平比RS-232-C降低了,就不易损坏接口电路的芯片,且该电平与TTL电平兼容,可方便与TTL电路连接。
-
通信性能:
- RS-485的数据最高传输速率为10Mbps。
- RS-485接口是采用平衡驱动器和差分接收器的组合,抗共模干扰能力增强,即抗噪声干扰性好。
- RS-485接口的最大传输距离标准值为4000英尺(约1219米),实际上可达3000米。但传输速率与传输距离成反比,在较低的传输速率下才能达到最大的通信距离。
- RS-485总线一般最大支持32个节点,如果使用特制的485芯片,可以达到128个或更多节点。
-
应用:
- RS485广泛应用于工业环境中,特别是在要求通信距离为几十米到上千米时,如工矿、交通运输等企业的称重计量设备的数据采集。
- RS485因其传输距离远、抗干扰能力强、支持多节点和接线简单等特点,成为首选的串行接口。
-
半双工特性:
- RS485是一种半双工-异步-串行通信总线,意味着两者通信时,同一时刻只能由其中一方发送,另一方只能接收,不可以同时收发。
三、软件设计
我们一般看到的RS485串口数据接收的简单案例都是直接接收缓存区一直增加或者以回车键为结束的案例,但是这样没有办法正常解析接收到的数据并根据数据进行相应处理。其实,我们可以加一个定时器,因为串口数据一句话是连续发送的,只要从接收到第一个字节开始启动定时器,规定时间内没有收到下一个字节,即定时器倒计时结束,我们就可以认为本次数据接收完成了,接收到完整的一条数据以后,我们对其进行解析,发送的话,直接调用函数发送就可以了,只不过发送之前需要控制 RS485_RE脚切换成发送模式。
四、代码实现
1.初始化串口
//初始化IO 串口7
//bound:波特率
void RS485_Init(u32 bound)
{
//GPIO端口设置
GPIO_InitTypeDef GPIO_Initure;
//PCF8574_Init(); //初始化PCF8574,用于控制RE脚
__HAL_RCC_GPIOF_CLK_ENABLE(); //使能GPIOA时钟
__HAL_RCC_UART7_CLK_ENABLE(); //使能UART7时钟
GPIO_Initure.Pin=GPIO_PIN_6|GPIO_PIN_7; //PF6,7
GPIO_Initure.Mode=GPIO_MODE_AF_PP; //复用推挽输出
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_HIGH; //高速
GPIO_Initure.Alternate=GPIO_AF8_UART7; //复用为UART7
HAL_GPIO_Init(GPIOF,&GPIO_Initure); //初始化PF6,7
GPIO_Initure.Pin=GPIO_PIN_8; //PF8
GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP; //推挽输出
GPIO_Initure.Pull=GPIO_PULLDOWN; //下拉
GPIO_Initure.Speed=GPIO_SPEED_HIGH; //高速
//GPIO_Initure.Alternate=GPIO_AF8_UART7; //复用为UART7
HAL_GPIO_Init(GPIOF,&GPIO_Initure); //初始化PF8
//USART 初始化设置
UART7_RS485Handler.Instance=UART7; //UART7
UART7_RS485Handler.Init.BaudRate=bound; //波特率
UART7_RS485Handler.Init.WordLength=UART_WORDLENGTH_8B; //字长为8位数据格式
UART7_RS485Handler.Init.StopBits=UART_STOPBITS_1; //一个停止位
UART7_RS485Handler.Init.Parity=UART_PARITY_NONE; //无奇偶校验位
UART7_RS485Handler.Init.HwFlowCtl=UART_HWCONTROL_NONE; //无硬件流控
UART7_RS485Handler.Init.Mode=UART_MODE_TX_RX; //收发模式
HAL_UART_Init(&UART7_RS485Handler); //HAL_UART_Init()会使能UART7
__HAL_UART_DISABLE_IT(&UART7_RS485Handler,UART_IT_TC);
#if EN_UART7_RX
__HAL_UART_ENABLE_IT(&UART7_RS485Handler,UART_IT_RXNE);//开启接收中断
HAL_NVIC_EnableIRQ(UART7_IRQn); //使能USART1中断
HAL_NVIC_SetPriority(UART7_IRQn,3,3); //抢占优先级3,子优先级3
#endif
RS485_TX_Set(0); //设置为接收模式
TIM3_Init(500-1,9000-1); //定时器3初始化,定时器时钟为90M,分频系数为9000-1,
//所以定时器3的频率为90M/9000=10K,自动重装载为500-1,那么定时器周期就是50ms
}
初始化PF6,7为串口7的RX和TX,bound(波特率)咱们传入的是9600,一般RS485通讯用9600比较多,调试的串口用115200较多,然后就是一些基本的串口配置,最后初始化定时器3。
2.切换接发模式
//RS485模式控制.
//en:0,接收;1,发送.
void RS485_TX_Set(u8 en)
{
if(en==0){
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8, GPIO_PIN_RESET);
}else{
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8, GPIO_PIN_SET);
}
}
PF8引脚在拉低的时候RS485是接收模式,PF8引脚在拉高的时候RS485是发送模式。
3.初始化定时器
void TIM3_Init(u16 arr,u16 psc)
{
TIM3_Handler.Instance=TIM3; //通用定时器3
TIM3_Handler.Init.Prescaler=psc; //分频系数
TIM3_Handler.Init.CounterMode=TIM_COUNTERMODE_UP; //向上计数器
TIM3_Handler.Init.Period=arr; //自动装载值
TIM3_Handler.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;//时钟分频因子
HAL_TIM_Base_Init(&TIM3_Handler);
}
定时器时钟为90M,分频系数为9000-1,所以定时器3的频率为90M/9000=10K,自动重装载为500-1,那么定时器周期就是50ms,也就是50ms内RS485不传回下一个字节,即认为传输结束。
4.串口和定时器中断
//接收缓存区
u8 RS485_RX_BUF[256]; //接收缓冲,最大64个字节.
//接收到的数据长度
u8 RS485_RX_CNT=0;
u8 RS485RX_FLAG=0;//如果RS485接收到了数据 标志位变为1
void UART7_IRQHandler(void)
{
u8 res;
if((__HAL_UART_GET_FLAG(&UART7_RS485Handler,UART_FLAG_RXNE)!=RESET)) //接收中断
{
HAL_TIM_Base_Stop(&TIM3_Handler);
HAL_UART_Receive(&UART7_RS485Handler,&res,1,100);
if(RS485_RX_CNT<255)
{
RS485_RX_BUF[RS485_RX_CNT]=res; //记录接收到的值
RS485_RX_CNT++; //接收数据增加1
}
HAL_TIM_Base_Start_IT(&TIM3_Handler); //开定时器
}
}
//定时器3中断服务函数
void TIM3_IRQHandler(void)
{
HAL_TIM_IRQHandler(&TIM3_Handler);
}
//回调函数,定时器中断服务函数调用
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim==(&TIM3_Handler))
{
HAL_TIM_Base_Stop(&TIM3_Handler);
__HAL_TIM_CLEAR_IT(&TIM3_Handler, TIM_IT_UPDATE);
RS485RX_FLAG=1;
}
}
串口中断和定时器中断是最重要的,这里我们一起讲,这里的逻辑就是:串口接收到每一个字节,都先关闭定时器,然后接收完了再启动定时器,这样的话,只要定时器倒计时结束之前停止运行,就不会进中断,直到所有数据接收完成以后,没数据再进来,进入定时器的中断,此时,先结束定时器运行,然后清除中断标志位(不清除的话可能会多进入一次定时器中断),最后将接收标志位置1即为接收完成。
5.接收完成后处理
void led_task(void *pdata)
{
OS_ERR err;
while(1)
{
if(RS485RX_FLAG==1){
RS485RX_FLAG=0;
RS485_Have_Data();
OSTimeDlyHMSM(0,0,0,20,OS_OPT_TIME_HMSM_STRICT,&err); //延时2s
}
}
}
void RS485_Have_Data(){
RS485_Send_Data(RS485_RX_BUF,RS485_RX_CNT);
RS485_RX_CNT=0;
}
这里是写了一个任务,只要判断RS485RX_FLAG=1以后,就开始对接收到的数据进行处理,我们要先将RS485RX_FLAG赋值0,避免多次运行,然后调用处理函数,此函数功能就是将接收到的数据原封不动返回去就算成功了,一般正常项目中我们都会对接收数据解析然后返回对应的数据,最后处理完了以后,别忘了将RS485_RX_CNT赋值0,确保下次再来了新数据还是从接收缓存区的第一位开始赋值的。