QT+树莓派+STM32 搭建远程监控小车

一.描述

     由于最近大创项目需要用到树莓派采集视频图像,并且突然看到了桌子底下吃灰了一年多的坦克小车,想当时还是400块大洋买的,现在想来也是心在滴血。所以想把两者结合起来,花了两天时间做了个简单的QT远程监控小车。来抚平一下我内心的伤痛,要不是大创项目,想来我的树莓派也要吃灰。
     言归正传,本次小项目主要实现了一个基于树莓派和Qt上位机的远程监控小车。可以通过自制的遥控器和QT上位机实现两种控制方式,Qt上位机可以实时查看小坦克上的摄像头拍摄的画面。Qt上位机的指令通过socet 协议发送给树莓派,树莓派再将指令通过串口转发给STM32,STM32定时器输出PWM控制小车前进,并配以PID算法使小车能够走直线。其具体的结构框图如下所示:
系统整体框架

二. 环境概述

硬件:坦克小车主控 STM32F103RCT6
          遥控器主控:坦克小车主控 STM32F103RCT6
          树莓派:树莓派4B,官方CSI摄像头
软件版本:Keil5(5.30)
                  Qt5.9(5.90)
                  VS Code(1.52.1)
操作系统:Windows10 家庭版
                  树莓派镜像:2020-12-02-raspios-buster-armhf.img

三.效果展示

1.遥控器

     遥控器是自己做画的PCB做的板,有两层叠层的。(我应该拍一个立体效果的,哈哈)。右边四个按键可控制小车动向,左边四个按键的选择菜单。遥控器采用锂电池供电。
坦克小车遥控器

2.坦克小车全身照

     看起来接线有点乱啊,好久以前画的最小系统和电源板,现在来看的话应该画到一起的。那个绿色便是树莓派了。
坦克小车全身照

3.QT上位机控制界面

     Qt编写的上位机界面也比较简单,刚开始学,很多高级应用也没有使用到,左边窗口是树莓派摄像头传过来的实时监控画面。右边部分是控制界面,下面的输入框要输入正确的服务器IP和端口号。



Qt上位机界面

4.服务器运行界面

系统采用树莓派作为本地服务器,服务器使用C语言编写,启动过程中首先启动树莓派服务器,在启动 qt控制界面输入IP进行连接,连接成功后会出现如图所示的现象。
服务器运行界面


四.各部分具体实现过程

1.树莓派部分

     本篇默认大家的树莓派环境已经进行过换源等操作,并且已经安装了GCC、VIM、locate等工具。

a.MJPG-streamer的安装和使用

     MJPG-streamer是一款免费基于IP地址的视频流服务器,它的输入插件从摄像头读取视频数据,这个输入插件产生视频数据并将视频数据复制到内存中,它有多个输出插件将这些视频数据经过处理,其中最重要的输出插件是网站服务器插件,它将视频数据传送到用户浏览器中。

首先安装相应的依赖库:

sudo apt-get install subversion libjpeg8-dev imagemagick libv4l-dev cmake git

然后从github上 下载 MJPG-streamer,并编译, 安装

git clone https://github.com/jacksonliam/mjpg-streamer.git
cd mjpg-streamer/mjpg-streamer-experimental/  #进入文件夹
make all #编译安装
sudo make install

若是普通的免驱动摄像头,执行以下命令

./mjpg_streamer -i "./input_uvc.so" -o "./output_http.so -w ./www"

若是树莓派自带的CSI摄像头,执行以下命令

./mjpg_streamer -i "./input_raspicam.so" -o "./output_http.so -w ./www"

执行完以后,使用谷歌浏览器输入:树莓派的内网IP:8080便可以查看监控画面。

若是查看不到监控画面,可能存在的原因如下:
① 摄像头未使能
② 使能后,报错如下:
mmal: mmal_vc_component_create: failed to create component ‘vc.ril.camera’ (1:ENOMEM)
mmal: mmal_component_create_core: could not create component ‘vc.ril.camera’ (1)
mmal: Failed to create camera component
mmal: main: Failed to create camera component
mmal: Camera is not detected. Please check carefully the camera module is installed correctly
采用 vcgencmd get_camera命令查看后得到 supported=1 detected=0 ,大概率是摄像头硬件连接问题
重新拔插摄像头看是否解决

