Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
K210
电赛
GD32
【电子竞赛】基于立创梁山派+RT-Thread的21年电赛F题智能送药小车
发布于 2023-07-03 19:53:17 浏览:728
订阅该版
[tocm] ![基于立创梁山派+RT-Thread的21年电赛送药小车](https://oss-club.rt-thread.org/uploads/20230703/226d6b6d51fee8e99e780bcf4e5b0fb9.png.webp) ------------ **代码仓库:https://gitee.com/lcsc/medical_car** ------------ **硬件开源文档:https://oshwhub.com/li-chuang-kai-fa-ban/21-dian-sai-f-ti-zhi-neng-song-yao-xiao-che** ------------ **简要文章:** 1. [赛题分析](https://gitee.com/lcsc/medical_car/blob/master/4_Docs/%E8%B5%9B%E9%A2%98%E5%88%86%E6%9E%90.md) 2. [电路设计](https://gitee.com/lcsc/medical_car/blob/master/4_Docs/%E7%94%B5%E8%B7%AF%E8%AE%BE%E8%AE%A1.md) > **接下来主要介绍一下代码编写:** # 代码介绍 所有自己添加的文件都在[applications](https://gitee.com/lcsc/medical_car/tree/master/2_Code/applications)目录下。 # 梁山派GD32端 ## RTOS选择并建立模板 为了配合梁山派的全国产化,本次的RTOS选择国内优秀的RT-Thread,RT-Thread 是一款完全由国内团队开发维护的嵌入式实时操作系统(RTOS),具有完全的自主知识产权。经过 16 个年头的沉淀,伴随着物联网的兴起,它正演变成一个功能强大、组件丰富的物联网操作系统。 它与其他RTOS最大的区别是他有一个软件包库,有许多开源的大佬分享自己做的软件包。如果你选用支持RT-Thread的芯片来开发项目,就可以使用它丰富的软件包。比如说想读取MPU6050的数据,直接在ENV工具中选择MUP6050的软件包,重新生成工程,运行例程就可以获得数据了。比如想做一个按键驱动,在软件包中搜索 按钮,就会有数个软件包供你选择,你可以专注于做应用,底层的大部分驱动都是可以复用的。 梁山派目前已经适配基础版RT-Thread,[链接](https://github.com/RT-Thread/rt-thread/tree/master/bsp/gd32/arm/gd32470z-lckfb). 建议不要选择master分支进行开发,因为主分支处于一直开发的状态,可能会有意想不到的BUG。一般要选一个稳定的版本。我这里选用的RT-Thread-V4.1.1。 ## BSP底层初始化编写 当前梁山派还没有适配RT-Thread的PWM,ENCODER等外设资源,所以需要我们自己进行初始化配置。 主要有蜂鸣器PWM的配置,电机驱动PWM的配置,正交编码器的配置,舵机PWM的配置。 以蜂鸣器初始化为例。 首先要知道确切的系统时钟和定时器时钟频率。打开GD32参考手册的第四章节:复位和时钟单元(RCU)里面的图4-2.时钟树。再找到keil工程下面Drivers里面的board.c里面的SystemClock_Config。找到系统时钟之后找一下APB2和APB1的时钟频率。以蜂鸣器的PWM配置为例。可以看到BEEP_TIMER就是TIMER12,它挂在APB1时钟下,/* APB1 = AHB/4 */就是说APB1是60M,TIMER时钟由AHB时钟分频获得,它的频率可以等于CK_APBx、CK_APBx的两倍或CK_APBx 的四倍。详细信息请参考RCU_CFG1寄存器的TIMERSEL位,因为我们是给APB分频了的。所以定时器时钟等于 APB 时钟的两倍,所以这里的TIMER12时钟是60M*2=120Mhz,在这里的30-1,要-1是因为这里的计数是从0开始的。接下来就是简单的除法计算了。//120Mhz/30=4000000Hz 这里用到了自动初始化:RT-Thread总共有六个初始化顺序 > 1 INIT_BOARD_EXPORT(fn) > > 非常早期的初始化,此时调度器还未启动 > > 2 INIT_PREV_EXPORT(fn) > > 主要是用于纯软件的初始化、没有太多依赖的函数 > > 3 INIT_DEVICE_EXPORT(fn) > > 外设驱动初始化相关,比如网卡设备 > > 4 INIT_COMPONENT_EXPORT(fn) > > 组件初始化,比如文件系统或者 LWIP > > 5 INIT_ENV_EXPORT(fn) > > 系统环境初始化,比如挂载文件系统 > > 6 INIT_APP_EXPORT(fn) > > 应用初始化,比如 GUI 应用 ### bsp_beep 这段代码是一个嵌入式系统中的示例程序,主要用于初始化和控制蜂鸣器(beep)的功能。 代码的主要功能如下: 1. 导入头文件:包含了一些需要使用的头文件,包括`stdio.h`(标准输入输出)、`rtthread.h`(RT-Thread操作系统)、`board.h`(板级支持包)、`gd32f4xx_libopt.h`(GD32F4xx外设库)、`bsp_beep.h`(蜂鸣器驱动头文件)等。 2. `beep_pwm_gpio_init`函数:该函数用于初始化蜂鸣器的GPIO引脚。具体操作包括使能蜂鸣器所在的GPIO时钟、配置GPIO引脚为复用功能、设置GPIO输出选项等。 3. `beep_timer_init`函数:该函数用于初始化蜂鸣器的定时器(timer)。具体操作包括使能蜂鸣器定时器所在的时钟、重置定时器、配置定时器参数(如计数模式、时钟分频、计数方向、周期和预分频值等)、初始化定时器、配置定时器通道输出参数(如极性、输出使能、闲置状态等)、使能定时器等。 4. `beep_pwm_pulse_set`函数:该函数用于设置蜂鸣器的脉冲宽度。具体操作是通过配置定时器通道的脉冲值来控制蜂鸣器的输出。 5. `beep`函数:该函数用于控制蜂鸣器发出声音。具体操作是设置蜂鸣器的脉冲宽度为500,延时一段时间后再将脉冲宽度设置为0,实现蜂鸣器的开关控制。 6. `beep_test`函数:该函数是一个用于测试的函数,用于通过命令行输入参数来设置蜂鸣器的脉冲宽度。具体操作是通过输入参数获取脉冲宽度值,然后调用`beep_pwm_pulse_set`函数设置蜂鸣器的脉冲宽度。 7. `beep_init`函数:该函数用于初始化蜂鸣器。具体操作是调用`beep_pwm_gpio_init`函数和`beep_timer_init`函数来初始化蜂鸣器的GPIO引脚和定时器。 8. `INIT_BOARD_EXPORT(beep_init)`:这是一个用于在系统初始化阶段自动执行的宏,将`beep_init`函数注册为系统初始化函数,以便在系统启动时自动初始化蜂鸣器。 这段代码主要是用于初始化和控制蜂鸣器的功能,包括初始化GPIO引脚和定时器,以及设置蜂鸣器的脉冲宽度来控制声音的输出。 ### bsp_mootor 1. `motor_pwm_gpio_init()`:用于初始化电机驱动的PWM控制引脚。该函数通过配置引脚的模式和复用功能,将引脚设置为PWM输出模式。 2. `motor_timer_init()`:用于初始化电机驱动的硬件定时器。该函数通过配置定时器的参数,包括时钟分频、计数方向、周期和占空比等,设置定时器为PWM模式。 3. `motor1_in1_pwm_pulse_set()`、`motor1_in2_pwm_pulse_set()`、`motor2_in1_pwm_pulse_set()`、`motor2_in2_pwm_pulse_set()`:分别用于设置电机1和电机2的PWM脉宽值。这些函数通过配置定时器的通道和脉冲值,控制对应引脚的PWM输出。 4. `motor1_pwm_value_set()`、`motor2_pwm_value_set()`:用于调节电机1和电机2的PWM值,即控制电机的速度。通过调整对应引脚的PWM脉宽值来实现速度调节。 5. `motor_test()`:用于通过控制台输入命令来临时改变PWM值驱动电机转动。根据输入的命令和参数,调用相应的函数来控制电机的运动。 此外,代码还包括了一些初始化函数和命令导出,以便在系统启动时进行电机的初始化和在控制台中使用相应的命令来控制电机。 这段代码实现了电机驱动的初始化和控制,通过配置硬件定时器和PWM引脚,可以实现对电机速度的控制。 ### bsp_encoder 1. 引用了一些头文件,并定义了一些宏和结构体,包括编码器状态结构体(encoder_state_t)和主题定义(encoder_m1_topic、encoder_m2_topic)。 2. 在`encoder_gpio_init`函数中,对编码器的GPIO进行初始化,包括设置引脚的工作模式和复用功能。 3. 在`encoder_timer_init`函数中,对编码器的定时器进行初始化。这些定时器用于计算编码器的计数值和测量速度。函数中分别初始化了两个编码器(M1和M2)的定时器,并配置了输入捕获通道和编码器模式。还启用了定时器中断,并将相应的中断处理函数注册到中断向量表中。 4. `encoder_count_timer_init`函数用于初始化一个用于计数的定时器。 5. `M1_TIMER_IRQHANDLER`和`M2_TIMER_IRQHANDLER`是编码器定时器的中断处理函数。在中断处理函数中,根据定时器的计数值和重载值来判断编码器的溢出情况,并更新编码器的计数值。然后清除定时器的中断标志位。 6. `ENCODER_COUNT_TIMER_IRQHANDLER`是计数定时器的中断处理函数。在该函数中,根据定时器的中断标志位进行编码器计数值的更新,并计算编码器的速度。最后,清除计数定时器的中断标志位。 7. `set_encoder_struct_to_default`函数用于将编码器状态结构体初始化为默认值。 8. `encoder_topic_echo`函数是一个用于打印编码器状态的回调函数。它通过调用`mcn_copy_from_hub`函数从主题中获取编码器状态,并打印相关信息。 9. `encoder_init`函数是编码器的初始化函数。在该函数中,调用了前面提到的各个初始化函数,并将编码器状态结构体初始化为默认值。然后,通过调用`mcn_advertise`函数将回调函数注册为主题的广播函数,以便在接收到编码器状态更新时能够打印相关信息。 这段代码完成了对编码器的引脚和定时器的初始化配置,并通过中断处理函数和回调函数实现了编码器状态的更新和打印。 ### bsp_servo 代码的主要功能是初始化舵机的GPIO和定时器,并提供了设置舵机脉冲宽度的函数。通过控制定时器的输出脉冲宽度,可以控制舵机的角度。 下面是代码的主要结构和功能解释: 1. 首先,代码包含了一些头文件,包括stdio.h、rtthread.h、board.h等,这些头文件提供了所需的库函数和定义。 2. 然后定义了两个静态函数:`servo_pwm_gpio_init()`和`servo_timer_init()`。这些函数用于初始化舵机的GPIO和定时器。 3. `servo_pwm_gpio_init()`函数中,首先使能了舵机的GPIO时钟,然后配置了GPIO的模式和复用功能。最后,设置了输出选项,包括输出类型、输出速度等。 4. `servo_timer_init()`函数中,首先使能了舵机所使用的定时器的时钟。然后进行了定时器的初始化设置,包括计数方向、计数模式、计数周期、预分频等参数。接着,配置了定时器的通道输出模式和脉冲值,并设置了定时器的输出极性和空闲状态。最后,使能了定时器和高级定时器的主输出。 5. `servo1_pwm_pulse_set()`和`servo2_pwm_pulse_set()`函数分别用于设置舵机1和舵机2的脉冲宽度。这些函数通过调用`timer_channel_output_pulse_value_config()`函数来设置定时器的通道输出脉冲值,从而控制舵机的角度。 6. `servo_test()`函数是一个用于测试舵机控制的命令函数。通过命令行输入参数来选择要控制的舵机和脉冲宽度,然后调用相应的设置函数来控制舵机。 7. 最后,定义了一个`servo_init()`函数,用于初始化舵机控制。该函数调用了`servo_pwm_gpio_init()`和`servo_timer_init()`函数来进行初始化操作。 这段代码通过初始化GPIO和定时器来控制舵机的角度。通过调用相应的设置函数,可以根据输入的脉冲宽度来改变舵机的位置。 ## FINSH控制台调试使用 RT-Thread控制台是我非常喜欢的一个功能,他可以让你和嵌入式设备产生交互,可以用来调试和查看系统信息。就有点像平时windos的cmd命令或者linux的命令控制台。 可以让你自己定义控制台命令,比如说你配置好了电机PWM,想在运行中改变PWM的值,通过串口传入参数就可以修改了,如下面的代码块所示: ```C /** - @brief 电机的控制室命令,可以通过控制台输入命令来临时改变PWM值驱动电机转动 - @note None - @param argc:发给当前函数 命令行 总的参数的个数,他的值永远>=1。 argv: 是个字符串数组,用来存放指向字符串参数的指针数组,每一个元素指向一个以空格为分割的参数。 -如argv[0]指向程序运行的函数名称。 -如argv[1]指向解析出来的第一个参数,argv[2]指向再接下来的一个参数。 - @retval None */ static void motor_test(int argc, char**argv) { int16_t pwm_value = 0; /* 检查输入的变量是否有两个 */ if (argc < 3) { rt_kprintf("Please input'motor_test
'\n"); return; } if (!rt_strcmp(argv[1], "motor1")) { pwm_value = atoi(argv[2]); motor1_pwm_value_set(pwm_value); } else if (!rt_strcmp(argv[1], "motor2")) { pwm_value = atoi(argv[2]); motor2_pwm_value_set(pwm_value); } else {/* 输入的是其他内容 */ rt_kprintf("Please input'motor_test
'\n"); } } //导出命令到控制台 MSH_CMD_EXPORT(motor_test, motor test sample : motor_test motor1|motor2 in1|in2 pulse); ``` ## 小车的PID控制 一般的PID分为两种,增量式PID和位置式PID,它们的主要区别在于控制器输出的计算方式不同。 位置式PID的输出是根据当前误差、过去误差的积分以及未来误差的预测计算出来的。这种方法需要存储过去的误差信息,并且需要进行积分和微分运算,计算成本较高,但具有较好的稳定性和精度。 增量式PID则是根据当前误差和过去误差的变化率(即误差的一阶差分和二阶差分)计算出控制量。这种方法不需要存储过去的误差信息,只需要进行简单的加减运算,计算成本较低,但可能会出现积分饱和等问题。 因此,位置式PID适用于对精度要求较高的系统,而增量式PID适用于对计算成本要求较高的系统。 我就选择位置式PID了,梁山派性能是足够的。在位置式PID中,控制器的输出取决于当前时刻的误差、过去误差的积分以及未来误差的预测。这些因素通过三个控制参数(比例系数、积分时间常数和微分时间常数)进行调节,以实现系统的稳定性和响应速度的平衡。 在这里就不讲原理了,网上有很多大佬讲得都很好,大家可以自行搜索观看。B站其实是一个学习网站哈。 简单来说,PID就是测量出实际值,设定一个目标值,输出一个驱动值,让实际值尽量和目标值靠近。 ### 实际程序 在`module`目录中的`pid`目录里面 1. 头文件引用: 1. `#include "positional_pid.h"`:包含了定义了位置式PID控制器参数和函数的头文件。 2. `
`:包含了用于数学计算的函数的头文件。 2. 宏定义: 1. `ABS(x)`:一个宏定义,用于返回给定值的绝对值。 3. `positional_pid_params_t` 结构体: 1. 定义了位置式PID控制器的参数和状态。 4. `positional_pid_params_init` 函数: 1. 初始化位置式PID控制器的参数。 5. `positional_pid_set_value` 函数: 1. 设置位置式PID控制器的比例系数(kp)、积分系数(ki)和微分系数(kd)。 6. `positional_pid_compute` 函数: 1. 计算位置式PID控制器的输出值。 2. 首先检查控制器是否启用,如果启用,则执行以下操作: - 设置目标值和测量值。 - 计算误差(目标值与测量值之差)。 - 如果误差的绝对值大于设定的死区(dead_zone),则执行以下操作: - 计算比例输出项(p_out)。 - 计算积分输出项(i_out),并将其累加到之前的积分输出值上。 - 计算微分输出项(d_out),使用当前误差与上一次误差的差值。 - 计算总输出值(output),将比例、积分和微分输出项相加。 - 如果总输出值超过了设定的最大输出值(output_max),则将其限制为最大输出值。 - 如果总输出值低于设定的最小输出值(output_min),则将其限制为最小输出值。 - 更新上一次测量值、上一次输出值和上一次误差的记录。 - 返回计算得到的输出值。 3. 如果控制器未启用,则返回 0.0f。 7. `positional_pid_control` 函数: 1. 设置位置式PID控制器的状态(启用或禁用)。 8. `positional_pid_init` 函数: 1. 初始化位置式PID控制器。 2. 设置函数指针,将各个函数与相应的结构体成员关联起来。 3. 调用 `positional_pid_params_init` 函数初始化参数,并将控制器状态设置为启用状态(PID_ENABLE)。 ### 怎么用呢? 1. 在代码中包含 `"positional_pid.h"` 头文件。 2. 创建一个 `positional_pid_params_t` 类型的变量来存储 PID 控制器的参数和状态。 3. 使用 `positional_pid_init` 函数初始化 PID 控制器,传递比例系数 `kp`、积分系数 `ki`、微分系数 `kd`,死区 `dead_zone`,输出上限 `output_max` 和输出下限 `output_min`。 4. 使用 `positional_pid_set_value` 函数设置 PID 控制器的参数(可选)。 5. 使用 `positional_pid_control` 函数控制 PID 控制器的使能状态(可选)。 6. 在需要计算 PID 控制器输出的地方,使用 `positional_pid_compute` 函数,并传递目标值和测量值作为参数,它将返回 PID 控制器的输出值。 ### 速度环 在这个送药小车题目中,最先要解决的就是小车轮子的速度。这个问题是一个非常关键的问题。这是因为小车电机的速度直接影响了小车的行驶速度和精度,而行驶速度和精度又是小车比赛中获胜的关键因素之一。如果小车电机的速度不稳定,那么小车的行驶速度和精度就会受到影响,导致小车无法在比赛中达到最佳表现。因此,在进行智能车比赛之前,需要先解决小车电机速度问题,保证小车电机的速度稳定、准确,并且能够根据需要进行调整。并不是说给一个确定的电压或者确定的PWM值就能让电机保持到一个准确的转速,比如每个电机的绕线,轴承,机械性能,负载能力,各个接触点的摩擦力这些都有差别,小车轮子所受到的摩擦力的不同也会影响小车的前进速度。 在前面的BSP底层驱动初始化中我们已经解决了改变PWM值从而控制电机速度和获取编码器计数的问题。通过编码器我们可以进行电机的转速测量,通过改变PWM值我们可以让电机提高或者降低转速。在这个条件下,我们的目标值是让小车电机稳定在一个转速,测量值是小车当前由编码器测量得到的实际速度,输出值是电机的PWM值。通过调节PID的三个参数,让目标值变化时,实际值可以又快又准的接近目标值。 ### 位置环 依靠编码器累积下来的数值确定轮子具体转了多少各脉冲,根据这个脉冲来计算小车电机转过的位置。位置环的PID,测量值就实际的编码器脉冲,目标值是想要控制电机选择多少脉冲,输出值是速度环的速度。 ### 角度环 依靠IMU解算出来的航向角,角度环的PID,测量值是当前角度,目标值是想要转到的角度,输出值是电机的速度,当然我实际中是还没用角度环的,小车的旋转是依靠位置环来进行的。 ### 巡红线环 依靠K210返回来的巡线中线偏移,测量值是红线相较屏幕中线的偏移,目标值是想让红线偏移为0,输出值是电机的速度。 ## 通讯协议实现 为了解决通讯的粘包,分包,校验问题,采用开源的RT-Thread `Upacker`软件包来实现。他的[仓库](https://github.com/aeo123/upacker)实现了,C,C++,java,python的实现方式。 数据格式如下所示: ```C 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,表示数据包的总长度为255个字节。 - 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,包长为255个字节,校验位分布在Header校验和check字段中。这个数据结构的目的是在通信或数据传输中定义数据包的格式和内容,以便发送方和接收方能够正确解析和处理数据。 在移植的时候,只需要实现发送数据的函数和解包成功后的处理回调函数就可以了。 ### 为什么呢要用ringbufer呢 使用ringbufer主要有以下几个作用的原因和作用: 1. 数据缓存:环形缓冲区可以作为一个数据缓存区域,用于存储串口接收或发送的数据。当数据到达时,可以将其存储在环形缓冲区中,而不必立即处理数据。这种缓存机制可以确保数据的可靠接收和传输,而不会丢失任何数据。 2. 解决数据速率不匹配:环形缓冲区可以解决串口接收和处理之间的数据速率不匹配问题。例如,当串口接收数据的速度比处理数据的速度快时,环形缓冲区可以临时存储多个接收到的数据,以供后续处理。这样可以避免数据丢失或接收溢出的情况发生。 3. 异步通信:环形缓冲区允许异步的数据传输。串口通信通常是异步的,发送端和接收端的速率可能不同。通过使用环形缓冲区,可以在发送和接收之间建立一个缓冲区,使得数据可以在不同的时刻被发送和接收,而不需要发送和接收方同时处于活动状态。 4. 提高系统响应性:使用环形缓冲区可以提高系统的响应性能。当有新的数据到达时,它可以立即被存储在环形缓冲区中,而不需要等待处理。这允许系统能够更快地响应其他任务或中断,而不会因为串口数据的到达而阻塞。 5. 简化数据处理:环形缓冲区提供了一种简化数据处理的机制。通过使用适当的读取和写入指针,可以方便地从环形缓冲区中读取和写入数据。这样的机制使得数据处理的代码更加简洁和高效。 需要注意的是,在使用环形缓冲区时,需要合理设计和管理读取和写入指针,以确保数据的正确性和完整性。此外,当环形缓冲区已满或已空时,需要适当处理这些条件,以避免数据的丢失或阻塞系统。 有了upacker,我们可以只专注于payload,也就是上图中的data部分。 暂定通讯协议如下所示: ### K210toMCU | 负载包 | 含义 | 数据所对应的意义 | | ------------ | ------------------------------------------------ | ----------------------------------- | | 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 | ### MCUtoK210 | 负载包 | 含义 | 数据所对应的意义 | | --------------------------- | ---------------- | ----------------------- | | payload[0] | 设置K210工作模式 | 0:将K210切换至巡线模式 | | | | 1:将K210切换至数字识别模式| > 小车地图位置对应信息 ```C //具体位置信息分配如下所示,请对照实际地图理解 /* f g ------ ------ | | | | | | | | |4__________________3__________________5| | | | | | | | | | | | | __|___ | __|___ e | h | | | | | c|_________2__________|d | | | | | | | | | | | | | a|.........1..........|b | | | | | | | | | ....... 0 */ ``` ### 小车1to小车2 | 负载包 | 含义 | 数据所对应的意义 | 对应代码中的结构体 | | --------------------------------------- | ----------------- | ------------------------------------------------------------ | ------------------- | | payload[0] | 小车1要去的药房号 | 识别到的数字:1,2,3,4,5,6,7,8 | car1_to_car2_info_t | | payload[1] | 病房 a 的病房号 | 固定为1 | | | payload[2] | 病房 b 的病房号 | 固定为2 | | | payload[3] | 病房 c 的病房号 | 1,2,3,4,5,6,7,8(0:表示还不知道) | | | payload[4] | 病房 d 的病房号 | 1,2,3,4,5,6,7,8(0:表示还不知道) | | | payload[5] | 病房 e 的病房号 | 1,2,3,4,5,6,7,8(0:表示还不知道) | | | payload[6] | 病房 f 的病房号 | 1,2,3,4,5,6,7,8(0:表示还不知道) | | | payload[7] | 病房 g 的病房号 | 1,2,3,4,5,6,7,8(0:表示还不知道) | | | payload[8] | 病房 h 的病房号 | 1,2,3,4,5,6,7,8(0:表示还不知道) | | | payload[9] | 小车1位置信息 | 可选:a,b,c,d,e,f,g,h,0,1,2,3,4,5(对照上图注释)(字符类型'a','1') | | | payload[10] | 控制小车2继续运行 | 0x00:小车2等着别动 | | | | | 0x01:小车2可以继续运动了(发挥部分2用)| | | ### 小车2to小车1 | 负载包 | 含义 | 数据所对应的意义 | 对应代码中的结构体 | | ---------- | --------------------------------- | ------------------------------------------------- | ------------------ | | payload[0] | 小车2位置信息(也算替代一个心跳包) | 可选:a,b,c,d,e,f,g,h,0,1,2,3,4,5(对照上图注释) | | | ## 小车姿态解算 加速度计用于测量物体在三个空间轴上的加速度,而陀螺仪用于测量物体在三个空间轴上的角速度(旋转速率)。磁力计用于测量物体在三个空间轴上的磁场强度。 姿态解算的目标是根据IMU提供的数据,计算出物体相对于某个参考坐标系的旋转姿态,通常以欧拉角(如俯仰角、横滚角和偏航角)或四元数的形式表示。 用开源的Fusion来实现姿态解算。如果只有IMU的话,姿态解算后的就偏航角一直在往一个方向偏,单纯的三轴加速度计+三轴角速度计是没办法解决这个问题的。因为偏航角的平面与重力相互垂直,重力在偏航角平面的投影为0,所以无法观测偏航角,即使偏航角发生变化,加速度计测出的值也不会改变。但是,加上磁力计以后就能解决偏航角的漂移问题了,因为磁力计可以观测到偏航角。不过,在地球南北极等地方,由于磁力线与地球重力方向重合,磁力计也无法观测到偏航角。每个地方的磁场都是不一样的,所以需要校准。 ## 各个数据在系统中的发布与订阅 可能有人会说数据直接用全局变量传递不就行了。 在实时操作系统(RTOS)中,不推荐或不建议使用全局变量来传递信息的原因主要有以下几点: 1. 竞争条件:在多任务环境下,全局变量可能会面临竞争条件的问题。当多个任务同时读写同一个全局变量时,会导致数据的不一致性和错误的结果。这是因为任务的执行是并发的,无法控制它们的执行顺序。 2. 数据共享和保护:全局变量被所有任务共享,这意味着多个任务可以同时访问和修改该变量。如果没有正确的数据保护机制,可能会导致数据损坏或冲突。 3. 可维护性和调试困难:使用全局变量传递信息可能导致代码的可维护性和调试的困难。由于全局变量可以被任何任务修改,追踪问题的根源和调试错误可能会变得更加困难。 为了避免上述问题,RTOS提供了一些机制来进行任务间的通信和数据传递,例如: 1. 消息队列:任务可以通过消息队列来发送和接收消息。每个任务有自己的私有消息队列,通过发送和接收消息来进行通信,避免了全局变量的竞争条件和数据共享问题。 2. 信号量:信号量可以用于同步任务的执行和共享资源的访问。任务可以通过申请和释放信号量来控制对共享资源的访问,并确保任务之间的互斥性和同步性。 3. 事件标志组:事件标志组可以用于任务之间的通知和事件触发。一个任务可以等待一个或多个事件标志的状态变化,并在事件发生时被唤醒执行相应的操作。 通过使用这些RTOS提供的通信机制,可以更安全地在任务之间传递信息,避免竞争条件和数据共享问题,并提高代码的可维护性和调试效率。 采用RT-Thread软件包uMCN,uMCN (Micro Multi-Communication Node) 提供了一种基于发布/订阅模式的安全跨线程/进程的通信方式。在系统中,uMCN 被广泛应用于任务和模块间的数据通信。使用发布-订阅(Publish-Subscribe)机制可以提供更灵活和高效的任务间通信方式,支持一对一、一对多、多对一和多对多的通信模式。这种机制可以解决使用全局变量传递信息可能带来的竞争条件和数据共享问题。 发布-订阅机制基于事件驱动的思想,其中包含两个角色:发布者(Publisher)和订阅者(Subscriber)。发布者负责发布事件或消息,而订阅者则注册对感兴趣的事件或消息进行订阅。他提供了一种松散耦合的通信方式,允许任务或模块之间通过发布和订阅消息来进行通信,而不需要直接知道彼此的存在。 在使用发布-订阅模式获取数据时,通常涉及以下角色和操作: 1. 发布者(Publisher):负责生成和发布数据或事件。发布者将数据发送到一个或多个特定的主题(Topic),而不关心具体的订阅者。 2. 订阅者(Subscriber):订阅者对特定的主题感兴趣,并通过订阅该主题来接收与之相关的数据或事件。 3. 主题(Topic):主题是数据或事件的分类或标识符,发布者根据主题将数据发送到相应的通道,而订阅者根据主题来选择订阅的数据源。 使用发布-订阅模式获取数据的好处在于,系统中的任务或模块之间解耦合,发布者和订阅者之间不直接依赖于彼此的存在,从而提高了系统的可扩展性和灵活性。此外,发布-订阅模式还能够支持多对多的通信,允许多个订阅者同时接收相同的数据或事件,实现了信息的分发和共享。 那他的优势有哪些呢: 1. 松耦合性:发布-订阅机制使任务之间的通信更加松耦合。发布者不需要直接知道订阅者的存在,也不需要关心具体的订阅者数量和位置。订阅者只需要订阅感兴趣的事件或消息,从而实现任务之间的解耦。 2. 灵活性:发布-订阅机制支持多对多的通信模式,一个发布者可以有多个订阅者,一个订阅者也可以订阅多个发布者的事件或消息。这种灵活性使得任务之间可以方便地建立复杂的通信关系。 3. 扩展性:通过发布-订阅机制,可以方便地扩展系统,添加新的发布者或订阅者,而不需要修改现有的任务逻辑。这种扩展性使得系统更具可维护性和可扩展性。 ## 题目实现步骤 1. 实现小车的前进后退旋转。 2. 实现小车红线循迹和药房区域识别并停车。 3. 实现数字识别并传递给GD32。 4. 实现近端病房送药并返回。 5. 实现中远端病房送药并返回。 6. 实现双车送药。 这个每个人有不同的写法,大致都是这样,这部分没什么好说的,按照想出来的逻辑写出来就行。 # K210端 ## 红色循迹线识别与门口区域识别 巡线就是寻找红色色块,设置好阈值,划分好ROI(感兴趣区),就可以进行红色循迹线的识别。门口区域是一片小黑色方框,可以划分一个ROI,让K210去计算这个ROI内部是否有足够多的小色块。 1. 导入所需的模块和库:导入了一些必要的模块和库,包括摄像头模块、图像处理模块、显示模块等。 2. 进行初始化设置:对摄像头进行了一些初始化设置,包括设置摄像头的频率、曝光、像素格式和帧大小等。 3. 打印内存分配情况:使用了一些函数来打印当前的内存分配情况,包括堆内存和栈内存的可用空间。 4. 程序运行选择和状态设置:定义了一些变量用于设置程序的运行模式和状态,例如巡线模式、数字识别模式等。 5. 串口配置:配置了一个串口用于与其他设备进行通信,设置了串口的波特率和缓冲区长度。 6. 按键和蜂鸣器配置:注册了一个GPIO引脚作为按键输入,并通过中断来检测按键的状态。同时,通过定时器配置了一个PWM通道来控制蜂鸣器的频率和占空比。 7. 数据传输配置:定义了一个数据传输类,用于存储要传输给其他设备的数据。 8. 感兴趣区配置:定义了一些感兴趣区域的位置和参数,用于在图像中标定特定区域。 9. 寻找色块区配置:定义了一些色块位置信息和阈值,用于在图像中寻找特定颜色的色块。 10. 色块连线区配置:定义了一些参数和函数,用于在图像中绘制连接色块的线条。 ## 数字识别 就是那老三样: 1. 采集数据集 2. 对数据集进行标注 3. 开始训练 ## 串口发送和接收 和GD32端一样,采用Upacker进行数据的解包,押包和发送。具体代码[看这里](https://gitee.com/lcsc/medical_car/blob/master/2_Code/applications/sensor/k210/pycode/main.py)
0
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
原子号
这家伙很懒,什么也没写!
文章
3
回答
1
被采纳
0
关注TA
发私信
相关文章
1
K210 esp32 初始化完成后程序崩溃
2
K210 BSP 在线调试有问题,裸跑官方没问题
3
关于交叉编译工具链路径问题
4
关于K210使用RW007的注意
5
如何在k210上移植rt-thread nano
6
RTT下K210两路SPI不能同时使用问题,求大神
7
K210平台,开启NTP功能后。stack overflow
8
K210 交流一起共建生态
9
K210 spi驱动问题
10
请教K210编译报错
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
USB
DMA
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
FAL
rt-smart
ESP8266
I2C_IIC
WIZnet_W5500
UART
ota在线升级
PWM
cubemx
freemodbus
flash
packages_软件包
BSP
潘多拉开发板_Pandora
定时器
ADC
GD32
flashDB
socket
中断
Debug
编译报错
msh
SFUD
rt_mq_消息队列_msg_queue
keil_MDK
ulog
MicroPython
C++_cpp
本月问答贡献
a1012112796
20
个答案
3
次被采纳
张世争
11
个答案
3
次被采纳
踩姑娘的小蘑菇
7
个答案
3
次被采纳
rv666
9
个答案
2
次被采纳
用户名由3_15位
13
个答案
1
次被采纳
本月文章贡献
程序员阿伟
9
篇文章
2
次点赞
hhart
3
篇文章
4
次点赞
RTT_逍遥
1
篇文章
6
次点赞
大龄码农
1
篇文章
5
次点赞
ThinkCode
1
篇文章
1
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部