嵌入式開發中,常常會自定義一些協議格式,比如用于板與板之間的通信、客戶端與服務端之間的通信等。
自定義的協議格式可能有很多種,本篇文章我們來介紹一種很常用、實用、且靈活性很高的協議格式——ITLV格式。
什么是ITLV格式?
大家可能看到網絡上的很多文章用的是TLV(Tag、Length、Value)格式數據。實際中,可以根據實際需要進行修改。我們這里稍微改一下,實際上也是大同小異的。
我們這里的ITLV各字段的含義:
I:ID或Index,用于區分是什么數據。
T:Type,代表數據類型,如int、float等。
L:Length,表示數據的長度(Value的長度)。
V:Value,表示實際的數據。
其中,I、T、L是固定長度的,在制定具體的數據協議之前,需要評估好當前項目的數據會有多少、數據的最大長度是多少,考慮好后續數據擴展也可以保證協議通用。一般I設置為1~2字節,T設置為1字節,L設置為1~4字節。
下面我們制定一個格式:
實際中,如果在物聯網系統中數據傳輸,我們用戶自定義的協議字段可能就只包含如上四個字段就可以了。比如我們公司的云平臺上的用戶數據格式用的就是類似ITLV這樣的格式。用戶在制定協議時的協議字段包含如上字段就可以了。
沒有包頭做一些數據區分,也沒有校驗字段,只包含如上字段就能保證數據可靠傳輸嗎?
因為端云通信采用MQTT,基于TCP,TCP的特點就是可靠的,網絡協議中會帶有校驗。并且,實際在傳輸用戶數據時,還會再用戶數據之前增加一些字段區分這就是用戶數據。所以,其實基于它的設備SDK來進行開發,操作的數據就是如上的數據。
但是,如果應用于板與板之間的通信,只包含如上字段自然是有風險的。我們至少還需要還要包頭、校驗字段。
實際中根據需要還可以增加其它字段,比如如果需要分包發送,還需要增加包號;如果多塊板之間進行通信,還需要增加發送數據目標地址等。
這里我們增加包頭與校驗字段:
其中:
(1)Head固定為0x55、0xAA。
(2)Length為1字節,即Value最大為256B。
ITLV格式數據處理
下面以例子來演示ITLV格式數據的處理。
下面我們以上面我們制定的協議編寫A板的組包、解析代碼。
1、設計相關數據結構
首先,我們創建一個協議格式結構體:
#pragmapack(1) //協議格式 typedefstruct_protocol_format { uint16_thead; uint8_tid; uint8_ttype; uint8_tlength; uint8_tvalue[]; }protocol_format_t;
type字段的取值:
//TLV數據類型type typedefenum_tlv_type { TLV_TYPE_UINT8, TLV_TYPE_INT8, TLV_TYPE_UINT16, TLV_TYPE_INT16, TLV_TYPE_UINT32, TLV_TYPE_INT32, TLV_TYPE_STRING, TLV_TYPE_FLOAT, TLV_TYPE_BYTE_ARR,//字節數組 }tlv_type_e;
下面設計我們的收、發數據結構,大致思路如下:
我們創建一個總的結構體,用于管理A板往B板發送及A板接受來自B板的數據:
//總的協議數據 typedefstruct_protocol_data { protocol_id_eid; protocol_value_tvalue; }protocol_data_t;
其中,成員id是一個枚舉:
左右滑動查看全部代碼>>>
//數據ID typedefenum_protocol_id { //A板發往B板 PROTOCOL_ID_A_TO_B_BASE=0x00, PROTOCOL_ID_A_TO_B_CTRL_CMD, PROTOCOL_ID_A_TO_B_DATE_TIME, PROTOCOL_ID_A_TO_B_END=0x7F, //B板發往A板 PROTOCOL_ID_B_TO_A_BASE=0x80, PROTOCOL_ID_B_TO_A_WORK_STATUS, PROTOCOL_ID_B_TO_A_END=0xFF, }protocol_id_e;
包含著A->B、B->A的ID,因為ID是用1個字節標識,收、發的ID各預留一半,新增的ID在各自的BASE ID及END ID之間添加。
成員value是一個聯合體,用于管理A->B、B->A的value數據:
左右滑動查看全部代碼>>>
//所有協議數據value值 typedefunion_protocol_value { protocol_value_a_to_b_ta_to_b_value; protocol_value_b_to_a_tb_to_a_value; }protocol_value_t;
a_to_b_value及b_to_a_value也是聯合體,用于管理更細分的數據:
左右滑動查看全部代碼>>>
//A板發往B板的數據value值 typedefunion_protocol_value_a_to_b { protocol_data_ctrl_cmd_tctrl_cmd; protocol_data_time_tdate_time; }protocol_value_a_to_b_t; //B板發往A板的數據value值 typedefunion_protocol_value_b_to_a { protocol_data_work_status_twork_status; }protocol_value_b_to_a_t;
更細分的數據:
左右滑動查看全部代碼>>>
//控制命令 typedefenum_ctrl_cmd { CTRL_CMD_LED_ON, CTRL_CMD_LED_OFF }ctrl_cmd_e; typedefstruct_protocol_data_ctrl_cmd { ctrl_cmd_ecmd; }protocol_data_ctrl_cmd_t; //時間數據 typedefstruct_protocol_data_time { intyear; intmon; intmday; inthour; intmin; intsec; }protocol_data_time_t; //工作狀態 typedefenum_work_status { WORK_STATUS_NORMAL, WORK_STATUS_ERROR }work_status_e; typedefstruct_protocol_data_work_status { work_status_estatus; }protocol_data_work_status_t;
明確了我們需要進行交互的數據的類型之后,解析來我們就可以根據它們的特點來編寫組包、解析函數了。
2、組包
大致思路如下:
組包函數:
左右滑動查看全部代碼>>>
intprotocol_data_packet(uint8_t*buf,uint16_tlen,protocol_data_t*protocol_data) { intret=-1; intvalue_len=0; intoffset=0; protocol_format_t*p_protocol_format=NULL; if(!buf||!protocol_data||lenid) { casePROTOCOL_ID_A_TO_B_CTRL_CMD: { printf("PROTOCOL_ID_A_TO_B_CTRL_CMD "); value_len=sizeof(protocol_data->value.a_to_b_value.ctrl_cmd); printf("protocol_format.length=%d ",value_len); break; } casePROTOCOL_ID_A_TO_B_DATE_TIME: { printf("PROTOCOL_ID_A_TO_B_DATE_TIME "); value_len=sizeof(protocol_data->value.a_to_b_value.date_time); printf("value_len=%d ",value_len); break; } default: break; } //為協議格式數據申請內存 p_protocol_format=(protocol_format_t*)malloc(sizeof(protocol_format_t)+value_len); if(NULL==p_protocol_format) { printf("mallocerror "); returnret; } //填充協議數據各字段 p_protocol_format->head=PROTOCOL_HEAD; p_protocol_format->id=protocol_data->id; p_protocol_format->type=TLV_TYPE_BYTE_ARR; p_protocol_format->length=value_len; if(p_protocol_format->length<=?PROTOCOL_VALUE_MAX_LEN) ????{ ????????memcpy(p_protocol_format->value,&protocol_data->value.a_to_b_value,p_protocol_format->length); } else { printf("protocol_format.length>PROTOCOL_VALUE_MAX_LEN "); } //計算校驗值 uint32_tcrc_data_len=sizeof(protocol_format_t)+value_len; uint16_tcrc16=crc16_x25_check((uint8_t*)p_protocol_format,crc_data_len); printf("crc16=%#x ",crc16); //struct->buf memcpy(buf,p_protocol_format,crc_data_len); offset+=crc_data_len; memcpy(buf+offset,&crc16,sizeof(uint16_t)); offset+=sizeof(uint16_t); //釋放內存 free(p_protocol_format); p_protocol_format=NULL; returnoffset; }
3、解包
大致思路如下:
解包函數:
左右滑動查看全部代碼>>>
//解包函數 voidprotocol_data_parse(protocol_data_t*protocol_data,uint8_t*buf,uint16_tlen) { protocol_format_t*p_protocol_format=NULL; if(!buf||!protocol_data||lenstruct memcpy(p_protocol_format,buf,sizeof(protocol_format_t)+value_len); printf("protocol_data->id=%#x ",p_protocol_format->id); //通過數據ID來解析各對應的數據 switch(p_protocol_format->id) { casePROTOCOL_ID_B_TO_A_WORK_STATUS: { printf("PROTOCOL_ID_B_TO_A_WORK_STATUS "); uint8_twork_status_len=sizeof(protocol_data->value.b_to_a_value.work_status); if(p_protocol_format->length==work_status_len) { memcpy(&protocol_data->value.b_to_a_value.work_status,p_protocol_format->value,p_protocol_format->length); } else { printf("p_protocol_format->lengtherror "); } break; } default: break; } //釋放內存 free(p_protocol_format); p_protocol_format=NULL; }
4、CRC16校驗
CRC16分很多種:CRC16-X25、CRC16-MODBUS、CRC16-XMODEM等。
這里我們使用CRC16-X25:
staticconstunsignedshortcrc16_table[256]= { 0x0000,0x1189,0x2312,0x329b,0x4624,0x57ad,0x6536,0x74bf, 0x8c48,0x9dc1,0xaf5a,0xbed3,0xca6c,0xdbe5,0xe97e,0xf8f7, 0x1081,0x0108,0x3393,0x221a,0x56a5,0x472c,0x75b7,0x643e, 0x9cc9,0x8d40,0xbfdb,0xae52,0xdaed,0xcb64,0xf9ff,0xe876, 0x2102,0x308b,0x0210,0x1399,0x6726,0x76af,0x4434,0x55bd, 0xad4a,0xbcc3,0x8e58,0x9fd1,0xeb6e,0xfae7,0xc87c,0xd9f5, 0x3183,0x200a,0x1291,0x0318,0x77a7,0x662e,0x54b5,0x453c, 0xbdcb,0xac42,0x9ed9,0x8f50,0xfbef,0xea66,0xd8fd,0xc974, 0x4204,0x538d,0x6116,0x709f,0x0420,0x15a9,0x2732,0x36bb, 0xce4c,0xdfc5,0xed5e,0xfcd7,0x8868,0x99e1,0xab7a,0xbaf3, 0x5285,0x430c,0x7197,0x601e,0x14a1,0x0528,0x37b3,0x263a, 0xdecd,0xcf44,0xfddf,0xec56,0x98e9,0x8960,0xbbfb,0xaa72, 0x6306,0x728f,0x4014,0x519d,0x2522,0x34ab,0x0630,0x17b9, 0xef4e,0xfec7,0xcc5c,0xddd5,0xa96a,0xb8e3,0x8a78,0x9bf1, 0x7387,0x620e,0x5095,0x411c,0x35a3,0x242a,0x16b1,0x0738, 0xffcf,0xee46,0xdcdd,0xcd54,0xb9eb,0xa862,0x9af9,0x8b70, 0x8408,0x9581,0xa71a,0xb693,0xc22c,0xd3a5,0xe13e,0xf0b7, 0x0840,0x19c9,0x2b52,0x3adb,0x4e64,0x5fed,0x6d76,0x7cff, 0x9489,0x8500,0xb79b,0xa612,0xd2ad,0xc324,0xf1bf,0xe036, 0x18c1,0x0948,0x3bd3,0x2a5a,0x5ee5,0x4f6c,0x7df7,0x6c7e, 0xa50a,0xb483,0x8618,0x9791,0xe32e,0xf2a7,0xc03c,0xd1b5, 0x2942,0x38cb,0x0a50,0x1bd9,0x6f66,0x7eef,0x4c74,0x5dfd, 0xb58b,0xa402,0x9699,0x8710,0xf3af,0xe226,0xd0bd,0xc134, 0x39c3,0x284a,0x1ad1,0x0b58,0x7fe7,0x6e6e,0x5cf5,0x4d7c, 0xc60c,0xd785,0xe51e,0xf497,0x8028,0x91a1,0xa33a,0xb2b3, 0x4a44,0x5bcd,0x6956,0x78df,0x0c60,0x1de9,0x2f72,0x3efb, 0xd68d,0xc704,0xf59f,0xe416,0x90a9,0x8120,0xb3bb,0xa232, 0x5ac5,0x4b4c,0x79d7,0x685e,0x1ce1,0x0d68,0x3ff3,0x2e7a, 0xe70e,0xf687,0xc41c,0xd595,0xa12a,0xb0a3,0x8238,0x93b1, 0x6b46,0x7acf,0x4854,0x59dd,0x2d62,0x3ceb,0x0e70,0x1ff9, 0xf78f,0xe606,0xd49d,0xc514,0xb1ab,0xa022,0x92b9,0x8330, 0x7bc7,0x6a4e,0x58d5,0x495c,0x3de3,0x2c6a,0x1ef1,0x0f78 }; uint16_tcrc16_x25_check(uint8_t*data,uint32_tlength) { unsignedshortcrc_reg=0xFFFF; while(length--) { crc_reg=(crc_reg>>8)^crc16_table[(crc_reg^*data++)&0xff]; } return(uint16_t)(~crc_reg)&0xFFFF; }
5、測試代碼
下面我們編寫組包、解包測試代碼:
組包控制命令數據,并把組包之后的發送緩沖區中的數據打印出來。
組包時間數據,并把組包之后的發送緩沖區中的數據打印出來。
從一個模擬的工作狀態接受緩沖區數據中解析工作狀態數據并打印出來。
測試代碼如:
左右滑動查看全部代碼>>>
//微信公眾號:嵌入式大雜燴 #include#include #include"protocol_tlv.h" intmain(intarc,char*argv[]) { staticuint8_tsend_buf[PROTOCOL_MAX_LEN]={0}; protocol_data_tprotocol_data_send={0}; intsend_len=0; printf(" ==============================testpacket=========================================== "); //模擬組包發送控制命令 bzero(send_buf,sizeof(send_buf)); bzero(&protocol_data_send,sizeof(protocol_data_t)); protocol_data_send.id=PROTOCOL_ID_A_TO_B_CTRL_CMD; protocol_data_send.value.a_to_b_value.ctrl_cmd.cmd=CTRL_CMD_LED_OFF; send_len=protocol_data_packet(send_buf,PROTOCOL_MAX_LEN,&protocol_data_send); printf("sendctrldata="); print_hex_data_frame(send_buf,send_len); //模擬組包發送時間數據 bzero(send_buf,sizeof(send_buf)); bzero(&protocol_data_send,sizeof(protocol_data_t)); protocol_data_send.id=PROTOCOL_ID_A_TO_B_DATE_TIME; protocol_data_send.value.a_to_b_value.date_time.year=2022; protocol_data_send.value.a_to_b_value.date_time.mon=8; protocol_data_send.value.a_to_b_value.date_time.mday=20; protocol_data_send.value.a_to_b_value.date_time.hour=8; protocol_data_send.value.a_to_b_value.date_time.min=8; protocol_data_send.value.a_to_b_value.date_time.sec=8; send_len=protocol_data_packet(send_buf,PROTOCOL_MAX_LEN,&protocol_data_send); printf("senddate_timedata="); print_hex_data_frame(send_buf,send_len); printf(" ==============================testparse=========================================== "); //模擬解析工作狀態數據 uint8_twork_status_buf[11]={0x55,0xAA,0x81,0x08,0x04,0x01,0x00,0x00,0x00,0xf2,0x88}; protocol_data_tprotocol_data_recv={0}; uint16_tcalc_crc16=crc16_x25_check(work_status_buf,sizeof(work_status_buf)-2); uint16_trecv_crc16=(uint16_t)(work_status_buf[10]<8)?|?work_status_buf[9]; ????if?(calc_crc16?==?recv_crc16) ????{ ????????protocol_data_parse(&protocol_data_recv,?work_status_buf,?sizeof(work_status_buf)); ????????printf("work_status?=?%d ",?protocol_data_recv.value.b_to_a_value.work_status.status); ????} ?return?0; }
編譯、運行:
對照著我們制定的協議,數據完全正確!
ITLV格式的其它用法
ITLV格式具有很強的靈活性,我們這里使用的數據類型Type為字節數組,其實使用字符串類型也很常用,比如為了協議具備更強的可讀性、方便調試,可以在Value字段里再封裝一層JSON格式數據。其實我覺得Type的選項只保留字節數組及字符串就夠用了,可以滿足所有情況。
當然,可能有些數據長度總是定長的,也可以用其它定長的類型。比如數據都是一些定長的類型,那么L字段也可以省略掉。實際中,比較通用的做法就是:全用字節數組或者全用字符串。別混著用,代碼可能會很混亂。
-
通信
+關注
關注
18文章
5977瀏覽量
135871 -
服務端
+關注
關注
0文章
66瀏覽量
6989 -
數據協議
+關注
關注
0文章
7瀏覽量
5881
原文標題:一種靈活性高效的輕量級通信協議
文章出處:【微信號:strongerHuang,微信公眾號:strongerHuang】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論