在termios库中,标志位是如何表示的?

5

我是C语言和驱动程序编程的新手。目前,我正在使用Debian编写用户空间驱动程序,以通过USB与RS232通信。在研究过程中,我找到了以下代码片段。

tty.c_cflag     &=  ~PARENB;            // No Parity
tty.c_cflag     &=  ~CSTOPB;            // 1 Stop Bit
tty.c_cflag     &=  ~CSIZE;
tty.c_cflag     |=  CS8;                // 8 Bits

我知道这些代码的后果,但是只有在每个控制标志常量(PARENB、CSTOPB等)具有与这些标志组合相同的长度时,这些操作才有意义。我似乎无法通过任何文件验证这一点(这是我对C语言的主要抱怨之一,很难找到易于理解的文档),以确认这一点。
我想确保我正确理解了程序,因为这是一种纯归纳方法,我不确定为什么这些标志会被存储成这样。是否有人可以验证这些发现,或指出我可能忽略的事情?
例如:
tty.c_cflag hypothetically is 4-bits long, each of the flags from the
previous code block corresponding to bits 3, 2, 1, 0. Then I believe the
following is how these are stored, if we were to say flags PARENB (3) and
CSTOPB (2) are high, and the other two flags are disabled. 

tty.c_cflag = 1100
PARENB = 1000
CSTOPB = 0100
CSIZE = 0000
CS8 = 0000
2个回答

12
在C语言中,你能找到的最好的文档就是源代码本身,你可以在计算机上的/usr/include/termios.h找到它(实际上可能分布在其中一个或多个包含文件中)——这里是我所基于的面向苹果的基于BSD的termios.h,根据你使用的Unix版本不同,值可能会发生变化。
在那里,你将发现你的tty对象是struct termios类型,定义如下:
struct termios {
    tcflag_t    c_iflag;    /* input flags */
    tcflag_t    c_oflag;    /* output flags */
    tcflag_t    c_cflag;    /* control flags */
    tcflag_t    c_lflag;    /* local flags */
    cc_t        c_cc[NCCS]; /* control chars */
    speed_t     c_ispeed;   /* input speed */
    speed_t     c_ospeed;   /* output speed */
};

所以c_cflag的类型是tcflag_t,它由以下行定义:

typedef unsigned long   tcflag_t;

unsigned long被期望是4个字节,即32位。

然后你在示例中使用的所有标志都被定义如下;使用8字节值:

#define PARENB      0x00001000  /* parity enable */
#define CSTOPB      0x00000400  /* send 2 stop bits */
#define CSIZE       0x00000300  /* character size mask */
#define CS8         0x00000300      /* 8 bits */
话虽如此,工作方式是作为一个位数组使用,这意味着每个位对于一个功能都是重要的。这是一种常用的方法,因为位运算在处理能力上是“便宜”的(CPU可以在一个周期内进行位运算),并且在内存空间上也是“便宜”的,因为不是使用32个布尔值的数组来存储值(布尔类型的大小为1字节以存储一个二进制值),而是每个字节可以存储8个二进制值。

另一个优点和优化是,因为你的CPU至少是32位的,并且很可能在2015年是64位的,所以可以在一个CPU周期内对32个值应用掩码。

位掩码的另一种替代表示方法是创建像以下结构体:

struct tcflag_t {
    bool cignore;
    uint8_t csize;
    bool cstopb;
    bool cread;
    bool parenb;
    bool hupcl;
    bool clocal;
    bool ccts_oflow;
    bool crts_iflow;
    bool cdtr_iflow;
    bool ctdr_oflow;
    bool ccar_oflow;
};

这将会是12个字节。要更改它们,您需要执行12个操作。

然后,您可以在字节上执行的操作遵循布尔逻辑,该逻辑由真值表定义:

And(&)、Or(|)和Not(~)真值表:

| a | b | & |    | a | b | | |    | a | ~ |
| - | - | - |    | - | - | - |    | 0 | 1 |
| 0 | 0 | 0 |    | 0 | 0 | 0 |    | 1 | 0 |
| 0 | 1 | 0 |    | 0 | 1 | 1 |
| 1 | 0 | 0 |    | 1 | 0 | 1 |
| 1 | 1 | 1 |    | 1 | 1 | 1 |

我们通常将And运算符称为“强制为零”,将Or运算符称为“强制为1”,因为除非两个值都是1,否则And将得到0,除非两个值都是0,否则Or将得到1
所以,如果我们考虑tty.c_cflag = 0x00000000,并且您想启用奇偶校验:
tty.c_cflag |= PARENB;

