Linux termios 修改串口 read() 函数读取后的第一个字符

6
我的termios设置正在使用read()修改从串口读取的第一个字符。我有一个微控制器与Linux机器通信。微控制器响应于从Linux机器发送的指令。设置如下:
  • 微控制器(PIC24F) RS485端口 <--> RS485转USB转换器 <--> Ubuntu PC。
当我运行像Cutecom这样的终端程序时,一切都按计划进行。我发送一个命令字符到PIC,并得到一个响应,但是当我使用我的命令行程序时,第一个字符被修改。以下是我的代码:
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>

#define DEVICE "/dev/ttyUSB0"
#define SPEED B38400 

int main()
{
    struct termios tio; //to hold serial port settings
    struct termios stdio; //so we can accept user input
    struct termios old_stdio; //save the current port settings
    int tty_fd; //file descriptor for serial port
    int res, n, res2, read1, wri;
    char buf[255];
    char buf2[255]; 

    //save the current port settings
    tcgetattr(STDOUT_FILENO,&old_stdio); 

    //setup serial port settings
    bzero(&tio, sizeof(tio));
    tio.c_iflag = 0;
    tio.c_iflag = IGNPAR | IGNBRK | IXOFF;
    tio.c_oflag = 0;
    tio.c_cflag = CS8 | CREAD | CLOCAL; //8n1 see termios.h 
    tio.c_lflag = ICANON;

    //open the serial port
    tty_fd=open(DEVICE, O_RDWR | O_NOCTTY); 

    //set the serial port speed to SPEED
    cfsetospeed(&tio,SPEED); 

    //apply to the serial port the settings made above
    tcsetattr(tty_fd,TCSANOW,&tio); 

    for(n = 5; n > 0; n--)
    {
    printf("Please enter a command: ");
    (void)fgets(buf2, 255, stdin);
    (void)write(tty_fd, buf2, strlen(buf2));                   
    printf("Ok. Waiting for reply.");
    res = read(tty_fd, buf, 255);       
    printf("Read:%d START%d %d %d %d %dFINISH\n",res,buf[0],buf[1],buf[2],buf[3],
    buf[4]);              
    }

    //close the serial port 
    close(tty_fd); 

    //restore the original port settings
    tcsetattr(STDOUT_FILENO,TCSANOW,&old_stdio); 

    return EXIT_SUCCESS; 
}

以下是我得到的结果示例。

  • 当PIC发送“00000\n”时,输出为:读取:6 START-16 48 48 48 48FINISH
  • 当PIC发送“23456\n”时,输出为:读取:6 START-14 51 52 53 54FINISH
  • 当PIC发送“34567\n”时,输出为:读取:6 START-14 52 53 54 55FINISH
  • 当PIC发送“45678\n”时,输出为:读取:6 START-12 53 54 55 56FINISH
  • 当PIC发送“56789\n”时,输出为:读取:6 START-12 54 55 56 57FINISH

由于某些termios设置的原因,第一个字符被搞乱了。这一定是termios设置问题,因为当我运行Cutecom时,相同的测试输入返回的结果完全相同。我一遍又一遍地阅读手册页面,尝试在输入控制上使用所有不同的设置,但无论我做什么都无法解决这个问题。

对于一个简单的解决方法,我可以将我的数据向右移动一个字符,但想避免这样做。

有人遇到过这样的问题或者有任何关于此问题的想法吗?

非常感谢。

28/3/13 Austin提出了一个很好的建议。对于那些感兴趣的人,以下是两个输出:

  • 首先是我程序中的 termios 设置

    速率 38400 baud; 行数 0; 列数 0; 行 = 0; 中断 = ; 退出 = ; 擦除 = ; 杀死 = ; 结束文件 = ; 结尾1 = ; 结尾2 = ; 开关 = ; 开始 = ; 停止 = ; 暂停 = ; 重打 = ; 字符消去 = ; lnext = ; 刷新 = ; 最小值 = 0; 时间 = 0; -parenb -parodd cs8 -hupcl -cstopb cread clocal -crtscts ignbrk -brkint ignpar -parmrk -inpck -istrip -inlcr -igncr -icrnl -ixon ixoff -iuclc -ixany -imaxbel -iutf8 -opost -olcuc -ocrnl -onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0 -isig icanon -iexten -echo -echoe -echok -echonl -noflsh -xcase -tostop -echoprt -echoctl -echoke

  • 然后是 cutecom 使用的设置

    速率 38400 baud; 行数 0; 列数 0; 行 = 0; 中断 = ^C; 退出 = ^\; 擦除 = ^?; 杀死 = ^U; 结束文件 = ^D; 结尾1 = ; 结尾2 = ; 开关 = ; 开始 = ^Q; 停止 = ^S; 暂停 = ^Z; 重打 = ^R; 字符消去 = ^W; lnext = ^V; 刷新 = ^O; 最小值 = 60; 时间 = 1; -parenb -parodd cs8 hupcl -cstopb cread clocal -crtscts ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr -icrnl -ixon -ixoff -iuclc -ixany -imaxbel -iutf8 -opost -olcuc -ocrnl -onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0 -isig -icanon -iexten -echo -echoe -echok -echonl -noflsh -xcase -tostop -echoprt -echoctl -echoke

