USRP初学者使用手册(基础配置及bug记录)——Linux+Clion(单台X310收发)

文章目录


前言

由于这是实验室已经配好的电脑,所以没有硬件驱动的安装过程,不好意思啦

⚠️ 注意:单台USRP设备自收发不要用MATLAB

在Windows + MATLAB环境下,用单台USRP做同步收发时,while循环会因为TX/RX阻塞互相等待而卡死,无法通过MATLAB内部机制解决。

切换到**Linux + C++(pthread)**后,将发送和接收分别放在独立线程中,彻底解决了这个问题。如果你在做单台设备的雷达感知或loopback实验,直接用C++ + UHD库,不要在MATLAB上浪费时间。


一、配置以太网(针对X310)

1.连接设备,设置以太网静态IP

设备连接到电脑上的网口上,实验室用的万兆网口

打开电脑设置------网络------以太网(enp1s0f1 是本机连接USRP的网卡接口名)

手动配置IP为192.168.40.3,设置完重新开关 一下以太网

2.Linux环境初始化

在运行 uhd_usrp_probe 或任何UHD程序之前,先在终端执行以下命令:

bash 复制代码
# 增大网络缓冲区,防止USRP数据流丢包(over/underflow)
sudo sysctl -w net.core.rmem_max=24912805
sudo sysctl -w net.core.wmem_max=24912805
# 开启巨帧(Jumbo Frame),降低网络传输开销,适配USRP 1GigE/10GigE接口
sudo ifconfig enp1s0f1 mtu 9000

enp1s0f1 是本机连接USRP的网卡接口名,根据实际情况替换(可用 ip link 查看)。

3.检测连接

初始化后在终端执行,看到下面这样的输出即为连接成功

bash 复制代码
uhd_usrp_probe

二、Clion+Cmake入门(新建项目开始)

由于之前从未接触过c++项目,所以这部分会比较琐碎,供我以后操作使用

原始项目是GitHub的通信项目(两台N210收发)https://github.com/bmorgan5/fun_ofdm

1.理清项目结构

接手的项目往往如山一样堆积,所以从0开始理项目结构很重要

项目主要包括两个内容:

1.file_comm:发射已经写好的dat文件内容,接收后保存为dat文件,不做任何处理,交由matlab处理

2.realtime_comm:完整的发射---接收---解调---译码流程,但很简略,没有任何复杂处理,朴实无华的64点fft

x310_comm/

├── CMakeLists.txt

├── cmake/

│ └── Modules/

│ ├── FindUHD.cmake

│ └── FindFFTW3.cmake

├── common/

│ └── CMakeLists.txt

├── file_comm/

│ ├── CMakeLists.txt

│ └── file_comm_main.cpp

├── realtime_comm/

│ ├── CMakeLists.txt

│ └── realtime_comm_main.cpp

├── data/

│ ├── input/

│ └── output/

└── build/

文件夹 职责
cmake/Modules/ Find 模块,从旧项目直接复制
common/ 两个 target 共用的代码(暂为空)
file_comm/ 文件读写通信的全部源码
realtime_comm/ 实时 USRP 通信的全部源码
data/ 运行时数据文件,不参与编译
build/ cmake 构建产物,不入 git

2.项目文件初始设置

新建clion项目

在 CLion 中新建 C++ 项目,命名为 x310_comm。CLion 会自动生成一个顶层 main.cpp 和 CMakeLists.txt,把自动生成的 main.cpp 直接删掉。

建立目录结构(空文件夹)

在项目根目录下手动创建以下文件夹:

cmake/Modules/

common/

file_comm/

realtime_comm/

data/input/

data/output/

build/

复制 Find 模块

从旧项目的 cmake/Modules/ 目录,把以下两个文件直接复制到新项目的 cmake/Modules/:

FindUHD.cmake

FindFFTW3.cmake

写 CMakeLists.txt(AI辅助)

顶层 CMakeLists.txt

common/CMakeLists.txt

file_comm/CMakeLists.txt

realtime_comm/CMakeLists.txt

创建占位 main 文件

在 file_comm/ 和 realtime_comm/ 各新建对应的 .cpp 文件,内容先只写:

cpp 复制代码
int main() { return 0; }

编译验证

Cmake流程:编辑CMakeList.txt------终端cmake------在Clion中点文件重新加载Cmake项目

进入空的build文件夹---右键在终端打开,执行以下命令

bash 复制代码
cmake ..
make

