很多系统都支持POSIX终端(串口)接口。程序可以利用这个接口来改变终端的参数,比如,波特率,字符大小等等。要使用这个端口的话,你必须将头文件包含到你的程序中。这个头文件中定义了终端控制结构体和POSIX控制函数。
与串口操作相关的最重要的两个POSIX函数可能就是tcgetattr(3)和tcsetattr(3)。顾名思义,这两个函数分别用来取得设设置终端的属性。调用这两个函数的时候,你需要提供一个包含着所有串口选项的termios结构体:
termios结构体成员
成员 | 描述 |
c_cflag | 控制选项 |
c_lflag | 行选项 |
c_iflag | 输入选项 |
c_oflag | 输出选项 |
c_cc | 控制字符 |
c_ispeed | 输入波特率(NEW) |
c_ospeed | 输出波特率(NEW) |
通过termios结构体的c_cflag成员可以控制波特率,数据的比特数,parity,停止位和硬件流控制。下面这张表列出了所有可以使用的常数。
c_cflag常数
常量 | 描述 |
CBAUD | Bit mask for baud rate |
B0 | 0 baud (drop DTR) |
B50 | 50 baud |
B75 | 75 baud |
B110 | 110 baud |
B134 | 134.5 baud |
B150 | 150 baud |
B200 | 200 baud |
B300 | 300 baud |
B600 | 600 baud |
B1200 | 1200 baud |
B1800 | 1800 baud |
B2400 | 2400 baud |
B4800 | 4800 baud |
B9600 | 9600 baud |
B19200 | 19200 baud |
B38400 | 38400 baud |
B57600 | 57,600 baud |
B76800 | 76,800 baud |
B115200 | 115,200 baud |
EXTA | External rate clock |
EXTB | External rate clock |
CSIZE | Bit mask for data bits |
CS5 | 5 data bits |
CS6 | 6 data bits |
CS7 | 7 data bits |
CS8 | 8 data bits |
CSTOPB | 2 stop bits (1 otherwise) |
CREAD | Enable receiver |
PARENB | Enable parity bit |
PARODD | Use odd parity instead of even |
HUPCL | Hangup (drop DTR) on last close |
CLOCAL | Local line - do not change "owner" of port |
LOBLK | Block job control output |
CNEW_RTSCTS/CRTSCTS | Enable hardware flow control (not supported on all platforms) |
在传统的POSIX编程中,当连接一个本地的(不通过调制解调器)或者远程的终端(通过调制解调器)时,这里有两个选项应当一直打开,一个是CLOCAL,另一个是CREAD。这两个选项可以保证你的程序不会变成端口的所有者,而端口所有者必须去处理发散性作业控制和挂断信号,同时还保证了串行接口驱动会读取过来的数据字节。
波特率常数(CBAUD,B9600等等)通常指用到那些不支持c_ispeed和c_ospeed成员的旧的接口上。后面文章将会提到如何使用其他POSIX函数来设置波特率。
千万不要直接用使用数字来初始化c_cflag(当然还有其他标志),最好的方法是使用位运算的与或非组合来设置或者清除这个标志。不同的操作系统版本会使用不同的位模式,使用常数定义和位运算组合来避免重复工作从而提高程序的可移植性。
设置波特率 ?不同的操作系统会将波特率存储在不同的位置。旧的编程接口将波特率存储在上表所示的c_cflag成员中,而新的接口实装则提供了c_ispeed和c_ospeed成员来保存实际波特率的值。
程序中可是使用cfsetospeed(3)和cfsetispeed(3)函数在termios结构体中设置波特率而不用去管底层操作系统接口。下面的代码是个非常典型的设置波特率的例子。
struct termios options; /* * Get the current options for the port... */ tcgetattr(fd, &options);/* * Set the baud rates to 19200... */ cfsetispeed(&options, B19200); cfsetospeed(&options, B19200); /* * Enable the receiver and set local mode... */ options.c_cflag |= (CLOCAL | CREAD); /* * Set the new options for the port... */ tcsetattr(fd, TCSANOW, &options);函数tcgetattr(3)会将当前串口配置回填到termio结构体option中。然后,程序设置了输入输出的波特率并且将本地模式(CLOCAL)和串行数据接收(CREAD)设置为有效,接着将新的配置作为参数传递给函数tcsetattr(3)。常量TCSANOW标志所有改变必须立刻生效而不用等到数据传输结束。其他另一些常数可以保证等待数据结束或者刷新输入输出之后再生效。
tcsetattr常量
常量 | 描述 |
TCSANOW | Make changes now without waiting for data to complete |
TCSADRAIN | Wait until everything has been transmitted |
TCSAFLUSH | Flush input and output buffers and make the change |
不同的系统上可能支持不同的输入输出速度,所以,通过串口连接两台机器或者设备的时候,应该将波特率设置成两者中较小的那个,即MIN(speed1, speed2)。
设置字符大小 ?设置字符大小的时候,这里却没有像设置波特率那么方便的函数。所以,程序中需要一些位掩码运算来把事情搞定。字符大小以比特为单位指定:
options.c_flag &= ~CSIZE; /* Mask the character size bits */ options.c_flag |= CS8; /* Select 8 data bits */ 设置奇偶校验 ?与设置字符大小的方式差不多,这里仍然需要组合一些位掩码来将奇偶校验设为有效和奇偶校验的类型。UNIX串口驱动可以生成even,odd和no parity位码。设置space奇偶校验需要耍点小手段。
- No parity (8N1)
- Even parity (7E1)
- Odd parity (7O1)
- Space parity is setup the same as no parity (7S1)
某些版本的UNIX系统支持通过CTS(Clear To Send)和RTS(Request To Send)信号线来设置硬件流控制。如果系统上定义了CNEW_RTSCTS和CRTSCTS常量,那么很可能它会支持硬件流控制。使用下面的方法将硬件流控制设置成有效:
options.c_cflag |= CNEW_RTSCTS; /* Also called CRTSCTS将它设置成为无效的方法与此类似:
options.c_cflag &= ~CNEW_RTSCTS; 本地设置 ?本地模式成员变量c_lflag可以控制串口驱动怎样控制输入字符。通常,你可能需要通过c_lflag成员来设置经典输入和原始输入模式。
成员变量c_lflag可以使用的常量
ISIG | Enable SIGINTR, SIGSUSP, SIGDSUSP, and SIGQUIT signals |
ICANON | Enable canonical input (else raw) |
XCASE | Map uppercase \lowercase (obsolete) |
ECHO | Enable echoing of input characters |
ECHOE | Echo erase character as BS-SP-BS |
ECHOK | Echo NL after kill character |
ECHONL | Echo NL |
NOFLSH | Disable flushing of input buffers after interrupt or quit characters |
IEXTEN | Enable extended functions |
ECHOCTL | Echo control characters as ^char and delete as ~? |
ECHOPRT | Echo erased character as character erased |
ECHOKE | BS-SP-BS entire line on line kill |
FLUSHO | Output being flushed |
PENDIN | Retype pending input at next read or input char |
TOSTOP | Send SIGTTOU for background output |
经典输入是以面向行设计的。在经典输入模式中输入字符会被放入一个缓冲之中,这样可以以与用户交互的方式编辑缓冲的内容,直到收到CR(carriage return)或者LF(line feed)字符。
选择使用经典输入模式的时候,你通常需要选择ICANON,ECHO和ECHOE选项:
options.c_lflag |= (ICANON | ECHO | ECHOE); 选择原始输入 ?原始输入根本不会被处理。输入字符只是被原封不动的接收。一般情况中,如果要使用原始输入模式,程序中需要去掉ICANON,ECHO,ECHOE和ISIG选项:
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); 输入选项 ?可以通过输入模式成员c_iflag来控制从端口上收到的字符的输入过程。与c_cflag一样,c_iflag的最终值是想要使用的所有状态的位运算OR的组合。
c_iflag成员可以使用的常量
常量 | 描述 |
INPCK | Enable parity check |
IGNPAR | Ignore parity errors |
PARMRK | Mark parity errors |
ISTRIP | Strip parity bits |
IXON | Enable software flow control (outgoing) |
IXOFF | Enable software flow control (incoming) |
IXANY | Allow any character to start flow again |
IGNBRK | Ignore break condition |
BRKINT | Send a SIGINT when a break condition is detected |
INLCR | Map NL to CR |
IGNCR | Ignore CR |
ICRNL | Map CR to NL |
IUCLC | Map uppercase to lowercase |
IMAXBEL | Echo BEL on input line too long |
当程序在c_cflag中设置了奇偶校验成员(PARENB)的时候,程序就需要将输入奇偶校验设置成为有效。与奇偶校验相关的常量有INPCK,IGNPAR,PARMRK和ISTRIP。一般情况下,你可能需要选择INPCK和ISTRIP将奇偶校验设置为有效同时从接收字串中脱去奇偶校验位:
options.c_iflag |= (INPCK | ISTRIP);IGNPAR是一个比较危险选项,即便有错误发生时,它也会告诉串口驱动直接忽略奇偶校验错误给数据放行。这个选项在测试链接的通讯质量时比较有用而通常不会被用在实际程序中。
PARMRK会导致奇偶校验错误被标志成特殊字符加入到输入流之中。如果IGNPAR选项也是有效的,那么一个NUL(八进制000)字符会被加入到发生奇偶校验错误的字符前面。否则,DEL(八进制177)和NUL字符会和出错的字符一起送出。
设置软件流控制 ?软件流控制可以通过IXON,IXOFF和IXANY常量设置成有效:
options.c_iflag |= (IXON | IXOFF | IXANY);将其设置为无效的时候,很简单,只需要对这些位取反:
options.c_iflag &= ~(IXON | IXOFF | IXANY);XON(start data)和XOFF(stop data)字符却是在c_cc数组中定义的,下面会详细描述这个数组。
输出选项 ?成员变量c_oflag之中包括了输出过滤选项。和输入模式相似,程序可以选择使用经过加工的或者原始的数据输出。
c_oflag成员的常量
常量 | 描述 |
OPOST | Postprocess output (not set = raw output) |
OLCUC | Map lowercase to uppercase |
ONLCR | Map NL to CR-NL |
OCRNL | Map CR to NL |
NOCR | No CR output at column 0 |
ONLRET | NL performs CR function |
OFILL | Use fill characters for delay |
OFDEL | Fill character is DEL |
NLDLY | Mask for delay time needed between lines |
NL0 | No delay for NLs |
NL1 | Delay further output after newline for 100 milliseconds |
CRDLY | Mask for delay time needed to return carriage to left column |
CR0 | No delay for CRs |
CR1 | Delay after CRs depending on current column position |
CR2 | Delay 100 milliseconds after sending CRs |
CR3 | Delay 150 milliseconds after sending CRs |
TABDLY | Mask for delay time needed after TABs |
TAB0 | No delay for TABs |
TAB1 | Delay after TABs depending on current column position |
TAB2 | Delay 100 milliseconds after sending TABs |
TAB3 | Expand TAB characters to spaces |
BSDLY | Mask for delay time needed after BSs |
BS0 | No delay for BSs |
BS1 | Delay 50 milliseconds after sending BSs |
VTDLY | Mask for delay time needed after VTs |
VT0 | No delay for VTs |
VT1 | Delay 2 seconds after sending VTs |
FFDLY | Mask for delay time needed after FFs |
FF0 | No delay for FFs |
FF1 | Delay 2 seconds after sending FFs |
通过在c_oflag成员变量中设置OPOST选项的方法程序可以选择加工过的输入。
options.c_oflag |= OPOST;在所有选项当中,你可能只需要使用ONLCR选项来将行分隔符映射到CR-LF组合对上。其他选项主要是历史遗留,仅仅与行打印机和终端跟不上串行数据的年代有关。
选择原始输出 ?原始输出方式可以通过在c_oflag中重置OPOST选项来选择:
options.c_oflag &= ~OPOST;如果OPOST选项被设置成无效的话,其他c_oflag中的选项都会失效。
控制字符 ?字符数组c_cc里面包括了控制字符的定义和超时参数。这个数组的每个元素都是以常量定义的。
成员变量c_cc中的控制字符
常量 | 描述 | 键 |
VINTR | Interrupt | CTRL-C |
VQUIT | Quit | CTRL-Z |
VERASE | Erase | Backspace (BS) |
VKILL | Kill-line | CTRL-U |
VEOF | End-of-file | CTRL-D |
VEOL | End-of-line | Carriage return (CR) |
VEOL2 | Second end-of-line | Line feed (LF) |
VMIN | Minimum number of characters to read | - |
VSTART | Start flow | CTRL-Q (XON) |
VSTOP | Stop flow | CTRL-S (XOFF) |
VTIME | Time to wait for data (tenths of seconds) | - |
用来做软件流控制的字符包含在数组c_cc的VSTART和VSTOP元素里面。通常情况下,它们应该被设置成DC1(八进制021)和DC3(八进制023),它们在ASCII标准中代表着XON和XOFF字符。
设置读取超时 ?UNIX串口驱动提供了设置字符和包超时的能力。数组c_cc中有两个元素可以用来设置超时:VMIN和VTIME。在经典输入模式或者通过open(2)和fcntl(2)函数传递NDELAY选项时,超时设置会被忽略。
VMIN可以指定读取的最小字符数。如果它被设置为0,那么VTIME值则会指定每个字符读取的等待时间。
如果VMIN不为零,VTIME会指定等待第一个字符读取操作的时间。如果在这个指定时间中可以开始读取某个字符,直到VMIN个数的所有字符全部被读取,其他读取操作将会被阻塞(等待)。也就是说,一旦读取第一个字符,串口驱动的预期就是接收到整个字符包(一共VMIN字节)。如果在允许的时间内没有字符被读取,那么read(2)调用就会返回0。通过这个方法可以确切得告诉串口驱动程序需要读取N个字节,而且read(2)调用只会返回N或者0。然而,超时设置只对第一个字符的读取操作有效,所以,如果因为某些原因驱动程序在N字节的包中丢失某个字符的话,read(2)调用将会一直等下去。
VTIME可以以十分之一秒为单位指定等待字符输入的时间。如果VTIME设置为0(默认情况),除非open(2)或者fcntl(2)函数设置了NDELAY选项,否则read(2)将会永久得阻塞(等待)。
调制解调器通讯 ?说到串口通讯就不得不提一下通过调剂解调器通讯的方式。这里给出的程序例子都适用于支持“事实上的”标准AT命令集的调制解调器。
什么是调制解调器 ?调制解调器是一种可以将数字信号的串行数据转化为模拟信号频率的设备。通过这种转换,信息就可以通过像电话线或者有线电视线缆那样的模拟数据链路来传输了。口语中,经常将调制解调器称作“猫”。标准的电话调制解调器可以将串行数据转化为能够通过电话线传输的音频;因为这种转化非常之快又非常复杂,所以如果你去听一下的话,这些音频很像是大声尖叫时发出来的声音。
今天可以见到的调制解调器可以通过电话线每秒传输53000比特——5.3Kbps——的数据。还有就是,大多数调制解调器都使用数据压缩技术,这样就可以将某些类型数据的传输比特率提高到100kbps。
与调制解调器通讯 ?于调制解调器通讯的第一步就是要以原始输入模式打开和配置串口。
int fd; struct termios options;/* open the port */ fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY | O_NDELAY); fcntl(fd, F_SETFL, 0); /* get the current options */ tcgetattr(fd, &options); /* set raw input, 1 second timeout */ options.c_cflag |= (CLOCAL | CREAD); options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); options.c_oflag &= ~OPOST; options.c_cc[VMIN] = 0; options.c_cc[VTIME] = 10; /* set the options */ tcsetattr(fd, TCSANOW, &options);接下来就需要和调制解调器建立通讯连接。最好的办法就是给调制解调器发送“AT”命令。这也会让比较只能的调制解调器探测到你正在使用的波特率。如果正确地连接到调制解调器上,并且调制解调器开启电源,它会返回一个回应信号“OK”。
int /* O - 0 = MODEM ok, -1 = MODEM bad */ init_modem(int fd) /* I - Serial port file */ { char buffer[255]; /* Input buffer */ char *bufptr; /* Current char in buffer */ int nbytes; /* Number of bytes read */ int tries; /* Number of tries so far */ for (tries = 0; tries < 3; tries ++) { /* send an AT command followed by a CR */ if (write(fd, "AT\r", 3) < 3) continue; /* read characters into our string buffer until we get a CR or NL */ bufptr = buffer; while ((nbytes = read(fd, bufptr, buffer + sizeof(buffer) - bufptr - 1)) > 0) { bufptr += nbytes; if (bufptr[-1] == '\n' | bufptr[-1] == '\r') break; } /* nul terminate the string and see if we got an OK response */ *bufptr = '\0'; if (strncmp(buffer, "OK", 2) == 0) return (0); } return (-1); } 标准调制解调器命令 ?大多数调制解调器都支持“AT”命令集。之所以这样叫是因为这个命令集中的每个命令都是以“AT”字符开头。每个命令都是以第一列的AT开头字符后面跟上特殊命令参数和一个回车符CR(八进制015)。调制解调器处理完这条命令之后会根据命令回复一些文本消息。
- ATD 拨号 [#b39592a6]
通过ATD命令可以拨打一个指定号码。除过号码和分隔符(-)以外,你还可以指定以音频("T")或者脉冲("P")方式拨号,暂停一秒(",")和等待拨号音("W"):
ATDT 555-1212 ATDT 18008008008W1234,1,1234 ATD T555-1212WP1234调制解调器可能回复下面列出的某个消息:
NO DIALTONE BUSY NO CARRIER CONNECT CONNECT baud- ATH 挂断
通过ATH命令可以让调制解调器挂断。因为,调制解调器如果在“命令”模式的话,你可能就不能打普通电话了。
如果DTR信号线掉了的话,大部分调制解调器也会挂断。你可以将波特率设置成0并且持续至少1秒来做到这一点。再次让DTR掉落同样也可以把调制解调器重新拉回命令模式。
调制解调器成功挂断以后,它会回复一个"NO CARRIER"回来。如果调制解调器仍然保持连接,它则会发送"CONNECT"或者"CONNECT baud"这样的消息。
- ATZ 重置调制解调器
通过ATZ命令可以重置调制解调器。重置之后它会回复字符串"OK"。
- 与调制解调器通讯的常见问题
首先,也是最重要的一点,千万不要使用回声输入(input echoing)。回声输入会导致调制解调器和计算机之间产生反馈循环。
其次,当发送调制解调器命令时,命令必须以回车(CR)而不是换行(NL)结束。C语言中回车的字符常量是"\r"。
最后,处理调制解调器通讯的时候,要一定保证你使用了调制解调器支持的波特率。虽然大多数调制解调器都支持自动探测波特率,但你也会注意到某些(通常是19.2kbps或者比较老的调制解调器)有局限性。
高级串口编程 ?所谓高级串口编程其实说的就是使用更直接的底层的ioctl(2)和select(2)系统调用来操作串口。
串口的ioctl ?前文中曾经提到使用tcgetattr和tcsetattr函数来配置串口。UNIX环境下,这些函数都是使用ioctl(2)系统调用来实现的。
系统调用ioctl可以带三个参数:
int ioctl(int fd, int request, ...);显然,fd参数对于串口编程来说就是串口设备文件的文件描述符咯。而request参数是在头文件中定义的常量,而且一般不会超出下表所列的范围。
串口的IOCTL请求
REQUEST | 描述 | POSIX函数 |
TCGETS | Gets the current serial port settings. | tcgetattr |
TCSETS | Sets the serial port settings immediately. | tcsetattr(fd, TCSANOW, &options) |
TCSETSF | Sets the serial port settings after flushing the input and output buffers. | tcsetattr(fd, TCSAFLUSH, &options) |
TCSETSW | Sets the serial port settings after allowing the input and output buffers to drain/empty. | tcsetattr(fd, TCSADRAIN, &options) |
TCSBRK | Sends a break for the given time. | tcsendbreak, tcdrain |
TCXONC | Controls software flow control. | tcflow |
TCFLSH | Flushes the input and/or output queue. | tcflush |
TIOCMGET | Returns the state of the "MODEM" bits. | None |
TIOCMSET | Sets the state of the "MODEM" bits. | None |
FIONREAD | Returns the number of bytes in the input buffer. | None |
TIOCMGET ioctl可以取得当前调制解调器的状态位。这个状态位囊括了除去RXD和TXD信号线的所有RS-232信号,这些都在下表中列出。
控制信号常量
常量 | 描述 |
TIOCM_LE | DSR (data set ready/line enable) |
TIOCM_DTR | DTR (data terminal ready) |
TIOCM_RTS | RTS (request to send) |
TIOCM_ST | Secondary TXD (transmit) |
TIOCM_SR | Secondary RXD (receive) |
TIOCM_CTS | CTS (clear to send) |
TIOCM_CAR | DCD (data carrier detect) |
TIOCM_CD | Synonym for TIOCM_CAR |
TIOCM_RNG | RNG (ring) |
TIOCM_RI | Synonym for TIOCM_RNG |
TIOCM_DSR | DSR (data set ready) |
例如下面这个程序片段,你可以通过给ioctl带一个用来保存状态位的整形变量的指针来取得状态位。
#include #include int fd; int status; ioctl(fd, TIOCMGET, &status);