linux 如何在没有非本机库的情况下在 C 中通过串行发送原始二进制数据

How to send raw binary data over serial in C without non-native libraries in linux

我目前正在尝试通过串口将十进制格式的原始二进制数据发送到外部设备。我目前在缓冲区数组中有数据,但希望它采用这样的结构:

struct packetData{
   
    uint8_t sync1;
    uint8_t sync2;
    uint16_t messageId;
    uint16_t dataWordCount;
    uint16_t flags;
    uint16_t checksum;
};

我也在使用 9600 波特,并使用 cfmakeraw 设置了所有 termios 设置,我目前正在使用:

  #include <stdio.h> 
 #include <stdint.h>
 #include <unistd.h> 
 #include <fcntl.h>   
 #include <termios.h> 
 #include <string.h>  
 #include <errno.h>   
 #include <stdlib.h>   
int flags = O_RDWR | O_NOCTTY | O_NDELAY;
    fd = open(device, flags);
    uint16_t buf_tx[BUFFER_SIZE] = {255,129,191,0,2057,0};
    if(fd == -1){
        printf("\n Failed to open port! ");
        return -1;
    }
    
    tcgetattr(fd, &tty);      //Get the current attributes of the Serial port 
    cfmakeraw(&tty);
    cfsetispeed(&tty, B9600); //Set read speed as 9600 baud                
    cfsetospeed(&tty, B9600); //Set write speed as 9600 baud              
    if((tcsetattr(fd, TCSANOW, &tty)) != 0){
         printf("Error! Can't set attributes.\n");
         return -1;
    }
     
    else{
         printf("Connection successful! \n");
    }
while(x < 1000){
memset(buf_tx, 0, sizeof(buf_tx));
        
        tcflush(fd, TCOFLUSH);
        if(y < 5){
            if(write(fd, buf_tx, 5) == -1){
                printf("\n");
                printf("Error>>: %s\n",strerror(errno));
                y++;
            }
        }
        tcflush(fd, TCIOFLUSH);
        usleep(1000); 
        x++;  
}

此代码不是完整代码,只是 setup/write 部分,因此无需担心其语法。如果可能的话,最好不要那个缓冲区数组并直接使用结构,但我会尽我所能。

看来你手上的串口打开差不多了。我更喜欢自己明确设置 termios 成员组件,但是 cfmakeraw() 也完全没问题。

您应该考虑的是,有一个单独的函数来一次发送一个或多个这些结构。例如,

int write_all(const int fd, const void *buf, const size_t len)
{
    const char *data = buf;
    size_t      written = 0;
    ssize_t     n;

    while (written < len) {
        n = write(fd, data + written, len - written);
        if (n > 0) {
            written += n;
        } else
        if (n != -1) {
            /* C library bug, should never occur */
            errno = EIO;
            return -1;
        } else {
            /* Error; n == -1, so errno is already set. */
            return -1;
        }
    }

    /* Success. */
    return 0;
}

如果所有数据都成功写入,函数将 return 0,如果发生错误,errno 设置为 -1。

要发送 struct packetData pkt; 只需使用 write_all(fd, &pkt, sizeof pkt)
要发送完整数组 struct packetData pkts[5];,请使用 write_all(fd, pkts, sizeof pkts)。 要从 pkts[i] 开始发送 n 个数据包,请使用 write_all(fd, pkts + i, n * sizeof pkts[0]).

但是,您不想使用 tcflush()。它并没有按照您的想法行事;它实际上只是丢弃数据。

相反,为了确保您写入的数据已经传输,您需要使用tcdrain(fd)

我建议不要在 write_all() 函数的末尾添加 tcdrain(fd),因为它会阻塞、暂停程序,直到数据传输完毕。这意味着你应该只在你做一些需要另一端收到传输的事情之前使用 tcdrain() ;例如在尝试读取响应之前。

然而,如果这是一个 query-response 接口,并且您确实打算也从串行设备读取,您应该设置 tty.c_cc[VMIN]tty.c_cc[VTIME] 以反映您打算如何使用界面。我更喜欢异步 full-duplex 操作,但这需要 select()/poll() 处理。对于仅具有这些确切结构的 half-duplex、,您可以将 tty.c_cc[VMIN] = sizeof (struct packetData)tty.c_cc[VTIME] = 30 一起使用,这会导致 read() 尝试等待直到完整结构可用,但最多 30 分秒(3.0 秒)。 tty.c_cc[VMIN] = 1; tty.c_cc[VTIME] = 1; 之类的更常见;如果在十分之一秒(0.1 秒)内没有收到其他数据,则会导致 read() 到 return 短计数(甚至 0!)。然后,接收函数可以遵循以下几行:

int read_all(const int fd, void *buf, const size_t len)
{
    char *const ptr = buf;
    size_t      have = 0;
    ssize_t     n;

    /* This function is to be used with half-duplex query-response protocol,
       so make sure we have transmitted everything before trying to
       receive a response. Also assumes c_cc[VTIME] is properly set for
       both the first byte of the response, and interbyte response interval
       in deciseconds. */
    tcdrain(fd);

    while (have < len) {
        n = read(fd, ptr + have, len - have);
        if (n > 0) {
            have += n;
        } else
        if (n == 0) {
            /* Timeout or disconnect */
            errno = ETIMEDOUT;
            return -1;
        } else
        if (n != -1) {
            /* C library bug, should never occur */
            errno = EIO;
            return -1;
        } else {
            /* Read error; errno set by read(). */
            return -1;
        }
    }
    /* Success; no errors. */
    return 0;
}

如果这个returns -1errno == ETIMEDOUT,对方回答的时间太长了。缓冲区中可能有剩余的延迟响应,您可以使用 tcflush(TCIFLUSH)(或使用 tcflush(TCIOFLUSH) 丢弃任何尚未传输的写入数据)。这种情况下的同步有点困难,因为上面的 read_all() 函数没有 return 它接收了多少字节(因此也没有丢弃部分结构的多少字节)。

有时接口总是使用 return 字节数,但也会设置 errno(如果没有发生错误则设置为 0,否则设置为非零值 error constant ).这对于 query-response 接口读写函数会更好,但许多程序员发现这个用例“奇怪”,尽管它完全符合 POSIX.1 标准(这是此处的相关标准).