模拟频谱分析仪(Linux c++ Qt)

        此Demo由三个小项目组成,分布是模拟的硬件采集频谱数据端,后台处理端以及qt前端,于Linux系统下进行开发,使用的软件为clion和QtCreator,编程语言为c++,使用了linux下的boost库(1.72),多线程和TCP,UDP以及c++的一些新特性,为本人自己想的练手的小项目.

1.项目架构

        整体设计采集端不断产生数据,通过udp广播给后台处理端,后台处理端将数据通过tcp传给qt前端,而控制指令由qt前端通过udp传给采集端(理论上应该是qt前端到后台处理端再传给采集端(作者偷懒了,直接前端连接采集端)[后续也可以封装自己独特的通讯协议]


2.运行结果

模拟硬件采集频谱数据端运行结果:

 后台处理端运行结果:

  qt前端运行结果:

 

3.实现过程

        a.模拟硬件采集端实现

        项目结构如图:

 

        整个项目使用Cmake进行管理,结果清晰.

        config.h为配置文件,定义了udp端口地址以及频谱相关的参数, 方便更改代码如下,其中的关键字constexpr可以在编译时进行求值,能提高程序运行效率

//配置文件

#ifndef FPGADEMO_CONFIG_H
#define FPGADEMO_CONFIG_H

#include <cstdlib> //c++头文件  主要用于定义一些常量 关键字constexpr 可以在编译时进行求职 提高程序运行效率

//UDP广播配置
constexpr u_int16_t Udp_BroadCasT_Port = 8888;   //udp端口号  广播端口用于在网络中向所有设备发送消息。
constexpr const char * Udp_BroadCast_IP = "255.255.255.255"; //定义了 UDP 广播的 IP 地址,值为 "255.255.255.255",这是一个特殊的 IP 地址,表示向本地网络中的所有设备发送广播消息

//控制端口配置
constexpr u_int16_t Udp_Config_Port = 8889; //控制端口用于接收和处理控制命令

//数据配置

/*
    Sample_Rate:定义了采样率,值为 44100 Hz。采样率表示每秒对信号进行采样的次数,44100 Hz 是音频处理中常用的采样率。
    FFT_Size:定义了快速傅里叶变换(FFT)的大小,值为 1024。FFT 是一种用于将时域信号转换为频域信号的算法,FFT 大小决定了频域分辨率。
    Data_Interval_Ms:定义了数据发送的时间间隔,单位为毫秒,值为 20 毫秒。这个参数用于控制数据发送的频率。
    Spectrum_Size:定义了频谱数据的大小,值为 FFT_Size / 2。在 FFT 变换中,频谱数据的有效部分通常是 FFT 结果的一半。
 * */
constexpr int Sample_Rate = 44100; //采样率
constexpr int FFT_Size = 1024;  //FFT大小
constexpr int Data_Interval_Ms = 20; //数据发送间隔
constexpr int Spectrum_Size = FFT_Size / 2; //频谱数据大小

// 套接字重用配置
constexpr bool REUSE_ADDRESS = true;      // 允许地址重用 在网络编程中,地址重用允许在同一端口上同时绑定多个套接字,避免因端口被占用而导致的错误

#endif //FPGADEMO_CONFIG_H

        logger.h是封装的一个打印日志的实例(另外一个博客中封装介绍了更好的版本)


#ifndef FPGADEMO_LOGGER_H
#define FPGADEMO_LOGGER_H


#include <iostream>
#include <string>
#include <iomanip>  //输入输出流操作符库 用于格式化输出
#include <chrono>   //时间处理库英语获取当前时间 添加时间的时间戳

/**
 * @brief 日志记录类,提供不同级别的日志输出功能
 *
 * 使用单例模式确保全局唯一日志实例
 * 支持不同级别的日志输出:INFO, WARNING, ERROR
 * 自动添加时间戳和日志级别前缀
 */
class Logger {
public:
    // 日志级别枚举
    enum class Level{
        INFO,
        WARNING,
        ERROR
    };

    // 获取单例实例
    static Logger& getInstance()
    {
        static Logger instance;
        return instance;
    }

    // 禁用拷贝和赋值,确保单例的正确性
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;

    // 日志输出方法
    void log(Level level, const std::string& message)
    {
        auto now = std::chrono::system_clock::now();
        auto now_time = std::chrono::system_clock::to_time_t(now);

        std::cout << "[" << std::put_time(std::localtime(&now_time), "%Y-%m-%d %H:%M:%S") << "] ";

        switch(level) {
            case Level::INFO: std::cout << "[INFO] "; break;
            case Level::WARNING: std::cout << "[WARNING] "; break;
            case Level::ERROR: std::cout << "[ERROR] "; break;
        }

        std::cout << message << std::endl;
    }

private:
    Logger() = default; // 私有构造函数确保单例
};

// 日志宏定义,方便使用
#define LOG_INFO(msg) Logger::getInstance().log(Logger::Level::INFO, msg)
#define LOG_WARNING(msg) Logger::getInstance().log(Logger::Level::WARNING, msg)
#define LOG_ERROR(msg) Logger::getInstance().log(Logger::Level::ERROR, msg)

#endif //FPGADEMO_LOGGER_H

         data_generator类用于生成频谱数据

#ifndef FPGADEMO_DATA_GENERATOR_H
#define FPGADEMO_DATA_GENERATOR_H

#include <vector>
#include <cstdint>
#include <random>
#include <mutex>
#include <boost/noncopyable.hpp>

/**
 * @brief 模拟数据生成器,生成频谱数据
 */

class data_generator : private boost::noncopyable {


public:
    data_generator();
    ~data_generator() = default;
    /**
     * @brief 生成模拟频谱数据
     * @return 包含频谱数据的vector,大小为SPECTRUM_SIZE
     */
    std::vector<float> generateSpectrumData();

    /**
     * @brief 设置基频
     * @param freq 基频频率(Hz)
     */
    void setBaseFrequency(float freq);

private:
    float baseFrequency;                 // 当前基频
    std::mt19937 rng;                    // 随机数生成器
    std::uniform_real_distribution<float> noiseDist; // 噪声分布
    std::mutex mutex_;                   // 保护基频的互斥锁
};


#endif //FPGADEMO_DATA_GENERATOR_H

        udp_Brodcaster用于广播频谱数据以及接受前端发送的指令,接收和发送互不影响

//
// Created by Administrator on 2025/4/25.
//

#ifndef FPGADEMO_UDPBROADCASTER_H
#define FPGADEMO_UDPBROADCASTER_H

#include <vector>   //容器
// 包含 Boost.Asio 库的头文件,Boost.Asio 是一个跨平台的网络和底层 I/O 编程库,用于实现异步网络操作
#include <boost/asio.hpp>
// 包含 Boost.Thread 库的头文件,用于实现多线程编程
#include <boost/thread.hpp>
// 包含 Boost.Atomic 库的头文件,提供原子操作,可用于多线程环境下的同步
#include <boost/atomic.hpp>
//包含 Boost.Signals2 库的头文件,用于实现信号与槽机制,允许对象之间进行松耦合的通信
#include <boost/signals2.hpp>
#include "config.h"
/**
 * @brief UDP广播器类,负责发送频谱数据和接收控制命令
 *
 * 使用Boost.Asio进行异步网络操作
 * 使用独立线程进行数据广播
 * 支持地址重用选项解决"Address already in use"问题
 */
class UdpBroadcaster {
public:
    UdpBroadcaster();
    ~UdpBroadcaster();

    /**
     * @brief 开始广播
     */
    void startBroadcasting();

    /**
     * @brief 停止广播
     */
    void stopBroadcasting();

    /**
     * @brief 设置要广播的数据
     * @param data 频谱数据
     */
    void setBroadcastData(const std::vector<float>& data);

    /**
     * @brief 信号:当收到控制命令时触发
     */
    // 定义一个信号,当收到控制命令时,会调用所有连接到这个信号的槽函数
    boost::signals2::signal<void(float)> onControlCommandReceived;



private:
    void broadcastThread();               // 广播线程函数,用于在独立线程中进行数据广播
    void startReceiveControl();          // 开始接收控制命令,启动异步接收操作
    //处理接收到的控制指令的回调函数,当接收到控制指令命令时会被调用
    void handleControlCommand(const boost::system::error_code& error,
                              std::size_t bytes_transferred);

    // Boost.Asio 的 I/O 服务对象,负责管理异步操作的事件循环
    boost::asio::io_service ioService_;
    // UDP 广播套接字,用于发送频谱数据
    boost::asio::ip::udp::socket broadcastSocket_;
    // UDP 控制套接字,用于接收控制命令
    boost::asio::ip::udp::socket controlSocket_;
    // 广播端点,指定广播数据的目标地址和端口
    boost::asio::ip::udp::endpoint broadcastEndpoint_;
    // 控制端点,指定接收控制命令的本地地址和端口
    boost::asio::ip::udp::endpoint controlEndpoint_;
    // 远程端点,记录发送控制命令的客户端地址和端口
    boost::asio::ip::udp::endpoint remoteEndpoint_;
    // 广播线程对象,用于执行广播线程函数
    boost::thread broadcastThread_;
    // I/O 服务线程对象,用于运行 I/O 服务的事件循环
    boost::thread ioServiceThread_;
    // 原子布尔变量,用于表示广播是否正在运行,可在多线程环境下安全访问
    boost::atomic<bool> running_;

    // 当前要广播的频谱数据,使用 vector 存储浮点数
    std::vector<float> currentData_;
    // 互斥锁,用于保护 currentData_ 的并发访问,确保线程安全
    boost::mutex dataMutex_;

    // 控制命令缓冲区,用于存储接收到的控制命令,大小为一个浮点数的字节数
    std::array<char, sizeof(float)> controlBuffer_;
};


#endif //FPGADEMO_UDPBROADCASTER_H

         fpga_simulator拥有协调数据生成和广播,处理控制指令以及提供运行状态

#ifndef FPGADEMO_FPGASIMULATOR_H
#define FPGADEMO_FPGASIMULATOR_H
#include "data_generator.h"
#include "UdpBroadcaster.h"
#include "Logger.h"
/**
 * @brief FPGA模拟器主类
 *
 * 负责协调数据生成和广播
 * 处理控制命令
 * 提供运行状态管理
 */

class FpgaSimulator {
public:
    FpgaSimulator();
    ~FpgaSimulator();

    /**
     * @brief 运行模拟器
     */
    void run();
    void stop();
private:
    data_generator dataGen_;              // 数据生成器
    UdpBroadcaster broadcaster_;         // UDP广播器
    boost::atomic<bool> running_;
};


#endif //FPGADEMO_FPGASIMULATOR_H
        b.后台处理端实现

          项目结构如图:

        此部分也是Cmake进行管理,其中config,logger以及udp相关与上面的模拟端类似,不再说明

        tcpserver用于作为tcp的服务端,负责处理客户端连接和数据广播,线程池处理多个客户端以及提供连接断开事件通知

//
// Created by Administrator on 2025/4/30.
//

#ifndef BACKEND_SERVER_TCPSERVER_H
#define BACKEND_SERVER_TCPSERVER_H

#include <vector>
#include <set>
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/signals2.hpp>
#include "config.h"
#include "Logger.h"
#include <boost/asio/post.hpp>  // Required for boost::asio::post
/**
 * @brief TCP服务器类
 *
 * 处理客户端连接和数据广播
 * 使用线程池处理多个客户端
 * 提供连接/断开事件通知
 */
class TcpServer {
public:
    using ClientConnectedCallback = std::function<void(boost::asio::ip::tcp::socket&)>;
    using ClientDisconnectedCallback = std::function<void(boost::asio::ip::tcp::socket&)>;

    TcpServer();
    ~TcpServer();

    void start();
    void stop();
    void broadcastData(const std::vector<float>& data);

    void setClientConnectedCallback(ClientConnectedCallback callback);
    void setClientDisconnectedCallback(ClientDisconnectedCallback callback);

private:
    void acceptThread();
    void startAccept();
    void handleAccept(boost::asio::ip::tcp::socket* socket,
                      const boost::system::error_code& error);
    void clientHandler(boost::asio::ip::tcp::socket* socket);

    boost::asio::io_service ioService_;
    boost::asio::ip::tcp::acceptor acceptor_;
    boost::thread acceptThread_;
    boost::asio::thread_pool ioThreadPool_; //线程池
    boost::atomic<bool> running_;

    std::set<boost::asio::ip::tcp::socket*> clients_;
    boost::mutex clientsMutex_;

    ClientConnectedCallback clientConnectedCallback_;
    ClientDisconnectedCallback clientDisconnectedCallback_;

    const size_t threadPoolSize_ = 4;  // 显式存储线程池大小
};

#endif //BACKEND_SERVER_TCPSERVER_H

         backendServer部分用于整和UDP接收和TCP服务功能,管理数据转发流程,

#ifndef BACKEND_SERVER_H
#define BACKEND_SERVER_H

#include "UdpReceiver.h"
#include "TcpServer.h"
#include "Logger.h"
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <csignal>

/**
 * @brief 后端服务器主类
 * 
 * 整合UDP接收和TCP服务功能
 * 管理数据转发流程
 * 提供完整的生命周期管理
 */
class BackendServer {
public:
    BackendServer();
    ~BackendServer();

    void run();
    void stop();

private:
    void onUdpDataReceived(const std::vector<float>& data);
    void onTcpClientConnected(boost::asio::ip::tcp::socket& socket);
    void onTcpClientDisconnected(boost::asio::ip::tcp::socket& socket);

    UdpReceiver udpReceiver_;
    TcpServer tcpServer_;
    std::vector<float> lastReceivedData_;
    boost::mutex dataMutex_;
    boost::atomic<bool> running_;
};

#endif // BACKEND_SERVER_H
        c.qt前端实现

        项目结构如图:

        整个项目有qt的.pro文件进行管理(可以封装成qmake管理的形式)

        logger类负责日志打印不进行介绍

        spectrumwidget封装了项目需要的频谱控件

#ifndef SPECTRUMWIDGET_H
#define SPECTRUMWIDGET_H

#include <QWidget>
#include <QVector>

class SpectrumWidget : public QWidget
{
    Q_OBJECT

public:
    explicit SpectrumWidget(QWidget *parent = nullptr);

    void setData(const QVector<float>& data);
    void setColorGradient(const QLinearGradient& gradient);
    void setBackgroundColor(const QColor& color);
    void setBarWidthRatio(float ratio);

protected:
    void paintEvent(QPaintEvent *event) override;

private:
    QVector<float> spectrumData;
    QLinearGradient gradient;
    QColor backgroundColor;
    float barWidthRatio;
};

#endif // SPECTRUMWIDGET_H

         tcp,udp类不再讲解,mainwindow主要绘制了界面,整和这些功能

4.知识点总结

        1. UDP协议

        理论

                无连接协议,发送数据前不需要建立连接

                不保证数据顺序和可靠性,但传输效率高

                适合实时性要求高、可容忍少量丢失的场景(如视频流、实时游戏)

        基础实现

// 创建UDP socket
boost::asio::ip::udp::socket socket(ioService);
socket.open(boost::asio::ip::udp::v4());

// 设置地址重用
socket.set_option(boost::asio::socket_base::reuse_address(true));

// 绑定端口
socket.bind(boost::asio::ip::udp::endpoint(
    boost::asio::ip::udp::v4(), port));

// 异步接收
socket.async_receive_from(buffer(data), senderEndpoint,
    [](const boost::system::error_code& error, size_t bytes) {
        // 处理接收数据
    });
         2. TCP协议

        理论

                面向连接的可靠传输协议

                保证数据顺序和完整性

                适合需要可靠传输的场景(如文件传输、远程控制)

        基础实现

// 创建TCP acceptor
boost::asio::ip::tcp::acceptor acceptor(ioService,
    boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port));