看到以下输出说明骨架配置成功:

3.编辑file_comm部分

(1)函数文件

file_comm_main.cpp函数和CMakeLists.txt如附录所示,不多赘述

(2)数据文件

需要把matlab生成的发送帧文件(tx_waveform.dat)放在data文件夹中

然后就可以直接运行得到接收端data了!保存为rx_result.dat,便于拿回matlab处理

运行是在右上角选择配置运行的,不是matlab那样直接运行文件

4.编辑realtime_comm部分

(1)函数文件

把原项目的函数代码粘贴进realtime_comm文件夹,realtime_comm_main.cpp函数和CMakeLists.txt如附录所示,不多赘述

(2)运行结果

核心代码

file_comm部分

file_comm_main.cpp

cpp 复制代码
#include <iostream>
#include <vector>
#include <complex>
#include <thread>
#include <atomic>
#include <chrono>
#include <fstream>

#include <uhd/usrp/multi_usrp.hpp>
#include <uhd/stream.hpp>

using namespace std;

static atomic<bool> stop_signal_called(false);

// ======== 发送线程函数 ========
void trans(
    vector<complex<double>>        buff,
    uhd::tx_streamer::sptr         tx_stream,
    uhd::tx_metadata_t             md,
    size_t                         total_num_samps,
    double                         timeout,
    vector<uhd::time_spec_t>       frame_start_times)
{
    int tx_num = (int)frame_start_times.size();
    for(int i = 0; i < tx_num; i++)
    {
        if(stop_signal_called) break;
        md.has_time_spec  = true;
        md.time_spec      = frame_start_times[i];
        md.start_of_burst = false;
        md.end_of_burst   = false;

        size_t num_acc_samps = 0;
        while(num_acc_samps < total_num_samps)
        {
            size_t samps_to_send = min(total_num_samps - num_acc_samps, buff.size());
            size_t num_tx_samps  = tx_stream->send(&buff.front(), samps_to_send, md, timeout);
            md.has_time_spec     = false;
            num_acc_samps       += num_tx_samps;
        }
        md.end_of_burst = true;
        tx_stream->send("", 0, md);
        cout << "[DBG] 第 " << i << " 帧发送完,样本数: " << total_num_samps << endl;
    }
}

// ======== 接收函数 ========
void recv(
    uhd::usrp::multi_usrp::sptr    usrp,
    size_t                         rx_total_num_samps,
    vector<uhd::time_spec_t>       frame_start_times,
    ofstream&                      rx_log)
{
    uhd::stream_args_t stream_args("fc64", "");
    stream_args.channels = {0};
    uhd::rx_streamer::sptr rx_stream = usrp->get_rx_stream(stream_args);

    uhd::stream_cmd_t stream_cmd(uhd::stream_cmd_t::STREAM_MODE_NUM_SAMPS_AND_DONE);
    stream_cmd.num_samps  = rx_total_num_samps;
    stream_cmd.stream_now = false;

    uhd::rx_metadata_t md;
    vector<complex<double>> buff(rx_stream->get_max_num_samps());
    vector<void*> buffs;
    buffs.push_back(&buff.front());

    int num = (int)frame_start_times.size();

    for(int i = 0; i < num; i++)
    {
        if(stop_signal_called) break;

        stream_cmd.time_spec = frame_start_times[i];
        rx_stream->issue_stream_cmd(stream_cmd);

        size_t num_acc_samps = 0;
        vector<complex<double>> all_samples;

        while(!stop_signal_called && num_acc_samps < rx_total_num_samps)
        {
            size_t num_rx_samps = rx_stream->recv(buffs, buff.size(), md, 1.0, true);
            if(stop_signal_called) break;

            if(md.error_code == uhd::rx_metadata_t::ERROR_CODE_TIMEOUT)
            {
                cout << "Timeout while streaming\n";
                break;
            }
            if(md.error_code == uhd::rx_metadata_t::ERROR_CODE_OVERFLOW)
                continue;
            if(md.error_code == uhd::rx_metadata_t::ERROR_CODE_LATE_COMMAND)
            {
                cerr << "[WARN] Late command, skipping frame " << i << "\n";
                all_samples.clear();
                num_acc_samps = rx_total_num_samps;
                break;
            }
            if(md.error_code != uhd::rx_metadata_t::ERROR_CODE_NONE)
            {
                cerr << "Receiver error: " << md.strerror() << "\n";
                break;
            }

            all_samples.insert(all_samples.end(),
                               buff.begin(), buff.begin() + num_rx_samps);
            num_acc_samps += num_rx_samps;
        }

        if(all_samples.empty()) continue;

        cout << "[DBG] 收到样本数: " << all_samples.size() << endl;

        // 写帧长(4字节),再写IQ数据(float32 interleaved)
        uint32_t frame_len = (uint32_t)all_samples.size();
        rx_log.write((char*)&frame_len, sizeof(uint32_t));
        for(auto& c : all_samples)
        {
            float re = (float)c.real();
            float im = (float)c.imag();
            rx_log.write((char*)&re, sizeof(float));
            rx_log.write((char*)&im, sizeof(float));
        }
        cout << "[DBG] 已写入帧,样本数: " << frame_len << endl;
    }

    // 结束RX流
    uhd::stream_cmd_t stop_cmd(uhd::stream_cmd_t::STREAM_MODE_STOP_CONTINUOUS);
    stop_cmd.stream_now = true;
    rx_stream->issue_stream_cmd(stop_cmd);

    vector<complex<double>> drain_buff(rx_stream->get_max_num_samps());
    vector<void*> drain_buffs(1, &drain_buff.front());
    uhd::rx_metadata_t drain_md;
    while(true)
    {
        size_t nsamps = rx_stream->recv(drain_buffs, drain_buff.size(), drain_md, 0.1, false);
        if(nsamps == 0) break;
    }
    this_thread::sleep_for(chrono::milliseconds(50));
}

