STC15单片机-上位机通过Modbus-RTU协议与开发板通信

上位机通过Modbus-RTU协议与开发板通信

Modbus协议

Modbus是一种串行通信协议,是Modicon公司(现在的施耐德电气Schneider Electric)于 1979年为使用可编程逻辑控制器(PLC)通信而发表。Modbus 已经成为工业领域通信协议的业界标准(De facto),并且现在是工业电子设备之间常用的连接方式。

更详细的介绍可看这篇文章:http://t.csdn.cn/n1T5O

实验前梳理

要进行Modbus通信,就要先写一个上位机软件,是用C#语言编写的(还不会,先用现成的);然后电脑通过USB转485工具与开发板的A、B端子相连接,Modbus协议使用RS-485进行传输;

在这里插入图片描述

实验功能

获取PCB板温度,并在数码管上显示;

按键2可以通过单击、双击、长按调整PWM灯的亮度

上位机通过Modbus协议实时获取PCB板的温度、PWM灯的亮度

上位机通过Modbus协议可以设置PWM灯的亮度

本次实验的报文格式

上位机读取开发板的板载温度和PWM灯的亮度,这不是某个位状态,而是寄存器数值,所以功能码是03,读单个或多个保持寄存器的值

在这里插入图片描述

上位机还可以设置PWM灯的亮度,所以用到的功能码是06,写单个保持寄存器

在这里插入图片描述

程序

文件结构

在这里插入图片描述

main.c ->主函数文件,包含main函数等;

Public.c ->公共函数文件,包含Delay延时函数等;

Sys_init ->系统初始化函数,包含GPIO初始化函数等;

ADC.c->ADC初始化,采集ADC值等;

NTC.c ->NTC外设函数,包含查表,获取环境温度等;

Modbus.c ->Modbus外设函数,主要包含Modbus协议解析函数,读寄存器函数和写寄存器函数;

CRC-16.c->校验函数,主要包含CRC-16校验函数。

与上一节数码管显示温度代码相比改动的地方

main.c:获取PCB板温度,数码管显示温度,状态机扫描按键2,按键2按下触发调整PWM灯亮度,串口1协议解析

系统主循环
while(1)
{
    //获取PCB板温度
    NTC.Get_Temperature_Value(); 
    //数码管显示温度
    TM1620.Disp_Temperture();    

    //延时500ms
    /*这里不能用Public.Delay_ms(500),这样的话延时就太久了,上位机发送指令下来单片机可能收不到,所以改为用while循环
    循环500次,每次延时1毫秒,然后进行按键2的状态机扫描*/
    i = 500;	
    while(i--)
    {
        Public.Delay_ms(1);
        KEY2.KEY_Detect(); //状态机扫描按键2	每隔10毫秒扫描一次
		
        //这两条if语句起从机监听作用
        if(UART1.ucRec_Flag == TRUE)//如果接收到上位机发送的指令,则用break退出循环,执行下面调整PWM灯亮度
        {
            break;
        }
        if(KEY2.KEY_Flag == TRUE)	//如果按键2被按下,则跳出循环,及时改变PWM灯的亮度
        {
            break;
        }
    }
    //按键触发调整PWM灯亮度
    PWM.PWM_LED_Adjust_Brightness(); 
    //串口1协议解析
    UART1.Protocol();                
}

public.c:增加内存清除函数Memory_Clr

/*
* @name   Memory_Clr
* @brief  内存清除函数
* @param  pucBuffer -> 要清除的内存首地址
* @param  LEN -> 要清除内存长度
* @retval None   
*/
static void Memory_Clr(uint8_t *pucBuffer,uint16_t LEN)
{
    uint16_t i;
    for(i=0;i<LEN;i++)
    {
        *(pucBuffer+i) = (uint8_t)0;
    }
}

public.h:注释掉宏定义Monitor_Run_Code,定义时是往串口USB发送的,不定义就是RS-485发送,可在UART1.c文件中切换串口

UART1.c:串口1初始化函数 Init() 处,用预编译命令设置AUXR1寄存器,如果public.h中宏定义了Monitor_Run_Code,则将串口1映射到P30、P31引脚(串口USB调试信息);如果没定义Monitor_Run_Code,则将串口1映射到P36、P37引脚(RS-485传输)

//把串口1映射到USB转TTL模块连接的P30和P31引脚,默认该两位是0
#ifdef Monitor_Run_Code
	AUXR1  &= ~(S1_S1);         //AUXR1第7位清0
	AUXR1  &= ~(S1_S0);         //AUXR1第6位清0
#endif

