2

我正在使用 SocketCAN 在嵌入式设备(SOC / ARM 内核 / Linux)上测试 CAN 接口,我想使用高效的代码尽可能快地发送数据进行测试。

我可以将 CAN 设备(“can0”)作为 BSD 套接字打开,并使用“write”发送帧。这一切都很好。

我的桌面显然可以比 CAN 传输速率更快地生成帧(我使用的是 500000 bps)。为了有效发送,我尝试在套接字文件描述符上使用“选择”来等待它准备好,然后是“写入”。但是,无论发送缓冲区的状态如何,“select”似乎都会立即返回,并且“write”也不会阻塞。这意味着当缓冲区填满时,我从“write”(返回值 -1)收到错误,并且 errno 设置为 105(“没有可用的缓冲区空间”)。

这意味着我必须等待任意时间,然后再次尝试写入,这似乎非常低效(轮询!)。

这是我的代码(C,为简洁而编辑):

printf("CAN Data Generator\n");

int skt;      // CAN raw socket
struct sockaddr_can addr;
struct canfd_frame frame;

const int WAIT_TIME = 500;

// Create socket:
skt = socket(PF_CAN, SOCK_RAW, CAN_RAW);

// Get the index of the supplied interface name: 
unsigned int if_index = if_nametoindex(argv[1]);

// Bind CAN device to socket created above:
addr.can_family = AF_CAN;
addr.can_ifindex = if_index;
bind(skt, (struct sockaddr *)&addr, sizeof(addr));

// Generate example CAN data: 8 bytes; 0x11,0x22,0x33,...
// ...[Omitted]

// Send CAN frames:
fd_set fds;
const struct timeval timeout =  { .tv_sec=2, .tv_usec=0 };
struct timeval this_timeout;
int ret;
ssize_t bytes_writ;

while (1)
{
    // Use 'select' to wait for socket to be ready for writing:
    FD_ZERO(&fds);
    FD_SET(skt, &fds);
    this_timeout = timeout;
    ret = select(skt+1, NULL, &fds, NULL, &this_timeout);

    if (ret < 0)
    {
        printf("'select' error (%d)\n", errno);
        return 1;
    }
    else if (ret == 0)
    {
        // Timeout waiting for buffer to be free
        printf("ERROR - Timeout waiting for buffer to clear.\n");
        return 1;
    }
    else
    {
        if (FD_ISSET(skt, &fds))
        {
            // Ready to write!
            bytes_writ = write(skt, &frame, CAN_MTU);
            if (bytes_writ != CAN_MTU)
            {
                if (errno == 105)
                {
                    // Buffer full! 
                    printf("X"); fflush(stdout);
                    usleep(20);  // Wait for buffer to clear
                }
                else
                {
                    printf("FAIL - Error writing CAN frame (%d)\n", errno);
                    return 1;
                }
            }
            else
            {
                printf("."); fflush(stdout);
            }
        }
        else
        {
            printf("-"); fflush(stdout);
        }
    }
    usleep(WAIT_TIME);
}

当我将每帧 WAIT_TIME 设置为高值(例如 500 uS)以使缓冲区永远不会填满时,我看到以下输出:

CAN Data Generator
...............................................................................
................................................................................
...etc

哪个好!在 500 uS 时,我得到 54% 的 CAN 总线利用率(根据 canbusload 实用程序)。

但是,当我尝试延迟 0 以最大化我的传输速率时,我看到:

CAN Data Generator
................................................................................
............................................................X.XX..X.X.X.X.XXX.X.
X.XX..XX.XX.X.XX.X.XX.X.X.X.XX..X.X.X.XX..X.X.X.XX.X.XX...XX.X.X.X.X.XXX.X.XX.X.
X.X.XXX.X.XX.X.X.X.XXX.X.X.X.XX.X.X.X.X.XX..X..X.XX.X..XX.X.X.X.XX.X..X..X..X.X.
.X.X.XX.X.XX.X.X.X.X.X.XX.X.X.XXX.X.X.X.X..XX.....XXX..XX.X.X.X.XXX.X.XX.XX.XX.X
.X.X.XX.XX.XX.X.X.X.X.XX.X.X.X.X.XX.XX.X.XXX...XX.X.X.X.XX..X.XX.X.XX.X.X.X.X.X.

最初的点“。” 显示缓冲区已满;一旦缓冲区已满,“X”开始出现,这意味着“write”调用失败并出现错误 105。

跟踪逻辑,这意味着“select”必须返回并且“FD_ISSET(skt, &fds)”为真,尽管缓冲区已满!(或者我错过了什么?)。

SockedCAN 文档只是说“使用 write(2) 系统调用可以类似地编写 CAN 帧

这篇文章建议使用“选择”。

这篇文章建议“写”不会阻止 CAN 优先级仲裁,但不涵盖其他情况。

那么“选择”是正确的方法吗?我的“写”应该阻止吗?我可以使用哪些其他选项来避免轮询?

4

2 回答 2

