通訊協議可以理解為約束多設備通訊的一套規則,像Modbus,TCP/IP, BLE都是在生產生活常用的協議。不過協議落實到實際應用后,就可以理解為對數據的結構化處理,我之前寫的串口點亮LED的實現就涉及了簡單的協議制定,對於嵌入式Linux來說,那一套協議當然也可以實踐,但是那套協議有個重要的缺陷,協議內部從起始端的數據接收,一直到發送端的數據接收,都是和硬件強耦合的,這就造成如果我們由多種途徑修改內部數據,協議很難被復用,另外每一次最后的執行都是直接操作硬件,當然只有串口控制時沒有問題,但當有多個渠道(如網絡,CAN,BLE等模塊)同時操作硬件時,涉及硬件的同步問題繁瑣且很難約束,因此本節就改進之前的協議,進行代碼的實現。
參考資料
1.嵌入式學習筆記(綜合提高篇 第一章) -- 利用串口點亮/關閉LED燈
2.《C++ Primer Plus》
協議制定
協議的制定在大致的數據發送和返回數據結構上可以與原有的協議大致一致。
上位機發送指令包含起始位,地址位(用於多機通訊),數據長度(指示內部后面的數據長度),實際數據,CRC校驗位這些基礎結構,不過增加了數據編號位,它是2字節的隨機數,在處理完成后可以用於上位機驗證返回的數據是否正常,不過對比可以發現,原先協議里面的指令不在上位機數據結構,這個后面會提到。
下位機返回指令也是起始位,地址位,ACK應答狀態,數據長度,數據區和CRC校驗位,同時包含編號用於上位機的校驗,
上位機發送數據結構:
下位機返回數據結構:
確認了通訊的結構后,下位機代碼就可以實現了,其中接收數據代碼如下:

1 int protocol_info::check_receive_data(int fd) 2 { 3 int nread; 4 int CrcRecv, CrcCacl; 5 struct req_frame *frame_ptr; 6 7 /*從設備中讀取數據*/ 8 nread = this->device_read(fd, &this->rx_ptr[this->rx_size], 9 (this->max_buf_size-this->rx_size)); 10 if(nread > 0) 11 { 12 this->rx_size += nread; 13 frame_ptr = (struct req_frame *)this->rx_ptr; 14 15 /*接收到頭不符合預期*/ 16 if(frame_ptr->head != PROTOCOL_REQ_HEAD) { 17 USR_DEBUG("No Valid Head\n"); 18 this->rx_size = 0; 19 return RT_FAIL; 20 } 21 22 /*已經接收到長度數據*/ 23 else if(this->rx_size > 5){ 24 int nLen; 25 26 /*設備ID檢測*/ 27 if(frame_ptr->id != PROTOCOL_DEVICE_ID) 28 { 29 this->rx_size = 0; 30 USR_DEBUG("Valid ID\n"); 31 return RT_FAIL; 32 } 33 34 /*獲取接收數據的總長度*/ 35 this->rx_data_size = LENGTH_CONVERT(frame_ptr->length); 36 37 /*crc冗余校驗*/ 38 nLen = this->rx_data_size+FRAME_HEAD_SIZE+CRC_SIZE; 39 if(this->rx_size >= nLen) 40 { 41 /*計算head后到CRC尾之前的所有數據的CRC值*/ 42 CrcRecv = (this->rx_ptr[nLen-2]<<8) + this->rx_ptr[nLen-1]; 43 CrcCacl = this->crc_calculate(&this->rx_ptr[1], nLen-CRC_SIZE-1); 44 if(CrcRecv == CrcCacl){ 45 this->packet_id = LENGTH_CONVERT(frame_ptr->packet_id); 46 return RT_OK; 47 } 48 else{ 49 this->rx_size = 0; 50 USR_DEBUG("CRC Check ERROR!. rx_data:%d, r:%d, c:%d\n", this->rx_data_size, CrcRecv, CrcCacl); 51 return RT_FAIL; 52 } 53 } 54 } 55 } 56 return RT_EMPTY; 57 }
因為是嵌入式Linux開發,因此推薦使用C++, 封裝可以讓代碼結構更加清晰,從代碼的實現可以看到實現包含:硬件的數據接收,起始位檢測,數據編號的獲取,以及后續數據的接收和數據的CRC校驗,至於發送數據,則主要是創建發送數據的接口,代碼如下:

