树莓派小车实验
课程简介
这是我们大四上开设的一门实践课程,最基本的实现目标是实现简单的小车循迹,有点像上学期计控实验但换了个环境且不用简单的传感器实现。
感觉逻辑都差不多哈,检测到该拐了就拐就好。不过计控实验用的环境是类似于数电模块的绘图式编程,感觉更直观些。之后这个代码写完后就知道是不是一样了,感觉八九不离十。
硬件介绍
树莓派(主板)
小车已经搭建完毕
树莓派
系统基于Linux(从未接触过)
基于ARM的微型电脑主板,以SD/MicroSD卡为内存硬盘,卡片主板周围有1/2/4个USB接口和一个10/100 以太网接口(A型没有网口),可连接键盘、鼠标和网线,同时拥有视频模拟信号的电视输出接口和HDMI高清视频输出接口,以上部件全部整合在一张仅比信用卡稍大的主板上,具备所有PC的基本功能只需接通电视机和键盘,就能执行如电子表格、文字处理、玩游戏、播放高清视频等诸多功能。 Raspberry Pi B款只提供电脑板,无内存、电源、键盘、机箱或连线。
大概就是之前的实验都是在电脑上编程然后录入板子,这次是在板子上直接做,树莓派自己就是个主机,插个显示屏和键盘直接做就行了
前置工作
四个人一起买了一张sd卡,然后在sd卡上安装树莓派镜像文件,需要用到SDFormatter来格式化,以及32位的Diskmager来安装系统。
准备工作:
准备SD卡,8GB以上(8G即可),最好是高速卡(class4以上,建议用10);
下载树莓派镜像文件;
Windows下安装镜像的工具:Win32DiskImager。
安装步骤:
解压系统镜像压缩文件,得到img镜像文件;
将SD卡连接到电脑(读卡器或卡托),格式化SD卡,用PanasonicSDFormatter_ha软件;
解压并打开Win32DiskImager工具,选择小车系统的镜像文件,选择SD卡的盘符,写入;
把写好的卡装入树莓派;
注意事项:
Linux的分区在Windows下看不到,可以用分区软件看。所以安装完系统,内存卡显示剩余容量几十M是正常的;
Ext3属于Linux的文件系统;
swap区是Linux的虚拟内存区,在物理内存不够用的时候做缓存用。
安装完成后把卡插进小车,再去将树莓派连接到网络上,然后就可以使用Windows的远程桌面控制树莓派。
登录ID:pi
key:raspberry
在Linux中可以用cd命令切换目标目录
cd [dirName]
pwd:pwd命令以绝对路径的方式显示用户当前工作目录。(pwd = print working Directory)
总结:cd用来切换目录,pwd用来打印工作目录。这两个是初学Linux的第一个命令行,也是任何操作的基础。
在这里贴出Linux常用命令供之后参考使用
mkdir:mkdir命令用来创建目录。该命令创建由dirname命名的目录。如果在目录名的前面没有加任何路径名,则在当前目录下创建由dirname指定的目录;如果给出了一个已经存在的路径,将会在该目录下创建一个指定的目录。在创建目录时,应保证新建的目录与它所在目录下的文件没有重名。 (mkdir = make directory)
cp:cp命令用来将一个或多个源文件或者目录复制到指定的目的文件或目录。所有目标文件指定的目录必须是己经存在的,cp命令不能创建目录。如果没有文件复制的权限,则系统会显示出错信息。(cp = CoPy)
mv:mv命令用来对文件或目录重新命名,或者将文件从一个目录移到另一个目录中,将一组文件移至一个目标目录中。(mv = MoVe)
touch:touch命令有两个功能:一是用于把已存在文件的时间标签更新为系统当前的时间(默认方式),它们的数据将原封不动地保留下来;二是用来创建新的空文件。
rm:rm命令可以删除一个目录中的一个或多个文件或目录,也可以将某个目录及其下属的所有文件及其子目录均删除掉。对于链接文件,只是删除整个链接文件,而原有文件保持不变。(rm = ReMove)
注意:使用rm命令要格外小心。因为一旦删除了一个文件,就无法再恢复它。所以,在删除文件之前,最好再看一下文件的内容,确定是否真要删除。rm命令可以用-i选项,这个选项在使用文件扩展名字符删除多个文件时特别有用。使用这个选项,系统会要求你逐一确定是否要删除。这时,必须输入y并按Enter键,才能删除文件。如果仅按Enter键或其他字符,文件不会被删除。
小组内的同学对摄像头的试用
在代码编译完成后使用sudo命令运行
Linux sudo命令以系统管理者的身份执行指令,也就是说,经由 sudo 所执行的指令就好像是 root 亲自执行。
使用权限:在 /etc/sudoers 中有出现的使用者。
命令为
sudo ./hjduino 2000
下面贴上一些常用的快速指令方便之后查阅使用
摄像头
打开摄像头软件位置
cd mjpg-streamer/mjpg-streamer-experimental
打开摄像头指令,复制到LX终端
./mjpg_streamer -i “./input_raspicam.so” -o “./output_http.so -w ./www”
指令说明
xxx.c是要编译的c文件,xxx是编译出来的执行文件,运行编译出来的程序是sudo ./xxx
WIFI程序hjduino.c
编译指令
gcc hjduino.c -o hjduino -lwiringPi -lpthread
运行命令
sudo -/hjduino 2000
循迹程序xj.c
编译指令
gcc xj.c -o xj -lwiringPi -lpthread
运行命令
sudo ./xj
红外避障程序bz.c
编译指令
gcc bz.c -o bz -lwiringPi -lpthread
运行命令
sudo ./bz
测距程序cj.c
编译指令
gcc cj.c -o cj -lwiringPi
运行命令
sudo ./cj
超声避障pwm csb.c
编译指令
gcc csb.c -o csb -lwiringPi -lpthread
运行命令
sudo ./csb
停车 .c
编译指令
gcc stop.c -o stop -lwiringPi -lpthread
运行命令
sudo ./stop
Opencv及QT的安装
第一步先更换源,网上有清华源即输入
sudo nano /etc/apt/sources.lest
后以
deb http://mirrors.tuna.tsinghua
deb-src http://mirrors.tuna.tsin
代替,保存后退出。
由于有时候会由于内存问题导致编译被挂起,所以以
sudo nano /etc/dphys-swapfile
命令增加交换空间,将CONF_SWAPSIZE从默认值100更改为1024
保存后退出,并运行
sudo /etc/init.d/dphys-swapflie r
使其生效。
之后就可以下载opencv了。
先下载库及一些需要的工具,由于库要从Github捞出来,而Github服务器不在国内甚至不支持断点续传(仿佛回到了前几年的steam),只能靠拼脸下载好再拷贝到树莓派上。
下载好了就可以开始编译,运行安装,检查所需要的库有没有安装成功即可。
然后是QT的安装,相较opcv要简便很多,Opencv抓到的图经处理后我们在qt中编译。
主程序
首先我们对速度进行百分化处理,这样我们只需输入0-100间的数字来调整速度,便于我们把握速度快慢的程度。
在此只贴出前进的修改逻辑,左右转什么的都一样。其实没什么作用只是自己看的舒服方便后续编程。
void run(int n) // 前进函数,100 is maxium.
{
softPwmWrite(4,0); //左轮前进
softPwmWrite(1,5*n);
softPwmWrite(6,0); //右轮前进
softPwmWrite(5,5*n);
printf("run");
delay(100);
}
我们的想法是在正前方设置合理的距离,当在这个距离内时说明要拐弯,再去做左右判断。
由于我们Opencv处理后的图像为320*240,所以经过参数调试后我们选取120-200行的160列(即中间列)作为拐弯的判断距离。
由于防止噪声的影响,我们设置只有出现四个黑像素点时才判定为需要拐弯,此时将拐弯标志位设置为1,同时还可以获得到底是第几行出现黑点,将其返回为black_row,需要时可以打印。
for(int i = 210,j = 0;i>=118;i--)
{
if(f2.ptr<uchar>(i)[160] == 0)
j++;
if (j>=4)
{
black_row = i;
center_black=1;
j=0;
break;
}
}
当判断出需要拐弯时,将发现的行数向下移动25格,再由中间向两边检索看边线出现在哪一边。判断逻辑同前。
if(center_black)
{
for(int i =160,j=0;i<=320;i++)
{
if(f2.ptr<uchar>(black_row+40)[i]==0)
{
j++;
}
if(j>=4)
{
right_detected = 1;
break;
}
}
得到置为1的标志位right_detected,在此情况下是160-320列检测到边线,即需要左拐。所以执行
left(TURNSPEED);
left_detected = 0;
right_detected = 0;
center_black = 0;
请务必记得将标志位清零。右拐同理。
以上编程主要由组内胡jh同学完成。
但在试跑后我发现,会出现两种意料外的情况:
①如果小车速度偏快而来不及处理左右拐时,边线会到120-200行下方,这样小车会认为前方不需要拐弯就根本不会进入左右拐程序。
②小车在跑直线时如果有一点偏移,而前方是空白的赛道,那么小车会继续偏移压线直到离边线很近时将其识别为弯道,再作左右拐。
两种情况都如下图Gif所示:
而在正常前进与拐弯时我注意到,由于我们摄像头位置较低,小车的最下面一行是不会出现黑色区域的。如下图Gif及图片所示:
所以我认为可以由最后一行再做一个保底判断,当最后一行检测到赛道时,将其归入一定要拐弯的情况,再根据检测到赛道的区域来左右拐。
所以我新设置了两个标志位left_danger和right_danger,作二次判断来保证不会出现以上两种情况。
for(int i = 20,j = 0;i <300;i++)
{
if(f2.ptr<uchar>(239)[i] == 0)
j++;
if (j>=3)
{
danger_row = i;
if (danger_row > 160)
{
right_danger = 1;
}
else if(danger_row < 160)
{
left_danger = 1;
}
break;
}
}
在做完第一种逻辑判断后,进入保底逻辑判断,再决定是否拐弯
if (left_danger)
{
right(TURNSPEED);
cout<<"danger_right"<<endl;
left_danger = 0;
}
else if (right_danger)
{
left(TURNSPEED);
cout<<"danger_left"<<endl;
right_danger = 0;
}
else
{
run(RUNSPEED);
cout<<"else_run"<<endl;
}
即先后经过两次检测:
if(center_black)
{
for(int i =160,j=0;i<=320;i++)
{
if(f2.ptr<uchar>(black_row+25)[i]==0)
{
j++;
}
if(j>=3)
{
right_detected = 1;
break;
}
}
for(int i =160,j=0;i>0;i--)
{
if(f2.ptr<uchar>(black_row+25)[i]==0)
{
j++;
}
if(j>=3)
{
left_detected = 1;
break;
}
}
if (left_detected&&right_detected)
{
run(RUNSPEED);
left_detected = 0;
right_detected = 0;
center_black = 0;
}
else if (right_detected)
{
left(TURNSPEED);
left_detected = 0;
right_detected = 0;
center_black = 0;
}
else {
right(TURNSPEED);
left_detected = 0;
right_detected = 0;
center_black = 0;
}
}
else
{
if (left_danger)
{
right(TURNSPEED);
cout<<"danger_right"<<endl;
left_danger = 0;
}
else if (right_danger)
{
left(TURNSPEED);
cout<<"danger_left"<<endl;
right_danger = 0;
}
else
{
run(RUNSPEED);
cout<<"else_run"<<endl;
}
}
此部分编程由我完成。
完整代码
见汇报ppt。
其实也不差什么了,就剩头文件和变量初始化的内容以及最后停止防止他乱跑加入的brake。
实验心得:
本次实验是第一次以pc端外的独立主机编程的实验,但本质上没有区别。我发现巡线的准确率与反馈的信号速率也相关,由于存在一定延迟,我们将检测的线稍稍提前,来适应这一段误差。通过这次实验我们加强了逻辑能力与编程技巧(虽然没有什么复杂的地方),但是我确实学到了!提高了!嗯。
感谢老师给我们的教导与及时答疑,在查阅中我了解到树莓派几年前本是大家都想要却很难获取到的东西,这足以说明其完善与普适性。感谢老师提供给我们这种机会来训练我们的综合能力,提前预祝老师新年快乐。