③ 得到 supported=1 detected=1后,说明摄像头已经连接正常,但是调用 raspistill -v -o test.jpg
后报错如下:
mmal: mmal_vc_component_enable: faiLED to enable component: ENOSPC
mmal: camera component couldn’t be enabled
mmal: main: Failed to create camera component
mmal: Failed to run camera app. Please check for firmware updates
则可能是有多个进程在使用摄像头,使用top命令查看是否有在后台占用摄像头的进程,使用kill
将其杀掉,再次运行raspistill,成功拍照

b…MJPG-streamer开机自启动

     若是执行完操作后看到了监控画面,则我们可以让树莓派开机自启动该工具。首先,在程序make目录下新建脚本文件open_mjpg_streamer.sh,名字随便取
我的路径是:/home/pi/mjpg-streamer-master/mjpg-streamer-experimental
然后打开文件:sudo nano open_mjpg_streamer.sh,添加内容如下:
./mjpg_streamer -i “./input_raspicam.so” -o “./output_http.so -w ./www”
echo “mjpg_streamer is runing” (输出提示信息)
然后在 /etc/rc.local 中加入以下内容:
cd /home/pi/mjpg-streamer-master/mjpg-streamer-experimental
sh open_mjpg_streamer.sh &
cd -
重启树莓派,大功告成。

c.树莓派服务器部分的实现

     树莓派用C语言上写了一段程序,主要实现对客户端发送过来的数据进行接收和转换,并通过串口发送给单片机。由于接收的是字符串,需要将接受的指令转换为16进制发送给单片机,指令的具体数据帧含义如下:

①指令数据帧

0XAA(帧头)
0X00——0X04(指令)依次为 :停止 前进 左转 后退 右转
0XDD(帧尾)

②文件详细和说明

在这里插入图片描述

TcpServer.c:服务器与客户端采用Socket通信,我主要是在以前华清远见的老师给的历程上修改的。仅供学习使用,在这就不贴出来了。
Tools.c:16进制字符串转16进制函数。具体实现方式如下:

#define IS_POINT 2
#define IS_NUM 1
#define IS_CHAR 0
#define MXA_STR_LEN 50

/*=========================================================
Function Name:ToolsHexStrToHex
Description:将含16进制的字符串转化为16进制数组
Input:HexStr:输入16进制字符串 HexNumBuff:输出10进制字符串
Output:无
Notes:转化的最长字符串由STRHTOT_MAX_LEN决定,最好不要太长!!!
===========================================================*/
int ToolsHexStrToHex(const char *HexStr, int *HexNumBuff)
{
    const char *TempStr;
    int *TempNumarr;
    char *Rest;
    char Array[MXA_STR_LEN] = {0};
    int n = 0, i = 0, k = 0, m = 0;

    //防止越界
    if (strlen(HexStr) >= MXA_STR_LEN)
    {
        return -1;
    }

    TempStr = HexStr;
    TempNumarr = HexNumBuff;

    while (*TempStr != '\0')
    {
        while ((*TempStr != ' ') && (*TempStr != '\0'))
        {
            Array[k] = *TempStr;
            k++;
            TempStr++;
        }
        if ((*TempStr == ' ') || (*TempStr == '\0'))
        {
            Array[k] = '\0';
            *TempNumarr = strtol(Array, &Rest, 16);
            TempNumarr++;
            k = 0;
            if (*TempStr != '\0')
            {
                TempStr++;
            }
        }
    }

    return 0;
}


static int isCharOrNum(char ch)
{
    if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F'))
    {
        return IS_NUM;
    }
    else if (ch == '.')
    {
        return IS_POINT;
    }
    else
    {
        return IS_CHAR;
    }
 }  

RaspiCarCtrl.c:树莓派端主文件,通过调用前面的文件中的函数,实现接收数据并串口转发给树莓派的功能。

#include <wiringPi.h>
#include <unistd.h>
#include <wiringSerial.h>
#include <string.h>

