Bootstrap

ftdi_sio应用学习笔记 2 - 操作串口

目录

1. 打开设备

2. 关闭设备

3. 设置波特率/数据位/停止位/校验方式

3.1 波特率

3.1.1 标准波特率

3.1.2 自定义波特率

3.2 数据位

3.3 停止位

3.4 校验

4. 设置流控

4.1 硬件流控

4.2 软件流控

5. 写数据

6. 读数据

7. 验证

7.1 打开设备

7.2 关闭设备

7.3 设置参数

7.4 读写数据

7.5 波特率

7.6 硬件流控

7.7 软件流控

7.8 校验

7.9 随机数据测试


可以参考minicom的源代码。

参考文档:Linux串口编程 - wybliw - 博客园

1. 打开设备

常规的打开设备比较简单,串口设备的地址均在/dev目录内,名字为ttyUSBn,n = 0到255.

int ftdi_sio_uart::open_device(char *device_name) {
    int fd = open(device_name, O_RDWR | O_NONBLOCK);
    return fd;
}

完整的设备名一般是"/dev/ttyUSB0"(ttyUSB0为例),参数O_RDWR表示串口可读写,O_NONBLOCK表示非阻塞模式,表示程序不关心DCD信号状态,即程序不关心另一端是否已经连接。

可以通过pid和接口位置打开设备,这种方式只要通过遍历之前find_devices里面生成的devices_list,找到第n个匹配的设备名。

int ftdi_sio_uart::open_device(int pid, int n, int interface) {
    struct ftdi_device_info *dev_list = devices_list;
    while (dev_list != nullptr) {
        if(i == n) {
            char name_path[buffer_size];
            int fd;
            sprintf(name_path, "/dev/%s", dev_list->tty_name[interface]);
            printf("Open device: %s\n", name_path);
            start_stop = 0;
            fd = open(name_path, O_RDWR | O_NOCTTY | O_NDELAY);
            return fd;
         } else {
            i++;
         }
    }
    return -1;
}

还可以通过可以自定义的Serial Number来辨别设备,序列号是和设备唯一对应的,所以不需要参数n。

int ftdi_sio_uart::open_device(const char *serial_number, int interface) {
    struct ftdi_device_info *dev_list = devices_list;
    while (dev_list != nullptr) {
        //printf("serial:%s == %s\n", dev_list->serial_number, serial_number);
        if((strcmp(dev_list->serial_number, serial_number) == 0) && dev_list->tty_name[interface][0] != '\0') {
            char name_path[buffer_size];
            int fd;
            sprintf(name_path, "/dev/%s", dev_list->tty_name[interface]);
            printf("Open device: %s\n", name_path);
            fd = open(name_path, O_RDWR | O_NONBLOCK);
            return fd;
        }
        dev_list = dev_list->next;
    }
    return -1;
}

2. 关闭设备

void ftdi_sio_uart::close_device(int fd) {
    close(fd);
}

3. 设置波特率/数据位/停止位/校验方式

3.1 波特率

波特率分2种情况,一种是标准波特率,即Linux已经定义好的波特率,另一种就是自定义波特率。

对于FTDI设备,波特率的范围并不一定是和Linux设置一样。例如FT230X(USB FS设备)范围为300到3M,它的内部频率为3M,而FT232H(USB HS设备)的范围为183到12M,它的内部频率为3M或12M。所有的波特率都是基于内部频率分频得来。

而分频系数有:n + 0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875,而n的范围是2和16384,而n为0是特殊值,表示最大频率,等于内部频率,n=1对于内部频率为3M,波特率为2M。

比如波特率为500K,n = 3M / 500K = 6, 即分频系数为n + 0;如果为490K,n = 3M / 490K = 6.122,即分频系数取最近的指6.125,这里就产生了误差,实际波特率是3M / 6.125 = 489795.9,误差小于+/- 3%。

3.1.1 标准波特率

在文件/usr/include/x86_64-linux-gnu/bits/termios-baud.h里面有定义高于57600的波特率定义。