// 异步接受连接
boost::asio::ip::tcp::socket socket(ioService);
acceptor.async_accept(socket, 
    [](const boost::system::error_code& error) {
        // 处理新连接
    });

// 异步读写
boost::asio::async_read(socket, buffer(data),
    [](const boost::system::error_code& error, size_t bytes) {
        // 处理读取数据
    });
        3. 多线程编程

        理论

                并发执行多个任务

                提高CPU利用率

                需要处理线程同步问题

        基础实现

#include <thread>
#include <vector>

void worker(int id) {
    std::cout << "Worker " << id << " running\n";
}

int main() {
    std::vector<std::thread> threads;
    
    // 创建线程
    for(int i = 0; i < 5; ++i) {
        threads.emplace_back(worker, i);
    }
    
    // 等待所有线程完成
    for(auto& t : threads) {
        t.join();
    }
}
        4. 原子量(atomic)

        理论

        不可分割的操作

        无需锁的线程安全访问

        适合简单变量的并发访问

        基础实现

#include <atomic>

std::atomic<int> counter(0);

void increment() {
    for(int i = 0; i < 1000; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed);
    }
}

// 多线程安全访问
std::thread t1(increment);
std::thread t2(increment);
         5. C++信号槽绑定

         理论

                观察者模式的实现

                松耦合的事件通知机制

                信号发出时自动调用连接的槽函数

        基础实现

