这篇文章主要是对基于安卓的单臂机器人软件方面设计和调试做阐述,设计用到的软件主要有Android Studio,Keil uVision;调试用到的软件主要有STC烧录软件的串口调试助手、putty(用于登录路由器)、WinSCP(对路由器的文件系统进行配置管理)、tcp232-test(一款专门用于测试网络-串口通信的软件)。
系统软件设计
整体方案
因为经过权衡之后,发现本设计的难点是APP的制作、上位机和下位机的通信以及监控视频的解码显示,所以前期一边设计硬件电路一边学习APP的制作,但重心放在APP的制作上。做好APP的时候就开始根据电路原理图写单片机程序,为了实现两者之间的通信,要事先制定通信协议,通信成功后,然后APP和单片机联合进行各个模块的调试,先是小车行驶控制,循迹控制,避障控制,机械臂控制,把摄像头实时监控放到最后。
通信协议
通信协议的作用是让下位机知道上位机需要它完成什么指令,根据实际需要和方便下位机的判断,我规定通信协议数组长度为5,以十六进制0XFF作为开头结尾,命令放在中间,如0XFF,0X00,0X00,0X00,0XFF表示正常模式。其中,为了能在APP上控制行驶速度,我将通信协议第3个空出来发送速度的百分比,单片机程序中将百分比取出来用来设置pwm波的占空比,从而控制机器人的速度。另外,为了便于下位机的判断,模式选择指令以0X00开头,机器人行驶指令以0X10开头,机械臂动作指令以0X11开头。之所以把灯控和调速设为和正常模式优先级相同,是因为考虑到在任何情况下都可以灯控和调速。
上下位机通信协议:
APP程序设计
APP主要是仿照QQ登录设计四个界面,第一个界面是一张简单的图片,展示三秒后跳到登录界面,登录界面里用来输入WiFi模块的IP地址和端口,还有取视频的网络地址,之后就是正在登录界面,进度条到百分之一百后就跳到控制界面,需要注意的是各个界面间需要传递WiFi模块的IP地址和端口,还有取视频的网络地址。在控制界面里创建Socket通信,还需要为各个按钮、多选框和单选框设置监听器,只要选中这些控件,就分别新建一个线程将设置好的指令发送出去,为方便操作,控制界面设置为横屏,控件的布局尽量有序,大小适中,为了方便修改通信协议,在程序里我把通信协议放到数组里,每次发送都是发送一个数组。
App程序流程图:
单片机程序设计
根据通信协议,单片机需要通过串口判断出APP发送来的指令,单片机接收到数据后,先要判断是不是帧头,是的话再将后面的数据存到数组中,直到再次接收到0XFF,一帧数据接收完毕,再根据命令执行动作。
单片机需要体现它模块化设计的思想,将各个模块程序单独放在.h头文件中,方便其它程序进行调用。因为设置了三种模式,所以主程序初始化各模块之后循环判断上位机需要执行的模式,然后再根据模式的种类去执行相应的动作,其中调速和灯光控制优先级和模式判断一样,主要是为了方便在每一个模式中都可以进行调速和灯光控制。正常模式里可以控制小车的运动和机械臂的动作,循迹模式里小车能够循迹和避障,避障模式里小车只能避障,在后两种模式中不能控制小车运动和机械臂动作。
单片机主程序流程图:
系统软件调试
路由器软件调试
先用软件putty登录路由器看摄像头是否挂载成功,选择登录方式为SSH,输入指令查看摄像头是否挂载成功,出现video说明挂载成功。
摄像头挂载:
为了能正常浏览视频和下位机正常通信,需要分别设置安装在路由器上面的软件mjpg-stream和ser2net。它们都可以通过软件winscp登录到路由器的文件管理系统进行更改,输入指令查看是否修改成功,然后再重启路由器。经过测试,摄像头分辨率支持320240,640480,其中640*480时画面有闪屏和黑屏,获取录像的端口也可以修改;通信波特率最低为9600,最高为57600,设为其它数值时都不能正常通信。
mjpg-stream设置:
ser2net设置:
单片机和APP通信调试
单片机串口通信调试
目的是看看单片机的串口能不能收发数据,为的是下一步APP和单片机可以实现通信。预期结果:电脑给单片机发规定长度的数据,单片机都将数据通过串口原封不动返回给电脑,串口通信成功,用到的软件是STC烧录软件的串口调试助手。
串口通信测试代码:
#include <STC15F2K60S2.H>
#define fosc 12000000
#define baud 9600
#define com_long 5 //通信协议命令数组的长度,一个汉字占两个十六进制,占四个二进制,命令组的长度要是中文字符的个数的两倍
bit flag=0;
unsigned char a[com_long];
int i=0,j,k=1;
void UartInit(void) //9600bps@12.000MHz
{
PCON &= 0x7F; //波特率不倍速
SCON = 0x50; //8位数据,可变波特率
T2L=65536-fosc/baud/4; //设定独立波特率发生器重装值
T2H =(65536-fosc/baud/4)>>8;
AUXR |= 0x15; //串口1选择独立波特率发生器为波特率发生器,启动独立波特率发生器,独立波特率发生器时钟为Fosc,即1T
ES=1; //打开串口中断
EA=1;
}
void main()
{
UartInit();
while(1)
{
if(a[0]==0xff&&a[4]==0xff&&flag==1)
{ ES=0; //不关中断的话不行
for(j=0;j<com_long;j++)
{
SBUF =a[j];
while(TI==0);
TI=0;
}
flag=0;
ES=1;
}
}
}
void s1interrupt() interrupt 4
{
/* RI=0;
if(flag==0)
{
a[i]=SBUF;
i++;
if(i==com_long)
{
flag=1;
i=0;
}
}
*/
unsigned char temp;
RI=0;
temp=SBUF;
if(temp==0XFF && i<2)
{
a[0]=0XFF;
i++;
}
else
{
a[k]=temp;
k++;
}
if(i==2)
{
a[0]=0XFF;
a[4]=0XFF;
i=0;
k=1;
flag=1;
}
}
调试结果(电脑给单片机发规定长度的数据,单片机都将数据通过串口原封不动返回给电脑):
APP通过Socket与单片机通信调试
原理:这里因为我们的路由器刷了固件的,路由器就成了Socket的服务端,我们手机连上它后就相当于是客户端。手机通过Socket发送指令给服务端,服务端(路由器)通过安装在它上面的软件ser2net将网络指令转成串口信号给单片机,这里调试用tcp232-test。
App上Socket相关代码:
//开辟新线程打开socket
Thread connect_Thread1 = new Thread(socket_init_runn);
connect_Thread1.start();
//runnable重写run方法,carIP是路由器的IP,carPort是路由器的端口,这些在路由器调试那里配置
Runnable socket_init_runn =new Runnable() {
@Override
public void run() {
try {
clientSocket = new Socket(InetAddress.getByName(carIP), Integer.parseInt(carPort));
writer = clientSocket.getOutputStream();
} catch (Exception e) {
e.printStackTrace();
}
}
};
App上的通信协议(每按一个按钮,发送相关协议指令就可以了,这里代码太多,只以发送机器人前进的指令为例):
//通信协议
private byte carForward[] = new byte[]{(byte) 0xFF, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0XFF};
//这里不懂为啥要开个线程来发送指令,不开的话不成功
Thread carForward_Thread = new Thread(carForward_runn);
carForward_Thread.start();
Runnable carForward_runn =new Runnable() {
@Override
public void run() {
try {
writer.write( carForward);//把机器人前进的指令写进去
writer.flush();//记得要flush一下
} catch (Exception e) {
e.printStackTrace();
}
}
};
得到的结果如下(每次按APP上的相关按钮都会发送相应的指令,与通信协议符合,WiFi的串口与网络数据传输成功):
APP视频解码
这是重点也是难点,装在路由器的软件Mjpg-Streamer是一个开源项目,允许我们以HTTP方式访问linux UVC兼容摄像头,其基本功能是从一个uvc内核摄像头读取内容,然后将它推送到本地的8080端口上面,就是一个本地的视频服务器。我们手机APP连上WIFI后,这个软件就把摄像头的视频传回来,我们在APP上面得到的是一张张图片,这就需要我们通过一定的方法将图片解码画在画布上,形成视频的效果。
视频解码关键代码:
//视频解码部分,回传回来的内容是以 content_length: 开头的,这里用到状态机的思路
mythread thread;
class mythread extends Thread{
SurfaceHolder holder;
boolean reflsh=true;
int bufsize=512*1024;//视频图片缓冲区长度
byte[] jpg_buf=new byte [bufsize];//视频图片缓存
int readsize=4096;//每次获取流的最大长度
byte [] buffer=new byte[readsize];
public mythread(SurfaceHolder holder){
this.holder=holder;
}
public void run() {
super.run();
while (reflsh){
URL murl=null;
HttpURLConnection mconnect=null;
try {
murl=new URL(cameraIP);
} catch (MalformedURLException e) {
e.printStackTrace();
}
try {
mconnect =(HttpURLConnection) murl.openConnection();
int read=0;
int status=0;
int jpg_count = 0;
while(cameraSwitch){
read =mconnect.getInputStream().read(buffer,0,readsize);
if(read>0){
for(int i=0;i<read;i++){
switch (status){
case 0:
if(buffer[i]==(byte) 'C')
status++;
else
status=0;
break;
case 1:
if(buffer[i]==(byte) 'o')
status++;
else
status=0;
break;
case 2:
if(buffer[i]==(byte) 'n')
status++;
else
status=0;
break;
case 3:
if(buffer[i]==(byte) 't')
status++;
else
status=0;
break;
case 4:
if(buffer[i]==(byte) 'e')
status++;
else
status=0;
break;
case 5:
if(buffer[i]==(byte) 'n')
status++;
else
status=0;
break;
case 6:
if(buffer[i]==(byte) 't')
status++;
else
status=0;
break;
case 7:
if(buffer[i]==(byte) '-')
status++;
else
status=0;
break;
case 8:
if(buffer[i]==(byte) 'L')
status++;
else
status=0;
break;
case 9:
if(buffer[i]==(byte) 'e')
status++;
else
status=0;
break;
case 10:
if(buffer[i]==(byte) 'n')
status++;
else
status=0;
break;
case 11:
if(buffer[i]==(byte) 'g')
status++;
else
status=0;
break;
case 12:
if(buffer[i]==(byte) 't')
status++;
else
status=0;
break;
case 13:
if(buffer[i]==(byte) 'h')
status++;
else
status=0;
break;
case 14:
if(buffer[i]==(byte) ':')
status++;
else
status=0;
break;
case 15:
if(buffer[i]==(byte) 0xff)
status++;
jpg_count=0;
jpg_buf[jpg_count++]=(byte)buffer[i];
break;
case 16:
if(buffer[i]==(byte) 0xD8){
status++;
jpg_buf[jpg_count++]=(byte) buffer[i];
}
else {
if(buffer[i]!=(byte)0xff)
status=15;
}
break;
case 17:
jpg_buf[jpg_count++]=(byte)buffer[i];
if(buffer[i]==(byte) 0xff)
status++;
if(jpg_count>=bufsize)
status=0;
break;
case 18:
jpg_buf[jpg_count++]=(byte)buffer[i];
if(buffer[i]==(byte) 0xD9){
status=0;
{
Canvas mcanvas=msurfaceholder.lockCanvas();
//mcanvas.drawColor(Color.WHITE);
Bitmap bm= BitmapFactory.decodeStream(new ByteArrayInputStream(jpg_buf));
//int left=random.nextInt(mcanvas.getWidth());
//int top=random.nextInt(mcanvas.getHeight());
//mcanvas.drawBitmap(bm,left,top,new Paint());
int width=bm.getWidth();
int height=bm.getHeight();
int newWidth=2160;
int newHeight=1080;
float scaleWidth=((float)newWidth)/width;
float scaleHeight=((float)newHeight)/height;
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth,scaleHeight);
bm=Bitmap.createBitmap(bm,0,0,width,height,matrix,true);
bm.getWidth();
bm.getHeight();
Log.e("newWidth","newWidth"+bm.getWidth());
Log.e("newHeight","newHeight"+bm.getHeight());
mcanvas.drawBitmap(bm,0,0,new Paint());
msurfaceholder.unlockCanvasAndPost(mcanvas);
try {
Thread.sleep(1000/800);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
else if((buffer[i]!=(byte)0xff))
status=17;
break;
default:
status=0;
break;
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
void mystop(){
reflsh=false;
}
}
机械臂软件调试
这部分是单片机部分的重点难点,如何由一个定时器产生四路PWM给四个舵机使用,这部分可以参考利用舵机制作简单机械臂
最后,因为我自己个人能力有限,如果以上所写有不妥之处,希望各位朋友批评指正,一起进步。单片机的超声波模块代码,循迹代码以及工程文件等我就不分享了,只是希望借此能给一些朋友提供解决问题的思路,如果想了解更多关于机器人的资讯,可以访问:
视频:第三届型车型秀大赛
视频:零基础入门学用 Arduino 教程 - MeArm篇 开发机械臂程序-
资源:小R科技-WIFI机器人网·机器人创意工作室中国FPV-WIFI机器人网·机器人创意工作室