3

快速查看canbusload:184后,它似乎计算了效率(总线上的#data/#total 位)。

另一方面,据此,对于8 字节帧,CAN 总线的最大效率约为 57%,所以你似乎离那个 57% 并不远......我会说你确实在淹没总线。

当设置 500uS 延迟、500kbps 总线比特率、8 字节帧时,它会为您提供 228kbps 的(控制+数据)比特率,低于 CAN 总线的最大比特率,因此,这里没有瓶颈。

此外,由于在这种情况下仅监视 1 个套接字,因此您实际上不需要pselect. 所有你可以做的pselect和 1 个套接字可以在没有pselect和使用的情况下完成write

免责声明:以下,这只是猜测,因为我现在无法测试它,对不起。) 至于为什么 的行为pselect,认为缓冲区可能具有字节语义,所以它告诉你还有更多字节的空间(1 at最少),不一定需要更多的can_frame。因此,在返回时,pselect并不通知您可以发送整个 CAN 帧。我想你可以通过使用SIOCOUTQRx 缓冲区的最大大小来解决这个问题SO_SNDBUF,但不确定它是否适用于 CAN 套接字(最好使用SO_SNDLOWAT标志,但在 Linux 的实现中它是不可更改的)。

所以,回答你的问题:

  1. “选择”是正确的方法吗? 好吧,你可以用两种方法,要么 要么(p)selectwrite因为你只在等待一个文件描述符,所以没有真正的区别。
  2. 我的“写”应该阻止吗?如果发送缓冲区中没有可用的单个字节,则应该这样做。
  3. 我可以使用哪些其他选项来避免轮询?也许通过ioctlSIOCOUTQgetsockopt和减法SO_SNDBUF......你需要自己检查一下。或者,也许您可​​以将发送缓冲区大小设置为的倍数,sizeof(can_frame)并查看它是否在小于sizeof(can_frame)可用时让您发出信号。

无论如何,如果您有兴趣获得更精确的计时,您可以使用 BCM 插座。在那里,您可以指示内核以特定间隔发送特定帧。设置后,进程在内核空间中运行,无需任何系统调用。这样就避免了用户内核缓冲区的问题。我会测试不同的费率,直到canbusload显示总线利用率没有上升。

于 2018-03-10T15:18:40.047 回答
1

select 和 poll 对我来说适用于 SocketCan。但是,需要仔细配置。

一些背景:

在用户应用程序和硬件之间,有 2 个缓冲区:

  1. 套接字缓冲区,其大小(以字节为单位)由 setsockopt 的 SO_SNDBUF 选项控制
  2. 驱动程序的 qdisc,其大小(以数据包为单位)由“ifconfig can0 txqueuelen 5”命令控制。数据路径是:用户应用程序“写”命令->套接字缓冲区->驱动程序的qdisc->硬件TX邮箱。

沿此路径存在 2 个流控制点:

  1. 当没有可用的 TX 邮箱时,驱动程序冻结驱动程序的 qdisc (__QUEUE_STATE_DRV_XOFF),以防止更多的数据包从驱动程序的 qdisc 出列到 HW。当 TX 邮箱空闲时(在 TX 完成中断时),它将被取消冻结。
  2. 当套接字缓冲区超过其容量的一半时,轮询/选择块,直到套接字缓冲区超过其容量的一半。

现在,假设套接字缓冲区有空间容纳 20 个数据包,而驱动程序的 qdisc 有空间容纳 5 个数据包。我们还假设硬件有单个 TX 邮箱。

  1. poll/select 让用户最多写入 10 个数据包。
  2. 这些数据包被向下移动到套接字缓冲区。
  3. 其中 5 个数据包继续并填充驱动程序的 qdisc。
  4. 驱动程序从驱动程序的 qdisc 中取出第一个数据包,将其放入 HW TX 邮箱并冻结驱动程序的 qdisc(=不再出队)。现在驱动程序的 qdisc 中有 1 个数据包的空间
  5. 第 6 个数据包成功地从套接字缓冲区向下移动到驱动程序的 qdisc。
  6. 第 7 个数据包从套接字缓冲区向下移动到驱动程序的 qdisc,但由于没有空间 - 它被丢弃并生成错误 105(“没有可用的缓冲区空间”)。

解决办法是什么?在上述假设中,让我们为 8 个数据包配置套接字缓冲区。在这种情况下,poll/select 将在 4 个数据包后阻止用户应用程序,确保驱动程序的 qdisc 中有所有这 4 个数据包的空间。

但是,套接字缓冲区配置为字节,而不是数据包。应按如下方式进行转换:每个 CAN 数据包在套接字缓冲区中占用约 704 个字节(其中大部分用于套接字结构)。因此,要将套接字缓冲区配置为 8 个数据包,字节大小应为 8*704:

int size = 8*704;
setsockopt(s, SOL_SOCKET, SO_SNDBUF, &size, sizeof(size));
于 2021-12-29T15:29:40.757 回答