// ======== main ========
int main()
{
    // ---------- 参数 ----------
    double freq               = 3.51e9;
    double rate               = 100e6;
    double tx_gain            = 20.0;
    double rx_gain            = 20.0;
    string device_addr        = "192.168.40.2";
    double seconds_in_future  = 0.05;
    const int frames_per_batch = 10;
    const double frame_period  = 0.5;
    int run_nums               = 200;   // 总帧数

    // ---------- 初始化USRP ----------
    cout << "初始化USRP..." << endl;
    auto usrp = uhd::usrp::multi_usrp::make(
        uhd::device_addr_t("type=x300,master_clock_rate=200e6,addr=" + device_addr)
    );

    usrp->set_tx_subdev_spec(uhd::usrp::subdev_spec_t("B:0"));
    usrp->set_rx_subdev_spec(uhd::usrp::subdev_spec_t("A:0"));
    usrp->set_tx_rate(rate);
    usrp->set_rx_rate(rate);
    usrp->set_tx_freq(uhd::tune_request_t(freq));
    usrp->set_rx_freq(uhd::tune_request_t(freq));
    usrp->set_tx_gain(tx_gain);
    usrp->set_rx_gain(rx_gain);

    cout << "TX率: " << usrp->get_tx_rate()/1e6 << " Msps" << endl;
    cout << "RX率: " << usrp->get_rx_rate()/1e6 << " Msps" << endl;

    this_thread::sleep_for(chrono::milliseconds(200));

    // ---------- 读取MATLAB生成的发射波形 ----------
    ifstream wf(string(PROJECT_DATA_DIR) + "/tx_waveform.dat", ios::binary);
    if(!wf) { cerr << "无法打开tx_waveform.dat" << endl; return 1; }

    wf.seekg(0, ios::end);
    size_t file_bytes = wf.tellg();
    wf.seekg(0, ios::beg);

    size_t num_samples = file_bytes / (2 * sizeof(float));
    vector<float> raw(num_samples * 2);
    wf.read(reinterpret_cast<char*>(raw.data()), file_bytes);
    wf.close();

    vector<complex<double>> tx_samples(num_samples);
    for(size_t i = 0; i < num_samples; i++)
        tx_samples[i] = complex<double>(raw[2*i], raw[2*i+1]);

    cout << "读取波形: " << num_samples << " 个样本" << endl;

    size_t tx_total_num_samps = tx_samples.size();
    size_t rx_total_num_samps = (size_t)(tx_samples.size() * 1.5);

    // ---------- 构造TX streamer ----------
    uhd::stream_args_t tx_args("fc64", "");
    tx_args.channels = {0};
    uhd::tx_streamer::sptr tx_stream = usrp->get_tx_stream(tx_args);

    // ---------- 打开接收数据文件(全程只开一次)----------
    ofstream rx_log(string(PROJECT_DATA_DIR) + "/rx_result.dat", ios::binary);
    if(!rx_log) { cerr << "无法创建rx_all.dat" << endl; return 1; }

    // ---------- 主循环 ----------
    cout << "开始收发..." << endl;

    int batch_idx    = 0;
    int total_frames = 0;

    while(!stop_signal_called && total_frames < run_nums)
    {
        usrp->set_time_now(uhd::time_spec_t(0.0));

        vector<uhd::time_spec_t> frame_start_times(frames_per_batch);
        for(int i = 0; i < frames_per_batch; i++)
            frame_start_times[i] = uhd::time_spec_t(
                seconds_in_future + frame_period * static_cast<double>(i));

        uhd::tx_metadata_t tx_md;
        thread tx_thread([&](){
            trans(tx_samples, tx_stream, tx_md,
                  tx_total_num_samps, 0.5, frame_start_times);
        });

        recv(usrp, rx_total_num_samps, frame_start_times, rx_log);

        tx_thread.join();

        batch_idx++;
        total_frames += frames_per_batch;
        cout << "完成第 " << batch_idx << " 批,共 " << total_frames << " 帧" << endl;
    }

    rx_log.close();
    cout << "结束,数据已保存至 rx_all.dat" << endl;
    return 0;
}

