modbus协议是以主从的方式通信的,也就是上位机发送指令,下位机应答机制,发起通信的一直是上位机,下位机只要应答就好了。
modbus协议被设计出来是针对PLC应用的,这里我们可以简单的模拟PLC环境,可以在单片机里面设计一块共享区,该区域是上位机和下位机共享的,均可以读取或写入该区域的值,所有的modbus协议都是针对该快区域的操作,下位机也是根据这块区域的值做相应的操作。
这块共享区我们用结构体来表示,这里我们只用了两个变量:
/*modbus 16位值的定义,起始地址0000H,每一个值为16位 int型,占两个字节 */ struct MODBUS_ADD{ int LED_value;//地址:0000H LED灯的值,该值得低8位代表分表代表LED1--LED8 int LED_ctrl;//地址:0001H 控制指令 };
struct MODBUS_ADD modbus_Addt;//声明一个modbus结构体变量 struct MODBUS_ADD *modbusAdd;//结构体指针,指向这个变量
void main() { SystemInit(); init_MODBUS(); modbus_Addt.LED_ctrl = COMM_PC; while(1) { //将需要交互的数据读取到公共区 /*start*/ if(modbus_Addt.LED_ctrl != COMM_PC) { modbus_Addt.LED_value = LED_PORT; } /*end*/ //同步公共区数据到实际运行效果 /*start*/ switch(modbus_Addt.LED_ctrl) { case COMM_PC: LED_PORT = ~(uchar)(modbus_Addt.LED_value & 0x00ff); break; case COMM_FLOW: LedFlow(); break; default: LED_PORT = ~(uchar)(modbus_Addt.LED_value & 0x00ff); break; } /*end*/ } }
51单片机与上位机通信采用串口的方式,串口中断负责接收和发送数据,这里我们还用到了一个定时器,负责监控当前modbus的状态,判断这一帧数据是否完成,如果判断为一帧数据接收完成,就解析该帧数据,并执行相应的指令。
注意一下rec_time_out这个变量,这个变量在定时器中断里面是不断自加的,但在串口中断里面就清零了,这样做的意义是判断一帧数据是否接收完成,如果rec_time_out这个变量值大于某个值,说明在一段时间是没有数据接收的,可以认为数据接收接收,当然上位机那边必须满足一帧数据是连续发送的
串口中断程序如下,这里用到了串口中断发送数据帧,具体解析可以参考我的另一篇博客 http://blog.csdn.net/liucheng5037/article/details/48831993:
//串口中断 void SerISR() interrupt 4 using 2 { if(RI == 1) { unsigned char data_value; RI=0; if(send_buf.busy_falg == 1) return;//发送未完成时禁止接收 data_value = SBUF; rec_time_out = 0;//一旦接收到数据,清空超时计数 switch(rec_stat) { case PACK_START: rec_num = 0; if(data_value == PACK_START)//默认刚开始检测第一个字节,检测是否为本站号 { modbus_recv_buf[rec_num++] = data_value; rec_stat = PACK_REC_ING; } else { rec_stat = PACK_ADDR_ERR; } break; case PACK_REC_ING: // 正常接收 modbus_recv_buf[rec_num++] = data_value; break; case PACK_ADDR_ERR: // 地址不符合 等待超时 帧结束 break; default : break; } } if(TI == 1) //进入发送完成中断,检测是否有需要发送的数据并进行发送 { TI = 0; send_buf.index++; if(send_buf.index >= send_buf.length) { send_buf.busy_falg = 0;//发送结束 return; } SBUF = send_buf.buf[send_buf.index];//继续发送下一个 } }
/* 定时器中断 1ms*/ void Time0ISR() interrupt 1 using 1 { TL0 = T1MS; //reload timer0 low byte TH0 = T1MS >> 8; //reload timer0 high byte if(PACK_REC_OK == time_out_check_MODBUS()) { //成功接收一帧数据后,处理modbus信息,同步公共区数据 function_MODBUS(modbus_recv_buf); } }
/*超时帧检测,在1ms定时器里面运行,返回当前状态*/ int time_out_check_MODBUS(void) { rec_time_out++; if(rec_time_out == 9) // 数据接收超时5ms,给程式足够长的处理时间 { rec_stat = PACK_START; rec_num = 0; } else if((rec_time_out == 4) && (rec_num > 4)) // 超时数据帧结束4ms { rec_stat = PACK_REC_OK; // modbus_rtu->rec_num = 0; } return rec_stat; }
void function_MODBUS(unsigned char *rec_buff) { switch(rec_buff[1]) // 功能码索引 { case 1: // 01功能码:读取线圈(输出)状态 读取一组逻辑线圈的当前状态(ON/OFF) //read_coil(); break; case 2: //02功能码:读取输入状态 读取一组开关输入的当前状态(ON/OFF) //read_input_bit(); break; case 3: //03功能码:读取保持型寄存器 在一个或多个保持寄存器中读取当前二进制值 read_reg(rec_buff); break; case 4: //04功能码:读取输入寄存器 在一个或多个输入寄存器中读取当前二进制值 read_reg(rec_buff); break; case 5: //05功能码 :强制(写)单线圈(输出)状态 强制(写)一个逻辑线圈通断状态(ON/OFF) //force_coil_bit(); break; case 6: //06功能码:强制(写)单寄存器 把二进制写入一个保持寄存器 force_reg(rec_buff); break; case 15: //force_coil_mul(); break; case 16: //16功能码:强制(写)多寄存器 把二进制值写入一串连续的保持寄存器 force_reg(rec_buff); break; default: //modbus_send_buff[1] = rec_buff[1] | 0X80; //modbus_send_buff[2] = ERR_FUN_CODE; // 不合法功能号 //send_num = 5; break; } rec_stat = PACK_START;//发送之后使缓存回到初始状态 rec_num = 0; }
/* function:对应modbus功能号03,04 批量读寄存器 input:rec_buf接收到的指令 send_data需要发送的指令 */ void read_reg(unsigned char * rec_buff) { unsigned char begin_add = 0; unsigned char data_num = 0; unsigned char *piont; unsigned int send_CRC; unsigned int send_num; int i; begin_add = rec_buff[3]*2;//地址1字节 data_num = rec_buff[5]*2;//需要读取的字节数 send_num = 5 + data_num; // 5个固定字节+数据个数 addr1 + fun1 + num1 ++ crc2 rec_buff[2] = data_num;//字节数 piont = (unsigned char *)modbusAdd; //将结构体转换为字符数组,便于后面的循环读取或写入 for(i=0;i{ rec_buff[3+i] = piont[begin_add +i]; } send_CRC = comp_crc16(rec_buff, send_num-2); rec_buff[send_num-2] = send_CRC >> 8; rec_buff[send_num -1] = send_CRC; send_count = send_num; PutNChar(rec_buff , send_count); } /* function:对应modbus功能号06和16,单个和批量写寄存器 input:rec_buf接收到的指令 send_data需要发送的指令 */ void force_reg(unsigned char * rec_buf) { unsigned char fun_code,begin_add,data_num;//功能码,开始地址,数据长度 unsigned int send_num;//发送数据长度 unsigned char *piont; unsigned int send_CRC; int i; // send_data[0] = rec_buf[0]; //获取站号 fun_code = rec_buf[1]; //获取功能码 // send_data[1] = fun_code; // send_data[2] = rec_buf[2];//获取起始地址 // send_data[3] = rec_buf[3]; begin_add = rec_buf[3]*2; piont = (unsigned char *)modbusAdd; //将结构体转换为字符数组,便于后面的循环读取或写入 if(fun_code == 6)//写单个寄存器,返回指令与接收的指令完全一样 { piont[begin_add] = rec_buf[4];//寄存器高位写入 piont[begin_add+1] = rec_buf[5];//寄存器低位写入 send_num = 8;// } else if(fun_code == 16)//写多个寄存器 { data_num = rec_buf[5]*2; send_num = 8; for(i=0;i { piont[begin_add+i] = rec_buf[7+i]; } } send_CRC = comp_crc16(rec_buf, send_num-2);//CRC校验 rec_buf[send_num-2] = send_CRC >> 8; rec_buf[send_num -1] = send_CRC; send_count = send_num; PutNChar(rec_buf , send_count); }
『本文转载自网络,版权归原作者所有,如有侵权请联系删除』