我仍在研究并会在取得进展时更新此帖子。

2013年3月29日 问题仍然存在。我甚至找到了 Cutecom 的源代码并使用了它们的 termios 设置,但问题依旧存在。第一个字符被损坏!!!

  • Here are the Termios settings from my program. Cannot set flush for some reason.

    speed 38400 baud; rows 0; columns 0; line = 0; intr = ^?; quit = ^\; erase = ^H; kill = ^U; eof = ^D; eol = ; eol2 = ; swtch = ; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ; min = 60; time = 1; -parenb -parodd cs8 hupcl -cstopb cread clocal -crtscts ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr -icrnl -ixon -ixoff -iuclc -ixany -imaxbel -iutf8 -opost -olcuc -ocrnl -onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0 -isig -icanon -iexten -echo -echoe -echok -echonl -noflsh -xcase -tostop -echoprt -echoctl -echoke

  • And my new code:

    #include <stdlib.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <termios.h>
    #include <sys/ioctl.h>
    
    #define DEVICE "/dev/ttyUSB0"
    #define SPEED B38400 
    
    int main()
    {
    struct termios tio; //to hold serial port settings
    struct termios stdio; //so we can accept user input
        struct termios old_stdio; //save the current port settings
        int tty_fd; //file descriptor for serial port
        int retval, res, n, res2, read1, wri;
        char buf[255];
        char buf2[255]; 
    
    
        tty_fd = open(DEVICE, O_RDWR | O_NDELAY);
        if(tty_fd < 0)
        {
            perror(DEVICE);
            exit(-1);
        }
        printf("Init 1 complete.\n");
    
        tcflush(tty_fd, TCIOFLUSH);
    
        int f = fcntl(tty_fd, F_GETFL, 0);
        fcntl(tty_fd, F_SETFL, f & ~O_NDELAY);
    
        retval = tcgetattr(tty_fd, &old_stdio);
        if(retval != 0)
        {
            perror(DEVICE);
            exit(-1);
        }
        printf("Init 2 complete.\n");
    
        struct termios newtio;
        retval = tcgetattr(tty_fd, &newtio);
        if(retval != 0)
        {
            perror(DEVICE);
            exit(-1);
        }
        printf("Init 3 complete.\n");
    
        cfsetospeed(&newtio, SPEED);
        cfsetispeed(&newtio, SPEED);
    
        newtio.c_cflag = (newtio.c_cflag & ~CSIZE) | CS8;
        newtio.c_cflag |= CLOCAL | CREAD;
        newtio.c_cflag &= ~(PARENB | PARODD);
        newtio.c_cflag &= ~CRTSCTS;
        newtio.c_cflag &= ~CSTOPB;
    
        newtio.c_iflag = IGNBRK;
        newtio.c_iflag &= ~(IXON | IXOFF | IXANY);
    
        newtio.c_lflag = 0;
    
        newtio.c_oflag = 0;
    
        newtio.c_cc[VTIME] = 1;
        newtio.c_cc[VMIN] = 60;
        newtio.c_cc[VINTR] = 127; 
        newtio.c_cc[VQUIT] = 28;
        newtio.c_cc[VERASE] = 8;
        newtio.c_cc[VKILL] =  21;
        newtio.c_cc[VEOF] = 4;
        newtio.c_cc[VSTOP] = 19;
        newtio.c_cc[VSTART] = 17;
        newtio.c_cc[VSUSP] = 26;
        newtio.c_cc[VREPRINT] = 18;
        newtio.c_cc[VFLSH] = 15;
        newtio.c_cc[VWERASE] = 23;
        newtio.c_cc[VLNEXT] = 22;
    
    
        retval = tcsetattr(tty_fd, TCSANOW, &newtio);
        if(retval != 0)
        {
            perror(DEVICE);
            exit(-1);
        }
        printf("Init 4 complete.\n");
    
        int mcs = 0;
        ioctl(tty_fd, TIOCMGET, &mcs);
        mcs |= TIOCM_RTS;
        ioctl(tty_fd, TIOCMSET, &mcs);
    
        retval = tcgetattr(tty_fd, &newtio);
        if(retval != 0)
        {
            perror(DEVICE);
            exit(-1);
        }
        printf("Init 5 complete.\n");
    
        newtio.c_cflag &= ~CRTSCTS;
    
        retval = tcsetattr(tty_fd, TCSANOW, &newtio);
        if(retval != 0)
        {
            perror(DEVICE);
            exit(-1);
        }
        printf("Init 6 complete.\n");
    
    
        for(n = 5; n > 0; n--)
        {
        printf("Please enter a command: ");
        (void)fgets(buf2, 255, stdin);
        (void)write(tty_fd, buf2, strlen(buf2));
        printf("Ok. Waiting for reply\n");
        res = read(tty_fd, buf, 255);       
        printf("Read:%d START%d %d %d %d %dFINISH\n",res,buf[0],buf[1],buf[2], buf[3],
        buf[4]);              
        }
    
        //restore the original port settings
        tcsetattr(tty_fd, TCSANOW, &old_stdio); 
    
        close(tty_fd);
    
        return EXIT_SUCCESS; //return all good
    }
    

