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和端口号。
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
没有积分的可以私我,回的有点慢,见谅。