1 /** 2 * 生成發送的數據包格式 3 * 4 * @param ack 應答數據的狀態 5 * @param size 應答有效數據的長度 6 * @param pdata 應答有效數據的首指針 7 * 8 * @return 執行執行的結果 9 */ 10 int protocol_info::create_send_buf(uint8_t ack, uint16_t size, uint8_t *pdata) 11 { 12 uint8_t out_size, index; 13 uint16_t crc_calc; 14 15 out_size = 0; 16 this->tx_ptr[out_size++] = PROTOCOL_ACK_HEAD; 17 this->tx_ptr[out_size++] = PROTOCOL_DEVICE_ID; 18 this->tx_ptr[out_size++] = (uint8_t)(this->packet_id>>8); 19 this->tx_ptr[out_size++] = (uint8_t)(this->packet_id&0xff); 20 this->tx_ptr[out_size++] = ack; 21 this->tx_ptr[out_size++] = (uint8_t)(size>>8); 22 this->tx_ptr[out_size++] = (uint8_t)(size&0xff); 23 24 if(size != 0 && pdata != NULL) 25 { 26 for(index=0; index<size; index++) 27 { 28 this->tx_ptr[out_size++] = *(pdata+index); 29 } 30 } 31 32 crc_calc = this->crc_calculate(&this->tx_ptr[1], out_size-1); 33 this->tx_ptr[out_size++] = (uint8_t)(crc_calc>>8); 34 this->tx_ptr[out_size++] = (uint8_t)(crc_calc&0xff); 35 36 return out_size; 37 }
這部分即為通訊相關的結構數據實現,通關協議的發送和接收結構的剝離,此時我們已經能夠處理實際的數據,下面也是主要改進內容。
數據的處理
在之前的協議設計中,指令是包含在上述數據結構中的,到具體執行的地方直接操作硬件,對於串口操作LED,流程如下:
在整個流程中,協議和串口,以及硬件綁定,這在多任務處理時,對於硬件的同步處理就比較困難,而且硬件的處理也是十分耗時的,特別是對於很多時候也影響通訊的效率,記得在操作系統的學習中,有特別經典的一句話,解耦的通常方法就是增加中間層,在本項目也是如此,在協議和硬件中增加緩沖數據層,這樣同步問題都在緩沖數據層處理,就避免了對硬件的資源搶占動作,修改后結構如下:
為了實現這個結構,就增加對於緩存數據的處理,其中緩存數據的處理結構如下:

1 class app_reg 2 { 3 public: 4 app_reg(void); 5 ~app_reg(); 6 int hardware_refresh(void); /*硬件的實際更新*/ 7 uint16_t get_multiple_val(uint16_t reg_index, uint16_t size, uint8_t *pstart); /*獲取寄存器的值*/ 8 void set_multiple_val(uint16_t reg_index, uint16_t size, uint8_t *pstart); /*設置寄存器的值*/ 9 int diff_modify_reg(uint16_t reg_index, uint16_t size, uint8_t *pstart, uint8_t *psrc); 10 private: 11 uint8_t reg[REG_NUM]; 12 pthread_mutex_t reg_mutex; /*數據讀取都要執行該鎖*/ 13 };
其中hardware_refresh就是實際對硬件的操作,其它協議通關get和set即可修改緩存數據,在協議中操作修改內部緩存數據就可以了,剩余硬件相關處理就由緩存數據管理,其中協議中的執行如下:

