⚙️简介::全国产化器件制作的基于立创梁山派的21年电赛F题智能送药小车
简要视频:
简要文章:
精简文档:
2_Code
目录,使用 ENV 工具执行 pkgs --update
下载缺失的软件包。scons --target=mdk5
更新MDK5工程文件。2_Code
目录下的project.uvprojx
即可正常编译使用了。project.uvprojx
即可正常编译使用了。所有自己添加的文件都在applications目录下。
为了配合梁山派的全国产化,本次的RTOS选择国内优秀的RT-Thread,RT-Thread 是一款完全由国内团队开发维护的嵌入式实时操作系统(RTOS),具有完全的自主知识产权。经过 16 个年头的沉淀,伴随着物联网的兴起,它正演变成一个功能强大、组件丰富的物联网操作系统。
它与其他RTOS最大的区别是他有一个软件包库,有许多开源的大佬分享自己做的软件包。如果你选用支持RT-Thread的芯片来开发项目,就可以使用它丰富的软件包。比如说想读取MPU6050的数据,直接在ENV工具中选择MUP6050的软件包,重新生成工程,运行例程就可以获得数据了。比如想做一个按键驱动,在软件包中搜索 按钮,就会有数个软件包供你选择,你可以专注于做应用,底层的大部分驱动都是可以复用的。
梁山派目前已经适配基础版RT-Thread,链接.
建议不要选择master分支进行开发,因为主分支处于一直开发的状态,可能会有意想不到的BUG。一般要选一个稳定的版本。我这里选用的RT-Thread-V4.1.1。
当前梁山派还没有适配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时钟是60M2=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 应用
这段代码是一个嵌入式系统中的示例程序,主要用于初始化和控制蜂鸣器(beep)的功能。
代码的主要功能如下:
stdio.h
(标准输入输出)、rtthread.h
(RT-Thread操作系统)、board.h
(板级支持包)、gd32f4xx_libopt.h
(GD32F4xx外设库)、bsp_beep.h
(蜂鸣器驱动头文件)等。beep_pwm_gpio_init
函数:该函数用于初始化蜂鸣器的GPIO引脚。具体操作包括使能蜂鸣器所在的GPIO时钟、配置GPIO引脚为复用功能、设置GPIO输出选项等。beep_timer_init
函数:该函数用于初始化蜂鸣器的定时器(timer)。具体操作包括使能蜂鸣器定时器所在的时钟、重置定时器、配置定时器参数(如计数模式、时钟分频、计数方向、周期和预分频值等)、初始化定时器、配置定时器通道输出参数(如极性、输出使能、闲置状态等)、使能定时器等。beep_pwm_pulse_set
函数:该函数用于设置蜂鸣器的脉冲宽度。具体操作是通过配置定时器通道的脉冲值来控制蜂鸣器的输出。beep
函数:该函数用于控制蜂鸣器发出声音。具体操作是设置蜂鸣器的脉冲宽度为500,延时一段时间后再将脉冲宽度设置为0,实现蜂鸣器的开关控制。beep_test
函数:该函数是一个用于测试的函数,用于通过命令行输入参数来设置蜂鸣器的脉冲宽度。具体操作是通过输入参数获取脉冲宽度值,然后调用beep_pwm_pulse_set
函数设置蜂鸣器的脉冲宽度。beep_init
函数:该函数用于初始化蜂鸣器。具体操作是调用beep_pwm_gpio_init
函数和beep_timer_init
函数来初始化蜂鸣器的GPIO引脚和定时器。INIT_BOARD_EXPORT(beep_init)
:这是一个用于在系统初始化阶段自动执行的宏,将beep_init
函数注册为系统初始化函数,以便在系统启动时自动初始化蜂鸣器。总体来说,这段代码主要是用于初始化和控制蜂鸣器的功能,包括初始化GPIO引脚和定时器,以及设置蜂鸣器的脉冲宽度来控制声音的输出。
motor_pwm_gpio_init()
:用于初始化电机驱动的PWM控制引脚。该函数通过配置引脚的模式和复用功能,将引脚设置为PWM输出模式。motor_timer_init()
:用于初始化电机驱动的硬件定时器。该函数通过配置定时器的参数,包括时钟分频、计数方向、周期和占空比等,设置定时器为PWM模式。motor1_in1_pwm_pulse_set()
、motor1_in2_pwm_pulse_set()
、motor2_in1_pwm_pulse_set()
、motor2_in2_pwm_pulse_set()
:分别用于设置电机1和电机2的PWM脉宽值。这些函数通过配置定时器的通道和脉冲值,控制对应引脚的PWM输出。motor1_pwm_value_set()
、motor2_pwm_value_set()
:用于调节电机1和电机2的PWM值,即控制电机的速度。通过调整对应引脚的PWM脉宽值来实现速度调节。motor_test()
:用于通过控制台输入命令来临时改变PWM值驱动电机转动。根据输入的命令和参数,调用相应的函数来控制电机的运动。此外,代码还包括了一些初始化函数和命令导出,以便在系统启动时进行电机的初始化和在控制台中使用相应的命令来控制电机。
总体来说,这段代码实现了电机驱动的初始化和控制,通过配置硬件定时器和PWM引脚,可以实现对电机速度的控制。
encoder_gpio_init
函数中,对编码器的GPIO进行初始化,包括设置引脚的工作模式和复用功能。encoder_timer_init
函数中,对编码器的定时器进行初始化。这些定时器用于计算编码器的计数值和测量速度。函数中分别初始化了两个编码器(M1和M2)的定时器,并配置了输入捕获通道和编码器模式。还启用了定时器中断,并将相应的中断处理函数注册到中断向量表中。encoder_count_timer_init
函数用于初始化一个用于计数的定时器。M1_TIMER_IRQHANDLER
和M2_TIMER_IRQHANDLER
是编码器定时器的中断处理函数。在中断处理函数中,根据定时器的计数值和重载值来判断编码器的溢出情况,并更新编码器的计数值。然后清除定时器的中断标志位。ENCODER_COUNT_TIMER_IRQHANDLER
是计数定时器的中断处理函数。在该函数中,根据定时器的中断标志位进行编码器计数值的更新,并计算编码器的速度。最后,清除计数定时器的中断标志位。set_encoder_struct_to_default
函数用于将编码器状态结构体初始化为默认值。encoder_topic_echo
函数是一个用于打印编码器状态的回调函数。它通过调用mcn_copy_from_hub
函数从主题中获取编码器状态,并打印相关信息。encoder_init
函数是编码器的初始化函数。在该函数中,调用了前面提到的各个初始化函数,并将编码器状态结构体初始化为默认值。然后,通过调用mcn_advertise
函数将回调函数注册为主题的广播函数,以便在接收到编码器状态更新时能够打印相关信息。总体而言,这段代码完成了对编码器的引脚和定时器的初始化配置,并通过中断处理函数和回调函数实现了编码器状态的更新和打印。
代码的主要功能是初始化舵机的GPIO和定时器,并提供了设置舵机脉冲宽度的函数。通过控制定时器的输出脉冲宽度,可以控制舵机的角度。
下面是代码的主要结构和功能解释:
servo_pwm_gpio_init()
和servo_timer_init()
。这些函数用于初始化舵机的GPIO和定时器。servo_pwm_gpio_init()
函数中,首先使能了舵机的GPIO时钟,然后配置了GPIO的模式和复用功能。最后,设置了输出选项,包括输出类型、输出速度等。servo_timer_init()
函数中,首先使能了舵机所使用的定时器的时钟。然后进行了定时器的初始化设置,包括计数方向、计数模式、计数周期、预分频等参数。接着,配置了定时器的通道输出模式和脉冲值,并设置了定时器的输出极性和空闲状态。最后,使能了定时器和高级定时器的主输出。servo1_pwm_pulse_set()
和servo2_pwm_pulse_set()
函数分别用于设置舵机1和舵机2的脉冲宽度。这些函数通过调用timer_channel_output_pulse_value_config()
函数来设置定时器的通道输出脉冲值,从而控制舵机的角度。servo_test()
函数是一个用于测试舵机控制的命令函数。通过命令行输入参数来选择要控制的舵机和脉冲宽度,然后调用相应的设置函数来控制舵机。servo_init()
函数,用于初始化舵机控制。该函数调用了servo_pwm_gpio_init()
和servo_timer_init()
函数来进行初始化操作。整体而言,这段代码通过初始化GPIO和定时器来控制舵机的角度。通过调用相应的设置函数,可以根据输入的脉冲宽度来改变舵机的位置。
RT-Thread控制台是我非常喜欢的一个功能,他可以让你和嵌入式设备产生交互,可以用来调试和查看系统信息。就有点像平时windos的cmd命令或者linux的命令控制台。
可以让你自己定义控制台命令,比如说你配置好了电机PWM,想在运行中改变PWM的值,通过串口传入参数就可以修改了,如下面的代码块所示:
/**
- @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 <motor1|motor2> <value(-1000 ~ +1000)>'\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 <motor1|motor2> <value(-1000 ~ +1000)>'\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中,控制器的输出取决于当前时刻的误差、过去误差的积分以及未来误差的预测。这些因素通过三个控制参数(比例系数、积分时间常数和微分时间常数)进行调节,以实现系统的稳定性和响应速度的平衡。
在这里就不讲原理了,网上有很多大佬讲得都很好,大家可以自行搜索观看。B站其实是一个学习网站哈。
简单来说,PID就是测量出实际值,设定一个目标值,输出一个驱动值,让实际值尽量和目标值靠近。
在module
目录中的pid
目录里面
#include "positional_pid.h"
:包含了定义了位置式PID控制器参数和函数的头文件。<math.h>
:包含了用于数学计算的函数的头文件。ABS(x)
:一个宏定义,用于返回给定值的绝对值。positional_pid_params_t
结构体:
positional_pid_params_init
函数:
positional_pid_set_value
函数:
positional_pid_compute
函数:
positional_pid_control
函数:
positional_pid_init
函数:
positional_pid_params_init
函数初始化参数,并将控制器状态设置为启用状态(PID_ENABLE)。"positional_pid.h"
头文件。positional_pid_params_t
类型的变量来存储 PID 控制器的参数和状态。positional_pid_init
函数初始化 PID 控制器,传递比例系数 kp
、积分系数 ki
、微分系数 kd
,死区 dead_zone
,输出上限 output_max
和输出下限 output_min
。positional_pid_set_value
函数设置 PID 控制器的参数(可选)。positional_pid_control
函数控制 PID 控制器的使能状态(可选)。positional_pid_compute
函数,并传递目标值和测量值作为参数,它将返回 PID 控制器的输出值。 在这个送药小车题目中,最先要解决的就是小车轮子的速度。这个问题是一个非常关键的问题。这是因为小车电机的速度直接影响了小车的行驶速度和精度,而行驶速度和精度又是小车比赛中获胜的关键因素之一。如果小车电机的速度不稳定,那么小车的行驶速度和精度就会受到影响,导致小车无法在比赛中达到最佳表现。因此,在进行智能车比赛之前,需要先解决小车电机速度问题,保证小车电机的速度稳定、准确,并且能够根据需要进行调整。并不是说给一个确定的电压或者确定的PWM值就能让电机保持到一个准确的转速,比如每个电机的绕线,轴承,机械性能,负载能力,各个接触点的摩擦力这些都有差别,小车轮子所受到的摩擦力的不同也会影响小车的前进速度。
在前面的BSP底层驱动初始化中我们已经解决了改变PWM值从而控制电机速度和获取编码器计数的问题。通过编码器我们可以进行电机的转速测量,通过改变PWM值我们可以让电机提高或者降低转速。在这个条件下,我们的目标值是让小车电机稳定在一个转速,测量值是小车当前由编码器测量得到的实际速度,输出值是电机的PWM值。通过调节PID的三个参数,让目标值变化时,实际值可以又快又准的接近目标值。
依靠编码器累积下来的数值确定轮子具体转了多少各脉冲,根据这个脉冲来计算小车电机转过的位置。位置环的PID,测量值就实际的编码器脉冲,目标值是想要控制电机选择多少脉冲,输出值是速度环的速度。
依靠IMU解算出来的航向角,角度环的PID,测量值是当前角度,目标值是想要转到的角度,输出值是电机的速度,当然我实际中是还没用角度环的,小车的旋转是依靠位置环来进行的。
依靠K210返回来的巡线中线偏移,测量值是红线相较屏幕中线的偏移,目标值是想让红线偏移为0,输出值是电机的速度。
为了解决通讯的粘包,分包,校验问题,采用开源的RT-Thread Upacker
软件包来实现。他的仓库实现了,C,C++,java,python的实现方式。
数据格式如下所示:
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主要有以下几个作用的原因和作用:
需要注意的是,在使用环形缓冲区时,需要合理设计和管理读取和写入指针,以确保数据的正确性和完整性。此外,当环形缓冲区已满或已空时,需要适当处理这些条件,以避免数据的丢失或阻塞系统。
有了upacker,我们可以只专注于payload,也就是上图中的data部分。
暂定通讯协议如下所示:
负载包 | 含义 | 数据所对应的意义 |
---|---|---|
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 |
负载包 | 含义 | 数据所对应的意义 |
---|---|---|
payload[0] | 设置K210工作模式 | 0:将K210切换至巡线模式 |
1:将K210切换至数字识别模式 |
小车地图位置对应信息
//具体位置信息分配如下所示,请对照实际地图理解
/*
f g
------ ------
| |
| |
| |
| |
|4__________________3__________________5|
| | |
| | |
| | |
| | |
__|___ | __|___
e | h
|
|
| | |
c|_________2__________|d
| | |
|
|
|
|
|
|
|
| | |
a|.........1..........|b
| | |
|
|
|
|
|
|
.......
0
*/
负载包 | 含义 | 数据所对应的意义 | 对应代码中的结构体 |
---|---|---|---|
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用) |
负载包 | 含义 | 数据所对应的意义 | 对应代码中的结构体 |
---|---|---|---|
payload[0] | 小车2位置信息(也算替代一个心跳包) | 可选:a,b,c,d,e,f,g,h,0,1,2,3,4,5(对照上图注释) |
加速度计用于测量物体在三个空间轴上的加速度,而陀螺仪用于测量物体在三个空间轴上的角速度(旋转速率)。磁力计用于测量物体在三个空间轴上的磁场强度。
姿态解算的目标是根据IMU提供的数据,计算出物体相对于某个参考坐标系的旋转姿态,通常以欧拉角(如俯仰角、横滚角和偏航角)或四元数的形式表示。
用开源的Fusion来实现姿态解算。如果只有IMU的话,姿态解算后的就偏航角一直在往一个方向偏,单纯的三轴加速度计+三轴角速度计是没办法解决这个问题的。因为偏航角的平面与重力相互垂直,重力在偏航角平面的投影为0,所以无法观测偏航角,即使偏航角发生变化,加速度计测出的值也不会改变。但是,加上磁力计以后就能解决偏航角的漂移问题了,因为磁力计可以观测到偏航角。不过,在地球南北极等地方,由于磁力线与地球重力方向重合,磁力计也无法观测到偏航角。每个地方的磁场都是不一样的,所以需要校准。
可能有人会说数据直接用全局变量传递不就行了。
在实时操作系统(RTOS)中,不推荐或不建议使用全局变量来传递信息的原因主要有以下几点:
为了避免上述问题,RTOS提供了一些机制来进行任务间的通信和数据传递,例如:
通过使用这些RTOS提供的通信机制,可以更安全地在任务之间传递信息,避免竞争条件和数据共享问题,并提高代码的可维护性和调试效率。
采用RT-Thread软件包uMCN,uMCN (Micro Multi-Communication Node) 提供了一种基于发布/订阅模式的安全跨线程/进程的通信方式。在系统中,uMCN 被广泛应用于任务和模块间的数据通信。使用发布-订阅(Publish-Subscribe)机制可以提供更灵活和高效的任务间通信方式,支持一对一、一对多、多对一和多对多的通信模式。这种机制可以解决使用全局变量传递信息可能带来的竞争条件和数据共享问题。
发布-订阅机制基于事件驱动的思想,其中包含两个角色:发布者(Publisher)和订阅者(Subscriber)。发布者负责发布事件或消息,而订阅者则注册对感兴趣的事件或消息进行订阅。他提供了一种松散耦合的通信方式,允许任务或模块之间通过发布和订阅消息来进行通信,而不需要直接知道彼此的存在。
在使用发布-订阅模式获取数据时,通常涉及以下角色和操作:
使用发布-订阅模式获取数据的好处在于,系统中的任务或模块之间解耦合,发布者和订阅者之间不直接依赖于彼此的存在,从而提高了系统的可扩展性和灵活性。此外,发布-订阅模式还能够支持多对多的通信,允许多个订阅者同时接收相同的数据或事件,实现了信息的分发和共享。
那他的优势有哪些呢:
这个每个人有不同的写法,大致都是这样,这部分没什么好说的,按照想出来的逻辑写出来就行。
巡线就是寻找红色色块,设置好阈值,划分好ROI(感兴趣区),就可以进行红色循迹线的识别。门口区域是一片小黑色方框,可以划分一个ROI,让K210去计算这个ROI内部是否有足够多的小色块。
就是那老三样:
和GD32端一样,采用Upacker进行数据的解包,押包和发送。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。