玩转物联网人工智能小车:利用超声波传感器和舵机实现自动避障【ESP32教程34】
摘要:本文介绍如何使用超声波传感器和舵机实现小车的自动避障功能
前边已经完成了小车的制作,接下来就该进行程序的设计和开发工作了。在开发之前先要做个简单的规划,明确整个程序都要实现哪些功能以及如何实现这些功能。
避障小车的工作原理在前面都讲解过了。因为程序相对来说算比较简单的程序,因此在这里采用结构化的设计方法,先用流程图的形式来大致规划一下小车的运行逻辑。
避障小车的工作流程图如下所示。
这个避障小车用到了三个外部的功能模块:舵机、超声波测距传感器和L298N电机驱动模块。所以在最初的初始化中需要对这三个模块使用的相关资源进行初始化。之后就是根据超声波传感器返回的前方障碍物的距离来驱动小车的行驶了。
本小车的程序开发将采用Arduino IDE开发环境来进行。虽然看着功能不少,但其实大部分的代码在前边都已经见到过了。很多代码都是直接拿过来用就可以了。
在进行具体的开发之前,先讲一个比较好的开发习惯。就是要将在程序中用到的一些常量信息,定义成宏,一方面是可以很直观的知道这个常量的含义,另一方面在将来需要修改这个常量值的时候,就不需要到整个程序里去逐行查找和修改了。在这里总结了几种需要定义成宏的信息:
- 可能修改的资源配置信息。例如:GPIO引脚号码、定时器序号、PWM通道序号等等。假如初始将右前轮的驱动选择了GPIO的21和22引脚,后来发现要使用21和22引脚进行IIC通讯,那么就需要将右前轮的控制切换到别的GPIO引脚。如果事先定义成了宏,那么,在预定义这里直接修改就可以了,否则就要去程序里寻找每一个控制右轮的地方,将使用的引脚进行修改。这样很容易发生漏改、误改的情况,不利于程序的维护工作。
- 系统运行的常数参数。例如这次用到的PWM控制器输出PWM的频率、PWM分辨率等。这种参数可能只用到了一次,并且也不太可能进行修改。但是,将其定义为宏常量,可以使整个程序的可读性提高。通过宏的名字就可以知道这个数据的含义,而不是盯着一个数字再去琢磨它是什么含义。
- 系统中需要调试、变动的参数,有时多个参数之间可能还存在某种制约的关系,这时最好将这些参数定义到一起,并给出详细的注释和使用说明。比如:与障碍物的距离小于多少的时候就要停止行驶,这个数据可能和PWM占空比率存在着一定的关联性。车速快,势必停车的距离要长一些,提前留有的余地要大一些。
总之,在程序中,尽量不要有很多不容易理解的、多次使用的常数,这会给程序的后期维护带来很大的麻烦。
下面,就是这个程序中定义的常量信息。
// GPIO config // 控制车轮的GPIO #define WHEEL_LEFT_1 32 #define WHEEL_LEFT_2 33 #define WHEEL_RIGHT_1 18 #define WHEEL_RIGHT_2 23
// 控制超声波传感器模块的GPIO #define ULTRASONIC_TRIG 25 #define ULTRASONIC_ECHO 26
// 舵机控制GPIO #define STEERING_ENGINE 19
// GPIO config End
#define MOTO_PWM_FREQ 10000 // PWM频率 #define MOTO_PWM_RESOLUTIONS_BIT 12 // PWM分辨率
#define DISTANCE_LIMIT 20 // 停车距离
#define WL1_CHANNEL 0 // 左轮通道 #define WL2_CHANNEL 1 #define WR1_CHANNEL 2 // 右轮通道 #define WR2_CHANNEL 3 #define MOTO_PWM_CH 5 // 舵机通道
#define SPEED_L pow(2,12)/2 // 左轮占空比分辨率 #define SPEED_R pow(2,12)/2
#define TURN_TIME 500 //转向时间
// 全局常量 const uint8_t wheels_pin[] = {WHEEL_LEFT_1, WHEEL_LEFT_2, WHEEL_RIGHT_1, WHEEL_RIGHT_2}; const uint8_t wheels_ch[] = {WL1_CHANNEL,WL2_CHANNEL,WR1_CHANNEL,WR2_CHANNEL};
|
在上面的代码中可以看到,定义的宏的名字都是全大写的,这并不是必须的,实际上是大小写都可以的,只是在C语言中形成了用全大写的宏代表定义的常量这样一个习惯。这样,在看到全大写的标识符时,就应该想到这是一个预定义的宏,代表了某个有特殊含义的常数。
当然,宏定义除了定义常量之外,还可以定义含有变量的表达式,并且可以嵌套定义,这不在本文的讨论范围。
接下来还要定义一些全局变量,用来保存一些全局的状态信息。初学者,很喜欢使用全局变量,因为全局变量使用太方便了,一次定义,到处都可以使用。全局变量可以想在哪里修改,就在哪里修改。不需要像局部变量那样,在函数之间传递来、传递去的,非常的方便。
但过度的使用全局变量,会造成很多问题的隐患,这是因为全局变量在程序的任何地方都可以访问或者修改这个全局变量的值。对于当前这种简单的单任务、单进程的程序还好一些,程序都是按顺序依次执行的,对全局变量的使用也属于独占型的,出现问题的可能性还不是很大。而一旦将来进入到多任务、多进程系统,那么同时执行的多个任务都可能修改全局变量的值,就很容易形成系统的混乱。比如,设置一个全局变量来表示小车运行的状态,当这个智能小车有多个不同类型的传感器时,由于每种传感器的测量数据,都可能影响小车的状态,那么在这些传感器同时工作都去修改这个全局变量的时候,就有可能导致小车运动状态的混乱。
在这里我定义了两个全局变量,一个是舵机对象,可以方便的在任何位置控制舵机的运动。另一个是用来记录上一次舵机所在的角度,这样做是为了减少对舵机的操作,当上一次已经在同一个位置的时候,就不需要再控制舵机转动了,提高运行的效率。
Servo servo = Servo(); // 舵机对象 int lastDir = 90; // 记录最后测距方向 |
好了,开发代码前的准备工作就先到这里了。接下来将讲解主程序的开发方法。
作者:一起玩儿科技