//把串口1映射到RS-485连接到的P37和P36引脚
#ifndef Monitor_Run_Code
	AUXR1 &= ~S1_S1;        //AUXR1第7位清0
	AUXR1 |=  S1_S0;        //AUXR1第6位置1
#endif

波特率改为9600,原来是115200;

预编译命令包括putchar发送函数重定义;在宏定义Monitor_Run_Code时,可以用printf函数将信息打印到串口上;没宏定义Monitor_Run_Code时,不能使用 printf 函数,否则会造成运行异常

/*
* @name   putchar
* @brief  字符发送函数重定向
* @param  ch:发送的字符
* @retval char   
*/
#ifdef Monitor_Run_Code
  extern char putchar(char ch)
  {
    UART1.UART_SendData((uint8_t)ch);   //在putchar函数内直接调用串口发送字符函数
    return ch;
  }
#endif

实现串口协议函数Protocol

/*
* @name   Protocol
* @brief  串口协议
* @param  None
* @retval None   
*/
static void Protocol()
{
  //判断经过RS-485传输后串口是否接收到数据
  if(UART1.ucRec_Flag == TRUE)
  {
    //过滤干扰数据0
    //Modbus协议中,主机发送的信息帧第一个字节是从机地址,范围是1 ~ 247,该if语句则判断是否是从机地址,0是广播地址
    if(ucRec_Buffer[0] != 0)
    {
      Timer0.usDelay_Timer = 0; //延时定时器清零,开始计时
      while(UART1.ucRec_Cnt < 8)
      {
       if(Timer0.usDelay_Timer >= TIMER_100MS) //100ms内没接收完8个字节,则跳出循环
       {
         break;
       }
      }
      //接收完8个字节数据,则进行协议分析
      Modbus.Protocol_Analysis(&UART1);
    }
    //清除缓存
    Public.Memory_Clr(ucRec_Buffer,(uint16_t)UART1.ucRec_Cnt);

    //重新接收
    UART1.ucRec_Cnt = 0;
    UART1.ucRec_Flag = FALSE;
  }
}

Timer0.h:增加延时定时器usDelay_Timer
Timer0.c:中断处理函数中usDelay_Timer++

NTC.h:增加用于Modbus发送的温度值uTemperature
NTC.c:将ADC采集值Temp赋值给uTemperature,给Modbus.c读寄存器函数调用,封装成帧发送给主机

  //用于Modbus传输到上位机的温度值
  NTC.uTemperature = Temp;

PWM.h:在结构体内增加声明指向PWM_Duty_Set()函数的指针
PWM.c:初始化指向PWM_Duty_Set()函数的指针,给后续主机通过Modbus协议写数据设置PWM灯亮度使用,用来设置占空比

增加CRC_16.h和CRC_16.c:CRC校验码的计算
#ifndef __CRC_16_H_
#define __CRC_16_H_

//定义CRC校验码的结构体
typedef struct
{
    uint16_t CRC;     //CRC校验值
    uint8_t  CRC_H;   //高位
    uint8_t  CRC_L;   //低位

    uint16_t (*CRC_Check)(uint8_t *,uint8_t );
}CRC_16_t;

/* extern variables-----------------------------------------------------------*/
extern CRC_16_t idata CRC_16;
/* extern function prototypes-------------------------------------------------*/ 

#endif
/********************************************************
  End Of File
********************************************************/
/* Includes ------------------------------------------------------------------*/
#include <main.h>

/* Private define-------------------------------------------------------------*/

/* Private variables----------------------------------------------------------*/
uint16_t CRC_Check(uint8_t *,uint8_t );
/* Public variables-----------------------------------------------------------*/
CRC_16_t idata CRC_16 = 
{
    0,
    0,
    0,
    CRC_Check
};
/*******************************************************
说明:CRC添加到消息中时,低字节先加入,然后高字节

CRC计算方法:
 1.预置1个16位的寄存器为十六进制FFFF(即全为1);称此寄存器为CRC寄存器;
 2.把第一个8位二进制数据(既通讯信息帧的第一个字节)与16位的CRC寄存器的低
 8位相异或,把结果放于CRC寄存器;
 3.把CRC寄存器的内容右移一位(朝低位)用0填补最高位,并检查右移后的移出位;
 4.如果移出位为0:重复第3步(再次右移一位);
 如果移出位为1:CRC寄存器与多项式A001(1010 0000 0000 0001)进行异或;
 5.重复步骤3和4,直到右移8次,这样整个8位数据全部进行了处理;
 6.重复步骤2到步骤5,进行通讯信息帧下一个字节的处理;
 7.将该通讯信息帧所有字节按上述步骤计算完成后,得到的16位CRC寄存器的高、低
 字节进行交换;
********************************************************/

