最近在学习Windows串口通信,以事件驱动模式开发相关应用时,肯定会用到以下几个函数:
SetCommMask(HANDLE hComm, DWORD dwEvtMask);
GetCommMask(HANDLE hComm, LPDWORD lpEvtMask);
WaitCommEvent(HANDLE hComm, LPDWORD lpEvtMask, LPOVERLAPPED lpOverlapped);
这几个函数的使用我就不再废话了,大家查MSDN就OK了。 我要说的重点是这几个函数应用了位掩码的方法来进行条件分支处理,非常自然,高效。无论对人 还是 对于机器来讲,都很优美。 非常值得我们在自己的工程实践中灵活运用。
其中的核心参数--需要捕获的事件代码,我们称其为“事件掩码”,用变量 dwEvtMask表示, 定义为16位长度,其可选值如下表所列。
英文单词或缩写的标识符常量,非常方便程序员理解,而其一一对应的十六进制常量值也早已在系统里预定义好了,各位直接使用意义直观的字符串常量就OK了。
Value | Meaning |
---|---|
EV_BREAK | A break was detected on input. |
EV_CTS | The CTS (clear-to-send) signal changed state. |
EV_DSR | The DSR (data-set-ready) signal changed state. |
EV_ERR | A line-status error occurred. Line-status errors are CE_FRAME, CE_OVERRUN, and CE_RXPARITY. |
EV_RING | A ring indicator was detected. |
EV_RLSD | The RLSD (receive-line-signal-detect) signal changed state. |
EV_RXCHAR | A character was received and placed in the input buffer. |
EV_RXFLAG | The event character was received and placed in the input buffer. The event character is specified in the device'sDCB structure, which is applied to a serial port by using theSetCommState function. |
EV_TXEMPTY | The last character in the output buffer was sent. |
这个被定义为16位长度的事件掩码变量,是我们这里要讨论的主角。 在我理解了Windows 在这里使用位掩码的机制之后,我建议你可以把它想象成一个由16盏信号灯排成一排所构成的一个信号灯箱,每盏灯只有开和关两种状态。
接下来我们再来看事件列表里的事件代码的Value,就是那个 0x开头的十六进制数,我们将它以二进制完全展开,如下所示:
事件代码 | 二进制表示 |
EV_BREAK | 0000 0000 0100 0000 |
EV_CTS | 0000 0000 0000 1000 |
EV_DSR | 0000 0000 0001 0000 |
EV_ERR | 0000 0000 1000 0000 |
EV_RING | 0000 0001 0000 0000 |
EV_RLSD | 0000 0000 0010 0000 |
EV_RXCHAR | 0000 0000 0000 0001 |
EV_RXFLAG | 0000 0000 0000 0010 |
EV_TXEMPTY | 0000 0000 0000 0100 |
秘密就在这里了。你有没有发现所有事件掩码值,要么是“1”,要么是2的正整数次幂,这样设计的结果就是,任何一个事件的掩码值的二进制表示里,“1”只出现一次,而所有九个事件的掩码值的二进制表示,同一位的纵向每一列里,“1”最多只出现一次!
现在我们对全部事件的二进制码进行位操作求 “|”,这有没有让你想到点什么?没错,就是当初我们设置要关注事件时,如果有两个或两个以上事件需要关注时,事件代码之间用的就是 "|" 操作。”|“ 操作之后,我们得到 “0000 0001 1111 1111”。这里一共九个”1“,而事件也正好是九个,不同位置上的”1“ 对应着不同的事件,这就是位掩码的意义所在。
/*小隐喻:刚才我说可以把事件掩码变量 dwEvtMask 想象成由十六个信号灯排成一排所组成的信号灯箱,我们每关注一个事件,就相当于接通对应信号灯的线路,但仅仅只是接通线路,还没有拉闸通电,当我们关注的事件真的发生了,那对应信号灯就会拉闸通电了。*/
接下来我们回到代码层面,在设置事件掩码时,如果我只关注一个事件,比如我只关注 “输入缓冲区收到了一个字符” 这么一个事件,那么
SetCommMask( hCom1, EV_RXCHAR); //此时事件掩码变量 dwEvtMask 的值实际上是”0“ ,但相应位置上的信号灯的线路已经接通,我们就用 ”0000 0000 0000 000?“ 来表示这种状态
如果我要关注两个事件,比如:
SetCommMask( hCom1, EV_RXCHAR | EV_ERR); //此时事件掩码变量 dwEvtMask 的值实际上是”0“,但有两盏信号灯的线路已接通,我们用 ”0000 0000 ?000 000?“来表示
那么三个,四个....哪怕全部九个事件都关注上,也是如此操作,如下:
SetCommMask( hCom1, EV_BREAK | EV_CTS | EV_DSR | EV_ERR | EV_RING | EV_RLSD | EV_RXCHAR | EV_RXFLAG | EV_TXEMPTY);
//此时事件掩码变量 dwEvtMask 的值仍是”0“,但九盏信号灯的线路都已接通,灯箱的状态为 ”0000 000? ???? ????“
当然我们也可以设置个灯箱指针,这样操作起来方便点:
DWORD *lpdwEvtMask = (EV_BREAK | EV_CTS | EV_DSR | EV_ERR | EV_RING | EV_RLSD | EV_RXCHAR | EV_RXFLAG | EV_TXEMPTY);
然后,我们启动监视功能:
WaitCommEvent(hCom1, lpdwEvtMask, NULL); //为聚焦今天的主题,这里暂不讨论重叠方式
一旦 事件掩码 变量里所罗列的事件中有任何一个或两个/三个...九个 事件发生,则其位掩码值将被填写进 dwEvtMask,即相应位被置1 (此动作由系统完成),在我的隐喻里,则是对应的信号灯通电点亮了。 当然,代码里是没有什么信号灯和可见光的(但是有智慧之光,呵呵),还是需要靠代码来说话。
SetCommMask( hCom1, EV_RXCHAR | EV_ERR); //假设我们现在关注两个事件
...
if ( WaitCommEvent(hCom1, lpdwEvtMask, NULL) ) //如果条件为真,则说明有关注的事件发生了,可能是一个,也可能是两个关注的事件都发生了,所以需要进一步鉴别
{
if ( dwEvtMask & EV_RXCHAR ) //如果EV_RXCHAR事件发生了,则dwEvtMask相应位被置”1“,条件运算的结果就是EV_RXCHAR事件的掩码值,既非零,又明确指出了是哪个事件
{
... //响应 EV_RXCHAR 事件
}
if ( dwEvtMask & EV_ERR )
{
... //响应 EV_ERR 事件
}
}
else
... //处理错误
这里的条件判断,体现出了运用位掩码的优势,其通过位运算就完成了事件筛选,代码阅读起来清晰易懂,而机器执行起来又绝对的高速度和高效率,所以我说这样的设计非常优美。
实际上,弄懂了这些之后,我们也完全可以在自己的代码实践中灵活运用 位掩码,从而使代码简洁,高效,优美。
延伸阅读:
http://blog.csdn.net/qsir/article/details/72457305
http://www.cnblogs.com/zyl910/archive/2012/03/12/noifopex1.html