目录
可以参考minicom的源代码。
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);
}