基于51单片机的Modbus从机通信系统 本文展示了一个基于51单片机的Modbus从机通信系统实现代码主要功能包括通信协议处理实现了Modbus RTU协议的01-06、10功能码解析包括线圈/寄存器读写操作包含CRC16校验计算和错误响应机制硬件控制通过P1、P2口控制继电器和蜂鸣器支持手动/自动模式切换提供32个位变量和32个寄存器变量空间数据存储使用片内EEPROM存储关键数据实现扇区擦除、字节读写等Flash操作函数系统架构采用串口中断T0定时器实现通信超时检测定义清晰的数据结构区位寻址/字寻址包含初始化、数据保存等系统函数该代码实现了完整的Modbus从站功能适用于工业控制场景具有结构清晰、功能完备的特点可直接应用于51系列单片机开发。#include REG52.H#include intrins.H#define uint unsigned int#define uchar unsigned chartypedef unsigned int u16;typedef unsigned char u8;typedef unsigned long u32;extern u8 idata sendBuf[75]; //定义发送数组最大允许接收16个字32个字节extern u8 idata receBuf[75]; //定义接收数组最大允许接收16个字32个字节写入extern u8 *ps;extern u8 *pr;extern u8 rece_index;extern u8 Reciver_bit;sbit relayP2^0;sbit beeP2^1; //main hu16 xdata Word[32]; //定义字寻址区32个short型 ,有符号型u8 xdata Bit[32]; // 定义位寻址区10-31内部位0-9输出口u8 xdata inputBit[32];u16 *pwWord;u16 *pbBit;u16 sendlength; //Modbus_slave_c51_2011_10_09.cvoid AnalyzeRecieve();void send_comm();void Recirve_01();void Recirve_02();void Recirve_03();void Recirve_04();void Recirve_05();void Recirve_06();void Recirve_10();void errorsend(u8 func,u8 type);u16 CalcCrc(unsigned char *chData,unsigned short uNo); //Modbus_slave_c51_2011_10_09.Hu8 Reciver_Buf;u8 rece_index0;u8 idata sendBuf[75]; //定义发送数组最大允许发送32个字64个字节u8 idata receBuf[75]; //定义接收数组最大允许接收32个字64个字节写入u8 *pssendBuf;u8 *prreceBuf;u8 comm_END;u8 comm_time_out;u8 Reciver_bit0;u16 R_ISP_L;u16 R_ISP_H; // Main.c//#define _nop_() //定义一个空函数/**************************************************************** 定义ISP/IAP操作命令控制寄存器地址ISP_CMD E5H ** 1、0x00: 待机命令ISP无操作 ** 2、0x01 对用户的应用程序FLASH区及数据FLASH区字节读 ** 3、0x02: 对用户的应用程序FLASH区及数据FLASH区字节编程 ** 4、0x03: 对用户的应用程序FLASH区及数据FLASH区字节擦除 ****************************************************************/#define RDCommand 0x01#define PRGCommand 0x02#define ERASECommand 0x03#define waittime 0x01 //定义CPU的等待时间 SAVE DATA.cvoid ISP_enable(void);void ISP_disable(void);void go_ISP(void);void sectorerase(u16 addr);void write_ISP(u16 st_addr,u8 w_data);u8 read_ISP(u16 rd_addr); //SAVE DATA.H/***************************************/void AnalyzeRecieve() //分析并生成响应报文{u16 _crc;u8 a1,a2;_crcCalcCrc(receBuf,(rece_index-2)); //计算校验CRCa1_crc0xff; //CRC低字节a2(_crc8)0xff; //CRC高字节if(a1*(pr(rece_index-2))a2*(prrece_index-1)) //校验正确{switch(*(pr1)){case 0x01:Recirve_01();break; //读位状态case 0x02:Recirve_02();break; //读输入位状态case 0x03:Recirve_03();break; //读寄存器case 0x04:Recirve_04();break; //读输入寄存器case 0x05:Recirve_05();break; //写单个位状态case 0x06:Recirve_06();break; //写单个寄存器case 0x10:Recirve_10();break; //写多个寄存器default:errorsend(*(pr1),0x04);break; //不支持的功能码}}else //返回校验错误代码{switch(*(pr1)){case 0x01:errorsend(0x01,0x08);break;case 0x02:errorsend(0x02,0x08);break;case 0x03:errorsend(0x03,0x08);break;case 0x04:errorsend(0x04,0x08);break;case 0x05:errorsend(0x05,0x08);break;case 0x06:errorsend(0x06,0x08);break;case 0x10:errorsend(0x10,0x08);break;}}}/***********从机响应函数***********************/void send_comm( ){u8 i;ES0; //先关闭串口中断以免产生通信再次中断for(i0;isendlength;i){SBUF*(psi); //发送响应报文while(!TI); //等待报文结束TI0;}ES1; //发送完成打开串口中断}/*分析01功能码报文产生响应报文*//***************************************/void Recirve_01(){u16 startadd;u16 bit_num;u8 startaddH,startaddL;u8 bit_numH,bit_numL;u16 i,j;u16 aakj;startaddH*(pr2);startaddL*(pr3);bit_numH*(pr4);bit_numL*(pr5);startadd(startaddH8)startaddL; //要返回的起始地址bit_num(bit_numH8)bit_numL; //要读的字节数量,单位是位if((startaddbit_num)32) //最多允许32个位从第4个位开始读{errorsend(0x01,0x02); //响应寄存器数量超出范围}else{*(ps0)0x01; //站号*(ps1)0x01; //功能码if((bit_num%8)0)*(ps2)(bit_num)/8; //要返回的字节数else*(ps2)((bit_num)/8)1; //不能整除8的时候要多返回一个字节for(i0;i*(ps2);i){*(ps3i)0; //先清零复位for(j0;j8;j) //每8个位状态组成一个字节返回{*(ps3i)(u8)((*(pbstartaddi*8j)0x01)j)*(ps3i); //低位在前高位在后}}aakjCalcCrc(sendBuf,(*(ps2)3)); //CRC校验*(ps3*(ps2))(u8)(aakj0xff); //CRC低字节*(ps4*(ps2))(u8)((aakj8)0xff); //CRC高字节sendlength*(ps2)5;}}/*分析02功能码报文产生响应报文*//***************************************/void Recirve_02(){u16 startadd;u16 bit_num;u8 startaddH,startaddL;u8 bit_numH,bit_numL;u16 i,j;u16 aakj;startaddH*(pr2);startaddL*(pr3);bit_numH*(pr4);bit_numL*(pr5);startadd(startaddH8)startaddL; //要返回的起始地址bit_num(bit_numH8)bit_numL; //要读的字节数量,单位是位if((startaddbit_num)32||startadd3) //本案例中只有4个输入位可供读{errorsend(0x01,0x02); //响应寄存器数量超出范围}else{for(i2;i6;i)inputBit[i-2](~(P3i))0x01; //先读出输入口的状态for(i4;i32;i)inputBit[i]0; //没有位状态清零*(ps0)0x01; //站号*(ps1)0x02; //功能码if((bit_num%8)0)*(ps2)(bit_num)/8; //要返回的字节数else*(ps2)((bit_num)/8)1; //不能整除8的时候要多返回一个字节for(i0;i*(ps2);i){*(ps3i)0; //先清零复位for(j0;j8;j) //每8个位状态组成一个字节返回{*(ps3i)(u8)((inputBit[startaddi*8j]0x01)j)*(ps3i); //低位在前高位在后}}aakjCalcCrc(sendBuf,(*(ps2)3)); //CRC校验*(ps3*(ps2))(u8)(aakj0xff); //CRC低字节*(ps4*(ps2))(u8)((aakj8)0xff); //CRC高字节sendlength*(ps2)5;}}/***************************************//*分析03功能码报文产生响应报文*/void Recirve_03(){u16 startadd;u16 length;u8 startaddH,startaddL;u8 lengthH,lengthL;u16 i;u16 aakj;startaddH*(pr2);startaddL*(pr3);lengthH*(pr4);lengthL*(pr5);startadd(startaddH8)startaddL; //要返回的起始地址length(lengthH8)lengthL; //要读的字节数量if((startaddlength)32) //最多只能返回32个寄存器,64个字节注意返回的长度不能超过发送数组长度否则会溢出导致错误{errorsend(0x03,0x02); //响应寄存器数量超出范围}else{*(ps0)0x01; //站号*(ps1)0x03; //功能码*(ps2)length*2; //要返回的字节数是请求报文的第五个字节*2for(i0;ilength;i){*(ps3i*2)((*(pwstartaddi))8)0xff; //返回寄存器值的高字节*(ps4i*2)(*(pwstartaddi))0xff; //返回寄存器值得低字节}aakjCalcCrc(sendBuf,(length*2)3); //CRC校验*(ps3length*2)(u8)(aakj0xff); //CRC低字节*(ps4length*2)(u8)((aakj8)0xff); //CRC高字节sendlength(length*2)5;}}/*分析04功能码报文产生响应报文*//*这边返回的是输入口的寄存器值*//***************************************//***************************************/void Recirve_04(){u16 startadd;u16 bit_num;u8 startaddH,startaddL;u8 bit_numH,bit_numL;u16 i,j;u16 aakj;startaddH*(pr2);startaddL*(pr3);bit_numH*(pr4);bit_numL*(pr5);startadd(startaddH8)startaddL; //要返回的起始地址bit_num(bit_numH8)bit_numL; //要读的字节数量,单位是位if((startaddbit_num)32||startadd1)//本案例中只有4个输入位1个寄存器可供读{errorsend(0x01,0x02); //响应寄存器数量超出范围}else{for(i2;i6;i)inputBit[i-2](~(P3i))0x01; //先读出输入口的状态for(i4;i32;i)inputBit[i]0; //没有位状态清零*(ps0)0x01; //站号*(ps1)0x04; //功能码*(ps2)bit_num*2;for(i0;i*(ps2);i){*(ps3i)0; //先清零复位for(j0;j8;j) //每8个位状态组成一个字节返回{*(ps3i)(u8)((inputBit[startaddi*8j]0x01)j)*(ps3i); //低位在前高位在后}}aakjCalcCrc(sendBuf,(*(ps2)3)); //CRC校验*(ps3*(ps2))(u8)(aakj0xff); //CRC低字节*(ps4*(ps2))(u8)((aakj8)0xff); //CRC高字节sendlength*(ps2)5;}}/*分析05功能码报文产生响应报文*//***************************************/void Recirve_05(){u16 startadd;u8 startaddH,startaddL;u8 bit_valueH,bit_valueL;u16 aakj;startaddH*(pr2);startaddL*(pr3);bit_valueH*(pr4);bit_valueL*(pr5);startadd(startaddH8)startaddL; //要写入的地址if(startadd32){errorsend(0x01,0x02); //响应寄存器数量超出范围}else{if(bit_valueH0xffbit_valueL0x00) //置位线圈*(pbstartadd)1;if(bit_valueH0x00bit_valueL0x00) //复位线圈*(pbstartadd)0;*(ps0)0x01; //站号*(ps1)0x05; //功能码*(ps2)startaddH; //地址高字节*(ps3)startaddL; //地址低字节*(ps4)bit_valueH; //地址高字节*(ps5)bit_valueL; //地址低字节aakjCalcCrc(sendBuf,6); //CRC校验*(ps6)(u8)(aakj0xff); //CRC低字节*(ps7)(u8)((aakj8)0xff); //CRC高字节sendlength8;}}/***************************************//*分析06功能码报文产生响应报文*/void Recirve_06(){u16 startadd;u16 wdata_06;u8 startaddH,startaddL;u8 wdataH_06,wdataL_06;u16 aakj;startaddH*(pr2);startaddL*(pr3);wdataH_06*(pr4);wdataL_06*(pr5);startadd(startaddH8)startaddL; //要写入的起始地址wdata_06(wdataH_068)wdataL_06; //要写入的数值if(startadd32) //寄存器地址超出范围errorsend(0x06,0x02); //响应寄存器数量超出范围else if(wdata_06-32768||wdata_0632767)errorsend(0x06,0x03); //响应数据错误else{*(pwstartadd)wdata_06; //将数值写入寄存器*(ps0)0x01; //站号*(ps1)0x06; //功能码*(ps2)startaddH; //返回地址高字节*(ps3)startaddL; //返回地址低字节*(ps4)(u8)(((*(pwstartadd))8)0xff); //返回寄存器值高字节*(ps5)(u8)(*(pwstartadd)0xff); //返回寄存器值低字节aakjCalcCrc(sendBuf,6); //CRC校验*(ps6)(u8)(aakj0xff); //CRC低字节*(ps7)(u8)((aakj8)0xff); //CRC高字节sendlength8; //返回8个字节长度}}/***************************************//*分析10功能码报文产生响应报文*/void Recirve_10(){u16 startadd;u16 register_num;u8 startaddH,startaddL;u8 register_numH,register_numL;u8 length;u16 i;u16 aakj;startaddH*(pr2);startaddL*(pr3);register_numH*(pr4);register_numL*(pr5);startadd(startaddH8)startaddL; //要返回的起始地址register_num(register_numH8)register_numL; //寄存器数量length*(pr6); //要写的字节数量if((startadd(length/2))32) //最多允许写32个寄存器{errorsend(0x10,0x02); //响应寄存器数量超出范围}else{for(i0;i(length/2);i) //将值写入寄存器{*(pwstartaddi)(*(pr7i*2)8)*(pr8i*2)0xff;}*(ps0)0x01; //站号*(ps1)0x10; //功能码*(ps2)startaddH; //返回地址高位*(ps3)startaddL; //返回地址低位*(ps4)register_numH;*(ps5)register_numL;aakjCalcCrc(sendBuf,6); //CRC校验*(ps6)(u8)(aakj0xff); //CRC低字节*(ps7)(u8)((aakj8)0xff); //CRC高字节sendlength8;}}/***************************************//*错误返回*/void errorsend(u8 func,u8 type){u16 _crc;u8 crcH,crcL;*(ps0)0x01; //返回站号switch(type){case 0x08:*(ps1)0x80func; //返回错误功能码*(ps2)0x08; //返回错误代码08CRC校验错误break;case 0x01:*(ps1)0x80func; //返回错误功能码*(ps2)0x01; //返回错误代码01功能码错误break;case 0x02:*(ps1)0x80func; //返回错误功能码*(ps2)0x02; //返回错误代码02地址错误break;case 0x03:*(ps1)0x80func; //返回错误功能码*(ps2)0x03; //返回错误代码03数据错误break;case 0x04:*(ps1)0x80func; //返回错误功能码*(ps2)0x04; //返回错误代码04不支持的功能码break;}_crcCalcCrc(sendBuf,3);crcH(u8)((_crc8)0xff);crcL(u8)(_crc0xff);*(ps3)crcL; //校验低字节*(ps4)crcH; //校验高字节sendlength5;}/*************************************************crc16校验码计算函数,计算算法1、设置crc校验寄存器的初值为0xffff;2、将报文的第一个字节与crc寄存器的低字节异或结果存入crc寄存器3、判断crc的第一位是否为1如果是1,crc右移1位后和0xa001异或如果为0则再移1位4、重复步骤3直到完成8个位5、重复步骤2、3、4直到完成所有字节6、返回计算结果***********************************************/u16 CalcCrc(unsigned char *chData,unsigned short uNo){u16 crc0xffff;u16 i,j;for(i0;iuNo;i){crc^chData[i];for(j0;j8;j){if(crc1){crc1;crc^0xA001;}elsecrc1;}}return (crc);}/************************************************/void ISP_enable(void){// ISP_CONTRISP_CONTR0X18; //初始化SP/IAP控制寄存器ISP_CONTRwaittime; //写入硬件延时注意这边是“|”运算ISP_CONTRISP_CONTR|0x80; //打开ISPEN,运行写入注意是“|”运算}/*关闭ISP/IAP功能*/void ISP_disable(void){ISP_CONTRISP_CONTR0x00; //关闭ISPEN,运行写入注意是“”运算ISP_TRIG0x00; //清空ISP命令控制触发器ISP_CMD0x00;// EA1; //打开中断}/*建立公用触发函数*/void go_ISP(void){ISP_TRIG0x46; //触发ISP/IAP命令字节1固定ISP_TRIG0xB9; //触发ISP/IAP命令字节2固定_nop_(); //执行一个空函数}/*扇区擦除函数*/void sectorerase(u16 addr){ISP_ADDRH(u8)(addr8); /*取地址的高位*/ISP_ADDRL(u8)(addr0xff); //地址的低位// EA0; //关闭总中断ISP_enable(); //打开ISP/IAP功能ISP_CMDERASECommand; //从新给ISP命令寄存器赋值这边赋的是0x03表示擦除go_ISP(); //执行触发命令将擦除命令写入ISP_disable(); //关闭ISP功能}/*写数据函数单个字节写入*/void write_ISP(u16 st_addr,u8 w_data){ISP_DATAw_data; //将要写入的数据存入ISP FLASH数据寄存区ISP_ADDRH(u8)(st_addr8); /*取地址的高位*/ISP_ADDRL(u8)(st_addr0xff); //地址的低位// EA0; //关闭总中断ISP_enable(); //打开ISP/IAP功能ISP_CMDPRGCommand; //从新给ISP命令寄存器赋值这边赋的是0x02表示写入go_ISP(); //触发写入ISP_disable(); //关闭ISP功能}/*读数据函数读单个字节*/u8 read_ISP(u16 rd_addr){ISP_ADDRH(u8)(rd_addr8); /*取地址的高位*/ISP_ADDRL(u8)(rd_addr0x00ff); //地址的低位ISP_CMDISP_CMD0xF8; // 清空ISP命令寄存器的第三位// EA0; //关闭总中断ISP_enable(); //打开ISP/IAP功能ISP_CMDRDCommand; //从新给ISP命令寄存器赋值这边赋的是0x01表示读出go_ISP(); //触发写入ISP_disable(); //关闭ISP功能return (ISP_DATA); //返回读出的数据}/************************************************//*初始化函数*/void init(){int i;TMOD0X21; //设定定时器1的工作方式为2及8位初值自动重装的8位定时器,用于产生波特率T0工作方式1用于判断通信帧结束TH10XFD; //设置定时器的初值为0xfd是按照9600的波特率计算出来的数值TL10XFD; //定时器高低位数值一样方式2下定时器自动将高位的数值装入低位所以设定的时间要一致TH0(65536-11111)/256; //T0设定为1ms定时器, 用于判断通信帧结束同时可用于其他的定时作用TL0(65536-11111)%256; //T0低位TR11; //启动T1定时器REN1; //运行串口接收数据REN1允许串口接收REN0禁止串口接收数据SM00; //设置串口工作方式为1即10位异步通信1起始位8数据位1停止位SM11; //同上REN、SM0、SM1同属于SCON寄存器但是SCON寄存器允许位寻址EA1; //开总中断ES1; //开串口中断ET01; //开定时器中断TR01; //启动T0定时器P10xff; //复位所有输出relay1;bee1;for(i0;i32;i) //复位所有{*(pbi)0;*(pwi)0;inputBit[i]0;}for(i0;i32;i) //读出存在内部E2PROM内的值{R_ISP_Lread_ISP(0x2000i*2); //读出数据低位R_ISP_Hread_ISP(0x2000i*21); //读出数据高位*(pwi)((R_ISP_H8)0xff00)(R_ISP_L0Xff); //组合数据}}/*********************************************************************************手动调试函数通过在触摸屏上操作相对应的位来控制单片机的手动输出相对应的位说明0x0: 手动自动切换位0自动1手动0x1-0x10输出口1-10***********************************************************************************/void adj(){u8 i,j0; //j要有初值0for(i1;i9;i){j(u8)((*(pbi)0x01)(i-1))j;}P1~j;relay(~(*(pb9)))0x01;bee(~(*(pb10)))0x01;}/**********************************************************************************将数值保存在内部E2PROM中0x11:在触摸屏上的保存按钮************************************************************************************/void saveData(){u8 i;sectorerase(0x2000); //擦除扇区这边写入的是第一扇区在擦除是整个扇区被擦除for(i0;i32;i){write_ISP((0X2000i*2),(*(pwi)0xff)); //保存低8位数据write_ISP((0x2000i*21),((*(pwi)8)0xff)); //保存高8位数据}*(pb11)0; //保存按妞复位}/*串口中断服务*/void ser() interrupt 4 //串口中断的序号是4{RI0; //产生串口中断时RI被硬件置1在串口中断服务程序中需要用软件清0comm_END4; //9600的波特率下等待3.5个字节需要约4ms的时间Reciver_bit1; //产生一次中断置一次位Reciver_BufSBUF; //将接收到的数据赋给一个变量receBuf[rece_index]Reciver_Buf; //将接收到的数据存入接收数组}/*T0中断服务程序*/void comm_stop() interrupt 1{u8 i;TH0(65536-11111)/256; //重装T0TL0(65536-11111)%256; //T0低位if(Reciver_bitcomm_END!0)comm_END--;if(comm_END0){Reciver_bit0; //帧接收对标志位清零if((*(pr0)0x01||*(pr0)0x00)rece_index8) //判断是否为本站地址或者是广播地址有效报文的字节数量最少是8个字节{AnalyzeRecieve(); //分析并生成响应报文if(*(pr0)0x01) //广播模式下不用返回send_comm(); //发送响应报文}comm_END4; //重新赋延时初值rece_index0; //数组长度清零for(i0;i75;i) //清空接收数组receBuf[i]0;Reciver_Buf0;}}/*主函数*//****************************************/void main(){init(); //执行初始化程序while(1) //执行运行程序{if(*(pb0)) adj(); //手动调试if(*(pb11)) saveData(); //保存数值}}