You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

双线程下阻塞式CAN套接字偶发延迟问题排查求助

问题:CAN通信随机延迟排查与解决

我正在为采用CAN通信协议的CubeMars电机开发库,该库使用两个线程:主线程处理大部分逻辑,监听线程负责监听CAN总线(接收电机的响应与确认包)并保存信息,供主线程后续读取。通常主线程向CAN总线发送指令后,电机发送的确认包会被监听线程捕获保存,主线程等待确认后再继续执行,整个过程耗时约500us。但约1%的随机场景下,耗时会骤升至10ms,打乱控制循环,且问题并非出自电机本身。

使用环境

  • CubeMars电机:AK60-6 v.1.1(MIT模式)
  • Raspberry 4B(搭载Arch Linux ARM)
  • MCP2515 CAN板
  • CAN通信,波特率1M
  • C++编写代码

相关代码

创建套接字代码

int MotorHandler::openSocket(const char* can_bus)
{
    // Create a CAN socket
    int s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    if (s < 0) {
        cout << "Error creating the socket! Exiting" << endl;
        exit(1);
    }
    else   
        cout << "Socket created successfully" << endl;

    // Set socket timeout
    struct timeval tv;
    tv.tv_sec = 0;
    tv.tv_usec = SOCKET_TIMEOUT_US;
    int success = setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
    if (success < 0) {
        cout << "Error setting the timeout to the CAN socket" << endl;
        exit(1);
    }

    // Bind the socket to the CAN bus
    struct sockaddr_can addr;
    struct ifreq ifr;
    strcpy(ifr.ifr_name, can_bus);
    ioctl(s, SIOCGIFINDEX, &ifr);

    memset(&addr, 0, sizeof(addr));
    addr.can_family = AF_CAN;
    addr.can_ifindex = ifr.ifr_ifindex;

    int result = bind(s, (struct sockaddr *)&addr, sizeof(addr));
    if (result < 0) {
        cout << "Binding error! Exiting" << endl;
        exit(1);
    }
    else
        cout << "Binding successful" << endl;

    return s;
}

监听线程函数

int Listener::listenerLoop(int s)
{
    bool stopThread = 0;

    cout << "Starting CAN bus monitoring" << endl;
    while(!stopThread) {

        struct can_frame frame;
        int nbytes = read(s, &frame, sizeof(can_frame));
        if (nbytes > 0)
            parseFrame(frame);

        // Thread sleep for scheduling
        std::this_thread::sleep_for(chrono::microseconds(10));
        {
            scoped_lock lock(m_mutex);
            stopThread = m_stopThread;  
        }
    }

    return 0;
}

已尝试操作

  • 最初使用非阻塞套接字,单线程控制单个电机时仍有随机延迟;切换为阻塞模式后,问题消失。
  • 在双线程库中设置套接字为阻塞模式,随机延迟问题仍存在。

推测问题可能出在缓冲或调度上,尝试过不同线程休眠时长但无效,请求协助排查原因与解决方法。


排查与解决建议

核心问题分析

  1. 线程调度延迟:Raspberry Pi 4B的Linux内核默认CFS调度器的最小调度粒度受HZ参数影响(Arch Linux ARM默认HZ=100,即10ms),监听线程主动调用sleep_for(10us)会强制放弃CPU,唤醒时间无法保证微秒级精度,这是10ms延迟的主要来源。
  2. 阻塞套接字的线程竞争:双线程模式下,监听线程在read()返回后主动休眠,可能错过电机响应的处理窗口,同时主线程的等待逻辑若存在轮询,也会增加额外开销。
  3. MCP2515硬件/内核缓冲:若CAN驱动层缓冲配置过小,可能导致帧堆积,后续一次性处理时出现延迟。

具体解决步骤

1. 移除监听线程的主动休眠

阻塞模式下,read()本身会在无数据时挂起线程,无需主动休眠,强制休眠会直接导致调度延迟。修改后的监听线程代码:

int Listener::listenerLoop(int s)
{
    bool stopThread = false;

    cout << "Starting CAN bus monitoring" << endl;
    while(!stopThread) {
        struct can_frame frame;
        int nbytes = read(s, &frame, sizeof(can_frame));
        if (nbytes > 0) {
            parseFrame(frame);
        }

        // 仅在检查停止标志时加锁,减少锁开销
        {
            scoped_lock lock(m_mutex);
            stopThread = m_stopThread;  
        }
    }

    return 0;
}

2. 设置监听线程为实时调度优先级

避免监听线程被低优先级线程抢占,保证响应及时性:

#include <pthread.h>

// 在监听线程启动后调用此函数
void setRealtimePriority(pthread_t thread) {
    struct sched_param param;
    param.sched_priority = sched_get_priority_max(SCHED_FIFO);
    pthread_setschedparam(thread, SCHED_FIFO, &param);
}

注意:需以root权限运行程序,或给程序添加CAP_SYS_NICE权限才能设置实时优先级。

3. 调整CAN套接字与内核缓冲

  • 临时调整CAN总线发送队列长度:
ip link set can0 txqueuelen 1000
  • 永久生效:在/etc/network/interfaces.d/can0中添加post-up ip link set can0 txqueuelen 1000
  • 在openSocket函数中设置套接字接收缓冲:
// bind操作完成后添加
int rcv_buf_size = 1024 * 1024;
setsockopt(s, SOL_SOCKET, SO_RCVBUF, &rcv_buf_size, sizeof(rcv_buf_size));

4. 优化主线程与监听线程的同步机制

将轮询等待改为条件变量通知,减少无效轮询开销:

// 监听线程解析到确认帧时
void parseFrame(struct can_frame frame) {
    // 帧处理逻辑...
    if (isAckFrame(frame)) {
        scoped_lock lock(m_mutex);
        m_ackReceived = true;
        m_cv.notify_one();
    }
}

// 主线程等待确认
void waitForAck() {
    unique_lock lock(m_mutex);
    m_cv.wait(lock, []{ return m_ackReceived; });
    m_ackReceived = false; // 重置标志
}

5. 优化MCP2515 SPI参数

提升SPI总线速度,避免SPI传输瓶颈:

# 临时生效
echo 8000000 > /sys/bus/spi/devices/spi0.0/max_speed_hz

验证方法

使用perf工具分析线程调度延迟:

perf record -g -p <监听线程PID>
perf report

查看是否存在大量sched_switch事件导致线程被抢占,或hrtimer相关的延迟。

内容的提问来源于stack exchange,提问作者Katarina

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 21:05:01