我完全不知道可以做什么或者从哪里继续下去。

涉及IT技术的内容,暂无法提供更多信息。请提供更具体的翻译要求。

你只报告了读取的前5个字节,但实际上读取到了6个字节,这是有原因的吗?你是否有手段监视通过RS-482和USB组合发送的内容,以查看微控制器是否真的发送了你期望的字节,或者信息在传输之前是否被修改?我没有使用过RS-482(但我还记得RS-232,不太喜欢),但是否存在可能会干扰的控制字符或消息框架?就好像第一个读取的字节被expected & 0xEE修改了一样... - Jonathan Leffler
我运行的其他测试一直显示从第一个字符中减去了64。如果我将64添加到read()存储在buf [0]中的结果中,我会得到正确的ASCII值。令人困惑的是,正确值的ASCII值没有设置第6位。因此,认为某处执行了a&= 0x1011111并不支持那个想法。我尝试了一些操作,但无法识别任何模式。 - Mitch Gulliver
0xEE 是一个错误;我的位操作有误,应该是 0xDE。数值 0xDE 来自于观察到当第一个字节为 '0'(0x30)时,你得到的是 0x10,而且 0x30 & 0xDE == 0x10(16)。同样地,当第一个字节为 '2'(0x32)时,你得到的是 14 或者 0x0E;0x3E & 0xDE = 0x0E。对于 '3'(0x33),0x33 & 0xDE == 0x0E(14);对于 '4'(0x34),0x34 & 0xDE == 0x0C(12);对于 '5'(0x35),0x35 & 0xDE == 0x0C(12)。因此,就好像第一个字节被与 0xDE 进行了 AND 操作。我想不出为什么会发生这种情况,但这解释了你所看到的模式。 - Jonathan Leffler
你能说一下 tty 设置中的关键区别是什么吗? - Jonathan Leffler
简单的解决方案:去掉 IXOFF。你的 C 程序启用了输入的 XON/XOFF 流控制(而 Cutecom 没有)。这也符合症状;XON/XOFF 流控制字符默认为 DC1DC4(十进制 17 到 20),但你已经将它们(.c_cc[VSTART].c_cc[VSTOP].c_cc[VSTATUS])替换为 NUL,这可能会导致内核驱动程序产生奇怪的起始字节。你基本上告诉内核驱动程序,你希望它根据特殊代码处理输入流控制,但是又将流控制的起始和停止代码都设置为 NUL(零字节)。 - Nominal Animal
显示剩余3条评论
2个回答

2

通过快速扫描您的代码,我没有发现任何明显的问题。如果您希望使用8位值,请考虑切换到unsigned char buf[]