/* Private function prototypes------------------------------------------------*/

/*
* @name   CRC_Check
* @brief  CRC校验
* @param  CRC_Ptr:数组指针
* @param  LEN:数组长度
* @retval uint16_t类型的CRC校验值   
*/
uint16_t CRC_Check(uint8_t *CRC_Ptr,uint8_t LEN)
{
    uint16_t CRC = 0;
    uint8_t i = 0;
    uint8_t j = 0;
    
    CRC = 0xFFFF;
    for(i=0;i<LEN;i++)  //CRC校验是校验一帧数据中CRC位置前面的所有数据,LEN根据实际情况改变
    {
        CRC ^= *(CRC_Ptr+i);//CRC_Ptr是8位,解引用取出某位后,与16位的CRC低8位进行异或操作
        for(j=0;j<8;j++)    //处理一个字节数据,8位
        {
            if(CRC & 0x0001)
            {
                CRC = (CRC >> 1) ^ 0xA001;//如果最低位是1,与0xA001进行异或
            }
            else
            {
                CRC = (CRC >> 1);//如果最低位一直是0,则一直右移
            }
        }
    }
    CRC = ((CRC >> 8) + (CRC << 8));    //交换高低字节
    return CRC;
}
/********************************************************
  End Of File
********************************************************/
增加Modbus.h和Modbus.c:

主要是Modbus协议解析,然后读寄存器和写寄存器再分别写成两个函数,读寄存器时是从机组帧并返回信息给主机,写寄存器时从机返回的信息与主机发来的一致,提取主机发来的PWM灯亮度值,调用PWM_Duty_Set()函数设置PWM灯亮度

#ifndef __Modbus_H_
#define __Modbus_H_

//定义结构体类型
typedef struct
{
    uint16_t Addr;                      //从机地址

    void (*Protocol_Analysis)(UART_t* );//协议分析

}Modbus_t;

/* extern variables-----------------------------------------------------------*/
extern Modbus_t idata Modbus;
/* extern function prototypes-------------------------------------------------*/ 

#endif
/********************************************************
  End Of File
********************************************************/

处理主机发送过来的信息帧步骤

1.先独立计算出前6位的CRC校验码,并与主机发来的CRC校验码进行比较,校验码相等则进行下一步,不相等则不进行下面操作,直接提示校验码出错

2.判断从机地址,是否是本从机的地址,是则进行下一步操作

3.判断功能码,如果是03读保持寄存器,则调用Modbus_Read_Register()函数,组帧并返回给主机;如果是06写保持寄存器,则调用Modbus_Write_Register()函数,返回相同的信息帧给主机,提取主机发送的数据

遇到的问题

在编写Modbus相关函数代码前,想测试一下数码管显示温度的同时按键2是否可以改变PWM灯的亮度,在写好UART1.c的预编译切换串口到USB传输或者RS-485传输,波特率设为9600,将重写的putchar函数也包括在预编译指令中后,编译烧写,发现按键2按下后PWM灯没反应,数码管显示的温度也一直不变

后来发现,在public.h头文件中,如果宏定义了 Monitor_Run_Code,则按键2正常调节亮度,数码管实时显示温度,但仍然没有响应上位机的消息;如果没有宏定义 Monitor_Run_Code,则开发板按键2按下后PWM灯没法调亮度,数码管显示的温度不实时,温度显示卡死,把手按在NTC电阻上仍然是同一个温度,上位机发送数据开发板也没有响应;

原因

PWM.c文件的PWM_LED_Adjust_Brightness函数中原本单击的输出语句是这样的(双击、长按的也类似,输出的信息不一样)

UART1.RS485_Set_SendMode();           //RS-485设置为发送模式
printf("KEY2 click detected\r\n\r\n");    //打印单击信息
UART1.RS485_Set_RecMode();            //RS-485设置为接收模式

查看使用到的代码源文件,发现PWM.c源文件的打印按键信息语句处有问题,与RS-485设置发送模式和接收模式没有关系,这只是一个引脚的状态位改变而已,没有宏定义Monitor_Run_Code语句时,虽然串口切换到了RS-485的引脚上,但那些ADC采集值,NTC电压,温度是用 printf 打印的,没有宏定义 Monitor_Run_Code 语句也就这些信息都没有进行打印,问题就出在了

printf("KEY2 click detected\r\n\r\n");    //打印单击信息