#include "/home/pi/QtRaspiCarCtrol/TcpServer.h"
#include "Tools.h"

#define BAUDRATE    115200

int main()
{
    int Socket_fd;  //socket 
    char RevData[50]; 
    int HexNumBuff[50] = {0};
    int fd;

    if(wiringPiSetup() == -1)
    {
	printf("Init Pin Failed\n");
	return (0);
    }

    fd = serialOpen("/dev/ttyAMA0",BAUDRATE);
    if(fd == -1)
    {
        printf("Open uart filed!\n");
	return (0);
    }
	
InitSocketServer:
    Socket_fd = TcpServerInit(8888,20);
    if(Socket_fd == -1)
    {
        printf("Server init failed\n");
        goto InitSocketServer;
    }
    printf("wait for a client\n");
    int connfd = TcpServerWaitConnect(Socket_fd) ;
        
    if(connfd == -1)
    {
       printf("client connect failed\n");
       close(Socket_fd);
       goto InitSocketServer;
    }

	
    int iReturn = 0 ;
    while(1)
    {
	memset(RevData,0,sizeof(RevData));
        memset(HexNumBuff,0,sizeof(HexNumBuff));
		
		
        iReturn = TcpServerRecieve(connfd,RevData,sizeof(RevData));
        if(iReturn > 0)
        {
	    ToolsHexStrToHex(RevData,HexNumBuff);
		
            for(int i =0; i<3; i++)
	    {
                printf("0x%X\n",HexNumBuff[i]);
		serialPutchar(fd,HexNumBuff[i]++);
            }
        }
	else
	{
            serialClose(fd);
	    close(Socket_fd);
	    goto InitSocketServer;
	}

    }
    serialClose(fd);	
    close(connfd);
    TcpServerDisconnect(Socket_fd);
    return 0 ;
}
③树莓派串口部分