由于您在Cutecom中有一个可工作的程序,您可以使用其termios设置作为参考来调试自己的程序。

在Cutecom运行在/dev/ttyUSB0上时,在另一个终端中运行以下命令以转储tty设置:

stty -a -F /dev/ttyUSB0

在运行程序时,尝试进行相同操作,并查找两个配置之间的差异。尝试设置终端设置以完全匹配Cutecom报告的设置。

更新:

由于修复termios设置未解决问题,因此可以尝试一些进一步的方法。我猜测存在某种时间问题。在Cutecom控制台上键入字符时,您会将每次发送一个字符到设备,每个字符之间有几毫秒的间隔。当使用程序时,在输入命令后将发送一个完整的字符缓冲区,其中字符将按驱动程序允许的速度依次发送。也许您的PIC程序无法处理数据流的时间,或者期望有两个停止位,而不是一个停止位,从而导致一些奇怪的返回代码。

开始最好的地方可能是回到源码。获取示波器或逻辑分析仪,并验证由PIC发送的数据是否正确。您必须理解位级波形,考虑起始和停止位。比较Cutecom和您的程序的波形。如果使用逻辑分析仪,请确保使用的时钟是您波特率的高倍数。例如32倍。

另一种调试方法是使用strace来验证由驱动程序返回的字符是否确实不正确,并且这不是您程序的问题。使用strace,您将能够看到程序的原始读取/写入以及内核返回的内容。在运行程序时使用strace -o ~/tmp/strace_output.txt -ttt -xx your_program来转储所有系统调用。有时,仅stracing程序的过程就足以显示时间错误。您可以将read / write的时间与Cutecom的strace进行比较。仅供测试,您可以添加自己的write()函数,该函数发送字符串但在每个字符之间延迟一小段时间。


这是一个很好的起点来比较这两个设置,但是我已经改变了我的代码以匹配这些设置,但仍然无法使其工作。 - Mitch Gulliver
@MitchGulliver 谢谢您的更新,可惜到目前为止还没有解决。我在答案中添加了更多信息,可能有助于调试。 - Austin Phillips
谢谢Austin Phillips。我需要完成一些大学作业,但当我再次开始这个项目时,我会尝试你的建议。也许是购买示波器的时候了。当我有进展时,我会更新我的帖子。 - Mitch Gulliver

0

我终于解决了问题。这是解决方法:

  • (void)write(tty_fd, buf2, 1);

问题已经解决,但不确定为什么会出现这种情况。问题是我的程序在向微控制器写入时添加了一个 \n。当我对 Cutecom 和我的程序进行 strace 时,我发现 Cutecom 只写入 '1',而我的程序会写入 "1\n"。我没有认真思考,因为当使用 Cutecom 发送字符时,您在用户提示中键入例如 1,然后按回车键。在 PIC 端,我的程序看起来像这样:

while(1)
{
    WATCHDOG();

    if(flag == 1)
    {
        char *start = str2;
        RS485_TXEN1();
        indicator = UART1_getch(); //get character sent from PC
        switch(indicator)
        {
            case '1' :                       
                    UART1_putstr("00000\n");
                    DAC_Write( DAC_CH_2, 4095);
                    break;
            case '2' :                      
                    UART1_putstr("23456\n");
                    DAC_Write( DAC_CH_2, 0);
                    break;
            case '3' :
                    UART1_putstr("34567\n");
                    break;
            case '4' :                       
                    UART1_putstr("45678\n");
                    break;
            case '\n' :
                    UART1_putch('\n');
                    break;
            default  :         
                    UART1_putstr("56789\n");                  
                    break;
        }
        RS485_RXEN1();
        flag = 0;
    }
} 

当一个字符到达UART RX时,会生成一个中断。我在这个中断服务例程中设置了“flag”,然后在main()中处理接收到的命令。不确定第一个字符是如何被修改的,看起来似乎有一些覆盖或写入中断发生,可能是由于“case'\n':”引起的。

最终进行了简单的修复,并学到了一些有价值的关于Linux系统和调试的经验。感谢所有提供建议的人。对于任何想要开始与Linux系统和微控制器进行接口的人来说,上述代码可能有助于您入门。


(void)write(tty_fd, buf2, 1); -- 忽略系统调用的返回代码是不好的编程习惯。 - sawdust

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接