而在/usr/include/x86_64-linux-gnu/bits/termios.h里面定义的是低于38400的波特率定义(15个特定值)。

在drivers/tty/tty_baudrate.c有这些定义如何转换成波特率值的,如果在c_cflag中定义baud部分的值为BOTHER,波特率的值由c_ospeed决定(即自定义波特率)。而标准波特率的值从数组baud_table中索引获取。设置波特率可以通过cfsetispeed和cfsetospeed设置。如果把Linux定义的标准波特率通过这2个函数设置就好。例如设置波特率9600,只需要

cfsetispeed(&options, B9600);
cfsetospeed(&options, B9600);

3.1.2 自定义波特率

对于FTDI设备,设置自定义的波特率,需要先设置波特率为B38400

if(i >= (int)(sizeof(baudrate) / sizeof(int))) {
    customed_baud = true;
    cfsetispeed(&options, B38400);
    cfsetospeed(&options, B38400);
}

然后用ioctl设置ASYNC_SPD_CUST标识和custom_divisor

if(customed_baud) {
    struct serial_struct serial_set;
    if (ioctl(fd, TIOCGSERIAL, &serial_set) < 0) {
        perror("ioctl fail\n");
        return -1;
    }
    serial_set.flags = ASYNC_SPD_CUST;
    serial_set.custom_divisor = serial_set.baud_base / baud;
    if((ioctl(fd, TIOCSSERIAL, &serial_set)) < 0){
        return -1;
    }

    if(ioctl(fd, TIOCGSERIAL, &serial_set) < 0) {
        return -1;
    } else {
        printf("custom_divisor:%d, baud_base: %d\n", serial_set.custom_divisor, serial_set.baud_base);
    }
}

3.2 数据位

Linux定义了4种数据位,设置c_cflag(控制模式标志)即可。

#define CSIZE	0000060
#define   CS5	0000000
#define   CS6	0000020
#define   CS7	0000040
#define   CS8	0000060

3.3 停止位

Linux定义了2种停止位,1位和2位。c_cflag的CSTOPB置1表示2位停止位,反之为1位停止位。

options.c_cflag |= CSTOPB;
options.c_cflag &= ~CSTOPB;

3.4 校验

设置c_cflag的PARENB为1表示启用奇偶校验。

设置c_cflag的PARODD为1表示设置奇偶校验为奇校验,为0表示偶校验。

设置c_iflag的INPCK表示启用输入奇偶校验,ISTRIP表示将输入字符从8位缩减为7位。

switch (parity) {
    case 'O':                     //奇校验
        options.c_cflag |= PARENB;
        options.c_cflag |= PARODD;
        options.c_iflag |= (INPCK | ISTRIP);
        //printf("parity is Odd\n");
        break;
    case 'E':                     //偶校验
        options.c_cflag |= PARENB;
        options.c_cflag &= ~PARODD;
        options.c_iflag |= (INPCK | ISTRIP);
        //printf("parity is Even\n");
        break;
    case 'N':                    //无校验
    default:
        options.c_cflag &= ~PARENB;
        //printf("parity is None\n");
        break;
}

4. 设置流控

流控分为关闭流控,硬件流控(RTS和CTS)和软件流控。

4.1 硬件流控

c_cflag的CRTSCTS控制硬流控的开关,c_iflag的IXON|IXOFF|IXANY控制软流控的开关。

if(type == SERIAL_FLOW_CTRL_NONE) {
    options.c_cflag &= ~CRTSCTS; //不使用流控制
} else if (type == SERIAL_FLOW_CTRL_HW) {
    options.c_cflag |= CRTSCTS;
} else if (type == SERIAL_FLOW_CTRL_SW) {
    options.c_iflag &= ~(IXON|IXOFF|IXANY);
    options.c_iflag |= IXON|IXOFF|IXANY; //使用软件流控制
}

RTS是输出信号,用于通知接收设备本设备可接收数据。CTS是输入信号,用来判断是否可以对对方设备发送数据。软件上增加一个设置RTS的API函数。

