# serial_protocol **Repository Path**: Bryan_He/serial_protocol ## Basic Information - **Project Name**: serial_protocol - **Description**: 串口通信协议框架 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 4 - **Forks**: 1 - **Created**: 2025-04-08 - **Last Updated**: 2025-12-13 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # serial_protocol ### 介绍 本串口通信协议框架分为三部分实现,分别是最底层的数据打包层、协议交互层和用户逻辑处理层, 1. 数据打包层: 负责数据打包,方便A,B设备在串口通信线上进行数据发送和接收。 2. 协议交互层:负责数据的发送,接收和应答交互时序处理。 3. 用户逻辑处理层:负责实际的命令解析,对解析后的命令做对应的事务处理和响应。 ![framework_image](./docs/images/serial_protocol_framework.png) ### 数据打包层 底层的数据结构由三部分组成,分别是数据包开始标识,数据块和数据包结束标识; 1. 数据包开始标识,以0xAA表示 2. 数据块,用户需要传输的有效数据,它由两部分组成,分别是N字节的数据块和2字节的CRC校验位 为了避免数据块的数据内容与开始、结束标识符冲突,我们需要将数据块中出现的0xAA,0x55和转义标识符(0x7E)进行转义,使开始,结束和转义标识符在数据包中保持唯一性。 3. 数据包结束标识,以0x55表示 ### 数据包结构 数据包 = {[0xAA][转义后的数据块][0x55]} 转义后的数据块 = 转义处理后的(原始数据块) 原始数据块 = {[数据(N字节)][CRC(2字节)]} CRC = calculate_crc([数据(N字节)]) ![serial_protocol_pkg_struct](./docs/images/serial_protocol_pkg_struct.png) ### 有效数据 有效数据 = {[CMD][Parameters]} ![ActiveDatasPkg](./docs/images/active_data_pkg.png) 有效数据由两部分组成,数据的第一个字节是命令字节,后续的数据为命令的参数,可以是0~N字节; 如下是系统定义的命令,用户自定义的命令由CMD_USER_START这个枚举变量开始, ```c typedef enum { CMD_UNKNOWN = 0x00, CMD_ACK_INFO, CMD_GET_INFO, CMD_SET_CONFIG, // 用户可在此添加自定义命令码 CMD_USER_START, } CommandCode; ``` 如下是用户定义的命令数据集,参考(./demo/serial_protocol_demo.h),由CMD_USER_START开始 ```c typedef enum { // 用户可在此添加自定义命令码 USR_CMD_CUSTOM = CMD_USER_START, /** 必需以这个枚举变量开始CMD_USER_START,避免和底层指令重复 */ USR_CMD_SET_VOL, USR_CMD_GET_TX_INFOR, USR_CMD_GET_RX_INFOR, USR_CMD_SET_VOICE_ENHANCE, } UsrCmdCode; ``` ### 数据包中的数据转义规则 0xAA → 0x7E01 0x55 → 0x7E02 0x7E → 0x7E03 ### CRC计算 使用CRC-16/CCITT-FALSE算法,多项式0x1021,初始值0xFFFF ```c uint16_t calculate_crc(const uint8_t *dat, uint32_t length) { uint16_t crc = 0xFFFF; for (uint32_t i = 0; i < length; i++) { crc ^= (uint16_t)dat[i] << 8; for (int j = 0; j < 8; j++) { if (crc & 0x8000) { crc = (crc << 1) ^ 0x1021; } else { crc <<= 1; } } } return crc; } ``` ### 协议交互层 交互协议相对比较简单,主要实现数据包的发送和应答包的接收,如设备A发送一包数据,设备B对接收到的数据包进行应答。另外就是一些异常情况的处理,如CRC校验失败,等待应答超时等。如下是各种情况下的交互时序。 1. 正常交互时序,由设备A发起,设备B接收,设备A发送完数据包后等待设备B的应答包; 设备B接收完数据包后,提取数据,校验CRC,处理数据内容,返回处理结果给协议层,协议层对数据进行封包,发送应答数据包给设备A,设备A解析完数据后,回调应用程序注册的回调函数,将应答结果反馈给用户程序A. ![normal protocol timing](./docs/images/normal_protocol_timing.png) 2. CRC校验失败,设备A发送数据包给设备B,由于线路干扰或者其他原因,导致CRC校验失败,此时设备A接收都CRC校验失败应答后会再次发送上一包数据,直到发送成功,否则会尝试发送3次,如果超过3次,则会返回CRC校验失败的结果给应用程序并结束这次数据传输 ![CRC check failed](./docs/images/CRC_check_failed.png) 3. 不支持的命令,当设备A发送了一些设备B不支持的命令包时,设备B会返回一个未知的应答包给设备A。 ![Unknown Command](./docs/images/unknown_cmd_timing.png) 4. 无应答处理时序,当设备A发送数据包给设备B时,设备A会在100mS内等待设备B的应答数据包,如果超时,则会再次发送当前的数据包,直到超过3次还未接收到应答包后,将会返回无应答信息给到应用程序A并结束本次数据传输 ![NO ACK timing](./docs/images/no_ack_timing.png) ### 使用说明 1. 初始化串行协议句柄,调用如下这个API接口函数 ```c void serial_protocol_init(ProtocolHandler *handler,const UserCommand *p_cmd_table,uint16_t cmd_table_size,serial_send s_send_handle) ``` * handler: 指向串行协议解析句柄 * p_cmd_table:指向用户自定义的指令表,当底层接收完一包数据,完成数据提取和CRC校验后查询 这个指令表,并调用指令对应的回调函数,用户的事务处理可在这个回调函数中实现, * cmd_table_size:指令表的成员大小 * s_send_handle:指向用户的串行发送数据函数,这个函数由用户自行实现 如下代码是一个简单的参考实例,基于windows实现两线程间的数据交互,整个代码实现请查看这个目录(./demo)下的源代码 ```c static CommandStatus A_vol_handler(const uint8_t* params, uint8_t len, uint8_t* response,uint16_t *response_len){ SP_LOG("A VOL:\n"); SP_LOG_RAW(params,len); SP_LOG("\n"); *response_len = 0; return STATUS_SUCCESS; } static int A_serial_send(uint8_t *p_buf,uint16_t send_len){ memcpy(channelAtoB->dat_pipline,p_buf,send_len); channelAtoB->dat_pip_len = send_len; SetEvent(channelAtoB->hDataReadyEvent); } const UserCommand tak_A_cmd_table[]={ {.code = FORMAT_USR_CMD_CODE(USR_CMD_SET_VOL),.handler = A_vol_handler} }; serial_protocol_init(&A_serial_handler,tak_A_cmd_table,sizeof(tak_A_cmd_table)/sizeof(UserCommand),A_serial_send); ``` 2. 接收串口数据,将如下这个接收函数放置在你的数据接收函数/接收中断函数中,接收外设发来的串行数据。 * handler 指向串行协议解析句柄 * p_buf 指向串行数据接收缓存 * len 串行数据缓存中的数据长度 ```c void serial_protocol_receive(ProtocolHandler *handler, uint8_t *p_buf, uint16_t len) ``` 3. 串行协议处理,将如下API函数放置在1mS的定时器或者1mS的线程中调用,1mS调用一次,串行协议的相关时序在这个接口中处理 ```c void serial_protocol_processing(ProtocolHandler *handler) ``` 4. 数据发送,用户可调用如下的API接口发送数据到外设, * handler 指向串行协议解析句柄 * cmd_buf 指向用户发送数据的缓存,缓存中的数据由用户自行组织 * cmd_len 发送数据的缓存大小 * ack_cb 应答回调函数,发送数据的完成情况的状态反馈,如外设的应答信息等 ```c void serial_protocol_send(ProtocolHandler *handler, uint8_t *cmd_buf, uint8_t cmd_len,serial_ack_cb ack_cb) ``` 如下是一个简单的应用实例: 设备A的部分应用程序 ```c static void A_serial_ack_cb(CommandCode cmd, CommandStatus status,const uint8_t* params, uint8_t len){ SP_LOG("A ack:%x,%x\n",cmd,status); } const uint8_t test_get_protocol_infor[]={CMD_GET_INFO,0x78,0x55,0xaa,0x5a,0x7e}; const uint8_t test_set_vol_infor[]={USR_CMD_SET_VOL,0xaa,0x55,0x7e,0x05}; const uint8_t test_escape_infor[]={CMD_SET_CONFIG,0x55,0xaa,0x7e,0x0a,0x37,0x55}; ... if(!strcmp(channelAtoB->rec_buf,"get_p_infor")){ serial_protocol_send(&A_serial_handler, test_get_protocol_infor, sizeof(test_get_protocol_infor),A_serial_ack_cb); }else if(!strcmp(channelAtoB->rec_buf,"set_vol")){ serial_protocol_send(&A_serial_handler, test_set_vol_infor, sizeof(test_set_vol_infor),A_serial_ack_cb); }else if(!strcmp(channelAtoB->rec_buf,"test_es")){ serial_protocol_send(&A_serial_handler, test_escape_infor, sizeof(test_escape_infor),A_serial_ack_cb); } ... ``` 设备B的部分应用程序 ```c static CommandStatus B_vol_handler(const uint8_t* params, uint8_t len, uint8_t* response,uint16_t *response_len){ SP_LOG("B VOL:\n"); SP_LOG_RAW(params,len); SP_LOG("\n"); *response_len = 0; return STATUS_SUCCESS; } static CommandStatus B_get_infor_handler(const uint8_t* params, uint8_t len, uint8_t* response,uint16_t *response_len){ SP_LOG("get infor:\n"); SP_LOG_RAW(params,len); SP_LOG("\n"); /*version V1.01*/ response[0]=0x01, response[1]=0x01, *response_len = 2; return STATUS_SUCCESS; } const UserCommand tak_B_cmd_table[]={ {.code = FORMAT_USR_CMD_CODE(USR_CMD_SET_VOL),.handler = B_vol_handler}, {.code = FORMAT_USR_CMD_CODE(CMD_GET_INFO),.handler = B_get_infor_handler} }; ``` ### 使用注意事项 1. 原始数据块长度不应超过254字节(256-2,CRC Field Used 2 Bytes) 2. 打包后的缓冲区需要预留足够空间(最大可能为原始数据长度的2倍 + 2字节) 3. 解包时需要完整接收数据包后再进行处理 4. CRC校验失败时会返回-1,需要调用方处理异常情况 5. 可以根据具体需求调整CRC算法或增加错误处理机制 ### 参与贡献 1. Fork 本仓库 2. 新建 Feat_xxx 分支 3. 提交代码 4. 新建 Pull Request