这条语句上,因为重写putchar函数也是用预编译包含了,没宏定义 Monitor_Run_Code也就没有重写putchar函数,所以这里的printf函数就不是往串口上输出的,应该是原原本本的C语言printf输出函数,输出到标准输出流上,用在单片机这里就导致了系统运行错误,把printf这条语句注释掉则系统运行正常,也可以改为#ifdef #endif形式;如果宏定义了Monitor_Run_Code语句,那putchar函数会被重写,这里的printf函数也就正常往串口发送信息

可改为:

#ifdef Monitor_Run_Code
	printf("KEY2 click detected\r\n\r\n");    //打印单击信息
#endif

更改后在没有涉及Modbus协议的前提下,数码管温度显示实时,按键2按下会调整PWM灯亮度

然后再确认Modbus协议代码没问题,编译烧写,发现上位机发送数据到开发板后,开发板会自动回应主机,同时上位机也获取到了开发板的温度和PWM灯亮度值,手动设置PWM灯亮度值时,PWM灯也会发生改变,程序运行正常且效果正确

看教程时需要注意的点

在UART1内部使用接收和发送数组时直接使用数组名,外部使用接收和发送数组时通过结构体指针调用

电脑开发一个上位机作主机,STC15开发板作从机,用MODBUS-RTU协议

Modbus只是定义了通信报文的格式,并没有定义数据的格式,数据格式要自己定义,所以行业中不同厂家的Modbus协议可能是不兼容的,因为数据的格式不一样

假如NTC获取的温度是 -30 ~ 70℃,要将温度编码传输,比如是-15.5度,先加30,结果再乘以2,得到29就传输过去,到达另一端后再解码,(29/2)-30 = -15.5

Modbus协议不一定是用RS-485接口,也可以用串口,CAN口,RS-232,RS-422等

通信格式的Address是通信地址:1 ~ 247 只有到247,其他地址可能有其他用途

寄存器地址定义中,地址40001被定义为PCB板温度,地址40002被定义为PWM灯亮度

主函数中while(1)循环内再用whlie循环实现延时,具有实时性,不能用Public.Delay_ms,会出错

重要方法

在main函数主循环中,获取PCB板温度并在数码管上显示,同时不断用状态机检测按键2,此时方法中没有加延时的话,串口打印的温度数据会非常快,按键虽然能起作用,但在串口中也看不到按键信息,很快就被刷上去了

普通延时方法

如果在数码管显示温度后用Delay_ms函数延时500ms,则按键2按下时会有很大概率检测不到,导致PWM灯没反应,因为按键2是定时器每隔10ms扫描状态机来检测的,速度很快,所以每个按下瞬间都能检测到,如果主函数中延时了500ms,就没有了实时性,所以这种延时方法对实时性要求高的操作来说是不可取的

while(1)
{
    //获取PCB板温度
    NTC.Get_Temperature_Value();
    //数码管显示温度
    TM1620.Disp_Tempareture();

    Public.Delay_ms(500);	//延时太久,可能按键2被按下瞬间还在延时里没有出来,导致按键2没有被检测到
    KEY2.KEY_Detect();
    //设置PWM灯亮度
    PWM.PWM_LED_Adjust_Brightness();
}

改进延时方法

将延时500ms改为延时1ms,共循环500次

先定义静态类型的16位无符号整型 i,然后要在while(1)里面赋值为500,因为main函数执行后,就一直在while(1)里循环执行,如果在定义时就初始化为500,那执行一遍后 i 就被减完了,而 i 又没有重新赋值,就没有延时效果,所以要在while(1)里面再赋值

这种写法在获取温度在数码管显示来看,是间隔了1 * 500 = 500ms才执行一次的,但按键2的检测只是延时了1ms,并不会影响定时器扫描状态机,当检测到按键2被按下时,立即退出剩余循环,实时设置PWM灯的亮度,所以这种方法在处理实时性操作时是比较可取的

int main(void)
{
	static uint16_t i = 0;
	//系统初始化
	Hradware.Sys_Init();
	//串口1发送初始化信息
	#ifdef Monitor_Run_Code
		printf("Initialization completed,system startup!\r\n\r\n");
	#endif
	//系统主循环
	while(1)
	{
		//获取PCB板温度
		NTC.Get_Temperature_Value();
		//数码管显示温度
		TM1620.Disp_Tempareture();

		i = 500;
		while(i--)
		{
			Public.Delay_ms(1);
			//状态机检测按键2
			KEY2.KEY_Detect();
			if(KEY2.KEY_Flag == TRUE)	//按键2被按下则退出循环,实时设置PWM灯亮度
			{
				break;
			}
		}
		//设置PWM灯亮度
		PWM.PWM_LED_Adjust_Brightness();
	}
}
  • 2
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值