基于51單片機modbusRTU從機設計
modbus協議是以主從的方式通信的,也就是上位機發送指令,下位機應答機制,發起通信的一直是上位機,下位機只要應答就好了。
本文引用地址:http://www.104case.com/article/201611/318646.htmmodbus協議被設計出來是針對PLC應用的,這里我們可以簡單的模擬PLC環境,可以在單片機里面設計一塊共享區,該區域是上位機和下位機共享的,均可以讀取或寫入該區域的值,所有的modbus協議都是針對該快區域的操作,下位機也是根據這塊區域的值做相應的操作。
這塊共享區我們用結構體來表示,這里我們只用了兩個變量:
/*modbus 16位值的定義,起始地址0000H,每一個值為16位 int型,占兩個字節 */struct MODBUS_ADD{int LED_value;//地址:0000H LED燈的值,該值得低8位代表分表代表LED1--LED8int 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*/}}
接下來看modbus協議具體怎么實現的,可以看到在主函數中是沒有參與這個協議的,也就是相當于modbus協議的實現是在另外一個線程中,主函數不需要關心實現的細節,這樣做的好處的是主函數可以近針對于自己的實現任務,二不用考慮任務的參數從哪來的
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 byteTH0 = T1MS >> 8; //reload timer0 high byteif(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; }
一幀數據接收成功后,執行方法就在函數function_MODBUS中,如下,指令解析和發動都是嚴格按照modbus協議來的,這里只是用到了協議的常用的幾個指令,大家可以自由擴展,
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
基于51單片機modbus下位機設計這里就結束了,這種方法是比較靈活了,將協議的實現單獨放在一層,避免與主函數有太多交互
評論