第一次使用树莓派的串口,可以进行一下操作:
树莓派串口通信(参考博客:https://blog.csdn.net/weixin_42108484/article/details/104039992)

1.树莓派有两个串口外
一个是硬件串口(/dev/ttyAMA0),另一个是mini串口(/dev/ttyS0)。
硬件串口有单独的波特率时钟源,性能好,稳定性强;mini串口功能简单,稳定性较差,波特率由CPU内核时钟提供,受内核时钟影响。
树莓派(3/4代)板载蓝牙模块,默认的硬件串口是分配给蓝牙模块使用的,而性能较差的mini串口是分配给GPIO串口 TXD0、RXD0。

2.更换串口映射
ls /dev -al命令查看到默认的串口分配方式
sudo raspi-config 关闭串口登录功能,使能硬件串口功能
配置GPIO串口:接着将串口配置为我们的GPIO串口,对输入sudo vim /boot/config.txt命令,
将下面两行内容添加到最后:dtoverlay=miniuart-bt(树莓派4)
重启树莓派,再次查看串口分配

采用minicom串口助手测试串口
首先输入sudo apt-get install minicom命令安装minicom,
安装完成后,输入minicom -D /dev/ttyAMA0 -b 9600启动minicom,测试。

gcc myuart.c -o runuart.o -l wiringPi 记得连接库编译

2.QT上位机部分

qt上位机写的比较简单,主要采用了QWebEngineView控件打开由MJPG-streamer传来的视频流。
其文件结构如下:
在这里插入图片描述

源代码如下(mainwindow.cpp):

#include "mainwindow.h"
#include "ui_mainwindow.h"


//主窗口
MainWindow::MainWindow(QWidget *parent):
      QMainWindow(parent),
      ui(new Ui::MainWindow)
{
    this->setFixedSize(1200,600); //设置窗体固定大小

    ui->setupUi(this);
    socket = NULL;

    //树莓派摄像头远程监控
    view = new QWebEngineView(this);
    view->load(QUrl("http://47.94.47.152/Cam.html")); //访问html配置文件
    view->resize(800,600);  //设置控件界面大小
    view->show();
}


void MainWindow::resizeEvent(QResizeEvent *)   //设置视频显示随窗口大小自动变化
{
    //view->resize(this->size());
}

MainWindow::~MainWindow()
{
    delete ui;
}

//==============================按键发送指令=================================
//前进按键按下
void MainWindow::on_pushButton_pressed()
{
    if(NULL == socket)
    {
        qDebug() << "Please connect" ;
        this->close();
    }
    else
    {
        socket->write("AA 01 DD",50);  //发送指令
        ui->textEdit->append("前进\n");
    }
}

//左转按键按下
void MainWindow::on_pushButton_2_pressed()
{
    if(NULL == socket)
    {
        qDebug() << "Please connect" ;
        this->close();
    }
    else
    {
        socket->write("AA 02 DD",50);  //发送指令
        ui->textEdit->append("左转\n");
    }
}

//后退按键按下
void MainWindow::on_pushButton_3_pressed()
{
    if(NULL == socket)
    {
        qDebug() << "Please connect" ;
        this->close();
    }
    else
    {
        socket->write("AA 03 DD",50);  //发送指令
        ui->textEdit->append("后退\n");
    }
}

//右转按键按下
void MainWindow::on_pushButton_4_pressed()
{
    if(NULL == socket)
    {
        qDebug() << "Please connect" ;
        this->close();
    }
    else
    {
        socket->write("AA 04 DD",50);  //发送指令
        ui->textEdit->append("右转\n");
    }
}

//创造按键一直按着小车才会一直走的效果
void MainWindow::on_pushButton_released()
{
    socket->write("AA 00 DD",50);  //发送指令
    //ui->textEdit->append("停止\n");
}

void MainWindow::on_pushButton_2_released()
{
    socket->write("AA 00 DD",50);  //发送指令
    //ui->textEdit->append("停止\n");
}

void MainWindow::on_pushButton_3_released()
{
    socket->write("AA 00 DD",50);  //发送指令
    //ui->textEdit->append("停止\n");
}

void MainWindow::on_pushButton_4_released()
{
    socket->write("AA 00 DD",50);  //发送指令
    //ui->textEdit->append("停止\n");
}

//============================================================================

//链接服务器
void MainWindow::on_LinkBtn_clicked()
{
    if(NULL == socket)
    {
        socket = new QTcpSocket();
        socket->connectToHost(ui->lineEdit->text(),ui->lineEdit_2->text().toUShort());
    }
    ui->textEdit->append("服务器连接成功\n");

    socket->write("AA EA DD",50);  //发送指令
}

3.遥控器和坦克小车部分

这两部分都是采用STM32F103RCT6为主控,代码较为简单,代码量有点多。所以就不全部进行粘贴了,需要完整工程的可以和我说。

遥控器和小车主要通过NRF2401无线模块进行通信,该模块与单片机使用硬件SPI通信,遥控器通过在定时器里定时调用发送函数发送指令数据,在主循环中通过按键改变要发送的数据。而在坦克小车中,通过串口三和SPI1分别接收来自树莓派和遥控器的数据,QT上位机连接树莓派服务器的一瞬间便会发送(0XAA 0XEA 0XDD)指令,单片机检测到之后便会先隔断遥控器对坦克小车的控制。
遥控器main函数部分截图
在这里插入图片描述
定时器中断里面部分代码截图
在这里插入图片描述
发送数据
在这里插入图片描述
坦克小车部分:

main.c

int main()
{
     NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2) ;    //中断优先级分组
	 delay_init() ;                          //延时函数初始化
	 uart_init(9600) ;                       //串口1初始化
     USART3_Init(115200);
	 MOTOR_PORT_INIT() ;                     //坦克电机驱动初始化
	 Nrf_Re_Init() ;                         //无线模块接受模式初始化
     All_Timer_Init() ;                      //定时器初始化
    
     Real_Speed = MEDD_SPEED_PWM ;
    
	 while(1)
	 {
         //printf("%d\r\n",tmp_buf[2]);
         
//         if(tmp_buf[2] == LOW_SPEED)       //调节速读
//         {
//             Real_Speed = LOW_SPEED_PWM ;
//             printf("L");
//         }
//         
//         else if(tmp_buf[2] == MID_SPEED)
//         {
//             Real_Speed = MEDD_SPEED_PWM ;
//             printf("M");
//         }
//         
//         else if(tmp_buf[2] == HIG_SPEED)
//         {
//             Real_Speed = HIGH_SPEED_PWM ;
//             printf("H");
//         }
         
		 if(NRF24L01_RxPacket(tmp_buf)==0 && gIsQtControlFlag == 0)//NRF2401接收到数据
		 {
			    switch(tmp_buf[1])
				{
				   case 0X10 :
				   {
				        Go_Ahead(TIM3, Real_Speed) ;
                        //printf("go ahead\n");
				       break ;
				   }
				   
				   case 0X11 :
				   {
				       Go_Back(TIM3, Real_Speed) ;
                       //printf("go back\n");
				       break ;
				   }
				   
				   case 0X12 :
				   {
				       Trun_Right(TIM3) ;
                       //printf("go right\n");
				       break ;
				   }
				   
				   case 0X13 :
				   {
				       Turn_Left(TIM3)  ;
                       //printf("go left\n");
				       break ;
				   }
				   
				   default:
					    Tank_Stop() ;
				       break ;  	
				}
		}
         
        else if(gIsQtControlFlag == 1)
        {
              switch(gQtCmdData)
              {
                  case 0x01: 
                  {
                      //问题############2020-12-15 ,随意设置前进速度不变?
                      Go_Ahead(TIM3, 4000) ;  
                      printf("go\r\n");
                      break;
                  }
                  case 0x02: 
                  {
                      Turn_Left(TIM3)  ;
                      printf("left\r\n");
                      break;
                  }
                  case 0x03: 
                  {
                      Go_Back(TIM3, 4000) ;
                      printf("back\r\n");
                      break;
                  }
                  case 0x04: 
                  {
                      Trun_Right(TIM3) ;
                      printf("right\r\n");
                      break;
                  }
                  default:
                      Tank_Stop() ;
                      break;
              }
        }
            
	}		
	 
}

