提示:文章
文章目录
- 前言
- 一、背景
- 二、linux评估板应用层
-
- [2.1 主体功能](#2.1 主体功能)
- [2.2 代码框架](#2.2 代码框架)
- [2.3 std::condition_variable理解](#2.3 std::condition_variable理解)
- [2.4 线程安全队列](#2.4 线程安全队列)
- [2.5 CRC校验](#2.5 CRC校验)
- [2.6 程序打印时分秒毫秒信息](#2.6 程序打印时分秒毫秒信息)
- [2.7 应用层代码编译脚本](#2.7 应用层代码编译脚本)
- 三、UI代码
-
- [3.1 qt程序和应用层程序联调](#3.1 qt程序和应用层程序联调)
- [3.2 UI程序和应用层程序通讯方式](#3.2 UI程序和应用层程序通讯方式)
- [3.3 服务器-客户端定义](#3.3 服务器-客户端定义)
- [3.4 尝试搭建自己的TCP工程](#3.4 尝试搭建自己的TCP工程)
-
- [3.4.1 qt程序(服务端)](#3.4.1 qt程序(服务端))
- [3.4.2 应用层程序(客户端)](#3.4.2 应用层程序(客户端))
- [3.5 在UI界面显示](#3.5 在UI界面显示)
-
- [3.5.1 UI界面增加显示文字控件](#3.5.1 UI界面增加显示文字控件)
- [3.5.2 在MainView编写代码](#3.5.2 在MainView编写代码)
- [3.5.3 UI界面显示时间](#3.5.3 UI界面显示时间)
- 总结
前言
前期疑问:
本文目标:
一、背景
最近在看创龙评估板代码,一开始是尝试能给自己写的下位机发送指令,然后从尝试一个功能,并解决问题,逐渐开始理解代码功能并分析框架,由此得到本篇文章的理解。
二、linux评估板应用层
2.1 主体功能
评估板任务主要包括对下位机执行测试任务,包括修改波特率,控制下位机LCD屏幕变化等任务,设计框架为创建一个一直运行的通讯线程,这个线程中创建一个loop_03函数,这个函数循环向下位机发收数据,持续获取下位机状态。另外有一个send_msg函数,这个函数发送16报文,依赖于一个gCmdQueue队列,当这个队列中有报文数据时,发送16报文,控制下位机执行修改对比度或者控制LCD屏幕变化等动作。
另外还涉及创建多个其他控制任务线程,这个线程主要是组建16报文并push到gCmdQueue队列,另外使用了条件变量std::condition_variable,这个条件变量对象通过存入16报文结构体,并push到gCmdQueue队列,只想cv.wait(),程序会阻塞在此代码处。这个结构体会被485任务获取,在485任务中发收报文完成后,会执行cv.notify_one(),这时候控制任务会继续执行,保证多线程的有序性。
2.2 代码框架
下面是代码框架流程图:
见附件。
2.3 std::condition_variable理解
其中有一些疑问,只使用cv不行吗,为什么还要使用锁,看起来锁并没有什么作用啊。
你这个问题问得非常关键,而且很多人第一次用 std::condition_variable 都会产生同样的疑惑:
cv.wait(lock) 里到底需不需要 lock? 我看 lock 好像没什么作用啊?
其实 lock 是必须的,而且是整个机制的核心。
如果你把 lock 去掉,这段代码会 直接崩溃 或 行为未定义。
我用你能完全看懂的方式,把它讲清楚。
- 为什么
cv.wait(lock)必须要 lock?
因为 wait() 的内部机制依赖这个锁:
wait(lock) 做了三件事:
- 检查条件(队列是否为空)
- 自动释放锁 → 线程进入睡眠
- 被 notify 唤醒后 → 自动重新加锁 → 继续执行
如果没有 lock:
-
wait() 无法释放锁
-
wait() 无法重新加锁
-
wait() 无法保证条件检查的原子性
-
wait() 无法保证唤醒后队列状态不被其他线程破坏
-
wait() 甚至无法编译(因为 wait 必须接收 unique_lock)
所以:
lock 是 wait 的"钥匙",没有它 wait 根本不能工作。
2.4 线程安全队列
其中有个gCmdQueue定义为threadSafeQueue<Cmd_Stru> gCmdQueue[SERIAL_PORT_NUM];,threadSafeQueue不是c/c++提供的类
cpp
#ifndef THREADSAFEQUEUE_H
#define THREADSAFEQUEUE_H
#include <queue>
#include <mutex>
#include <condition_variable>
#include <iostream>
#include <thread>
template<typename T>
class ThreadSafeQueue
{
public:
void push(const T &value)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_queue.push(value);
m_cond.notify_one(); // 通知一个等待的线程
}
// 尝试弹出,如果队列为空则立即返回false
bool try_pop(T &value)
{
std::lock_guard<std::mutex> lock(m_mutex);
if(m_queue.empty())
{
return false;
}
value = std::move(m_queue.front());
m_queue.pop();
return true;
}
// 等待直到队列不为空,然后弹出
void wait_and_pop(T &value)
{
std::unique_lock<std::mutex> lock(m_mutex);
m_cond.wait(lock, [this] { return !m_queue.empty(); });
value = std::move(m_queue.front());
m_queue.pop();
}
bool empty() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_queue.empty();
}
private:
mutable std::mutex m_mutex;
std::queue<T> m_queue;
std::condition_variable m_cond;
};
#endif // THREADSAFEQUEUE_H
上述代码中lock_guard是自动释放的锁。离开代码定义区域会自动释放锁。
另外m_cond.wait(lock, [this] { return !m_queue.empty(); });是lamuda表达式。
整句 lambda 的完整结构是: [捕获列表] (参数列表) -> 返回类型 { 函数体 } 你看到的版本是最简化的: [this] { return !m_queue.empty(); } 它省略了参数列表和返回类型,因为编译器能自动推导。
下面逐字解释:
2.[this] ------ 捕获列表(最关键) 意思:把当前对象的 this 指针带进来。 为什么要带 this? 因为你在 lambda 里访问了: cpp m_queue 这是类的成员变量,必须通过 this 才能访问。 所以 [this] 的意思就是: > "这个小函数里可以用 this->m_queue"。 如果你写成: [] { return !m_queue.empty(); } 会报错,因为 lambda 里找不到 m_queue。
3.{ return !m_queue.empty(); } ------ 函数体 这部分就是一个小函数的内容。 拆开理解: - return:返回一个值 - !m_queue.empty():判断队列是否不为空 等价于: cpp bool 临时函数() { return !m_queue.empty(); }
- 整句 lambda 的真实含义 把它翻译成"正常人能看懂的中文": > "这是一个临时小函数,它返回队列是否不为空。" 再翻译成 C++ 普通函数:
cpp bool check_not_empty() { return !m_queue.empty(); }只是 lambda 不需要起名字,也不需要写到外面。
针对lambda表达式,我还回顾了之前的博文,更新了父作用域,lambda文章
2.5 CRC校验
另外16报文或者03报文都会有CRC校验,CRC校验函数如下
cpp
unsigned short crc16(unsigned char *msg, int len)
{
unsigned char idx;
unsigned char high = 0xff;
unsigned char low = 0xff;
while(len--)
{
idx = low ^ *msg++;
low = high ^CRC16TABH[idx];
high = CRC16TABL[idx];
}
return (high * 0x100 + low);
}
2.6 程序打印时分秒毫秒信息
cpp
#include <sys/time.h>
struct timeval tv;
gettimeofday(&tv, NULL);
struct tm *timeinfo;
timeinfo = localtime(&tv.tv_sec);
long milliseconds = tv.tv_usec / 1000; // 微秒转毫秒
DPrint("serial_no:%d, index:%d, start 时间: %d-%02d-%02d %02d:%02d:%02d.%03ld \n", serial_no, i,
timeinfo->tm_year + 1900, timeinfo->tm_mon + 1, timeinfo->tm_mday,
timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec,
milliseconds);
上述代码可以打印分秒毫秒信息
2.7 应用层代码编译脚本
shell
#!/bin/bash
# 脚本执行打印信息
echo "shell run"
# 进入makefile文件路径
cd ./control
cd ./AutoTestCtrlProgram
# 显示所有文件
ls
# make编译
make -j2
#mv AutoTestCtrlProgram ../../
# 执行可执行文件
./AutoTestCtrlProgram
# 打印信息
echo "task is runing.."
# 移除可执行文件
rm -rf AutoTestCtrlProgram
# 回到shell脚本路径
cd ../../
ls
# 打印信息
echo "finish task"
三、UI代码
3.1 qt程序和应用层程序联调
应用层代码是make编译成可执行文件,执行文件,但是另外还有一个UI程序怎么执行呢?一直执行还是先后执行?
先执行UI程序,UI程序中有makefile文件,qt程序的执行流程是,
cpp
qmake AutoTest_UI.pro // 会生成MakeFile文件
make
#./AutoTest_UI
./AutoTest_UI &
执行可执行文件,这时候一直打印信息,无法执行应用层可执行文件。增加&字符可以在后台执行。
此时执行应用层程序,应用层程序也一直在打印信息,看不了应用层和UI程序交互的信息。方法是执行
shell
./control/AutoTestCtrlProgram/AutoTestCtrlProgram > app.log 2>&1 &
//后台打印调试信息到app.log文件
这时候可以看到应用层和UI程序交互的信息。
后台信息可以使用
shell
top -H
#查看后台程序
kill <PID>
3.2 UI程序和应用层程序通讯方式
目前代码使用的是tcp通讯。我就在想,难道只能使用tcp通讯吗?问了copilot,答案当然不是,除了tcp还可以使用UDP、管道、消息队列,共享内存等等方式。
当然上述方式我都没用过,也不太懂,🙂。但是明白一件事,这个方式并不是唯一的。
3.3 服务器-客户端定义
为什么qt程序是服务器,应用层是客户端。
谁先启动、谁等待别人来连,谁就是服务器。
Qt先启动->等别人来连->Qt就是服务器。
3.4 尝试搭建自己的TCP工程
尝试自己通过copilot建工程实现tcp。
3.4.1 qt程序(服务端)
qt创建工程,编辑main函数,修改如下
main.cpp文件
cpp
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
QThread *serverThread = new QThread();
TcpServerWorker *server = new TcpServerWorker(12345);
server->moveToThread(serverThread);
QObject::connect(serverThread, &QThread::started,
server, &TcpServerWorker::startServer);
QObject::connect(server, &TcpServerWorker::dataReceived,
&w, &MainWindow::processNetworkData);
serverThread->start();
QObject::connect(&a, &QCoreApplication::aboutToQuit, [=]() {
server->stopServer();
serverThread->quit();
serverThread->wait();
delete server;
delete serverThread;
});
return a.exec();
}
TcpServerWorker文件
cpp
#include "TcpServerWorker.h"
#include <QDebug>
TcpServerWorker::TcpServerWorker(int port, QObject *parent)
: QObject(parent), m_port(port), m_server(nullptr), m_client(nullptr)
{
}
void TcpServerWorker::startServer()
{
m_server = new QTcpServer(this);
connect(m_server, &QTcpServer::newConnection,
this, &TcpServerWorker::onNewConnection);
if (!m_server->listen(QHostAddress::Any, m_port)) {
qDebug() << "Server listen failed:" << m_server->errorString();
return;
}
qDebug() << "Server listening on port" << m_port;
}
void TcpServerWorker::stopServer()
{
if (m_client) {
m_client->close();
m_client->deleteLater();
}
if (m_server) {
m_server->close();
m_server->deleteLater();
}
}
void TcpServerWorker::onNewConnection()
{
m_client = m_server->nextPendingConnection();
qDebug() << "Client connected:" << m_client->peerAddress().toString();
connect(m_client, &QTcpSocket::readyRead,
this, &TcpServerWorker::onReadyRead);
}
void TcpServerWorker::onReadyRead()
{
QByteArray data = m_client->readAll();
emit dataReceived(data);
}
TcpServerWorker头文件
cpp
#ifndef TCPSERVERWORKER_H
#define TCPSERVERWORKER_H
#include <QObject>
#include <QTcpServer>
#include <QTcpSocket>
class TcpServerWorker : public QObject
{
Q_OBJECT
public:
explicit TcpServerWorker(int port, QObject *parent = nullptr);
public slots:
void startServer();
void stopServer();
void onNewConnection();
void onReadyRead();
signals:
void dataReceived(QByteArray data);
private:
int m_port;
QTcpServer *m_server;
QTcpSocket *m_client;
};
#endif // TCPSERVERWORKER_H
添加文件后构建,报错。
报错1:
shell
tcpServerWorker.h:5:10: error: 'QTcpServer' file not found,
解决方法,.pro文件添加QT += network。
报错2:
shell
main.cpp:12:33: error: allocation of incomplete type 'QThread' qobject.h:72:7: note: forward declara。
解决方法: 添加头文件#include
报错3:
shell
main.cpp:5:10: warning: non-portable path to file '"tcpServerWorker.h"'; specified path differs in case from file name on disk
tcpServerWorker类名和文件名大小写不一致。
报错4:
cpp
main.cpp:21:39: error: no member named 'processNetworkData' in 'MainWindow'
需要在MainWindow添加processNetworkData方法
MainWindow头文件修改成
cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
public slots:
void processNetworkData(const QByteArray &data);
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
MainWindow文件修改成
cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::processNetworkData(const QByteArray &data)
{
qDebug() << "MainWindow received:" << data;
// 如果你有 QTextEdit,可以这样显示:
// ui->textEdit->append(QString::fromUtf8(data));
}
报错5:
shell
mainwindow.cpp:18:5: error: calling 'debug' with incomplete return type 'QDebug'
添加头文件#include
解决完上述报错,运行qt程序,程序正常运行后,将HDMI线接在创龙HDMI接口后,显示器上能看到qt的UI界面。
3.4.2 应用层程序(客户端)
应用层代码
cpp
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
int main()
{
// 1. 创建 socket
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket");
return -1;
}
// 2. 设置服务器地址
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(12345); // Qt 服务器端口
server.sin_addr.s_addr = inet_addr("127.0.0.1"); // Qt 程序在同一块 ARM 板
// 3. 连接服务器
if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0) {
perror("connect");
return -1;
}
printf("Connected to Qt server!\n");
// 4. 发送数据
char *msg = "Hello from Linux client";
send(sock, msg, strlen(msg), 0);
// 5. 接收服务器回发的数据(如果有)
char buffer[1024] = {0};
int len = recv(sock, buffer, sizeof(buffer), 0);
if (len > 0) {
printf("Received from Qt: %s\n", buffer);
}
// 6. 关闭 socket
close(sock);
return 0;
}
qt代码和应用层代码传输到arm芯片中,先后编译qt程序和应用层程序
1、qt程序
shell
qmake tcpServer.pro // 执行完该条指令会生成MakeFile文件
make
./tcpServer & // 后台运行
打印日志
shell
[1] 4406
root@RK3568-Tronlong:/opt/zhuyf/autoTest/tcpSocket/tcpServer/tcpSocket# arm_release_ver: g24p0-00eac0, rk_so_ver: 7
Server listening on port 12345
2、运行应用层程序
shell
gcc tcpClient.cpp -o tcpClient // 编译
./tcpClient
打印日志
shell
Connected to Qt server!
Client connected: "::ffff:127.0.0.1"
MainWindow received: "Hello from Linux client"
[1] 4406
成功实现服务端和客户端的tcp通讯
3.5 在UI界面显示
下面想实现在UI界面显示服务端收到的报文信息
3.5.1 UI界面增加显示文字控件

需要注意objectName应该是textEdit,和代码要对应起来,并保存。
3.5.2 在MainView编写代码
编辑代码如下
cpp
void MainWindow::processNetworkData(const QByteArray &data)
{
qDebug() << "MainWindow received:" << data;
// 如果你有 QTextEdit,可以这样显示:
ui->textEdit->append(QString::fromUtf8(data));
}
报错1:
shell
mainwindow.cpp:21:10: error: no member named 'textEdit' in 'Ui::MainWindow'
保存并重新构建就好了。
写好代码后重新运行两个程序,运行qt程序时报错
shell
Server listen failed: "The bound address is already in use"
qt程序已经在后台运行,关闭僵尸进程
检查是否有旧进程占用
shell
ps -ef | grep qt // 查看进程
// 打印信息
root 5559 1150 0 11:39 pts/0 00:00:00 grep --color=auto qt
或者直接查看端口
shell
netstat -ntlp | grep 12345
// 打印信息
tcp6 0 0 :::12345 :::* LISTEN 4406/./tcpSocket
说明端口被旧的qt占用
杀掉旧进程
shell
kill -9 4406
进程被关闭,重新运行服务器程序。
再运行客户端程序,UI界面上会打印出客户端发送的日志。
3.5.3 UI界面显示时间
显示<时间:>这两个字,

后面显示刷新的时间,

编辑代码后运行,可以实现效果
代码链接:tcp代码
总结
未完待续