创龙评估板代码分析

提示:文章

文章目录

  • 前言
  • 一、背景
  • 二、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 去掉,这段代码会 直接崩溃行为未定义

我用你能完全看懂的方式,把它讲清楚。

  1. 为什么 cv.wait(lock) 必须要 lock?

因为 wait() 的内部机制依赖这个锁

wait(lock) 做了三件事:

  1. 检查条件(队列是否为空)
  2. 自动释放锁 → 线程进入睡眠
  3. 被 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(); }

  1. 整句 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代码


总结

未完待续

相关推荐
zx_zx_1232 小时前
传输层协议tcp (2)
服务器·网络·tcp/ip
贝拉学无止尽3 小时前
跨境电商如何搭建网络实操方案
网络·网络安全·跨境网络
lplum_4 小时前
2025第十届“楚慧杯”湖北省网络与数据安全实践能力竞赛 wp
网络·安全·web安全·网络安全·系统安全·密码学·新人首发
小谢取证4 小时前
侦查笔记:云服务器镜像快速勘验
网络
mounter6254 小时前
基于MLX设备的Devlink 工具全指南与核心架构演进
linux·运维·服务器·网络·架构·kernel
wefg14 小时前
【计算机网络】网络基础 - 1(网络协议/TCP/IP协议栈/局域网内外数据传输/数据封装、解包、分用)
linux·服务器·网络
Predestination王瀞潞6 小时前
计科-计网6-网络层「整理」
网络·计算机网络·架构·计网
南浦别a6 小时前
第三十六天---TCP通信
网络·网络协议·tcp/ip
Fly Wine7 小时前
Segement Routing(SR)BE场景超详细实验解析
网络