初级会员
- 积分
- 62
- 金钱
- 62
- 注册时间
- 2023-7-12
- 在线时间
- 10 小时
|
送药小车代码仓库:https://gitee.com/lcsc/medical_car更好的观看体验请去:https://lceda001.feishu.cn/wiki/ZDYbwqDfCiwVlckUEcScF0KSnRh
送药小车立创开源平台资料:https://oshwhub.com/li-chuang-kai-fa-ban/21-dian-sai-f-ti-zhi-neng-song-yao-xiao-che
立创梁山派与K210串口通信协议框架搭建在K210可以识别到色块和识别数字后,就需要把这些信息传递给立创梁山派了。而立创梁山派也需要控制K210去切换巡线模式和数字识别模式。所以需要规定一下他们之间的双向通信协议。
⚙️定义数据的通信协议K210to立创梁山派负载包
| 含义
| 数据所对应的意义
| payload[0]
| K210当前工作模式
| 0:巡线模式
|
|
| 1:数字识别模式
| payload[1]
| 当前路口识别结果
| 0:啥也没识别到
|
|
| 1:门口区域
| payload[2:3]
| 顶部巡线色块中心点相较屏幕中心的偏移像素,有正负
| 以像素点为单位
| payload[4:5]
| 中间巡线色块中心点相较屏幕中心的偏移像素,有正负
| 以像素点为单位
| payload[6:7]
| 左边巡线色块中心点相较屏幕中心的偏移像素,有正负
| 以像素点为单位
| payload[8:9]
| 右边巡线色块中心点相较屏幕中心的偏移像素,有正负
| 以像素点为单位
| payload[10]
| 最左边的数字(由K210计算坐标得出)
| 识别到的数字,可以是1,2,3,4,5,6,7,8
| payload[11]
| 最右边的数字(由K210计算坐标得出)
| 识别到的数字,可以是1,2,3,4,5,6,7,8
|
立创梁山派toK210负载包
| 含义
| 数据所对应的意义
| payload[0]
| 设置K210工作模式
| 0:将K210切换至巡线模式
|
|
| 1:将K210切换至数字识别模式
|
🔎为什么需要使用定义好的通信协议上面定义的是负载包中各个字节的具体含义,而在两个系统(K210和立创梁山派)之间进行通信,最主要的就是要解决可靠性和准确性,使用确定的通讯协议主要是出于以下几个点的考虑:
- 保证数据完整性:分包和粘包问题可能导致接收方无法正确解析数据。分包意味着接收方只收到了数据包的一部分,而粘包意味着接收方可能将多个数据包当作一个数据包处理。为了确保接收方能够准确地解析和处理数据,需要解决数据完整性问题。
- 保证数据正确性:在实际的串口(UART,我们一般使用的都是异步串口,同步串口不常用,他需要再多一根时钟线)通信中,可能会出现传输误码的情况。通过校验方法可以检测并纠正这些误码,确保接收方收到的数据是正确的。如果不解决校验问题,接收方可能会收到错误的数据,导致通信中断或者错误的执行,在一些有关人身安全的产品中是绝对不能出现的。
- 提高通信可靠性:通过明确的数据包边界、长度字段和校验和,接收方可以更准确地检测和处理数据包。在某些情况下,还可以使用应答机制来确保每个数据包都被正确接收。
- 简化数据处理:定义确定的通讯协议,可以简化接收方对数据的处理。接收方可以根据协议的规定来解析数据,而不需要处理复杂的边界判断和错误检测。
- 适应不同的应用场景:不同的设备和应用可能有不同的数据传输速率和容错要求。设计一个能够解决这些问题的通信协议,可以使串口通信在更多场景中得到应用。
❓如何解决串口通信粘包,分包,校验问题?基于以上的这些问题,我们是需要解决粘包,分包,校验问题的,这时候使用国产RT-Thread的好处就显现出来了,可以在RT-Thread软件包里面,搜索upacker,查看他的介绍可以了解到以下信息:
他的仓库实现了,C,C++,java,python的实现方式,对于立创梁山派来说,在使用RT-Thread标准版时只需要打开ENV配置工具,添加这个软件到工程文件就可以了。对于K210,是使用Micro python编程的,只需要把upacker的python实现源码复制到k210主程序里面并配置输入输出函数就可以了。
数据格式如下所示:
- Header 4BYTE Load
- ----------------------------------------------------------------------
- D0[7:0] |D1[7:0] |D2[5:0] |D2[7:6] |D3[1:0] |D3[7:2]
- ----------------------------------------------------------------------
- 包头 |包长(低8) |包长(高6) |Header校验[3:2] |Header校验[5:4] |check[7:2] |data
- ----------------------------------------------------------------------
- 0x55 |0XFF |0X3F |0X0C |0X30 |0XFC |XXXXX
复制代码
解释一下上面这个数据格式的意思,一个字节是八个位,上面D0后面的[0:7]就表示他的八个字节全部用来表示包头(一个字节是八个位嘛)。上面的D2[5:0]是用了位域,指的是用数据包的第2个字节中的0到5位,一个字节是8位。位域是一种数据结构,可以让数据占用更少的存储空间。在C语言里,位域可以用来存储一些只需要占用一个或几个二进制位的信息。他经常用在一些需要频繁操作数据的场合中,减少数据长度,提升数据打包效率。
- D0[7:0]: 数据包的包头,该字段的值为固定的0x55,用于标识数据包的起始。
- D1[7:0]: 数据包的长度的低8位,表示数据包的总长度。在示例中,该字段的值为0xFF,表示数据包的总长度的低八位为0xFF。
- D2[5:0]: 数据包的长度的高6位,表示数据包的总长度的高位。在示例中,该字段的值为0x3F,表示数据包的总长度的高6位为0x3F。
- D2[7:6]: Header校验的第2和第3位。在示例中,该字段的值为0x0C。
- D3[1:0]: Header校验的第4和第5位。在示例中,该字段的值为0x30。
- D3[7:2]: 校验位和数据字段。在示例中,该字段的值为0xFC。
- 数据包的具体含义和数据字段的结构可能根据实际应用而有所不同。示例中,包头为0x55,包长最大为16384个字节,校验位分布在Header校验和check字段中。这个数据结构的目的是在通信或数据传输中定义数据包的格式和内容,以便发送方和接收方能够正确解析和处理数据。
在移植的时候,只需要实现发送数据的函数和解包成功后的处理回调函数就可以了。
🐵通讯协议在立创梁山派的实现引入upacker软件包后,就需要对接对应串口的输入输出了:
具体代码在protocol_thread.c里面,在立创梁山派中用到的串口为uart2,在RT-Thread中,已经适配了串口驱动,所以在这里可以把uart2当作一个设备来使用。
初始化代码如下所示:
- //创建ringbufer
- uart2_rb = rt_ringbuffer_create(UART2_RING_BUFFER_LEN);
- if (uart2_rb == RT_NULL)
- {
- rt_kprintf("Can't create uart2 ringbffer");
- return;
- }
- //打开串口2设备
- /* 以读写及中断接收方式打开串口设备 */
- rt_device_open(k210_serial, RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_INT_RX);
- // 在打开串口设备之后对其参数进行修改
- config.baud_rate = BAUD_RATE_115200;
- if (RT_EOK
- != rt_device_control(k210_serial, RT_DEVICE_CTRL_CONFIG, &config))
- {
- rt_kprintf("change %s failed!\n", K210_UART_NAME);
- }
- //进行串口2DMA通道初始化(降低MCU压力)
- uart2_dma_config();
- // 初始化 k210 packer(押包解包)
- upacker_init(&k210_msg_packer, k210_protocol_handle_cb, k210_protocol_send);
复制代码
对接串口发送接口- /**
- * @brief k210发送接口
- * @note
- * [url=home.php?mod=space&uid=271674]@param[/url] *buff:
- * @param len:
- * @retval None
- */
- static void k210_protocol_send(uint8_t *buff, uint16_t len)
- {
- rt_device_write(k210_serial, 0, buff, len);
- }
复制代码
上面的函数在packer初始化的时候就对接到了k210_msg_packer上面,后面调用upacker_pack函数就可以发送数据包到串口了。
- // 初始化 k210 packer(押包解包)
- upacker_init(&k210_msg_packer, k210_protocol_handle_cb, k210_protocol_send);
复制代码
实例如下面所示:
- //控制K210进入巡线模式
- int send_uart_k210_to_find_lines(void)
- {
- static uint8_t temp = 0x00;
- upacker_pack(&k210_msg_packer, &temp, 1);
- return 0;
- }
- //控制K210进入数字识别模式
- int send_uart_k210_to_find_number(void)
- {
- static uint8_t temp = 0x01;
- upacker_pack(&k210_msg_packer, &temp, 1);
- return 0;
- }
复制代码
对接串口接收接口简单来说,送药小车接收K210传输过来的消息数据是这么流转的:串口DMA接收(到达16个字节后)->uart2的ringbuffer->系统空闲时upacker从ringbuffer中获取字符串
首先从uart2中的ringbuffer获取字符串,获取不到当前protocol_thread就延时挂起
- uart2_ringbufer_size = rt_ringbuffer_data_len(uart2_rb);
- if (rt_ringbuffer_data_len(uart2_rb) >= 1)
- {
- rt_ringbuffer_get(uart2_rb, &receive_char, 1);
- upacker_unpack(&k210_msg_packer, &receive_char, 1);
- }
- else
- {
- rt_thread_mdelay(1);
- }
复制代码
upacker成功校验之后就可以把传过来的数据发布了:
- /**
- * @brief k210消息解析回调
- * @note
- * @param *buff:
- * @param size:
- * @retval None
- */
- static void k210_protocol_handle_cb(uint8_t *buff, uint16_t len)
- {
- static rt_tick_t temp_tick;
- // 接收到payload,到了这里下面的数据就是已经校验过的了
- LOG_D("k210 pack len%d,count=%d", len, receive_count);
- receive_count++;
- if (rt_tick_get() - temp_tick >= 1000)
- {
- receive_count = 0;
- temp_tick = rt_tick_get();
- }
- k210_data.work_mode = buff[0];
- k210_data.recognition = buff[1];
- k210_data.top_block_offset = buff[2] + (buff[3] << 8);
- k210_data.center_block_offset = buff[4] + (buff[5] << 8);
- k210_data.left_block_offset = buff[6] + (buff[7] << 8);
- k210_data.right_block_offset = buff[8] + (buff[9] << 8);
- k210_data.left_number = buff[10];
- k210_data.right_number = buff[11];
- mcn_publish(MCN_HUB(k210_data_topic), &k210_data);
- }
复制代码
那么ringbuffer中的数据时怎么来的呢?
这里是采用DMA接收从串口过来的数据并放入ringbufer中,用DMA主要是为了降低CPU负担,如果用串口中断那么K210每发一个字节,立创梁山派就要来处理一次,如果处理的时间稍微长一点就可能导致下一个串口字节来的时候丢失。有了DMA之后就可以收到16个字节后再让CPU统一处理。关键代码如下:
- void uart2_dma_config(void)
- {
- dma_single_data_parameter_struct dma_init_struct;
- rcu_periph_clock_enable(UART2_DMA_RCU);
- dma_deinit(UART2_DMA, UART2_DMA_CH);
- dma_init_struct.periph_addr = (uint32_t)&USART_DATA(USART2);
- dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
- dma_init_struct.memory0_addr = (uint32_t)uart2_recv_buff;
- dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
- dma_init_struct.periph_memory_width = DMA_PERIPH_WIDTH_8BIT;
- dma_init_struct.circular_mode = DMA_CIRCULAR_MODE_DISABLE;
- dma_init_struct.direction = DMA_PERIPH_TO_MEMORY;
- dma_init_struct.number = USART_RECEIVE_DMA_ENABLE;
- dma_init_struct.priority = DMA_PRIORITY_ULTRA_HIGH;
- dma_single_data_mode_init(UART2_DMA, UART2_DMA_CH, &dma_init_struct);
- dma_channel_subperipheral_select(UART2_DMA, UART2_DMA_CH, DMA_SUBPERI4);
- dma_channel_enable(UART2_DMA, UART2_DMA_CH);
- dma_interrupt_enable(UART2_DMA, UART2_DMA_CH, DMA_CHXCTL_FTFIE);
- nvic_irq_enable(UART2_DMA_CH_IRQ, 1, 1);
- usart_dma_receive_config(USART2, USART_RECEIVE_DMA_ENABLE);
- usart_interrupt_disable(USART2, USART_INT_RBNE);
- usart_interrupt_enable(USART2, USART_INT_IDLE);
- }
- void UART2_DMA_CH_IRQ_HANDLER(void)
- {
- RT_USED static uint32_t temp_count, dma_count;
- rt_interrupt_enter();
- temp_count++;
- dma_count = dma_transfer_number_get(UART2_DMA, UART2_DMA_CH);
- rt_ringbuffer_put(uart2_rb, (rt_uint8_t *)&uart2_recv_buff,
- UART2_RECEIVE_LENGTH);
- if (dma_interrupt_flag_get(UART2_DMA, UART2_DMA_CH, DMA_INT_FLAG_FTF)
- == SET)
- {
- dma_interrupt_flag_clear(UART2_DMA, UART2_DMA_CH, DMA_INT_FLAG_FTF);
- }
- rt_interrupt_leave();
- }
复制代码
一次性接收完16个字节之后,就调用rt_ringbuffer_put,压到ringbuffer内存中,等待读取。
为什么呢要用ringbufer呢使用ringbufer主要有以下几个作用的原因和作用:
- 数据缓存:环形缓冲区可以作为一个数据缓存区域,用于存储串口接收或发送的数据。当数据到达时,可以将其存储在环形缓冲区中,而不必立即处理数据。这种缓存机制可以确保数据的可靠接收和传输,而不会丢失任何数据。
- 解决数据速率不匹配:环形缓冲区可以解决串口接收和处理之间的数据速率不匹配问题。例如,当串口接收数据的速度比处理数据的速度快时,环形缓冲区可以临时存储多个接收到的数据,以供后续处理。这样可以避免数据丢失或接收溢出的情况发生。
- 异步通信:环形缓冲区允许异步的数据传输。串口通信通常是异步的,发送端和接收端的速率可能不同。通过使用环形缓冲区,可以在发送和接收之间建立一个缓冲区,使得数据可以在不同的时刻被发送和接收,而不需要发送和接收方同时处于活动状态。
- 提高系统响应性:使用环形缓冲区可以提高系统的响应性能。当有新的数据到达时,它可以立即被存储在环形缓冲区中,而不需要等待处理。这允许系统能够更快地响应其他任务或中断,而不会因为串口数据的到达而阻塞。
- 简化数据处理:环形缓冲区提供了一种简化数据处理的机制。通过使用适当的读取和写入指针,可以方便地从环形缓冲区中读取和写入数据。这样的机制使得数据处理的代码更加简洁和高效。
有了upacker,我们可以只专注于payload,也就是upacker数据结构中的load部分。
🐶通讯协议在K210的实现在K210上面的实现就比较简单了,因为micro python在底层已经把串口的发送接收给封装好了。
首先初始化K210的串口:
- #串口配置区
- fm.register(6, fm.fpioa.UART1_TX, force=True)
- fm.register(7, fm.fpioa.UART1_RX, force=True)
- k210_uart = UART(UART.UART1, 115200, 8, 0, 0, timeout=1000, read_buf_len=4096)
复制代码
对接串口发送接口- #______________________________________________________________________________________________________________________________
- #发送数据到MCU,gd32是小端字节序
- #pack各字母对应类型
- #x pad byte no value 1
- #c char string of length 1 1
- #b signed char integer 1
- #B unsigned char integer 1
- #? _Bool bool 1
- #h short integer 2
- #H unsigned short integer 2
- #i int integer 4
- #I unsigned int integer or long 4
- #l long integer 4
- #L unsigned long long 4
- #q long long long 8
- #Q unsilong long long 8
- #f float float 4
- #d double float 8
- #s char[] string 1
- #p char[] string 1
- #P void * long
- def send_data_to_mcu(pack,global_uart_send_data):
- hex_data = ustruct.pack("<bbhhhhbb", #小端字节序
- global_uart_send_data.work_mode,
- global_uart_send_data.recognition,
- global_uart_send_data.top_block_offset, #以屏幕中线为0点
- global_uart_send_data.center_block_offset,
- global_uart_send_data.left_block_offset, #以屏幕中线为0点
- global_uart_send_data.right_block_offset,
- global_uart_send_data.left_number,
- global_uart_send_data.right_number,)
- pkg_data = pack.enpack(hex_data)
- k210_uart.write(pkg_data)
复制代码
上面的代码定义了一个名为send_data_to_mcu的函数,用来将数据打包成特定的格式,并通过串口发送给MCU(立创梁山派)。
- 字节序:在多字节数据类型中(比如float,int32_t等就是四个字节),字节序表示字节在内存中的排列顺序。有两种类型:大端字节序(Big-Endian)和小端字节序(Little-Endian)。大端字节序指最高有效字节在最低地址,最低有效字节在最高地址。小端字节序指最低有效字节在最低地址,最高有效字节在最高地址。立创梁山派用的国产芯片GD32使用的是小端字节序。
- ustruct库:Python中的ustruct库提供了将原始数据类型(如字符串、整数、浮点数)转换为字节数组的功能。这在处理二进制数据和与C语言结构体进行交互时非常有用,要传输也必须要要这样做。
send_data_to_mcu函数接受两个参数,一个是pack(Upacker的实例),另一个是global_uart_send_data(就是要发送给立创梁山派的K210数据)。使用ustruct库的pack()方法将global_uart_send_data中的数据打包成字节数组。在这里,使用了小端字节序格式(<),以及两种标识符:
- b:有符号字节(1字节整数)
- h:有符号短整数(2字节整数)
然后,使用pack.enpack()方法将打包后的字节数组封装成一个数据包,最后通过串口(k210_uart.write())将数据包发送给立创梁山派。
对接串口接收接口通过下面这个函数进行解包:
- read_str = pack.unpack(read_data, print_hex)
复制代码
数据校验成功后就会调用下面这个,如果传过来的命令变了就切换K210的工作模式(巡线模式或者数字识别模式):
- <pre class="ace-line ace-line old-record-id-AEvQdzj91osFIGxYtE4coJUZn6e"><code class="language-C" data-wrap="false">def print_hex(bytes):
- global work_mode,uart_cmd_need_change_mode
- hex_byte = [hex(i) for i in bytes]
- if is_upacker_recive_debug:
- print("-----"+" ".join(hex_byte))
- if bytes[0] == 0x00:
- work_mode = 0
- if bytes[0] == 0x01:
- work_mode = 1
- uart_cmd_need_change_mode = 1</code></pre><div class="ace-line ace-line old-record-id-QXyVdAftLo5EmNxDBGfcaZkCnRg"></div>
复制代码
|
|