然后tty.c_cflag将包含0x00001000,即0b1000000000000

接下来您需要设置7位大小:

tty.c_cflag |= CS7;

并且tty.c_cflag将包含0x00001200,即0b1001000000000


现在,让我们回到你的问题:你提供的“等效”示例并不真正代表实际情况,因为你认为CSIZECS8没有任何值。

那么,让我们来看一下你从示例中获取的代码:

tty.c_cflag     &=  ~PARENB;            // No Parity
tty.c_cflag     &=  ~CSTOPB;            // 1 Stop Bit
tty.c_cflag     &=  ~CSIZE;
tty.c_cflag     |=  CS8;                // 8 Bits

在这里,tty.c_cflag包含一个未知的值:
0b????????????????????????????????

你需要无校验位,一个停止位和8位数据大小。因此,你要否定“设置奇偶校验位”的值以关闭它:

~PARENB == 0b0111111111111

然后使用 And 操作符,您将强制将位设置为零:

tty.c_cflag &= ~PARENB —→ 0b???????????????????0????????????

然后您使用 CSTOPB 进行相同的操作:

tty.c_cflag &= ~CSTOPB —→ 0b???????????????????0?0??????????

最后是CSIZE

tty.c_cflag &= ~CSIZE  —→ 0b???????????????????0?000????????

对于CSIZE,目标是确保数据长度的两个位值被重置。 然后通过将值强制设置为1来设置正确的长度。
tty.c_cflag |= CS8     —→ 0b???????????????????0?011????????

实际上,将 CSIZE 重置为 00,然后将 CS8 设置为 11 是没有意义的,因为直接执行 tty.c_cflag |= CS8 就会将其设置为 11。但这是一个好习惯,以防您想从 CS8 更改为 CS7,这将仅设置其中的一个位,另一个保留原始值。

最后,当您打开串口时,库将检查这些值以配置端口,并对您未强制设置的所有其他值使用默认值,您将能够使用串口。

我希望我的解释有助于您更好地理解串口标志设置以及位掩码的使用。同样的原理也被用于许多其他事情,比如IPv4网络掩码、文件I/O等。


1
"一个 unsigned long 应该是 8 字节,即32位。" -- 嗯???!!! "而且你知道你想要...没有停止位" -- 不,最小规格是一个停止位。 - sawdust
"一个无符号长整型应该是8个字节,即32位。嗯,你说得对,我应该重新计算一下数学 :-)" - zmo
“而无符号长整型应该是8个字节,即32位”仍需要重新修改。32位永远不是8个字节。 - chux - Reinstate Monica
嗯...好的,我不确定为什么我会写那个,也不知道为什么我没有纠正它,但我刚刚修复了它,希望其余部分有意义☺。 - zmo
你是否需要在稍后使用if语句/switch语句来知道位数组中哪一位是开启的,并根据此决定要执行什么操作?我发现if语句的CPU成本也很高。 - John Sall
@JohnSall,我不明白你的评论。您可以使用任何条件语句来基于termios标志执行操作,而且它们并不那么昂贵(它可以仅为一条CPU指令,加上应用位数组掩码的几个步骤)。 - zmo

2
实际宏的值取决于平台(例如,在Linux上,CSTOPB定义为0100,而在某些BSD上则为02000)。这就是为什么您不应对其确切值做出假设的原因。
例如,CSIZE和CS8具有相同值的情况确实很常见,但在某些平台上可能不是这样,因此您首先要与CSIZE掩码的补码进行AND运算(将影响字符大小的所有位设置为零),然后再OR入这些位的值。如果您假定CS8与掩码具有相同的模式,则可以省略第一步,但在这种假设不成立的平台上,代码将以非常难以理解的方式执行错误操作,而且没有任何警告。
这里,PARENB和CSTOPB是单个位标志(恰好有一个1位),可以使用按位OR设置,并通过位AND其补码~清除。同时,包括CS8在内的字符大小可以具有任意数量的1位,包括零-它们更像是存储在较大整数的特定位中的小整数。CSIZE是一个掩码,在所有表示字符大小的位置上都有1位(任何CS5、CS6、CS7、CS8中的任何一个)-该掩码可用于提取确切的字符大小(例如,测试if ((tty.c_flag & CSIZE) == CS8)),或在设置之前将其清除(正如在这里使用tty.c_flag &= ~CSIZE的情况)。

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