int ftdi_sio_uart::set_rts(int fd, int onoff) {
    int status;
    if((flow_ctrl != SERIAL_FLOW_CTRL_HW))
        return 0;
    ioctl(fd, TIOCMGET, &status);
    if(onoff)
        status |= TIOCM_RTS;
    else
        status &= ~TIOCM_RTS;
    ioctl(fd, TIOCMSET, &status);
    return 0;
}

而CTS需要在写数据前判断是否有效。

4.2 软件流控

软件流控就是硬件上只有RXD和TXD,通过发送特殊的起始字符和结束字符表示串口通信的开始与结束。这2个字符添加一个api函数设置

int ftdi_sio_uart::set_sw_flow_control(int fd, char xon, char xoff) {
    struct termios options;
    if (tcgetattr(fd, &options) != 0){
        perror("SetupSerial fail\n");
        return -EIO;
    }
    options.c_cc[VSTART] = xon;
    options.c_cc[VSTOP] = xoff;
    tcflush(fd, TCIFLUSH);
    if ((tcsetattr(fd, TCSANOW, &options)) != 0) {
        perror("Serialport set error\n");
        return -EIO;
    }
    return 0;
}

5. 写数据

写数据比较简单,不过要考虑硬件流控和软件流控,如果设置为硬件流控时,读取CTS是否可以写数据。

char *wr_buf = buf;
int ret = 0;
if(flow_ctrl == SERIAL_FLOW_CTRL_HW) {
    int status;
    ioctl(fd, TIOCMGET, &status);
    printf("Read CTS=%d\n", status & TIOCM_CTS);
    if((status & TIOCM_CTS) == 0)
        return -EBUSY;
}

如果是软件流控,则需要将buf中的数据遍历,根据当前写的状态区分是否启动或停止发送。

else if(flow_ctrl == SERIAL_FLOW_CTRL_SW) {
    struct termios options;
    int i;
    int offset = 0;
        
    if (tcgetattr(fd, &options) != 0) {
        return -EIO;
    }
    wr_buf = (char *)malloc(len);
    for(i = 0; i < len; i++) {
        if(buf[i] == options.c_cc[VSTART]) {
            if(start_stop == 0)
                start_stop = 1;
            else
                wr_buf[offset++] = buf[i];
        } else if(buf[i] == options.c_cc[VSTOP]) {
            if(start_stop == 1)
                start_stop = 0;
        } else if(start_stop == 1) {
            wr_buf[offset++] = buf[i];
        }
    }
    len = offset;
}

流控资料很少,不确定是不是应该底层驱动实现才对,但是ftdi_sio好像没实现,所以这里相当于是应用层实现的。

6. 读数据

读数据采用循环读取的方式,在定时时间(参数timeout_ms)内判断串口是否有数据

FD_ZERO(&rfds);
FD_SET(fd, &rfds);
tv.tv_sec = timeout_ms / 1000;  //set the rcv wait time  
tv.tv_usec = timeout_ms % 1000 * 1000;  //100000us = 0.1s
retval = select(fd + 1, &rfds, NULL, NULL, &tv);

如果retval为0表示串口没数据,如果是正数表示有数据,则读串口数据,其他情况就是串口出错了。

if (retval == -1) {
    perror("Serialport no data\n");
    break;
} else if (retval) {
    ret = read(fd, buf + pos, len - pos);
    if (ret == -1) {
        printf("Serialport read no data\n");
        break;
    } else if (ret == 0) {
        printf("Serialport read no data\n");
        break;
    }
 
    pos += ret;
    if (len <= pos) {
        break;
    }
} else {
    //printf("retval:%d\n", retval);
    break;
}

7. 验证

7.1 打开设备

将FT4232H模块插入电脑,第一步遍历FTDI设备

ftdi_sio_uart uart;
int fd1, fd2;
uart.find_devices();

打开2个串口,一个串口通过PID打开,指定A口;另外一个串口通过系列号打开,指定B口。