file_comm中的CMakeLists.txt

cpp 复制代码
add_executable(file_comm file_comm_main.cpp)
target_compile_definitions(file_comm PRIVATE
        PROJECT_DATA_DIR="${CMAKE_SOURCE_DIR}/data"
)
target_include_directories(file_comm PRIVATE
        ${CMAKE_SOURCE_DIR}/common
)
target_link_libraries(file_comm PRIVATE
        ${UHD_LIBRARIES}
        Boost::filesystem Boost::system Boost::thread
        ${FFTW3_LIBRARIES}
        Threads::Threads
)

realtime_comm部分

realtime_comm_main.cpp

cpp 复制代码
#include <iostream>
#include <vector>
#include <complex>
#include <thread>
#include <atomic>
#include <chrono>
#include <cstring>

#include <uhd/usrp/multi_usrp.hpp>
#include <uhd/stream.hpp>

#include "receiver_chain.h"
#include "frame_builder.h"


using namespace std;
using namespace fun;

static atomic<bool> stop_signal_called(false);

// ======== 回调函数 ========
void callback(vector<vector<unsigned char>> payloads)
{
    for(auto& p : payloads)
        cout << "Received: " << string(p.begin(), p.end()) << endl;
}

// ======== 发送线程函数(完全照搬感知代码trans) ========
void trans(
    vector<complex<double>>        buff,
    uhd::tx_streamer::sptr         tx_stream,
    uhd::tx_metadata_t             md,
    size_t                         total_num_samps,
    double                         timeout,
    vector<uhd::time_spec_t>       frame_start_times)
{
    int tx_num = (int)frame_start_times.size();
    for(int i = 0; i < tx_num; i++)
    {
        if(stop_signal_called) break;
        md.has_time_spec  = true;
        md.time_spec      = frame_start_times[i];
        md.start_of_burst = false;
        md.end_of_burst   = false;

        size_t num_acc_samps = 0;
        while(num_acc_samps < total_num_samps)
        {
            size_t samps_to_send = min(total_num_samps - num_acc_samps, buff.size());
            size_t num_tx_samps  = tx_stream->send(&buff.front(), samps_to_send, md, timeout);
            md.has_time_spec     = false;
            num_acc_samps       += num_tx_samps;
        }
        md.end_of_burst = true;
        tx_stream->send("", 0, md);
        // 每帧发送完后
        cout << "[DBG] 第 " << i << " 帧发送完,样本数: " << total_num_samps << endl;
    }
}