1 /** 2 * 執行具體的指令, 並提交數據到上位機 3 * 4 * @param fd 執行的設備ID號 5 * 6 * @return 執行執行的結果 7 */ 8 int protocol_info::execute_command(int fd) 9 { 10 uint8_t cmd; 11 uint16_t reg_index, size; 12 uint8_t *cache_ptr; 13 app_reg *app_reg_ptr; 14 15 cmd = this->rx_data_ptr[0]; 16 reg_index = this->rx_data_ptr[1]<<8 | this->rx_data_ptr[2]; 17 size = this->rx_data_ptr[3]<<8 | this->rx_data_ptr[4]; 18 cache_ptr = (uint8_t *)malloc(this->max_buf_size); 19 this->tx_size = 0; 20 app_reg_ptr = get_app_reg(); 21 22 switch (cmd) 23 { 24 case CMD_REG_READ: 25 app_reg_ptr->get_multiple_val(reg_index, size, cache_ptr); 26 this->tx_size = this->create_send_buf(ACK_OK, size, cache_ptr); 27 break; 28 case CMD_REG_WRITE: 29 memcpy(cache_ptr, &this->rx_data_ptr[5], size); 30 app_reg_ptr->set_multiple_val(reg_index, size, cache_ptr); 31 this->tx_size = this->create_send_buf(ACK_OK, 0, NULL); 32 break; 33 case CMD_UPLOAD_CMD: 34 break; 35 case CMD_UPLOAD_DATA: 36 break; 37 default: 38 break; 39 } 40 free(cache_ptr); 41 42 /*發送數據,並清空接收數據*/ 43 this->rx_size = 0; 44 this->device_write(fd, this->tx_ptr, this->tx_size); 45 return RT_OK; 46 }
對於硬件的處理則由數據層管理,結構如下:

1 /** 2 * 根據寄存器更新內部硬件參數 3 * 4 * @param NULL 5 * 6 * @return NULL 7 */ 8 int app_reg::hardware_refresh(void) 9 { 10 uint8_t *reg_ptr; 11 uint8_t *reg_cache_ptr; 12 uint8_t is_reg_modify; 13 uint16_t reg_set_status; 14 15 reg_ptr = (uint8_t *)malloc(REG_CONFIG_NUM); 16 reg_cache_ptr = (uint8_t *)malloc(REG_CONFIG_NUM); 17 is_reg_modify = 0; 18 19 if(reg_ptr != NULL && reg_cache_ptr != NULL) 20 { 21 /*讀取所有的寄存值並復制到緩存中*/ 22 this->get_multiple_val(0, REG_CONFIG_NUM, reg_ptr); 23 memcpy(reg_cache_ptr, reg_ptr, REG_CONFIG_NUM); 24 25 /*有設置消息*/ 26 reg_set_status = reg_ptr[1] <<8 | reg_ptr[0]; 27 if(reg_set_status&0x01) 28 { 29 /*LED設置處理*/ 30 if(reg_set_status&(1<<1)) 31 { 32 led_convert(reg_ptr[2]&0x01); 33 } 34 35 /*修改beep*/ 36 if(reg_set_status&(1<<2)) 37 { 38 beep_convert((reg_ptr[2]>>1)&0x01); 39 } 40 41 reg_ptr[0] = 0; 42 reg_ptr[1] = 0; 43 is_reg_modify = 1; 44 } 45 46 /*更新寄存器狀態*/ 47 if(is_reg_modify == 1){ 48 if(this->diff_modify_reg(0, REG_CONFIG_NUM, reg_ptr, reg_cache_ptr) == RT_OK){ 49 is_reg_modify = 0; 50 } 51 else 52 { 53 free(reg_ptr); 54 free(reg_cache_ptr); 55 USR_DEBUG("modify by other interface\n"); 56 return RT_FAIL; 57 } 58 } 59 60 free(reg_ptr); 61 free(reg_cache_ptr); 62 } 63 else{ 64 USR_DEBUG("malloc error\n"); 65 } 66 67 return RT_OK; 68 }
這里就將對硬件的處理,就准換成了對內部緩存數據的處理,緩存數據由專用的線程管理,執行對硬件的操作。
定義發送的實際指令如下
返回應答數據格式則為
至此,我們就完成了協議層的操作,這時我們就可以通過串口操作硬件,且提供了多線程兼容的支持,在實現上述協議接口后,在結合上一章節的串口驅動和串口操作,就可以實現完整的功能。
下位機串口通訊實現
下位機串口通訊實現就比較簡單,首先需要提供支持串口協議通訊的結構,如下

1 class uart_protocol_info:public protocol_info 2 { 3 public: 4 uart_protocol_info(uint8_t *p_rx, uint8_t *p_tx, uint8_t *p_rxd, uint16_t max_bs): 5 protocol_info(p_rx, p_tx, p_rxd, max_bs){ 6 7 } 8 ~uart_protocol_info(){} 9 10 int device_read(int fd, uint8_t *ptr, uint16_t size){ 11 return read(fd, ptr, size); 12 } 13 int device_write(int fd, uint8_t *ptr, uint16_t size){ 14 return write(fd, ptr, size); 15 } 16 };
配置串口接口的應用層功能,滿足二進制讀寫,波特率,數據位,停止位和校驗位的設置

