1 关于Termios
termios 函数族提供了一个常规的终端接口,用于控制非同步通信端口。
Linux中的控制台界面,要大量用到Termios库的内容。但是,总的来看,这些东西已经过时了,应该用更简单的方案来代替它了,尽管它是POSIX标准的一部分。
termios的实现中,有大量的参数设置,还有伪终端的说法。
这里描述的大部分属性有一个 termios_p 类型的参数,它是指向一个 termios结构的指针。这个结构包含了至少下列成员:
tcflag_t
c_iflag;
输入模式
tcflag_t
c_oflag;
输出模式
tcflag_t
c_cflag;
控制模式
tcflag_t
c_lflag;
本地模式
cc_t
c_cc[NCCS];
控制字符
1.1 c_iflag 标志常量:
c_iflag变量控制终端的输入属性,有下表这些属性可供设置。
IGNBRK 忽略输入中的 BREAK 状态
BRKINT 如果设置了 IGNBRK,将忽略 BREAK。如果没有设置,但是设置了 BRKINT,那么 BREAK将使得输入和输出队列被刷新,如果终端是一个前台进程组的控制终端,这个进程组中所有进程将收到 SIGINT 信号。如果既未设置IGNBRK 也未设置 BRKINT,BREAK 将视为与 NUL 字符同义,除非设置了 PARMRK,这种情况下它被视为序列377 。
IGNPAR 忽略桢错误和奇偶校验错。
PARMRK 如果没有设置 IGNPAR,在有奇偶校验错或桢错误的字符前插入 377 。如果既没有设置 IGNPAR 也没有设置PARMRK,将有奇偶校验错或桢错误的字符视为 。
INPCK 启用输入奇偶检测。
ISTRIP 去掉第八位。
INLCR 将输入中的 NL 翻译为 CR。
IGNCR 忽略输入中的回车
ICRNL 将输入中的回车翻译为新行 (除非设置了 IGNCR)
IUCLC (不属于 POSIX) 将输入中的大写字母映射为小写字母
IXON 启用输出的 XON/XOFF 流控制
IXANY (不属于 POSIX.1;XSI) 允许任何字符来重新开始输出
IXOFF 启用输入的 XON/XOFF 流控制。
IMAXBEL (不属于 POSIX) 当输入队列满时响零。Linux 没有实现这一位,总是将它视为已设置。
1.2 C_oflag标示常量
控制终端的输出属性,有如下属性可供设置:
OPOST 启用具体实现自行定义的输出处理
OLCUC 将输出中的小写字母映射为大写字母
ONLCR 将输出中的换行符映射为回车-换行
OCRNL 将输出中的回车映射为换行符
ONOCR 不在第 0 列输出回车
ONLRET 不输出回车
OFILL 发送填充字符作为延时,而不是使用定时来延时
OFDEL 填充字符是 ASCII DEL (0177)。如果不设置,填充字符则是 ASCII NUL。
NLDLY 新行延时掩码。取值为 NL0 和 NL1。
NL0
NL1
CRDLY 回车延时掩码。取值为 CR0, CR1, CR2, 或 CR3。
CR0
CR1
CR2
CR3
TABDLY 水平跳格延时掩码。取值为 TAB0, TAB1, TAB2, TAB3 (或 XTABS)。取值为 TAB3,即XTABS,将扩展跳格为空格 (每个跳格符填充 8 个空格)。
TAB0
TAB1
TAB2
TAB3
XTABS
BSDLY 回退延时掩码。取值为 BS0 或 BS1。
BS0
BS1
VTDLY 竖直跳格延时掩码。取值为 VT0 或 VT1。
VT0
VT1
FFDLY 进表延时掩码。取值为 FF0 或 FF1。
FF0
FF1
1.3 c_cflag 标志常量:
CBAUD 波特率掩码,可设置如下列所示波特率
B0 B50 B75 B110 B134 B150 B200 B300 B600 B1200B1800 B2400 B4800 B9600 B19200 B38400 B57600B115200 B230400 B460800 这些波特率是posix标准,所有系统都应该支持。
CBAUDEX 由用户指定波特率,这个操作不一定会被支持(POSIX 规定波特率存储在 termios结构中,并未精确指定它的位置,而是提供了函数 cfgetispeed() 和 cfsetispeed() 来存取它。一些系统使用c_cflag 中 CBAUD 选择的位,其他系统使用单独的变量,例如 sg_ispeed 和 sg_ospeed 。
CSIZE 字符长度掩码。取值为 CS5, CS6, CS7, 或 CS8。
CS5, CS6, CS7, CS8 所有系统都应该支持这个操作
CSTOPB 设置两个停止位,而不是一个。
CREAD 打开接受者。
PARENB 允许输出产生奇偶信息以及输入的奇偶校验。
PARODD 输入和输出是奇校验。
HUPCL 在最后一个进程关闭设备后,降低 modem 控制线 (挂断)
CLOCAL 忽略 modem 控制线
LOBLK 从非当前 shell 层阻塞输出(用于 shell )
CIBAUD 输入速度的掩码。CIBAUD 各位的值与 CBAUD 各位相同,左移了 IBSHIFT 位
CRTSCTS 启用 RTS/CTS (硬件) 流控制。
ISIG
当接受到字符 INTR, QUIT, SUSP, 或 DSUSP 时,产生相应的信号
ICANON
启用标准模式 (canonical mode)。允许使用特殊字符 EOF, EOL, EOL2,ERASE, KILL, LNEXT, REPRINT, STATUS, 和 WERASE,以及按行的缓冲
XCASE
如果同时设置了 ICANON,终端只有大写。输入被转换为小写,除了以 前缀的字符。输出时,大写字符被前缀 ,小写字符被转换成大写。
ECHO
回显输入字符。
ECHOE
如果同时设置了 ICANON,字符 ERASE 擦除前一个输入字符,WERASE擦除前一个词
ECHOK
如果同时设置了 ICANON,字符 KILL 删除当前行
ECHONL
如果同时设置了 ICANON,回显字符 NL,即使没有设置 ECHO。
NOFLSH
禁止在产生 SIGINT, SIGQUIT 和 SIGSUSP信号时刷新输入和输出队列。
TOSTOP
向试图写控制终端的后台进程组发送 SIGTTOU 信号。
ECHOCTL
在读入下一个字符时,输入队列中所有字符被重新输出。
ECHOPRT
如果同时设置了 ICANON 和 IECHO,字符在删除的同时被打印
ECHOKE
如果同时设置了 ICANON,回显 KILL 时将删除一行中的每个字符,如同指定了 ECHOE 和ECHOPRT 一样。
FLUSHO
输出被刷新。这个标志可以通过键入字符 DISCARD 来开关
PENDIN
在读入下一个字符时,输入队列中所有字符被重新输出。
IEXTEN
启用实现自定义的输入处理。这个标志必须与 ICANON 同时使用,才能解释特殊字符EOL2,LNEXT,REPRINT 和 WERASE,IUCLC 标志才有效。
c_cc 数组定义了特殊的控制字符。符号下标 (初始值) 和意义如下列的内容所示:
VINTR (003, ETX, Ctrl-C, or also 0177, DEL, rubout) 中断字符。发出 SIGINT信号。当设置 ISIG 时可被识别,不再作为输入传递。
VQUIT (034, FS, Ctrl-) 退出字符。发出 SIGQUIT 信号。当设置 ISIG时可被识别,不再作为输入传递。
VERASE 0177, DEL, rubout, or 010, BS, Ctrl-H, or also #)删除字符。删除上一个还没有删掉的字符,但不删除上一个 EOF 或行首。当设置 ICANON 时可被识别,不再作为输入传递。
VKILL (025, NAK, Ctrl-U, or Ctrl-X, or also @) 终止字符。删除自上一个 EOF或行首以来的输入。当设置 ICANON 时可被识别,不再作为输入传递。
VEOF (004, EOT, Ctrl-D) 文件尾字符。更精确地说,这个字符使得 tty缓冲中的内容被送到等待输入的用户程序中,而不必等到 EOL。如果它是一行的第一个字符,那么用户程序的 read() 将返回0,指示读到了 EOF。当设置 ICANON 时可被识别,不再作为输入传递。
VTIME 非 canonical 模式读时的延时,以十分之一秒为单位。
VMIN 非 canonical 模式读的最小字符数。
VSWTC (not in POSIX; not supported under Linux; 0, NUL) 开关字符。(只为shl 所用。)
VSTART (021, DC1, Ctrl-Q) 开始字符。重新开始被 Stop 字符中止的输出。当设置 IXON时可被识别,不再作为输入传递。
VSTOP (023, DC3, Ctrl-S) 停止字符。停止输出,直到键入 Start 字符。当设置 IXON时可被识别,不再作为输入传递。
VSUSP (032, SUB, Ctrl-Z) 挂起字符。发送 SIGTSTP 信号。当设置 ISIG时可被识别,不再作为输入传递。
VEOL (0, NUL) 附加的行尾字符。当设置 ICANON 时可被识别。
VREPRINT (not in POSIX; 022, DC2, Ctrl-R) 重新输出未读的字符。当设置 ICANON 和IEXTEN 时可被识别,不再作为输入传递。
VDISCARD (not in POSIX; not supported under Linux; 017, SI, Ctrl-O)开关:开始/结束丢弃未完成的输出。当设置 IEXTEN 时可被识别,不再作为输入传递。
VWERASE (not in POSIX; 027, ETB, Ctrl-W) 删除词。当设置 ICANON 和 IEXTEN时可被识别,不再作为输入传递。
VLNEXT not in POSIX; 026, SYN, Ctrl-V)字面上的下一个。引用下一个输入字符,取消它的任何特殊含义。当设置 IEXTEN 时可被识别,不再作为输入传递。
VEOL2 (not in POSIX; 0, NUL) 另一个行尾字符。当设置 ICANON 时可被识别。
2. 串口驱动程序
编写Rtems的串口驱动程序比较简单,只需要填写一张驱动程序表即可:
typedef struct _console_fns {
bool(*deviceProbe)(int minor);
int(*deviceFirstOpen)(int major, int minor, void *arg);
int(*deviceLastClose)(int major, int minor, void *arg);
int(*deviceRead)(int minor);
int(*deviceWrite)(int minor, const char *buf, int len);
void(*deviceInitialize)(int minor);
void(*deviceWritePolled)(int minor, char cChar);
int(*deviceSetAttributes)(int minor, const struct termios *t);
booldeviceOutputUsesInterrupts;
} console_fns;
顾名思义:firstopen、lastclose是首次打开和最后一次关闭时调用的函数;我们可以不用管它。
Probe其实是一个钩子函数,目前还没有发现他的真实作用;
Deviceread是读取串口数据的函数,这个函数不涉及中断操作信号量,只是简单的读取数据,每次读取一个字节。如果设置了这个函数,rtems就会使用轮训的方式获取数据,否则应用程序会在驱动程序底层被挂起,直到中断中接收到数据为止。
deviceWrite功能也是直接把数据写入到串口fifo中,这个函数功能也比较简单,除非数据全部写入,否则不会退出。
deviceInitialize这个函数需要我们初始化串口,按照预先的配置:初始化比特率、设置中断处理函数、设置比特位、停止位等。这个函数是必须要完成的。
deviceWritePolled这个函数写一个字符到串口,除非写不成功,否则会一直等待下去。
deviceSetAttributes这个函数作用是设置串口的一些属性,例如波特率、数据位、停止位等信息.
deviceOutputUsesInterrupts这个参数至关重要,它有三个选项可供选择:
l为TERMIOS_POLLED这个选项时,底层就会一直轮训等待数据,但不会全部占用CPU,rtems也会隔一段时间查询一次串口的状态。
l为TERMIOS_IRQ_DRIVEN时,串口使用中断驱动的方式:一旦串口被打开,就会初始化一个tty,tty会同时包含一个发送缓冲和一个接收缓冲。发送与接收的数据都会被暂存在tty的数据结构中,一旦中断发生,中断处理程序中会把接收到的数据暂存在缓冲中。等待应用程序调用读取操作时再从缓冲中把数据取出,如果数据缓冲超出缓冲范围,而应用程序依然没有。需要注意的是,串口必须被打开后才会开启这个缓冲区。
l使用TERMIOS_TASK_DRIVEN这个选项时,rtems会分别为发送接收时间创建两个代理任务,代理任务里面不断的轮询串口状态,然后进行数据的发送与接收操作。这种模式对上的接口类似于TERMIOS_IRQ_DRIVEN的方式,只是使用任务扫描来实现中断的功能。
if (tty->device.outputUsesInterrupts ==TERMIOS_TASK_DRIVEN) {
sc = rtems_task_create (
rtems_build_name ('T', 'x', 'T', c),
TERMIOS_TXTASK_PRIO,
TERMIOS_TXTASK_STACKSIZE,
RTEMS_NO_PREEMPT | RTEMS_NO_TIMESLICE |
RTEMS_NO_ASR,
RTEMS_NO_FLOATING_POINT | RTEMS_LOCAL,
&tty->txTaskId);
if (sc != RTEMS_SUCCESSFUL)
rtems_fatal_error_occurred (sc);
sc = rtems_task_create (
rtems_build_name ('R', 'x', 'T', c),
TERMIOS_RXTASK_PRIO,
TERMIOS_RXTASK_STACKSIZE,
RTEMS_NO_PREEMPT | RTEMS_NO_TIMESLICE |
RTEMS_NO_ASR,
RTEMS_NO_FLOATING_POINT | RTEMS_LOCAL,
&tty->rxTaskId);
if (sc != RTEMS_SUCCESSFUL)
rtems_fatal_error_occurred (sc);
}
余下来以发送、接收数据为例,跟踪termios中处理的事情,首先看代码:
rtems_termios_puts (
const void *_buf, int len, structrtems_termios_tty *tty) {
constunsigned char *buf = _buf;
unsignedint newHead;
rtems_interrupt_level level;
rtems_status_code sc;
pooled方式的处理:直接发送、然后退出
if(tty->device.outputUsesInterrupts == TERMIOS_POLLED){
(*tty->device.write)(tty->minor,(void *)buf, len);
return;
}
IRQ_DRIVEN方式的处理:放置到缓冲区、触发第一次中断、然后等待退出
newHead =tty->rawOutBuf.Head;
while(len) {
newHead = (newHead + 1) % tty->rawOutBuf.Size;
rtems_interrupt_disable (level);
while (newHead == tty->rawOutBuf.Tail) {
tty->rawOutBufState = rob_wait;
rtems_interrupt_enable (level);
发送缓冲信号量作用很重要:获取操作权限,空闲状态、发送缓冲为空状态可以获取到该信号量。
sc = rtems_semaphore_obtain(tty->rawOutBuf.Semaphore,
RTEMS_WAIT,
RTEMS_NO_TIMEOUT);
if (sc != RTEMS_SUCCESSFUL)
rtems_fatal_error_occurred (sc);
rtems_interrupt_disable(level); 余下的操作需要在中断关闭的状态下完成
}
tty->rawOutBuf.theBuf[tty->rawOutBuf.Head]= *buf++;向缓冲写数据
tty->rawOutBuf.Head = newHead;
if (tty->rawOutBufState == rob_idle){如果发送队列处于空闲状态、
if (!(tty->flow_ctrl & FL_ORCVXOF)){如果硬件流控制没有禁止数据接收
(*tty->device.write)(tty->minor,发送一个字符,一旦发送完毕就会产生中断
(char*)&tty->rawOutBuf.theBuf[tty->rawOutBuf.Tail],1);
}
else{硬件流控制当前禁止发送数据
tty->flow_ctrl |=FL_OSTOP;记录标示:写操作被硬件流控制挂起
}
tty->rawOutBufState =rob_busy;进入忙状态
}
rtems_interrupt_enable (level);
len--;
}
}
如果使用中断的方式处理发送事件,中断中需要对应以上处理程序有套自己的方法。但是目前的处理上,发送事件并没有采用中断的方式,这里修改了源代码,直接把数据发送出去。日后会做修改,使用中断的方式发送数据。
接下来关注接收事件,关键代码如下:
if(rtems_termios_linesw[tty->t_line].l_read != NULL){是否存在度函数
sc =rtems_termios_linesw[tty->t_line].l_read(tty,args);
tty->tty_rcvwakeup = 0;
rtems_semaphore_release (tty->isem);
return sc;
}
if(tty->cindex == tty->ccount) {
tty->cindex = tty->ccount = 0;
tty->read_start_column =tty->column;
if (tty->device.pollRead != NULL
&&tty->device.outputUsesInterrupts ==TERMIOS_POLLED)
sc = fillBufferPoll(tty);轮询模式下读取数据
else
sc = fillBufferQueue(tty);使用队列缓冲读取数据
if (sc != RTEMS_SUCCESSFUL)
tty->cindex = tty->ccount = 0;
}
while(count &&(tty->cindex <tty->ccount)) {
*buffer++ =tty->cbuf[tty->cindex++];
count--;
}
队列缓冲模式下,串口中断通过操作接收信号量与发送队列进行互动操作。将要发送的数据首先被保存到缓冲队列中,然后在中断处理程序中读取缓冲队列的数据,放置到发送FIFO中。一旦发送缓冲中的数据被发送完毕,中断处理程序会知会发送fifo,发送FIFO就从等待信号量的状态中退出来,重新填入发送数据。
while (wait ) {
while ((tty->rawInBuf.Head !=tty->rawInBuf.Tail)&&
(tty->ccount < (CBUFSIZE-1))) {
unsigned char c;
unsigned int newHead;
newHead = (tty->rawInBuf.Head + 1) %tty->rawInBuf.Size;
c = tty->rawInBuf.theBuf[newHead];
tty->rawInBuf.Head = newHead;
if(((tty->rawInBuf.Tail-newHead+tty->rawInBuf.Size)
% tty->rawInBuf.Size)
< tty->lowwater) {
tty->flow_ctrl &= ~FL_IREQXOF;
if (((tty->flow_ctrl & (FL_MDXON |FL_ISNTXOF))
==(FL_MDXON | FL_ISNTXOF))
&&((tty->rawOutBufState == rob_idle)
|| (tty->flow_ctrl & FL_OSTOP))){
(*tty->device.write)(tty->minor,
(void*)&(tty->termios.c_cc[VSTART]),
1);
}
else if (tty->flow_ctrl & FL_MDRTS){
tty->flow_ctrl &= ~FL_IRTSOFF;
if (tty->device.startRemoteTx != NULL) {
tty->device.startRemoteTx(tty->minor);
}
}
}
if (tty->termios.c_lflag & ICANON){
if (siproc (c, tty))
wait = 0;
}
else {
siproc (c, tty);
if (tty->ccount >=tty->termios.c_cc[VMIN])
wait = 0;
}
timeout = tty->rawInBufSemaphoreTimeout;
}
if ( wait ) {这部分操作有可能会被挂起,直至缓冲中数据全部发送完毕
sc = rtems_semaphore_obtain(tty->rawInBuf.Semaphore,
tty->rawInBufSemaphoreOptions,
timeout);
if (sc != RTEMS_SUCCESSFUL)
break;
}
}
3.设置串口
对串口的设置最好能够遵循标准,否则会引出不必要的麻烦。设置还是遵循unix的风格,使用ioctl函数。
以下是一个设置串口的示例,:
structtermiosss;1.定义一个串口设置选项
hCom2= open("/dev/com2", O_RDWR | O_EXCL, 0x777);
if(hCom2 == 0) {
printk("dev/com2 open failed!");
}
ioctl(hCom2, RTEMS_IO_GET_ATTRIBUTES,&ss); 2.首先读取当前设置,在这个基础上进行修改
cfsetospeed(&ss,B115200);3.设置波特率为115200
ss.c_cflag = CS8 | (~CSIZE &ss.c_cflag);4.设置8位数据模式
ioctl(hCom2, RTEMS_IO_SET_ATTRIBUTES,&ss); 5.写入串口设置新内容
close(hCom2);
termios这个结构体在termios.h这个头文件中被定义,所以必须包含这个头文件。同样也必须包含<sys/ioccom.h>这个头文件,因为RTEMS_IO_SGT_ATTRIBUTESRTEMS_IO_SET_ATTRIBUTES在这个文件中被定义。
第一步第二步首先读取串口当前的默认设置选项,然后在这个基础上进行设置。
第2步首先读出当前串口的状态,在这个状态的基础上再做更改.
第3、4步操作是设置具体的内容,虽然第3步是通过调用一个函数实现设置termios结构,但事实上都是通过本节介绍的一些宏对数据进行操作。跟进cfsetospeed函数会发现进行的操作实际上也只是操作了一个宏而已。
设置波特率、数据模式之后,最后一步写数据进入串口,即完成了串口的操作。