// ======== 接收函数(完全照搬感知代码recv,去掉感知计算部分) ========
void recv(
    uhd::usrp::multi_usrp::sptr       usrp,
    size_t                             rx_total_num_samps,
    vector<uhd::time_spec_t>           frame_start_times,
    receiver_chain&                    rec_chain,
    void(*cb)(vector<vector<unsigned char>>))
{
    uhd::stream_args_t stream_args("fc64", "");
    stream_args.channels = {0};
    uhd::rx_streamer::sptr rx_stream = usrp->get_rx_stream(stream_args);

    uhd::stream_cmd_t stream_cmd(uhd::stream_cmd_t::STREAM_MODE_NUM_SAMPS_AND_DONE);
    stream_cmd.num_samps  = rx_total_num_samps;
    stream_cmd.stream_now = false;

    uhd::rx_metadata_t md;
    vector<complex<double>> buff(rx_stream->get_max_num_samps());
    vector<void*> buffs;
    buffs.push_back(&buff.front());

    int num = (int)frame_start_times.size();

    for(int i = 0; i < num; i++)
    {
        if(stop_signal_called) break;

        stream_cmd.time_spec = frame_start_times[i];
        rx_stream->issue_stream_cmd(stream_cmd);

        size_t num_acc_samps = 0;
        vector<complex<double>> all_samples;

        while(!stop_signal_called && num_acc_samps < rx_total_num_samps)
        {
            size_t num_rx_samps = rx_stream->recv(buffs, buff.size(), md, 1.0, true);
            if(stop_signal_called) break;

            if(md.error_code == uhd::rx_metadata_t::ERROR_CODE_TIMEOUT)
            {
                cout << "Timeout while streaming\n";
                break;
            }
            if(md.error_code == uhd::rx_metadata_t::ERROR_CODE_OVERFLOW)
                continue;
            if(md.error_code == uhd::rx_metadata_t::ERROR_CODE_LATE_COMMAND)
            {
                cerr << "[WARN] Late command, skipping frame " << i << "\n";
                all_samples.clear();
                num_acc_samps = rx_total_num_samps;
                break;
            }
            if(md.error_code != uhd::rx_metadata_t::ERROR_CODE_NONE)
            {
                cerr << "Receiver error: " << md.strerror() << "\n";
                break;
            }

            all_samples.insert(all_samples.end(),
                               buff.begin(), buff.begin() + num_rx_samps);
            num_acc_samps += num_rx_samps;
        }

        if(all_samples.empty()) continue;

        // 加这行,看收到了多少样本
        cout << "[DBG] 收到样本数: " << all_samples.size() << endl;

        auto packets = rec_chain.process_samples(all_samples);

        // 加这行,看解出了多少包
        cout << "[DBG] 解出包数: " << packets.size() << endl;

        cb(packets);

        // 送入接收链路处理
        //auto packets = rec_chain.process_samples(all_samples);
        //cb(packets);
    }

    // 优雅结束RX流
    uhd::stream_cmd_t stop_cmd(uhd::stream_cmd_t::STREAM_MODE_STOP_CONTINUOUS);
    stop_cmd.stream_now = true;
    rx_stream->issue_stream_cmd(stop_cmd);

    vector<complex<double>> drain_buff(rx_stream->get_max_num_samps());
    vector<void*> drain_buffs(1, &drain_buff.front());
    uhd::rx_metadata_t drain_md;
    while(true)
    {
        size_t nsamps = rx_stream->recv(drain_buffs, drain_buff.size(), drain_md, 0.1, false);
        if(nsamps == 0) break;
    }
    this_thread::sleep_for(chrono::milliseconds(50));
}

