嵌入式项目:基于QT与海思HI3861开发板设计的鸿蒙智能车_hi3861和qt
摘要:近年来,随着物联网(IoT)和边缘计算的快速发展,智能车作为嵌入式开发的经典载体,结合鸿蒙操作系统(HarmonyOS)的分布式能力,展现出强大的应用潜力。本文将分享一个基于海思HI3861开发板与QT软件设计的鸿蒙智能车项目,全文涵盖硬件选型、软件架构设计、功能实现与调试经验。
一、项目介绍
1. 目标
设计一款支持远程控制、环境感知(避障、循迹)和实时数据可视化的智能车,通过鸿蒙系统的低时延特性与QT的上位机界面实现高效交互。
大概结构设计
2. 核心组件
主控单元:海思HI3861开发板,负责通信和控制核心
从控板:STM32开发板,负责电机以及超声波距离获取、电压检测等。
上位机界面:QT软件、TCPUDP调试助手
涉及模块:温湿度模块、超声波模块、OLED屏幕、rgb灯、串口模块、网络模块、蜂鸣器等
通信方式:Wi-Fi(HI3861与QT端双向通信)
软件:VS code、QT(需要先下载)
二、硬件与软件架构设计
1. 硬件架构
HI3861开发板:作为核心控制器,获取处理各传感器的数据并与客户端进行通信,与STM32从机进行串口通信,将STM32采集的距离、电压信息发送给QT客户端,并通过串口操纵STM32开发板控制电机。
传感器层:
超声波传感器:实时检测距离(触发距离5-30cm可调)
温湿度传感器:采集环境温湿度情况并实时反映在QT客户端界面
电源管理:锂电池组+稳压模块,支持5V/3.3V输出。
整体设计效果:采用QT设计客户端与小车进行交互,通过按键向小车发送特定指令,控制小车运动、RGB灯、蜂鸣器等外设,小车实时反馈各传感器信息。
2. 软件架构
a. 鸿蒙端(HI3861)代码:
#include #include #include #include \"ohos_init.h\"#include \"cmsis_os2.h\"#include \"hal_bsp_wifi.h\" //wifi#include \"lwip/sockets.h\" //服务器#include \"hal_bsp_ssd1306.h\" //OLED#include \"hal_bsp_pcf8574.h\" //风扇、蜂鸣器#include \"hal_bsp_aw2013.h\" //RGB#include \"hal_bsp_sht20.h\" //温湿度#include \"hi_io.h\"#include \"hi_uart.h\"#define PWM_MAX 1000 // 最大 PWM#define PWM_MIN 0 // 最小 PWM#define PWM_STEP 100 // 每次加减速的步长int left_pwm = 500;int right_pwm = 500;float temp = 0, humi = 0;int distance = 0, carPower = 0;typedef enum{ MOTOR_STOP, MOTOR_RUN, MOTOR_BACK, MOTOR_LEFT, MOTOR_RIGHT} MotorDirection;MotorDirection current_direction = MOTOR_STOP;typedef enum{ SPEED_KEEP, // 保持当前速度 SPEED_UP, // 加速 SPEED_DOWN // 减速} SpeedChange;void motor_power_control(int power_on);void motor_move(MotorDirection direction);void adjust_speed(SpeedChange speed);static void uart2_init();static osThreadId_t Task1_ID; // 任务1 IDstatic osThreadId_t Task2_ID; // 任务2 IDstatic int create_tcp_server(unsigned int port){ // 1. 创建socket int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd bind(sockfd, (struct sockaddr *)&addr, sizeof(addr))) { printf(\"bind error\\n\"); return -1; } // 3. 监听 if (0 > listen(sockfd, 5)) { printf(\"listen error\\n\"); return -1; } printf(\"tcp server init success!\\n\"); // 4. 等待客户端连接 printf(\"wait client connect\\n\"); int connfd = accept(sockfd, NULL, NULL); if (connfd 0) { if (strcmp(buf, \"run\") == 0) { motor_move(MOTOR_RUN); } else if (strcmp(buf, \"back\") == 0) { motor_move(MOTOR_BACK); } else if (strcmp(buf, \"left\") == 0) { motor_move(MOTOR_LEFT); } else if (strcmp(buf, \"right\") == 0) { motor_move(MOTOR_RIGHT); } else if (strcmp(buf, \"stop\") == 0) { motor_move(MOTOR_STOP); } else if (strcmp(buf, \"quick\") == 0) { adjust_speed(SPEED_UP); } else if (strcmp(buf, \"slow\") == 0) { adjust_speed(SPEED_DOWN); } else if (strcmp(buf, \"buzzer\") == 0) { buzzer_button ^= 1; if (buzzer_button) set_buzzer(1); else set_buzzer(0); } else if (strcmp(buf, \"rgb\") == 0) { rgb_button ^= 1; if (rgb_button) AW2013_Control_RGB(123, 233, 10); else AW2013_Control_RGB(0, 0, 0); } } }}static void Task2(void){ // printf(\"temperature:%.2f\", temp); float last_temp, last_humi; hi_u8 recvBuff[200] = {0}; // 串口接收缓冲区 hi_u8 *pbuff = recvBuff; char uart_buff[20] = {0}; // 串口接收缓冲区 char last_len = 0; // 上次接收到的数据长度 while (1) { SHT20_ReadData(&temp, &humi); if (temp != last_temp || humi != last_humi) { last_humi = humi; last_temp = temp; } if (temp > 27.0) set_fan(1); else set_fan(0); hi_u32 len = hi_uart_read(HI_UART_IDX_2, uart_buff, sizeof(uart_buff)); if (len > 0) { // printf(\"uart_buff: %s\\n\", uart_buff); memcpy_s((char *)pbuff, len, (char *)uart_buff, len); pbuff += len; if (len PWM_MAX) ? PWM_MAX : left_pwm + PWM_STEP; right_pwm = (right_pwm + PWM_STEP > PWM_MAX) ? PWM_MAX : right_pwm + PWM_STEP; break; case SPEED_DOWN: left_pwm = (left_pwm - PWM_STEP < PWM_MIN) ? PWM_MIN : left_pwm - PWM_STEP; right_pwm = (right_pwm - PWM_STEP < PWM_MIN) ? PWM_MIN : right_pwm - PWM_STEP; break; case SPEED_KEEP: default: break; } motor_move(current_direction);}static void uart2_init(){ hi_io_set_func(HI_IO_NAME_GPIO_11, HI_IO_FUNC_GPIO_11_UART2_TXD); hi_io_set_func(HI_IO_NAME_GPIO_12, HI_IO_FUNC_GPIO_12_UART2_RXD); hi_uart_attribute attr; attr.baud_rate = 115200; attr.data_bits = 8; attr.parity = 0; attr.stop_bits = 1; if (HI_ERR_SUCCESS != hi_uart_init(HI_UART_IDX_2, &attr, NULL)) { printf(\"uart2 init fail\\n\"); }}
需要注意的是,一定要改动BUILD.gn文件,包含所使用的.c文件与头文件路径(博主在这上面栽了不少跟头),这里因为时间问题博主只新建了这一个工程.c文件,有些同学可能喜欢模块化编程,封装多个文件,所以记得全部加到BUILD.gn文件。同时,如果有多个工程时,在调试过程中需要将不必要的文件利用#进行注释,避免影响调试结果。
注:一旦BUILD.gn文件发生改动,就一定要进行rebuild。
BUILD.gn
# Copyright (c) 2020 Huawei Device Co., Ltd.# Licensed under the Apache License, Version 2.0 (the \"License\");# you may not use this file except in compliance with the License.# You may obtain a copy of the License at## http://www.apache.org/licenses/LICENSE-2.0## Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an \"AS IS\" BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License.static_library(\"cdut1\") { #双引号里面写模块名字 sources = [ #写需要用到的所有的.c文件 #\"./00_task.c\",不需要的文件在前面加上#注释掉 \"./01_pcf8574.c\", #\"./02_oled.c\", #\"./03_sht20.c\", #\"./04_uart.c\", #\"./05_wifi.c\", #\"./06_power.c\", \"//vendor/hqyj/fs_hi3861/common/bsp/src/hal_bsp_pcf8574.c\", \"//vendor\\hqyj\\fs_hi3861\\common\\bsp\\src\\hal_bsp_aw2013.c\", \"//vendor\\hqyj\\fs_hi3861\\common\\bsp\\src\\hal_bsp_ssd1306.c\", \"//vendor\\hqyj\\fs_hi3861\\common\\bsp\\src\\hal_bsp_sht20.c\", \"//vendor\\hqyj\\fs_hi3861\\common\\bsp\\src\\hal_bsp_wifi.c\", \"src\\device\\hisilicon\\hispark_pegasus\\sdk_liteos\\include\\hi_uart.h\", \"src\\device\\hisilicon\\hispark_pegasus\\sdk_liteos\\include\\hi_adc.h\", ] include_dirs = [ #需要用到的所有头文件的路径 \"//utils/native/lite/include\", \"//vendor/hqyj/fs_hi3861/common/bsp/include\", \"//kernel/liteos_m/kal/cmsis\", \"//base/iot_hardware/peripheral/interfaces/kits\", \"//foundation/communication/wifi_lite/interfaces/wifiservice\", ] }
b. QT部分代码:
form.cpp
#include \"form.h\"#include \"ui_form.h\"#include #include Form::Form(QWidget *parent) : QWidget(parent), ui(new Ui::Form){ ui->setupUi(this); socket=new QTcpSocket; connect(socket,&QTcpSocket::connected,this,&Form::connect_success); connect(socket,&QTcpSocket::readyRead,this,&Form::recv_data); socket->connectToHost(\"192.168.250.18\",8848); QPixmap pic(\":/res/pic/2.jpg\"); ui->label_2->setScaledContents(true); ui->label_2->setPixmap(pic); m_buttonW = ui->pushButton; m_buttonA = ui->pushButton_2; m_buttonS = ui->pushButton_4; m_buttonD = ui->pushButton_3; m_buttonD = ui->pushButton_3; m_buttonF = ui->buttonSpace; m_buttonQ = ui->buttonQ; m_buttonE = ui->buttonE; m_buttonR = ui->buttonR; m_buttonT = ui->buttonT; m_buttonF = ui->pushButton_5;}Form::~Form(){ delete ui;}void Form::keyPressEvent(QKeyEvent* event){ // 判断按下的键,并触发对应的按钮点击事件 if (event->key() == Qt::Key_W) { m_buttonW->click(); // 按下 W 键时触发 m_buttonW 的点击 } else if (event->key() == Qt::Key_A) { m_buttonA->click(); // 按下 A 键时触发 m_buttonA 的点击 } else if (event->key() == Qt::Key_S) { m_buttonS->click(); // 按下 S 键时触发 m_buttonS 的点击 } else if (event->key() == Qt::Key_D) { m_buttonD->click(); }// 按下 D 键时触发 m_buttonD 的点击 else if (event->key() == Qt::Key_Space) { m_buttonF->click(); // 按下 空格 键时触发 m_buttonSpace 的点击 } else if (event->key() == Qt::Key_Q) { m_buttonQ->click(); // 按下 Q 键时触发 m_buttonQ 的点击 } else if (event->key() == Qt::Key_E) { m_buttonE->click(); // 按下 E 键时触发 m_buttonE 的点击 } else if (event->key() == Qt::Key_R) { m_buttonR->click(); // 按下 R 键时触发 m_buttonR 的点击 } else if (event->key() == Qt::Key_T) { m_buttonT->click(); // 按下 T 键时触发 m_buttonT 的点击 } else if (event->key() == Qt::Key_F) { m_buttonF->click(); // 按下 F 键时触发 m_buttonF 的点击 }}void Form::on_pushButton_clicked(){ char buf[32]=\"run\"; socket->write(buf);}void Form::recv_data(){ unsigned char j,a; char buf[32]={0}; char bufdl[32]={0}; char bufjl[32]={0}; char bufwd[32]={0}; char bufsd[32]={0}; socket->read(buf,32);// QString bu = QString(\"电量:%1,%2\").arg(123.6).arg(); for(j=0,a=1;j<4;j++,a++) { bufjl[j]=buf[j]; } for(j=0,a=1;j<5;j++,a++) { bufdl[j]=buf[j+8]; } for(j=0,a=1;j<5;j++,a++) { bufwd[j]=buf[j+16]; } for(j=0,a=1;jlabel2->setText(bu2);ui->label->setText(bu);ui->label3->setText(bu3);ui->label4->setText(bu4);}void Form::on_pushButton_4_clicked(){ char buf[32]=\"back\"; socket->write(buf);}void Form::on_pushButton_2_clicked(){ char buf[32]=\"left\"; socket->write(buf);}void Form::on_pushButton_3_clicked(){ char buf[32]=\"right\"; socket->write(buf);}void Form::connect_success(){}void Form::on_buttonR_clicked(){ char buf[32]=\"buzzer\"; socket->write(buf);}void Form::on_buttonT_clicked(){ char buf[32]=\"rgb\"; socket->write(buf);}void Form::on_buttonE_clicked(){ char buf[32]=\"slow\"; socket->write(buf);}void Form::on_buttonQ_clicked(){ char buf[32]=\"quick\"; socket->write(buf);}void Form::on_pushButton_5_clicked(){ char buf[32]=\"stop\"; socket->write(buf);}
widget.cpp
#include \"widget.h\"#include \"ui_widget.h\"Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget){ ui->setupUi(this); ui->label->setText(\"账号:\"); ui->label2->setText(\"密码:\"); QPixmap pic(\":/res/pic/pic0.png\"); ui->label_2->setScaledContents(true); ui->label_2->setPixmap(pic);}Widget::~Widget(){ delete ui;}void Widget::on_pushButton_clicked(){ QString buf; QString buf2; buf=ui->label->text(); buf2=ui->label2->text(); if(buf==\"账号:123456\"&&buf2==\"密码:\") { QMessageBox::about(this,\"提示\",\"请输入密码\"); } else if(buf==\"账号:123456\"&&buf2!=\"密码:123456\") { QMessageBox::about(this,\"提示\",\"密码错误\"); } if(buf==\"账号:123456\"&&buf2==\"密码:123456\"){ socket->connectToHost(\"192.168.250.18\",8848); this->f=new Form; f->show(); this->close(); QMessageBox::about(this,\"提示\",\"登录成功\"); }}void Widget::connect_success(){}void Widget::recv_data(){ char buf[32]={0};socket->read(buf,32);}
三、关键功能实现
1. 鸿蒙与QT的通信机制
鸿蒙端建立服务器,连接手机热点(OLED屏幕显示IP地址作为小车是否连接成功的判断),QT端作为服务器与鸿蒙端建立联系,通过判断QT发送的数据进行调整。
static int create_tcp_server(unsigned int port)//创建TCP服务器{ // 1. 创建socket int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd bind(sockfd, (struct sockaddr *)&addr, sizeof(addr))) { printf(\"bind error\\n\"); return -1; } // 3. 监听 if (0 > listen(sockfd, 5)) { printf(\"listen error\\n\"); return -1; } printf(\"tcp server init success!\\n\"); // 4. 等待客户端连接 printf(\"wait client connect\\n\"); int connfd = accept(sockfd, NULL, NULL); if (connfd < 0) { printf(\"accept error\\n\"); return -1; } printf(\"client connect success!\\n\"); printf(\"connfd:%d\\n\", connfd); return connfd;}
当然,可以先通过使用调试工具(我叫它小飞机)进行调试,效果都一样,建立连接之前需要让小车连接热点获取IP地址,这也就是为什么我让屏幕显示IP地址,方便判断和调试。
注:一定要让小车与电脑连接到同一热点(如手机热点)
2. HI3861与STM32的通信
二者主要通过串口进行通信,STM32采集超声波距离、电量、电机左右轮转速等信息,
鸿蒙车串口协议:
{\\\"control\\\":{\\\"power\\\":\\\"on\\\"}}//开启电机{\\\"control\\\":{\\\"power\\\":\\\"off\\\"}}//关闭电机{\\\"control\\\":{\\\"turn\\\":\\\"run\\\",\\\"pwm\\\":{\\\"L_Motor\\\":%d,\\\"R_Motor\\\":%d}}}//前进{\\\"control\\\":{\\\"turn\\\":\\\"back\\\",\\\"pwm\\\":{\\\"L_Motor\\\":%d,\\\"R_Motor\\\":%d}}}//后退{\\\"control\\\":{\\\"turn\\\":\\\"left\\\",\\\"pwm\\\":{\\\"L_Motor\\\":%d,\\\"R_Motor\\\":%d}}}//左转{\\\"control\\\":{\\\"turn\\\":\\\"right\\\",\\\"pwm\\\":{\\\"L_Motor\\\":%d,\\\"R_Motor\\\":%d}}}//右转{\\\"control\\\":{\\\"turn\\\":\\\"stop\\\"}}//停止sprintf(buf,\"{\\\"control\\\":{\\\"turn\\\":\\\"run\\\",\\\"pwm\\\":{\\\"L_Motor\\\":%d,\\\"R_Motor\\\":%d}}}\",500,500);//例子,轮子速度300-1000{\"control\":{\"power\":\"on\"}}//开启电机{\"control\":{\"power\":\"off\"}}//关闭电机{\"control\":{\"turn\":\"run\",\"pwm\":{\"L_Motor\":300,\"R_Motor\":300}}}//前进{\"control\":{\"turn\":\"back\",\"pwm\":{\"L_Motor\":300,\"R_Motor\":300}}}//后退{\"control\":{\"turn\":\"left\",\"pwm\":{\"L_Motor\":0,\"R_Motor\":300}}}//左转{\"control\":{\"turn\":\"right\",\"pwm\":{\"L_Motor\":300,\"R_Motor\":0}}}//右转{\"control\":{\"turn\":\"stop\"}}//停止{\"status\":{\"distance\":5616,\"carPower\":12332,\"L_speed\":0,\"R_speed\":0}}//接收到的数据
3. QT界面交互设计

可以考虑设计自己喜欢的图片或者动图,为了方便工程移植,我们可以按照这种方式将图片放置在工程中(当然也可以在代码部分直接写图片文件的地址,但是在移植工程时如果对方的电脑没有这张图片或者地址不一样就要报错,需要重新改地址),各位可以查找具体教程。
四、开发挑战与解决方案
1. 用户交互问题
在QT界面设计时隐藏部分组件,使设计界面整洁干净;设置登录界面(根据情况不同分为密码错误、空密码、密码正确),登陆成功后跳转用户交互界面;为方便人为直接操作,选择不通过鼠标点击二十直接通过键盘控制,提高用户体验。
2. 代码优化
1. 客户端信息控制问题
实验过程中发现在判断客户端发来的run、stop等命令后进行判断,小车虽然可以正常运行,但一样的命令只会执行一次,导致运动控制出现问题,以下是博主的解决方案,仅供参考。
在每次客户端命令读取之前需要将存储数组buf清零,这样就可以正常执行
memset(buf, 0, 32);
2. 数据传输导致卡死问题
实验过程中发现传感器数据采集速度与传输速度极快,极易造成程序与QT显示界面卡死,在使用调试助手调试时发现在超高速传输情况下小车运行17秒作用就会卡死,短短几秒传输数十万次。 以下是博主的解决方案,仅供参考。
a. 添加适量延时(根据实际情况),这里博主添加了20ms的延时,在之前添加0.5秒的延时发现在按键按下后小车反应存在较大延迟,20ms延迟极小且程序不易卡死。
b. 为了防止短时间内传输巨量信息(绝大多数数据重复且无用),可以在发送数据前进行判断,当数据与上一个数据不一样时才进行传输,滤去了大量的无用数据(可以将温湿度,电量,距离分别进行判断,这里博主为了方便直接在距离判断下面把四个全发了)。
if (distance != last_sent_distance) { //这里使用-8%d,是为了防止在QT显示时高位显示0,影响美观和布局 sprintf(data_all, \"%-8d%-8d%-8.2f%-8.2f\", distance, carPower, temp, humi); write(connfd, data_all, strlen(data_all)); // 发送给客户端
3. 数据传输与显示问题
实验过程中发现分别设置不同数组传输温湿度、电量、距离信息时,QT客户端显示的传输数据容易出现混乱的现象,例如数据错位或者显示错误等,以下是博主的解决方案,仅供参考。
解决:使用 buf[32]
来存放四个数据,每个数据分配8位,但是当部分数据的实际值只占用3位时,剩余的5位可能会被下一个数据的位数填补,这会导致数据解析时错误地将部分数据与下一个数据捆绑在一起,所以在发送数据之前,可以使用零填充或空字符填充每个数据到8位的标准长度,这样可以保证每个数据部分有固定的长度。QT接收处也按照八位读取数据,如此操作后就可以避免数据混乱。
sprintf(data_all, \"%-8d%-8d%-8.2f%-8.2f\", distance, carPower, temp, humi);
当然了j,AI也给出了如明确数据分隔符、使用结构体或自定义数据类型等方法解决,大家可以自行尝试。
五、总结与展望
本项目通过HI3861的鸿蒙生态与QT的跨平台能力结合,验证了分布式架构在嵌入式设备中的优势。未来可进一步探索:
1. 集成鸿蒙的分布式软总线实现多车协作
2. 在QT端引入AI模型实现视觉导航
3. 添加更多复杂功能如图像识别、语音控制等,使项目趋于完善成熟
本项目为博主首次尝试QT与HI3861开发板,时间紧迫,导致项目中存在诸多不足之处,实现的功能也较为简单,各位有余力的情况下可以尝试加入其他功能,做成更加成熟完善的项目。
欢迎各位在评论区学习交流,共同进步,博主后续会继续不定期更新嵌入式相关内容,敬请关注。