fd1 = uart.open_device(0x6011, 0);
if(fd1 <= 0) {
    printf("open device 1 failed\n");
    return -1;
}
fd2 = uart.open_device("FT9PQ9R2", 1);
if(fd2 <= 0) {
    printf("open device 2 failed\n");
    return -1;
}

7.2 关闭设备

如果退出应用,需要关闭设备,且释放之前分配的内存。

uart.close_device(fd1);
uart.close_device(fd2);
uart.free_devices();

运行结果如下:

$ sudo ./ftdi_sio_app 
serial number:FT9PQ9R2
Found: ttyUSB0
Found: ttyUSB1
Found: ttyUSB2
Found: ttyUSB3
Open device: /dev/ttyUSB0
Open device: /dev/ttyUSB1

7.3 设置参数

 将2个串口都设置为无流控,同样的串口参数。

uart.set_flow_control(fd1, SERIAL_FLOW_CTRL_NONE);
uart.set_options(fd1, 115200, 8, 1, 'N');
uart.set_flow_control(fd2, SERIAL_FLOW_CTRL_NONE);
uart.set_options(fd2, 115200, 8, 1, 'N');

7.4 读写数据

将AD0(TXD0)和BD1(RXD1)短接,将AD1(RXD0)和BD0(TXD1)短接。循环发送串口数据

while(1) {
    usleep(8 * 1000);
        
    uart->write_bytes(fd1, str, strlen(str));
    len = uart->read_bytes(fd2, rd_buf, 256, 200);
    rd_buf[len] = '\0';
    if(len > 0)
        printf("read from device 2 %d: %s\n", len, rd_buf);
    else
        printf("serial no data : %d\n", len);
    flow_ctrl++;

    if(key_pressed()) 
        break;
}

7.5 波特率

在循环中循环改波特率,测试不同波特率接受数据是否正常。

while(1) {
    uart->set_options(fd1, baudrate[index], 8, 1, 'N');
    uart->set_options(fd2, baudrate[index], 8, 1, 'N');
    usleep(8 * 1000);

    uart->write_bytes(fd1, str, strlen(str));
    len = uart->read_bytes(fd2, rd_buf, 256, 200);
    rd_buf[len] = '\0';
    printf("baudrate test  --- ");
    if(len > 0) {
        printf("read from device 2 %d: %s\n", len, rd_buf);
        if(strncmp(rd_buf, "Hello World", 11) == 0) {
            printf("--- baud=%d test success\n", baudrate[index]);
        } else
            printf("*** baud=%d test failed\n", baudrate[index]);
    } else
        printf("serial no data : %d\n", len);
    index++;
    index %= 25;
    if(key_pressed()) 
        break;
}

标准波特率比较简单,对于非标准波特率,注意芯片的波特率会产生误差,需要注意并不是所有的波特率都可以正常工作。例如设置波特率为490000,可以看到没有数据读入。

这里有个问题,波特率太高(1M以上)串口会死掉,需要拔插。如果在循环中添加延时可以避免这个问题。

7.6 硬件流控

硬件流控有关的管脚是RTS和CTS,将2个串口的RTS和CTS互相短接,即RTS0接CTS1,CTS0接RTS1。

由串口0发送数据,在发送前先设置串口1的RTS为高,这里做个测试,循环中交替设置RTS高低。

 while(1) {
    if((flow_ctrl & 0x1) == 1) {
        uart.set_rts(fd2, 1);
    } else {
        uart.set_rts(fd2, 0);
    }
    usleep(16 * 1000);
        
    uart.write_bytes(fd1, (char *)"Hello World", strlen("Hello World"));
    len = uart.read_bytes(fd2, rd_buf, 256, 200);
    rd_buf[len] = '\0';
    printf("flow ctrl hw test  --- ");
    if(len > 0)
        printf("read from device 2: %s\n", rd_buf);
    else
        printf("serial no data : %d\n", len);
    flow_ctrl++;
 }