// ======== main ========
int main()
{
    // ---------- 参数 ----------
    double freq               = 3.51e9;
    double rate               = 50e6;
    double tx_gain            = 20.0;
    double rx_gain            = 20.0;
    string device_addr        = "192.168.40.2";
    double seconds_in_future  = 0.05;   // 每帧提前量
    const int frames_per_batch = 10;    // 每批帧数
    const double frame_period  = 0.5;   // 帧间隔(s)

    // ---------- 初始化USRP ----------
    cout << "初始化USRP..." << endl;
    auto usrp = uhd::usrp::multi_usrp::make(
        uhd::device_addr_t("type=x300,master_clock_rate=200e6,addr=" + device_addr)
    );

    usrp->set_tx_subdev_spec(uhd::usrp::subdev_spec_t("B:0"));
    usrp->set_rx_subdev_spec(uhd::usrp::subdev_spec_t("A:0"));
    usrp->set_tx_rate(rate);
    usrp->set_rx_rate(rate);
    usrp->set_tx_freq(uhd::tune_request_t(freq));
    usrp->set_rx_freq(uhd::tune_request_t(freq));
    usrp->set_tx_gain(tx_gain);
    usrp->set_rx_gain(rx_gain);

    cout << "TX率: " << usrp->get_tx_rate()/1e6 << " Msps" << endl;
    cout << "RX率: " << usrp->get_rx_rate()/1e6 << " Msps" << endl;

    this_thread::sleep_for(chrono::milliseconds(200));

    // ---------- 构造TX streamer(只创建一次,不会和RX同时存在) ----------
    uhd::stream_args_t tx_args("fc64", "");
    tx_args.channels = {0};
    uhd::tx_streamer::sptr tx_stream = usrp->get_tx_stream(tx_args);

    // ---------- 构造发送帧 ----------
    frame_builder fb;
    string s = "Hello World";
    vector<unsigned char> data(s.begin(), s.end());
    auto tx_samples = fb.build_frame(data, RATE_1_2_BPSK);

    size_t tx_total_num_samps = tx_samples.size();
    size_t rx_total_num_samps = (size_t)(tx_samples.size() * 1.5); // 多收一点保证帧完整

    receiver_chain rec_chain;

    // ---------- 主循环(完全照搬感知代码批次结构) ----------
    cout << "开始收发..." << endl;

    int batch_idx   = 0;
    int total_frames = 0;
    int run_nums    = 200; // 总帧数

    while(!stop_signal_called && total_frames < run_nums)
    {
        // 重置硬件时钟(与感知代码完全一致)
        usrp->set_time_now(uhd::time_spec_t(0.0));

        // 构造这一批的帧时间表
        vector<uhd::time_spec_t> frame_start_times(frames_per_batch);
        for(int i = 0; i < frames_per_batch; i++)
            frame_start_times[i] = uhd::time_spec_t(
                seconds_in_future + frame_period * static_cast<double>(i));

        // 启动发射线程
        uhd::tx_metadata_t tx_md;
        thread tx_thread([&](){
            trans(tx_samples, tx_stream, tx_md,
                  tx_total_num_samps, 0.5, frame_start_times);
        });

        // 主线程做接收(RX streamer在recv内部创建和销毁)
        recv(usrp, rx_total_num_samps, frame_start_times,
             rec_chain, callback);

        tx_thread.join();

        batch_idx++;
        total_frames += frames_per_batch;
        cout << "完成第 " << batch_idx << " 批,共 " << total_frames << " 帧" << endl;
    }

    cout << "结束" << endl;
    return 0;
}

realtime_comm中的CMakeLists.txt

cpp 复制代码
add_executable(realtime_comm
    realtime_comm_main.cpp
    channel_est.cpp
    fft.cpp
    fft_symbols.cpp
    frame_builder.cpp
    frame_decoder.cpp
    frame_detector.cpp
    interleaver.cpp
    modulator.cpp
    parity.cpp
    phase_tracker.cpp
    ppdu.cpp
    puncturer.cpp
    receiver_chain.cpp
    symbol_mapper.cpp
    timing_sync.cpp
    viterbi.cpp
)

target_include_directories(realtime_comm PRIVATE
    ${CMAKE_SOURCE_DIR}/common
)

target_link_libraries(realtime_comm PRIVATE
    ${UHD_LIBRARIES}
    Boost::filesystem Boost::system Boost::thread
    ${FFTW3_LIBRARIES}
    Threads::Threads
)
target_include_directories(realtime_comm PRIVATE
        ${CMAKE_SOURCE_DIR}/common
)
target_link_libraries(realtime_comm PRIVATE
        ${UHD_LIBRARIES}
        Boost::filesystem Boost::system Boost::thread
        ${FFTW3_LIBRARIES}
        Threads::Threads
)

过程中遇到的问题

相关推荐
网络安全许木4 小时前
自学渗透测试第14天(信息收集进阶与指纹识别)
linux·网络安全·渗透测试
xlq223224 小时前
40.线程控制
linux
郭涤生4 小时前
C++模板元编程理论基础简介
c++
CheerWWW4 小时前
C++学习笔记——栈内存与堆内存、宏、auto、std::array
c++·笔记·学习
WBluuue4 小时前
数据结构与算法:二项式定理和二项式反演
c++·算法
yashuk5 小时前
C语言 vs. C++ ,哪个更适合初学者?
c语言·c++·面向对象编程·初学者·学习路径
-许平安-5 小时前
MCP项目笔记十(客户端 MCPClient)
c++·笔记·ai·raii·mcp·pluginapi·plugin system
一只旭宝5 小时前
【C++ 入门精讲2】函数重载、默认参数、函数指针、volatile | 手写笔记(附完整代码)
c++·笔记
TechMasterPlus5 小时前
Linux U-Boot 与内核启动流程深度解析:从上电到 Shell 的完整之旅
linux·运维·服务器