最近做了一个获取温度传感器数据并显示到数码管上的需求,选型的传感器都是基于RS485-MODBUS-RTU协议进行通讯,串口参数均为一位起始位、8位数据位、一位停止位、无校验位,波特率统一设置为9600。
数码管及温度传感器链接如下,有需要了解详细协议文档的可去查看
https://item.taobao.com/item.htm?spm=a230r.1.14.35.6ba8b1dcD1fV02&id=635267329704&ns=1&abbucket=4#detail
https://item.taobao.com/item.htm?spm=a230r.1.14.152.75a533eelohANg&id=676374539800&ns=1&abbucket=4&mt=
1、485协议简介
RS485是一种典型的串口通讯协议,是基于串口通讯本身对电路信号进行调制的一种软件协议,RS-485总线标准规定了总线接口的电气特性标准即对于2个逻辑状态的定义:正电平在+2V~+6V之间,表示一个逻辑状态;负电平在-2V~-6V之间,则表示另一个逻辑状态;电信号采用差分传输的方式,可以有效的屏蔽线路外的电磁干扰实现超远距离通讯,其协议本身与串口协议相同,在开发方式上并无太大区别。
2、传感器连接和设备通讯协议介绍
传感器及数码管通过485口连接到开发板
温度传感器读取温度数据协议格式及参数
![在这里插入图片描述](https://img-blog.csdnimg.cn/670e098d6d6c488090a216acb746b107.png
数码管写入数据说明
3、板子485串口及引脚复用介绍
本次的F407板子485转换芯片是接在串口2上的,对应发送和接收引脚为PA2和PA3,收发控制引脚为PC0
4、配置串口所需GPIO并复用相关引脚
本例需要GPIOA和GPIOC分别进行配置以满足其功能需求
/*
* 函数名:_485_GPIO_Config
* 描述 :配置485GPIO引脚
* 输入 :无
* 输出 : 无
* 调用 :内部调用
*/
void _485_GPIO_Config (void)
{
//初始化GPIOA和C,PA2和AP3用于串口收发、PC0用于控制485发送使能
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA|RCC_AHB1Periph_GPIOC,ENABLE);
//将引脚复用到串口2, PS:实际引脚要根据485转换芯片接的引脚为准
GPIO_PinAFConfig(GPIOA,GPIO_PinSource2,GPIO_AF_USART2);
GPIO_PinAFConfig(GPIOA,GPIO_PinSource3,GPIO_AF_USART2);
GPIO_InitTypeDef GPIO_InitStructure;
//设置为复用模式
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
//设置为推挽模式
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
//选择PA2和PA3作为收发引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2|GPIO_Pin_3;
//设置引脚浮空
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
//配置引脚速度
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
//初始化GPIOA
GPIO_Init(GPIOA,&GPIO_InitStructure);
//初始化GPIOC引脚0 设置为输出模式
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_Init(GPIOC,&GPIO_InitStructure);
}
5、配置NVIC中断控制
PS:一个项目中只能配置一个NVIC分组方式
/*
* 函数名:_485_USART_NVIC_Config
* 描述 :配置串口中断
* 输入 :无
* 输出 : 无
* 调用 :内部调用
*/
void _485_USART_NVIC_Config (void)
{
//配置NVIC分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);
NVIC_InitTypeDef NVIC_InitStructure;
//设置中断通道
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd =ENABLE;
//配置响应优先级分组
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
//配置抢占优先级分组
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
//中断使能
NVIC_Init(&NVIC_InitStructure);
}
6、配置串口参数并使能
串口参数配置根据实际设备的需求来设置,可选择有无奇偶校验、数据帧长度、停止位长度、是否需要流控等
/*
* 函数名:_485_USART_Config
* 描述 :用于配置串口参数
* 输入 :无
* 输出 : 无
* 调用 :内部调用
*/
void _485_USART_Config (void)
{
//串口时钟使能
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);
USART_InitTypeDef USART_InitStructure;
//配置波特率
USART_InitStructure.USART_BaudRate = 9600;
//设置无流控
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
//设置发送和接收都使能
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
//设置无校验位
USART_InitStructure.USART_Parity = USART_Parity_No;
//设置一个停止位
USART_InitStructure.USART_StopBits = USART_StopBits_1;
//设置数据位为8位
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART2,&USART_InitStructure);
//串口使能
USART_Cmd(USART2,ENABLE);
//配置串口中断
USART_ITConfig(USART2,USART_IT_RXNE,ENABLE);
//初始默认串口为接收模式
GPIO_ResetBits(GPIOC,GPIO_Pin_0);
}
8、封装外部调用函数来统一启动配置
/*
* 函数名:_485_Init
* 描述 :初始化485所有配置
* 输入 :
* 输出 : 无
* 调用 :外部调用
*/
void _485_Init (void)
{
_485_GPIO_Config();
//先开启中断配置再开启串口配置,否则中断可能不生效
_485_USART_NVIC_Config();
_485_USART_Config();
}
9、封装CRC校验函数及清理缓存区函数
static const unsigned char auchCRCHi[]=
{
0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x01,0xC0, 0x80,0x41,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40, 0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x00,0xC1, 0x81,0x40,0x01,0xC0,0x80,0x41,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x00,0xC1,0x81,0x40, 0x01,0xC0,0x80,0x41,0x01,0xC0,0x80,0x41,0x00,0xC1, 0x81,0x40,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40, 0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x01,0xC0, 0x80,0x41,0x00,0xC1,0x81,0x40,0x00,0xC1,0x81,0x40, 0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40
};
static const unsigned char auchCRCLo[]=
{
0x00,0xC0,0xC1,0x01,0xC3,0x03,0x02,0xC2,0xC6,0x06, 0x07,0xC7,0x05,0xC5,0xC4,0x04,0xCC,0x0C,0x0D,0xCD,0x0F,0xCF,0xCE,0x0E,0x0A,0xCA,0xCB,0x0B,0xC9,0x09,0x08,0xC8,0xD8,0x18,0x19,0xD9,0x1B,0xDB,0xDA,0x1A, 0x1E,0xDE,0xDF,0x1F,0xDD,0x1D,0x1C,0xDC,0x14,0xD4, 0xD5,0x15,0xD7,0x17,0x16,0xD6,0xD2,0x12,0x13,0xD3,0x11,0xD1,0xD0,0x10,0xF0,0x30,0x31,0xF1,0x33,0xF3,0xF2,0x32,0x36,0xF6,0xF7,0x37,0xF5,0x35,0x34,0xF4,0x3C,0xFC,0xFD,0x3D,0xFF,0x3F,0x3E,0xFE,0xFA,0x3A,0x3B,0xFB,0x39,0xF9,0xF8,0x38,0x28,0xE8,0xE9,0x29, 0xEB,0x2B,0x2A,0xEA,0xEE,0x2E,0x2F,0xEF,0x2D,0xED, 0xEC,0x2C,0xE4,0x24,0x25,0xE5,0x27,0xE7,0xE6,0x26, 0x22,0xE2,0xE3,0x23,0xE1,0x21,0x20,0xE0,0xA0,0x60, 0x61,0xA1,0x63,0xA3,0xA2,0x62,0x66,0xA6,0xA7,0x67, 0xA5,0x65,0x64,0xA4,0x6C,0xAC,0xAD,0x6D,0xAF,0x6F,0x6E,0xAE,0xAA,0x6A,0x6B,0xAB,0x69,0xA9,0xA8,0x68,0x78,0xB8,0xB9,0x79,0xBB,0x7B,0x7A,0xBA,0xBE,0x7E,0x7F,0xBF,0x7D,0xBD,0xBC,0x7C,0xB4,0x74,0x75,0xB5,0x77,0xB7,0xB6,0x76,0x72,0xB2,0xB3,0x73,0xB1,0x71,0x70,0xB0,0x50,0x90,0x91,0x51,0x93,0x53,0x52,0x92,0x96,0x56,0x57,0x97,0x55,0x95,0x94,0x54,0x9C,0x5C,0x5D,0x9D,0x5F,0x9F,0x9E,0x5E,0x5A,0x9A,0x9B,0x5B,0x99,0x59,0x58,0x98,0x88,0x48,0x49,0x89,0x4B,0x8B,0x8A,0x4A,0x4E,0x8E,0x8F,0x4F,0x8D,0x4D,0x4C,0x8C,0x44,0x84,0x85,0x45,0x87,0x47,0x46,0x86,0x82,0x42,0x43,0x83,0x41,0x81,0x80,0x40
};
//获取CRC校验值
void Modbus_GetCRC16(unsigned char *RxBuffer,unsigned short usLength,unsigned char *destCRC16Hi,unsigned char *destCRC16Lo)
{
unsigned char uchCRCHi=0xFF;
unsigned char uchCRCLo=0xFF;
unsigned short uIndex ;
while(usLength--)
{
uIndex=(unsigned char)(uchCRCHi^*RxBuffer++);
uchCRCHi=(unsigned char)(uchCRCLo^auchCRCHi[uIndex]);
uchCRCLo=(unsigned char)(auchCRCLo[uIndex]);
}
*destCRC16Hi=uchCRCLo;
*destCRC16Lo=uchCRCHi;
}
/*
* 函数名:Clear_Buff
* 描述 :用于清除缓冲区数据方便下一轮接收
* 输入 :无
* 输出 : 无
* 调用 :内部调用
*/
void Clear_Buff (void)
{
for(int i = 0;i<Usart_Count;i++){
Usart_buff[i] = 0;
}
}
10、封装串口发送函数
//使用单字节数据发送前要使能发送引脚,发送后要使能接收引脚。
void Usart_Send( uint8_t ch )
{
//通过串口2发送数据
USART_SendData(USART2,ch);
//等待发送完毕
while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET);
//加短暂延时,保证数据发送完毕
Delay(0xFFF);
}
/*
* 函数名:Usart_Send_Str
* 描述 :发送指定长度字符串
* 输入 :*Str:字符串指针,length:字符串长度
* 输出 : 无
* 调用 :外部调用
*/
void Usart_Send_Str (uint8_t *Str,int8_t length)
{
uint8_t k = 0;
do{
Usart_Send(*(Str+k));
k++;
}while(k<length);
}
11、封装函数获取温度信息并在中断函数中设置状态机进行接收
温度传感器的读报文无变动需求,所以直接写死即可,在读报文时采用状态机的方式,即将当前报文状态分为报文头、数据帧、校验位等,使其在不同状态之间进行转换来保证数据不会接收错误并防止数据与报文头重复时无法区分。
//统计接收数据的个数
uint8_t Usart_Count = 0;
//定义接收缓冲区大小
#define UART_BUFF_SIZE 32
//定义接收缓冲区
uint8_t Usart_buff[UART_BUFF_SIZE];
//用于临时存储串口接收的数据
int8_t udata = 0;
//定义状态机枚举类型
typedef enum {
HEAD,
DATA,
CHECK,
END
}State;
/*
* 函数名:GetTemper
* 描述 :获取温度数据报文封装
* 输入 :无
* 输出 : 无
* 调用 :外部调用
*/
void GetTemper(void)
{
uint8_t sendArr[8];
sendArr[0] = 0x05;
//功能码3
sendArr[1] = 0x03;
sendArr[2] = 0x00;
//读寄存器起始地址
sendArr[3] = 0x01;
sendArr[4] = 0x00;
//读寄存器个数
sendArr[5] = 0x01;
sendArr[6] = 0xD4;
sendArr[7] = 0x4E;
for(int i=0; i<8; i++){
Usart_Send(sendArr[i]);
}
//简单延时保证数据发送完毕
Delay(0xFFFF);
//使能接收数据
_485_RX_EN();
//简单延时保证数据接收完毕
Delay(0xFFFF);
}
/*
* 函数名:USART2_IRQHandler
* 描述 :用于处理中断接收到的数据
* 输入 :无
* 输出 : 无
* 调用 :外部调用
*/
void USART2_IRQHandler (void)
{
//定义两个CRC校验位
uint8_t CRC_High,CRC_Low=0;
//初始化状态机状态
static State Curr_State = HEAD;
if(USART_GetITStatus(USART2,USART_IT_RXNE) == SET)
{
udata = USART_ReceiveData(USART2);
switch(Curr_State)
{
//接收帧头状态
case HEAD:
if(udata == 0x05)
{
Usart_buff[Usart_Count] = udata;
Curr_State = DATA;
Usart_Count++;
}
break;
//接收数据状态
case DATA:
if(Usart_Count<5){
Usart_buff[Usart_Count] = udata;
if(Usart_Count==4){
Curr_State = CHECK;
}
Usart_Count++;
break;
}
//CRC校验状态
case CHECK:
if(Usart_Count<7){
Usart_buff[Usart_Count] = udata;
Usart_Count++;
}
//长度为7时进行校验 PS:此处仅有温度传感器会上报数据所以不做过多处理
if(Usart_Count==7){
Modbus_GetCRC16(Usart_buff,5,&CRC_Low,&CRC_High);
if(Usart_buff[6]!=CRC_Low||Usart_buff[5]!=CRC_High){
Clear_Buff();
}
Usart_Count = 0;
Curr_State = HEAD;
}
break;
default:
break;
}
}
//清除中断
USART_ClearITPendingBit(USART2,USART_IT_RXNE);
}
12、封装显示温度符号及当前温度的函数
/*
* 函数名:showDegNum
* 描述 :显示温度符号
* 输入 :
* 输出 : 无
* 调用 :外部调用
*/
void showDegNum(void)
{
uint8_t numArray[8];
numArray[0] = 0x06;
numArray[1] = 0x06;
numArray[2] = 0x00;
numArray[3] = 0x00;
//存入温度符号对应值
numArray[4] = 0x00;
numArray[5] = 0x54;
//放入CRC校验值
numArray[6] = 0x89;
numArray[7] = 0x82;
for(int i=0;i<8;i++){
Usart_Send(numArray[i]);
}
//加短暂延时保证发送完毕
Delay(0xFFF);
}
/*
* 函数名:nixieTubeShow
* 描述 :数码管显示参数判断并调用串口
* 输入 :sign——参数正负,0为负,1为正,num2——参数值高8位,uint8_t num2——参数低8位
* 输出 : 无
* 调用 :外部调用
*/
void nixieTubeShow()
{
uint8_t nixieArray[13];
nixieArray[0]=0x06;
//功能码
nixieArray[1]=0x10;
nixieArray[2]=0x00;
//起始寄存器
nixieArray[3]=0x06;
nixieArray[4]=0x00;
//写寄存器个数2
nixieArray[5]=0x02;
//长度4字节
nixieArray[6]=0x04;
//小数1位
nixieArray[7]=0x01;
//三个数据位,这边温度精度只需要2个字节
nixieArray[8]=0x00;
//存入温度字节高位
nixieArray[9]=Usart_buff[3];
//存入温度字节低位
nixieArray[10]=Usart_buff[4];
//获取CRC校验值并放入数组中
Modbus_GetCRC16(nixieArray,11,nixieArray+12,nixieArray+11);
//判断温度数据若为0则不发送
if( nixieArray[9]!=0||nixieArray[10]!=0){
for(int i=0;i<13;i++){
Usart_Send(nixieArray[i]);
}
//加短暂延时保证发送完毕
Delay(0xFFFF);
}
}
13、在主函数中调用并方法获取并显示温度
PS:在发送和接收方法后必须要有延时尤其是低波特率下,否则数据可能无法接收完整,另在发送数据前一定要使能串口发送,并在发送完成后使能串口接收
/**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
//初始化485串口
_485_Init();
while(1)
{
_485_TX_EN();
GetTemper();
//加短暂延时,保证485发送数据完毕
Delay(0xFFFFF);
_485_TX_EN();
nixieTubeShow();
Delay(0xFFFFF);
showDegNum();
Delay(0xFFFFF);
}
}
static void Delay(__IO uint32_t nCount) //简单的延时函数
{
for(; nCount != 0; nCount--);
}
14、实际效果展示
ps:第一个为温度符号T
15、小结
485通讯本身并不复杂,只是在串口通讯的基础上来处理,有不懂的地方可以看注释,基本每一个参数配置都写了注释,一个是方便大家读代码,第二个也是对代码风格的调整,好的本文到此就结束了,感谢你的观看。