1 /** 2 * 配置Uart硬件的功能 3 * 4 * @param fd 設置的串口設備ID 5 * @param nSpeed 波特率 6 * @param nBits 數據位 7 * @param nEvent 奇偶校驗位 8 * @param nStop 停止位 9 * 10 * @return NULL 11 */ 12 static int set_opt(int fd, int nSpeed, int nBits, char nEvent, int nStop) 13 { 14 struct termios newtio; 15 struct termios oldtio; 16 17 if (tcgetattr(fd,&oldtio) != 0) { 18 perror("SetupSerial 1"); 19 return -1; 20 } 21 bzero( &newtio, sizeof(newtio)); 22 newtio.c_cflag |= CLOCAL | CREAD; 23 newtio.c_cflag &= ~CSIZE; 24 25 switch( nBits ) 26 { 27 case 7: 28 newtio.c_cflag |= CS7; 29 break; 30 case 8: 31 newtio.c_cflag |= CS8; 32 break; 33 default: 34 break; 35 } 36 37 switch(nEvent) 38 { 39 case 'O': 40 newtio.c_cflag |= PARENB; 41 newtio.c_cflag |= PARODD; 42 newtio.c_iflag |= (INPCK | ISTRIP); 43 break; 44 case 'E': 45 newtio.c_iflag |= (INPCK | ISTRIP); 46 newtio.c_cflag |= PARENB; 47 newtio.c_cflag &= ~PARODD; 48 break; 49 case 'N': 50 newtio.c_cflag &= ~PARENB; 51 break; 52 } 53 54 switch( nSpeed ) 55 { 56 case 2400: 57 cfsetispeed(&newtio, B2400); 58 cfsetospeed(&newtio, B2400); 59 break; 60 case 4800: 61 cfsetispeed(&newtio, B4800); 62 cfsetospeed(&newtio, B4800); 63 break; 64 case 9600: 65 cfsetispeed(&newtio, B9600); 66 cfsetospeed(&newtio, B9600); 67 break; 68 case 115200: 69 cfsetispeed(&newtio, B115200); 70 cfsetospeed(&newtio, B115200); 71 break; 72 case 460800: 73 cfsetispeed(&newtio, B460800); 74 cfsetospeed(&newtio, B460800); 75 break; 76 case 921600: 77 printf("B921600\n"); 78 cfsetispeed(&newtio, B921600); 79 cfsetospeed(&newtio, B921600); 80 break; 81 default: 82 cfsetispeed(&newtio, B9600); 83 cfsetospeed(&newtio, B9600); 84 break; 85 } 86 if( nStop == 1 ) 87 newtio.c_cflag &= ~CSTOPB; 88 else if ( nStop == 2 ) 89 newtio.c_cflag |= CSTOPB; 90 newtio.c_cc[VTIME] = 0; 91 newtio.c_cc[VMIN] = 0; 92 tcflush(fd,TCIFLUSH); 93 if((tcsetattr(fd,TCSANOW,&newtio))!=0) 94 { 95 perror("com set error"); 96 return -1; 97 } 98 // printf("set done!\n\r"); 99 return 0; 100 }
然后,就可以實現串口通訊的交互代碼

1 /** 2 * uart主任務執行流程 3 * 4 * @param arg 線程傳遞的參數 5 * 6 * @return NULL 7 */ 8 static void *uart_loop_task(void *arg) 9 { 10 int flag; 11 12 USR_DEBUG("Uart Main Task Start\n"); 13 write(com_fd, "Uart Start OK!\n", strlen("Uart Start OK!\n")); 14 15 for(;;){ 16 flag = upi_ptr->check_receive_data(com_fd); 17 if(flag == RT_OK){ 18 upi_ptr->execute_command(com_fd); 19 } 20 } 21 }
通過上述的所有流程,就實現了遠程管理的所有流程,此時就可以通過串口工具測試下協議的執行效果,如下:
代碼
完整的代碼參考https://github.com/Imx6ull-app/remote_manage中lower_app中關於下位機的實現。