串口三定时器中断里面代码

u8  USART3_RX_BUFF[MAX_BUFF_LENGTH];  //串口3接收缓存区
u16 USART3_RX_CNT=0;

uint8_t gIsQtControlFlag = 0 ;
uint8_t gQtCmdData = 0;




void USART3_IRQHandler()
{
    if(USART_GetITStatus(USART3,USART_IT_RXNE) != RESET) //中断产生
    {
        USART3_RX_BUFF[USART3_RX_CNT] = USART_ReceiveData(USART3) ;
        USART3_RX_CNT ++ ;
        USART_ClearITPendingBit(USART3, USART_IT_RXNE) ;
    }
    
    else if(USART_GetITStatus(USART3,USART_IT_IDLE) != RESET)  //空闲接收
    {
          USART3->SR;//先读SR    清除中断
		  USART3->DR;//再读DR
          USART3_RX_CNT = 0 ;  //实际需注销
          //printf("In");  
        
          if(USART3_RX_BUFF[0] == 0xAA  && USART3_RX_BUFF[2] == 0xDD)
          {
              if(USART3_RX_BUFF[1] == 0xEA)
              {
                  gIsQtControlFlag = !gIsQtControlFlag ;
                  printf("Qt Control\r\n");
              }
              
              gQtCmdData = USART3_RX_BUFF[1] ;
          }
          
          memset(USART3_RX_BUFF,0,sizeof(USART3_RX_BUFF)) ; 
    } 
    
        
}

五.总结

消耗了两天的时间让吃灰的树莓派和坦克底盘重新用起来了,不过估计过两天一套东西还是逃脱不了吃灰的命运。系统基本的功能全部实现,后期有时间的话想写个手机APP控制,毕竟通过电脑控制还是有些许的不方便。在这就是使用内网穿透工具,使得客户端可以较远距离控制坦克小车。整个项目的工程文件如果有需要的话我发出来。

由于太多人私信了,最近有点忙,有点回不过来,所以我把工程上传了上去,可以点击下载https://download.csdn.net/download/qq_42965223/18254465
没有积分的可以私我,回的有点慢,见谅。

posted @ 2023-06-23 14:24  NNV  阅读(564)  评论(2)    收藏  举报  来源