#include <boost/signals2.hpp>

// 定义信号
boost::signals2::signal<void(int)> valueChanged;

// 连接槽函数
valueChanged.connect([](int newValue) {
    std::cout << "Value changed to " << newValue << "\n";
});

// 触发信号
valueChanged(42);
        6. 回调函数

        理论

                函数作为参数传递

                异步操作完成后调用

                实现控制反转

        基础实现

// 定义回调类型
using Callback = std::function<void(const std::string&)>;

void fetchData(Callback callback) {
    // 模拟异步操作
    std::thread([callback]() {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        callback("Data loaded");
    }).detach();
}

// 使用回调
fetchData([](const std::string& result) {
    std::cout << result << "\n";
});
        7. auto关键字

        理论

                自动类型推导

                编译时确定类型

                简化复杂类型声明

        基础实现

auto i = 42; // int
auto d = 3.14; // double
auto v = std::vector<int>{1, 2, 3}; // std::vector<int>

// 结合lambda
auto func = [](int x) { return x * 2; };
        8. Lambda表达式

        理论

                匿名函数对象

                可以捕获上下文变量

                简化回调和小函数定义

        基础实现

// 基本形式
auto sum = [](int a, int b) { return a + b; };

// 捕获局部变量
int factor = 3;
auto multiply = [factor](int x) { return x * factor; };