这里加上usleep,否则很容易出错。

7.7 软件流控

类似硬件流控,在写数据前交替发送启动和结束的关键字。首先设置软件流控和关键字:

uart.set_flow_control(fd1, SERIAL_FLOW_CTRL_SW);
uart.set_options(fd1, 1000000, 8, 1, 'N');
uart.set_flow_control(fd2, SERIAL_FLOW_CTRL_SW);
uart.set_options(fd2, 1000000, 8, 1, 'N');

uart.set_sw_flow_control(fd1, 0x11, 0x13);

然后在while(1)循环中交替发送启动和结束的关键字

char str[] = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '\0' };
char stop = 0x13;
char start = 0x11;

if((flow_ctrl & 0x1) == 1) {
    uart.write_bytes(fd2, &start, 1);
    //printf("set uart to start\n");
} else {
    uart.write_bytes(fd2, &stop, 1);
    //printf("set uart to stop\n");
}

7.8 校验

定义2个数组来测试校验方式相同和不相同的情况

const char parity1_type[6] = {'N', 'O', 'E', 'O', 'N', 'E'};
const char parity2_type[6] = {'N', 'O', 'E', 'N', 'E', 'O'};

在while(1)中循环设置检验方式和发送数据。

while(1) {
    uart->set_options(fd1, 115200, 8, 1, parity1_type[parity]);
    uart->set_options(fd2, 115200, 8, 1, parity2_type[parity]);
    usleep(8 * 1000);
    uart->write_bytes(fd1, str, strlen(str));
    len = uart->read_bytes(fd2, rd_buf, 256, 200);
    rd_buf[len] = '\0';
    printf("parity port 1=%c:2=%c test  --- ", parity1_type[parity], parity2_type[parity]);
    if(len > 0)
        printf("read from device 2 %d: %s\n", len, rd_buf);
    else
        printf("serial no data : %d\n", len);
    parity++;
    parity %= 6;

    if(key_pressed()) 
        break;
}

此时可以看到如果校验方式不同,串口2是接收不到串口1发送的数据的,但是读到的数据是11个字节,而字符串本身是空的。将输出打印改为

if(len > 0)
    printf("read from device 2 %d: %s\n", len, rd_buf);

打印结果如下: 

parity port 1=N:2=N test  --- read from device 2 11: Hello World
parity port 1=O:2=O test  --- read from device 2 11: Hello World
parity port 1=E:2=E test  --- read from device 2 11: Hello World
parity port 1=O:2=N test  --- read from device 2 12: Hello ]z�c#�
parity port 1=N:2=E test  --- read from device 2 10: 
parity port 1=E:2=O test  --- read from device 2 11: 

这里有个问题,设置为O和N的情况,会收到乱码的情况,而且因为返回数据长度也会出现正常的情况,所以如何判断数据确实出错了?

7.9 随机数据测试

随机产生一组数据,数据长度从1到4096循环变化。注意,一次发送数据要根据芯片不同,例如H系列是4096,太长会出错。

while(1) {
    int rd_len;
    wr_buf = (char *)malloc(len);
    rd_buf = (char *)malloc(len);
    srand(time(NULL));  
    for (int i = 0; i < len; i++) {  
        wr_buf[i] = (unsigned char)rand() % 256;
    }

    uart->write_bytes(fd1, wr_buf, len);
    rd_len = uart->read_bytes(fd2, rd_buf, len, 200);
        
    printf("data test len = %04d --- ", len);
    if(rd_len < len) {
        printf("test failed\n");
    } else if(rd_len == len) {
        for(int i = 0; i < len; i++) {
            if(wr_buf[i] != rd_buf[i]) {
                printf("test failed %d, %d != %d\n", i, wr_buf[i], rd_buf[i]);
                break;
            }
        }
        printf("test success\n");
    }

    len++;
    if(len > 4096)
        len = 1;

    free(wr_buf);
    free(rd_buf);

    if(key_pressed()) 
        break;

     usleep(8 * 1000);
 }

;