第3章智能小车C语言编程 3.1wiringPi库的介绍 wiringPi库是由Gordon Henderson所编写并维护的一个用C语言写成的类库。起初主要是作为BCM2835芯片的GPIO库。而现在已经非常丰富,除了GPIO库以外,还包括I2C库、SPI库、UART库和软件PWM库等。 由于其与Arduino的wiring系统较为类似,故以此命名。它是采用GNU LGPLv3许可证的,可以在C或C++上使用,而且在其他编程语言上也有对应的扩展。 wiringPi库包含一个命令行工具gpio,它可以用来设置GPIO引脚,可以用来读写GPIO引脚,甚至可以在Shell脚本中使用来达到控制GPIO引脚的目的。 wiringPi库的安装方法可参照1.3.1节,下面介绍智能小车C语言编程使用的wiringPi库中的函数。 1. 初始化函数 int wiringPiSetup(void);该函数初始化wiringPi,程序将使用如图31所示的引脚定义图,具体引脚映射可通过gpio readall命令进行查看。程序必须调用初始化函数,否则不能正常工作。树莓派所有的GPIO引脚使用wiringPi编号。 图31树莓派引脚定义图 2. pinMode函数 void pinMode(int pin, int mode); 使用该函数可以将某个引脚设置为INPUT(输入)、OUTPUT(输出)、PWM_OUTPUT(脉冲输出)或者GPIO_CLOCK(GPIO时钟)。需要注意的是,仅有引脚1支持PWM_OUTPUT模式,仅有引脚7支持CLOCK输出模式。 3. digitalWrite函数 void digitalWrite(int pin, int value);使用该函数可以向指定的引脚写入HIGH(高)或者LOW(低),写入前,需要将引脚设置为输出模式。wiringPi将任何的非0值作为HIGH(高)来对待,因此,0是唯一能够代表LOW(低)的数值。 4. pwmWrite函数 void pwmWrite(int pin, int value); 使用该函数可以将值写入指定引脚的PWM寄存器中。树莓派板上仅有一个PWM引脚,即引脚1。可设置的值为0~1024。 5. digitalRead函数 void digitalRead(int pin); 使用该函数可以读取指定引脚的值,读取到的值为HIGH(1)或者LOW(0),该值取决于该引脚的逻辑电平的高低。 6. softPwmCreate函数 int softPwmCreate(int pin,int initialValue,int pwmRange); 该函数将会创建一个软件控制的PWM引脚。可以使用任何一个GPIO引脚,pwmRange参数可以为0(关)~100(全开)。返回值为0,代表成功,其他值代表失败。 7. softPwmWrite函数 void softPwmWrite(int pin, int value);该函数将会更新指定引脚的PWM值。value参数的范围将会被检查,如果指定的引脚之前没有通过softPwmCreate初始化,将会被忽略。 8. delay函数 void delay(unsigned int howLong);该函数将会中断程序执行至少howLong毫秒。因为Linux是多任务的,中断时间可能会更长。需要注意的是,最长的延迟值是一个无符号32位整数,其大约为49天。 9. delayMicroseconds函数 void delayMicroseconds(unsigned int howLong); 该函数将会中断程序执行至少howLong微秒。因为Linux是一个多任务的系统,因此中断时间可能会更长。需要注意的是,最长的延迟值是一个无符号32位整数,其大约为71分钟。如果延迟低于100微秒,将会使用硬件循环来实现;如果超过100微秒,将会使用系统nanosleep()函数来实现。 3.2智能小车移动控制3.2.1固定速度移动控制C语言控制智能小车移动使用pinMode和digitalWrite函数来实现。 1. void pinMode(int pin, int mode) 智能小车电机上的IN1~IN4四个引脚分别与树莓派的wiringPi编号为1,4,5,6这4个GPIO引脚连接,函数中的pin参数对应如表31所示的wiringPi编号;由于树莓派通过GPIO引脚向电机引脚输出高电平控制智能小车移动,因此mode参数应设置为OUTPUT。表31电机引脚编号 电机引脚wiringPi编号移动方式IN11左轮前进IN24左轮后退IN35右轮前进IN46右轮后退2. void digitalWrite(int pin, int value) 树莓派通过GPIO引脚向电机引脚输出高电平控制智能小车移动,value参数为1时表示树莓派引脚向电机引脚高电平;为0时表示输出低电平。控制智能小车左转的完整程序如下。#include #include #include int main() { wiringPiSetup(); /WiringPi GPIO/ pinMode (1, OUTPUT);//IN1 pinMode (4, OUTPUT);//IN2 pinMode (5, OUTPUT);//IN3 pinMode (6, OUTPUT);//IN4 while(1) { digitalWrite(1,0); digitalWrite(4,0); digitalWrite(5,1);//右轮前进 digitalWrite(6,0); } return 0; }首先添加wiringPi.h头文件,使用wiringPiSetup函数进行初始化,通过pinMode函数将树莓派wiringPi编号为1,4,5,6的引脚设置为输出模式,最后使用digitalWrite函数通过树莓派5号引脚向电机的IN3引脚输出高电平,从而控制智能小车右轮前进,实现左转。 3.2.2可变速度移动控制 使用上述实现方式智能小车只能以最大速度移动,为了能够灵活控制智能小车的移动速度需要使用PWM控制方式,但是树莓派硬件上支持的PWM输出的引脚有限(只有1号引脚支持PWM输出)。为了突破这个限制,wiringPi提供了软件实现的PWM输出API。基于PWM的智能小车移动控制使用softPwmCreate和softPwmWrite函数。 1. int softPwmCreate(int pin, int initialValue, int pwmRange) 其中,pin用来作为软件PWM输出的引脚;initalValue为引脚输出的初始值;pwmRange为PWM值的范围上限。 2. void softPwmWrite(int pin, int value) 其中,value为PWM引脚输出的值,电机的实际速度为: value/pwmRange×最大速度。 基于PWM的控制智能小车左转的完整程序如下。#include #include #include #include int main() { wiringPiSetup(); /WiringPi GPIO/ pinMode (1, OUTPUT);//IN1 pinMode (4, OUTPUT);//IN2 pinMode (5, OUTPUT);//IN3 pinMode (6, OUTPUT);//IN4 softPwmCreate(1,1,500); softPwmCreate(4,1,500); softPwmCreate(5,1,500); softPwmCreate(6,1,500); while(1) { softPwmWrite(1,0); softPwmWrite(4,0); softPwmWrite(5,250);//右轮前进 softPwmWrite(6,0); } return 0; }首先使用PWM需要包含头文件softPwm.h,初始化和引脚模式设置以后,使用softPwmCreate函数将1,4,5,6四个引脚设置为PWM模式,最后通过softPwmWrite函数在四个引脚设置输出值,智能小车将以半速(250/500×最大速度)左转。 3.2.3程序的编译和运行 将控制智能小车左转的程序保存为left.c,如图32所示,打开树莓派终端,输入:gcc left.c -o left -lwiringPi -lpthread sudo ./left图32程序的编译和执行 GCC(GNU Compiler Collection,GNU编译器套件)是由 GNU 开发的编程语言编译器。GCC原本作为GNU操作系统的官方编译器,现已被大多数类UNIX操作系统(如Linux、BSD、Mac OS X等)采纳为标准的编译器,GCC同样适用于微软的Windows。GCC和g++分别是GNU的C & C++编译器。GCC/g++在执行编译的时候一般有以下4步。 (1) 预处理: 生成.i的文件,进行宏的替换、注释的消除和找到相关的库文件,并将 #include文件的全部内容插入。 (2) 转换成汇编语言: 生成.s文件,.s文件表示汇编文件,用编辑器打开是汇编指令。 (3) 汇编文件转变为目标代码(机器代码): 生成.o的文件,.o是GCC生成的目标文件,用编辑器打开是二进制机器码。 (4) 连接目标代码: 生成可执行程序。 使用gcc left.c将编译出一个名为a.out的可执行文件;使用“o”可以指定编译程序的名字,gcc left.c o left将编译出一个名为left的可执行文件。 Linux平台下存在着大量库,本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。通俗地说就是把常用函数的目标文件打包在一起,提供相应函数的接口,便于程序员使用。按照库的使用方式又可分为动态库和静态库。 (1) 静态库后缀为“.a”,类似Windows平台的.lib文件,静态库可以简单地看成一组目标文件(.o文件)的集合,即很多目标文件经过压缩打包后形成的文件。比如在日常编程中,如果需要使用printf函数,就需要stdio.h的库文件,可是如果直接把对应函数源码编译后形成的.o文件提供给我们,将会对我们的管理和使用造成极大不便,于是可以使用“ar”压缩程序将这些目标文件压缩在一起,形成“libx.a”静态库文件。其中,“x”为库名。静态库在程序编译时会被连接到目标代码中,相当于将库中的函数加载到程序里,在编译的时候直接编译进去,这样在编译之后执行程序时将不再需要该静态库。编译之后程序文件大,但加载快,隔离性也好。所以它的优点就显而易见了,即编译后的执行程序不需要外部的函数库支持,因为所有使用的函数都已经被编译进去了。当然,这也会成为它的缺点,因为如果静态函数库改变了,那么程序必须重新编译。 (2) 动态库后缀为“.so”,类似Windows平台的.dll文件。动态库在程序编译时并不会被连接到目标代码中,而是仅在编译时引用,体积小,在程序运行到相关函数时才载入函数库里的相应函数,因此在程序运行时还需要动态库的存在。多个应用程序可以使用同一个动态库,启动多个应用程序的时候,只需要将动态库加载到内存一次即可。GCC/g++在编译时默认使用动态库。 (3) l参数用来指定程序要链接的库,l参数紧接着就是库名,那么库名跟真正的库文件名有什么关系呢?拿数学库来说,库名是m,则动态库文件名是“libm.so”,即把库文件名的头lib和尾.so去掉就是库名了。当使用动态库libwiringPi.so时,需要把“libwiringPi.so”复制到/usr/lib或者/lib里,编译时加上“lwringPi”参数即可;同时,使用wiringPi库中的函数也需要配套的头文件wiringPi.h。由于使用到了PWM函数,因此需要使用“lpthread”链接libthread.so动态库。 可以在树莓派终端输入find name "libwiringPi",验证是否存在libwiringPi.so动态链接库;输入 find name "libpthread",验证是否存在 libthread.so 动态链接库。 3.3超声波传感器的使用3.3.1传感器的连接如图33所示,超声波传感器包含Vcc、Gnd、Trig和Echo四个引脚,其中,Vcc和Gnd引脚对传感器供电,Trig和Echo引脚分别与树莓派的GPIO引脚连接。如图34所示,智能小车前方的超声波传感器的Trig引脚与树莓派物理编号为38的引脚连接,Echo引脚与树莓派物理编号为40的引脚连接;左方超声波传感器的Trig引脚与树莓派的物理编号为35的引脚连接,Echo引脚与树莓派物理编号为37的引脚连接;右方超声波传感器的Trig引脚与树莓派物理编号为29的引脚连接,Echo引脚与树莓派物理编号为31的引脚连接。树莓派GPIO引脚的物理编号和wiringPi编号的对应关系如表32所示。 图33超声波传感器图34树莓派接线图 表32编号对应关系 传感器wiringPi编号GPIO物理编号红外开关#1(左)1126红外开关#2(右)1024超声波传感器(前) Trig: 28, Echo: 29 Trig: 38, Echo: 40超声波传感器(左) Trig: 24, Echo: 25 Trig: 35, Echo: 37超声波传感器(右) Trig: 21, Echo: 22 Trig: 29, Echo: 31循迹传感器#1(左)2736循迹传感器#2(右)2632电机(左)电机(右)1 4 5 612 16 18 223.3.2工作原理 如图35所示,树莓派向传感器的Trig引脚输出一个10μs以上的高电平,超声波传感器模块内部自动向外发送8个40kHz的方波,然后树莓派等待传感器Echo引脚的高电平输出,一旦有高电平输出则记录一次时间,当变为低电平时再次记录一次时间,记录的时间差即为超声波往返的时间。根据往返时间可以计算出与障碍物之间的距离。 图35超声波时序图 3.3.3程序实现 右方超声波传感器测距的程序代码如下。#include #include #include #define Trig 21 #define Echo 22 void ultraInit() { pinMode(Echo, INPUT);//Echo为与传感器Echo引脚连接的树莓派的 //GPIO引脚 pinMode(Trig, OUTPUT); //Trig为与传感器Trig引脚连接的树莓派的 //GPIO引脚 } float disMeasure() { struct timeval tv1; struct timeval tv2; long start, stop; float dis; digitalWrite(Trig, LOW); delayMicroseconds(2); digitalWrite(Trig, HIGH);//树莓派向传感器Trig引脚输出高电平 delayMicroseconds(10);//保持10μs digitalWrite(Trig, LOW); while(!(digitalRead(Echo)==1)); //在树莓派等待传感器Echo //引脚的高电平输出 gettimeofday(&tv1, NULL); //获取当前时间 while(!(digitalRead(Echo)==0));//等待高电平结束 gettimeofday(&tv2, NULL); //获取当前时间 start=tv1.tv_sec1000000+tv1.tv_usec; //微秒级的时间 stop=tv2.tv_sec1000000+tv2.tv_usec; dis=(float)(stop-start)/100000034000/2;//求出距离,单位为厘米 return dis; } int main() { float dis; wiringPiSetup(); ultraInit(); while(1) { dis=disMeasure(); printf("distance is:%0.2fcm\\n",dis); delay(1000);//每隔1s输出一次 } return 0; }程序中使用到了timeval结构体。struct timeval { longtv_sec; / seconds / longtv_usec;/ microseconds / };其中,tv_sec记录秒,tv_usec记录微秒,直接使用gettimeofday即可获取时间。 3.4红外和循迹传感器的使用 #include #include #include #define LEFT 11//左红外 #define RIGHT 10//右红外 int main() { wiringPiSetup(); int SR,SL; while(1) { SR=digitalRead(RIGHT); printf("Rightsensor:%d\\n",SR); SL=digitalRead(LEFT); printf("Leftsensor:%d\\n",SL); } return 0; }智能小车左侧红外传感器与树莓派的wiringPi编号为11的GPIO引脚连接,右侧的红外传感器与树莓派wiringPi编号为10的引脚连接。左侧循迹传感器与树莓派的wiringPi编号为27的引脚连接,右侧循迹传感器与树莓派的wiringPi编号为26的引脚连接。使用digitalRead函数获取传感器的状态。 3.5应 用 案 例3.5.1基于超声波传感器的迷宫导航 #include #include #include #include #include #define Trig28 //前方超声波传感器 #define Echo29 #define BUFSIZE 512 void ultraInit(void) { pinMode(Echo, INPUT); pinMode(Trig, OUTPUT); } float disMeasure(void) { struct timeval tv1; struct timeval tv2; long start, stop; float dis; digitalWrite(Trig, LOW); delayMicroseconds(2); digitalWrite(Trig, HIGH);//树莓派向传感器Trig引脚输出高电平 delayMicroseconds(10);//保持10μs digitalWrite(Trig, LOW); while(!(digitalRead(Echo)==1)); //在树莓派等待传感器Echo引脚的高电 //平输出 gettimeofday(&tv1, NULL); //获取当前时间 while(!(digitalRead(Echo)==0));//等待高电平结束 gettimeofday(&tv2, NULL); //获取当前时间 start=tv1.tv_sec1000000+tv1.tv_usec;//微秒级的时间 stop=tv2.tv_sec1000000+tv2.tv_usec; dis=(float)(stop-start)/100000034000/2;//求出距离,单位为厘米 return dis; } void run() //前进 { softPwmWrite(4,0); softPwmWrite(1,250);//左轮前进 softPwmWrite(6,0); softPwmWrite(5,250);//右轮前进 } void brake(int time) //停车 { softPwmWrite(1,0); softPwmWrite(4,0); softPwmWrite(5,0); softPwmWrite(6,0); delay(time100);//执行时间,可以调整 } void left(int time) //左转(左轮不动,右轮前进) { softPwmWrite(1,0); softPwmWrite(4,0); softPwmWrite(5,250);//右轮前进 softPwmWrite(6,0); delay(time300); } void right(int time)//右转(右轮不动,左轮前进) { softPwmWrite(1,250); //左轮前进 softPwmWrite(4,0); softPwmWrite(5,0); softPwmWrite(6,0); delay(time300);//执行时间,可以调整 } void back(int time)//后退 { softPwmWrite(4,250); //左轮向后移动 softPwmWrite(1,0); softPwmWrite(6,250); //右轮向后移动 softPwmWrite(5,0); delay(time 200); //执行时间,可以调整 } int main(int argc, char argv[]) { float dis; wiringPiSetup(); /WiringPi GPIO/ pinMode (1, OUTPUT);//IN1 pinMode (4, OUTPUT);//IN2 pinMode (5, OUTPUT);//IN3 pinMode (6, OUTPUT);//IN4 softPwmCreate(1,1,500); softPwmCreate(4,1,500); softPwmCreate(5,1,500); softPwmCreate(6,1,500); while(1) { dis=disMeasure(); printf("distance=%0.2f cm\\n",dis);//输出当前超声波测得的距离 if(dis<30) { //测得前方障碍的距离小于30cm时做出如下响应 back(4);//后退800 ms left(4);//左转1200 ms } else { run();//无障碍时前进 } } return 0; }本案例只使用了前方的超声波传感器,当检测与前方障碍物距离小于30cm时,先后退800ms,然后向左旋转1200ms,否则前进。读者可以进一步完善迷宫导航算法。 3.5.2基于红外传感器的迷宫导航 #include #include #include #include #include #define LEFT 11//左红外 #define RIGHT 10//右红外 void run() //前进 { softPwmWrite(4,0); softPwmWrite(1,250); //左轮前进 softPwmWrite(6,0); softPwmWrite(5,250); //右轮前进 } void brake(int time) //停车 { softPwmWrite(1,0); softPwmWrite(4,0); softPwmWrite(5,0); softPwmWrite(6,0); delay(time100);//执行时间,可以调整 } void left() //左转 { softPwmWrite(4,250); //左轮后退 softPwmWrite(1,0); softPwmWrite(6,0); softPwmWrite(5,250);//右轮前进 } void right()//右转 { softPwmWrite(4,0); softPwmWrite(1,250);//左轮前进 softPwmWrite(6,250); //右轮后退 softPwmWrite(5,0); } void back()//后退 { softPwmWrite(4,250); //左轮后退 softPwmWrite(1,0); softPwmWrite(6,250); //右轮后退 softPwmWrite(5,0); } int main(int argc, char argv[]) { float dis int SR; int SL; wiringPiSetup(); /WiringPi GPIO/ pinMode (1, OUTPUT);//IN1 pinMode (4, OUTPUT);//IN2 pinMode (5, OUTPUT);//IN3 pinMode (6, OUTPUT);//IN4 softPwmCreate(1,1,500); softPwmCreate(4,1,500); softPwmCreate(5,1,500); softPwmCreate(6,1,500); while(1) { //有障碍物为LOW,没有障碍物为HIGH SR=digitalRead(RIGHT); SL=digitalRead(LEFT); if (SL==LOW&&SR==LOW) //前方有障碍物 { printf("BACK");//前面有物体时小车后退300ms再转弯 back(); delay(300);//后退300ms left();//左转600ms delay(600); } else if (SL==HIGH&&SR==LOW)//右方有障碍物 { printf("TURNING LEFT"); left(); delay(300); } else if (SR==HIGH&&SL==LOW)//左方有障碍物 { printf("TURNING RIGHT"); right (); delay(300); } else//前面没有障碍物则前进 { printf("GO"); run(); } } return 0; }本案例只使用了智能小车前方的两个红外避障传感器,经查阅表32可知,智能小车左侧红外传感器的wiringPi编号为11,右侧红外传感器的编号为10。当红外传感器检测到障碍物时,输出为LOW,否则为HIGH。读者可以进一步完善迷宫导航算法。 3.5.3二路循迹的实现 #include #include #include #include #include #define LEFT27//左循迹 #define RIGHT26//右循迹 void run() //前进 { softPwmWrite(4,0); softPwmWrite(1,250); softPwmWrite(6,0); softPwmWrite(5,250); } void brake() //停车 { softPwmWrite(1,0); softPwmWrite(4,0); softPwmWrite(5,0); softPwmWrite(6,0); } void left() //左转 { softPwmWrite(4,250); softPwmWrite(1,0); softPwmWrite(6,0); softPwmWrite(5,250); } void right()//右转 { softPwmWrite(4,0); softPwmWrite(1,250); softPwmWrite(6,250); softPwmWrite(5,0); } void back()//后退 { softPwmWrite(1,250); softPwmWrite(4,0); softPwmWrite(5,250); softPwmWrite(6,0); } int main(int argc, char argv[]) { float dis; int SR; int SL; wiringPiSetup(); pinMode (1, OUTPUT);//IN1 pinMode (4, OUTPUT);//IN2 pinMode (5, OUTPUT);//IN3 pinMode (6, OUTPUT);//IN4 softPwmCreate(1,1,500); softPwmCreate(4,1,500); softPwmCreate(5,1,500); softPwmCreate(6,1,500); while(1) { SR=digitalRead(RIGHT);//SR为LOW时表明在白色区域,否则表明压在 //黑线上 SL=digitalRead(LEFT); //SL为LOW时表明在白色区域,否则表明压在 //黑线上 if (SL==HIGH&&SR==HIGH)//在黑线上 { printf("GO"); run(); } else if (SL==HIGH&&SR==LOW) //左侧在黑线,右侧在白色区域,左转调整 { printf("LEFT"); left(); } else if (SR==HIGH&&SL==LOW)//右侧在黑线,左侧在白色区域,右转调整 { printf("RIGHT"); right(); } else//都是白色,停止 { printf("STOP"); brake(); } } return 0; }二路循迹利用位于智能小车前方底部的两个循迹传感器使智能小车沿着如图36所示的黑色赛道前进,经查阅表32可知,左侧的循迹传感器wiringPi引脚号为27,右侧循迹传感器引脚号为26,当引脚状态为HIGH时表明循迹传感器下方为黑色地面,否则为白色地面。 图36循迹赛道