// 在算法中使用
std::vector<int> nums{1, 2, 3};
std::sort(nums.begin(), nums.end(), 
    [](int a, int b) { return a > b; });
9. vector容器

        理论

                动态数组

                连续内存存储

                自动管理内存

        基础实现

#include <vector>

// 创建和初始化
std::vector<int> numbers = {1, 2, 3};

// 添加元素
numbers.push_back(4);

// 遍历
for(auto n : numbers) {
    std::cout << n << " ";
}

// 预分配空间
numbers.reserve(100);
        10. 线程池

        理论

                预先创建一组线程

                避免频繁创建销毁线程开销

                任务队列管理待执行任务

        基础实现

#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>

class ThreadPool {
public:
    ThreadPool(size_t threads) {
        for(size_t i = 0; i < threads; ++i) {
            workers.emplace_back([this] {
                while(true) {
                    std::function<void()> task;
                    {
                        std::unique_lock<std::mutex> lock(queueMutex);
                        condition.wait(lock, [this] { 
                            return stop || !tasks.empty(); 
                        });
                        if(stop && tasks.empty()) return;
                        task = std::move(tasks.front());
                        tasks.pop();
                    }
                    task();
                }
            });
        }
    }
    
    template<class F>
    void enqueue(F&& f) {
        {
            std::unique_lock<std::mutex> lock(queueMutex);
            tasks.emplace(std::forward<F>(f));
        }
        condition.notify_one();
    }
    
    ~ThreadPool() {
        {
            std::unique_lock<std::mutex> lock(queueMutex);
            stop = true;
        }
        condition.notify_all();
        for(std::thread &worker : workers)
            worker.join();
    }

private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex queueMutex;
    std::condition_variable condition;
    bool stop = false;
};


     

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值