日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

串行通信及一個(gè)通信演示程序

 ylw527 2010-09-14
Win 32系統(tǒng)為串行通信提供了全新的服務(wù)。傳統(tǒng)的OpenComm、ReadComm、WriteComm、CloseComm等函數(shù)已經(jīng)過(guò)時(shí),WM_COMMNOTIFY消息也消失了。取而代之的是文件I/O函數(shù)提供的打開和關(guān)閉通信資源句柄及讀寫操作的基本接口。

  新的文件I/O函數(shù)(CreateFile、ReadFile、WriteFile等)支持重疊式輸入輸出,這使得線程可以從費(fèi)時(shí)的I/O操作中解放出來(lái),從而極大地提高了程序的運(yùn)行效率。

12.3.1 串行口的打開和關(guān)閉

  Win 32系統(tǒng)把文件的概念進(jìn)行了擴(kuò)展。無(wú)論是文件、通信設(shè)備、命名管道、郵件槽、磁盤、還是控制臺(tái),都是用API函數(shù)CreateFile來(lái)打開或創(chuàng)建的。該函數(shù)的聲明為:

HANDLE CreateFile(

LPCTSTR lpFileName, // 文件名

DWORD dwDesiredAccess, // 訪問(wèn)模式

DWORD dwShareMode, // 共享模式

LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 通常為NULL

DWORD dwCreationDistribution, // 創(chuàng)建方式

DWORD dwFlagsAndAttributes, // 文件屬性和標(biāo)志

HANDLE hTemplateFile // 臨時(shí)文件的句柄,通常為NULL

);

  如果調(diào)用成功,那么該函數(shù)返回文件的句柄,如果調(diào)用失敗,則函數(shù)返回INVALID_HANDLE_VALUE。

  如果想要用重疊I/O方式(參見12.3.3)打開COM2口,則一般應(yīng)象清單12.4那樣調(diào)用CreateFile函數(shù)。注意在打開一個(gè)通信端口時(shí),應(yīng)該以獨(dú)占方式打開,另外要指定GENERIC_READ、GENERIC_WRITE、OPEN_EXISTING和FILE_ATTRIBUTE_NORMAL等屬性。如果要打開重疊I/O,則應(yīng)該指定 FILE_FLAG_OVERLAPPED屬性。

 

清單12.4

HANDLE hCom;

DWORD dwError;

hCom=CreateFile(“COM2”, // 文件名

GENERIC_READ | GENERIC_WRITE, // 允許讀和寫

0, // 獨(dú)占方式

NULL,

OPEN_EXISTING, //打開而不是創(chuàng)建

FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, // 重疊方式

NULL

);

if(hCom = = INVALID_HANDLE_VALUE)

{

dwError=GetLastError( );

. . . // 處理錯(cuò)誤

}

當(dāng)不再使用文件句柄時(shí),應(yīng)該調(diào)用CloseHandle函數(shù)關(guān)閉之。

12.3.2 串行口的初始化

  在打開通信設(shè)備句柄后,常常需要對(duì)串行口進(jìn)行一些初始化工作。這需要通過(guò)一個(gè)DCB結(jié)構(gòu)來(lái)進(jìn)行。DCB結(jié)構(gòu)包含了諸如波特率、每個(gè)字符的數(shù)據(jù)位數(shù)、奇偶校驗(yàn)和停止位數(shù)等信息。在查詢或配置置串行口的屬性時(shí),都要用DCB結(jié)構(gòu)來(lái)作為緩沖區(qū)。

  調(diào)用GetCommState函數(shù)可以獲得串口的配置,該函數(shù)把當(dāng)前配置填充到一個(gè)DCB結(jié)構(gòu)中。一般在用CreateFile打開串行口后,可以調(diào)用GetCommState函數(shù)來(lái)獲取串行口的初始配置。要修改串行口的配置,應(yīng)該先修改DCB結(jié)構(gòu),然后再調(diào)用SetCommState函數(shù)用指定的DCB結(jié)構(gòu)來(lái)設(shè)置串行口。

  除了在DCB中的設(shè)置外,程序一般還需要設(shè)置I/O緩沖區(qū)的大小和超時(shí)。Windows用I/O緩沖區(qū)來(lái)暫存串行口輸入和輸出的數(shù)據(jù),如果通信的速率較高,則應(yīng)該設(shè)置較大的緩沖區(qū)。調(diào)用SetupComm函數(shù)可以設(shè)置串行口的輸入和輸出緩沖區(qū)的大小。

  在用ReadFile和WriteFile讀寫串行口時(shí),需要考慮超時(shí)問(wèn)題。如果在指定的時(shí)間內(nèi)沒有讀出或?qū)懭胫付〝?shù)量的字符,那么ReadFile或WriteFile的操作就會(huì)結(jié)束。要查詢當(dāng)前的超時(shí)設(shè)置應(yīng)調(diào)用GetCommTimeouts函數(shù),該函數(shù)會(huì)填充一個(gè)COMMTIMEOUTS結(jié)構(gòu)。調(diào)用SetCommTimeouts可以用某一個(gè)COMMTIMEOUTS結(jié)構(gòu)的內(nèi)容來(lái)設(shè)置超時(shí)。

  有兩種超時(shí):間隔超時(shí)和總超時(shí)。間隔超時(shí)是指在接收時(shí)兩個(gè)字符之間的最大時(shí)延,總超時(shí)是指讀寫操作總共花費(fèi)的最大時(shí)間。寫操作只支持總超時(shí),而讀操作兩種超時(shí)均支持。用COMMTIMEOUTS結(jié)構(gòu)可以規(guī)定讀/寫操作的超時(shí),該結(jié)構(gòu)的定義為:

typedef struct _COMMTIMEOUTS {

DWORD ReadIntervalTimeout; // 讀間隔超時(shí)

DWORD ReadTotalTimeoutMultiplier; // 讀時(shí)間系數(shù)

DWORD ReadTotalTimeoutConstant; // 讀時(shí)間常量

DWORD WriteTotalTimeoutMultiplier; // 寫時(shí)間系數(shù)

DWORD WriteTotalTimeoutConstant; // 寫時(shí)間常量

} COMMTIMEOUTS,*LPCOMMTIMEOUTS;

  COMMTIMEOUTS結(jié)構(gòu)的成員都以毫秒為單位??偝瑫r(shí)的計(jì)算公式是:

總超時(shí)=時(shí)間系數(shù)×要求讀/寫的字符數(shù) + 時(shí)間常量

  例如,如果要讀入10個(gè)字符,那么讀操作的總超時(shí)的計(jì)算公式為:

讀總超時(shí)=ReadTotalTimeoutMultiplier×10 + ReadTotalTimeoutConstant

  可以看出,間隔超時(shí)和總超時(shí)的設(shè)置是不相關(guān)的,這可以方便通信程序靈活地設(shè)置各種超時(shí)。

  如果所有寫超時(shí)參數(shù)均為0,那么就不使用寫超時(shí)。如果ReadIntervalTimeout為0,那么就不使用讀間隔超時(shí),如果ReadTotalTimeoutMultiplier和ReadTotalTimeoutConstant都為0,則不使用讀總超時(shí)。如果讀間隔超時(shí)被設(shè)置成MAXDWORD并且兩個(gè)讀總超時(shí)為0,那么在讀一次輸入緩沖區(qū)中的內(nèi)容后讀操作就立即完成,而不管是否讀入了要求的字符。

  在用重疊方式讀寫串行口時(shí),雖然ReadFile和WriteFile在完成操作以前就可能返回,但超時(shí)仍然是起作用的。在這種情況下,超時(shí)規(guī)定的是操作的完成時(shí)間,而不是ReadFile和WriteFile的返回時(shí)間。

清單12.5列出了一段簡(jiǎn)單的串行口初始化代碼。

 

清單12.5 打開并初始化串行口

HANDLE hCom;

DWORD dwError;

DCB dcb;

COMMTIMEOUTS TimeOuts;

hCom=CreateFile(“COM2”, // 文件名

GENERIC_READ | GENERIC_WRITE, // 允許讀和寫

0, // 獨(dú)占方式

NULL,

OPEN_EXISTING, //打開而不是創(chuàng)建

FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, // 重疊方式

NULL

);

if(hCom = = INVALID_HANDLE_VALUE)

{

dwError=GetLastError( );

. . . // 處理錯(cuò)誤

}

 

SetupComm( hCom, 1024, 1024 ) //緩沖區(qū)的大小為1024

 

TimeOuts. ReadIntervalTimeout=1000;

TimeOuts.ReadTotalTimeoutMultiplier=500;

TimeOuts.ReadTotalTimeoutConstant=5000;

TimeOuts.WriteTotalTimeoutMultiplier=500;

TimeOuts.WriteTotalTimeoutConstant=5000;

SetCommTimeouts(hCom, &TimeOuts); // 設(shè)置超時(shí)

 

GetCommState(hCom, &dcb);

dcb.BaudRate=2400; // 波特率為2400

dcb.ByteSize=8; // 每個(gè)字符有8位

dcb.Parity=NOPARITY; //無(wú)校驗(yàn)

dcb.StopBits=ONESTOPBIT; //一個(gè)停止位

SetCommState(hCom, &dcb);

 

12.3.3 重疊I/O

  在用ReadFile和WriteFile讀寫串行口時(shí),既可以同步執(zhí)行,也可以重疊(異步)執(zhí)行。在同步執(zhí)行時(shí),函數(shù)直到操作完成后才返回。這意味著在同步執(zhí)行時(shí)線程會(huì)被阻塞,從而導(dǎo)致效率下降。在重疊執(zhí)行時(shí),即使操作還未完成,調(diào)用的函數(shù)也會(huì)立即返回。費(fèi)時(shí)的I/O操作在后臺(tái)進(jìn)行,這樣線程就可以干別的事情。例如,線程可以在不同的句柄上同時(shí)執(zhí)行I/O操作,甚至可以在同一句柄上同時(shí)進(jìn)行讀寫操作。“重疊”一詞的含義就在于此。

  ReadFile函數(shù)只要在串行口輸入緩沖區(qū)中讀入指定數(shù)量的字符,就算完成操作。而WriteFile函數(shù)不但要把指定數(shù)量的字符拷入到輸出緩沖中,而且要等這些字符從串行口送出去后才算完成操作。

  ReadFile和WriteFile函數(shù)是否為執(zhí)行重疊操作是由CreateFile函數(shù)決定的。如果在調(diào)用CreateFile創(chuàng)建句柄時(shí)指定了FILE_FLAG_OVERLAPPED標(biāo)志,那么調(diào)用ReadFile和WriteFile對(duì)該句柄進(jìn)行的讀寫操作就是重疊的,如果未指定重疊標(biāo)志,則讀寫操作是同步的。

  函數(shù)ReadFile和WriteFile的參數(shù)和返回值很相似。這里僅列出ReadFile函數(shù)的聲明:

BOOL ReadFile(

HANDLE hFile, // 文件句柄

LPVOID lpBuffer, // 讀緩沖區(qū)

DWORD nNumberOfBytesToRead, // 要求讀入的字節(jié)數(shù)

LPDWORD lpNumberOfBytesRead, // 實(shí)際讀入的字節(jié)數(shù)

LPOVERLAPPED lpOverlapped // 指向一個(gè)OVERLAPPED結(jié)構(gòu)

); //若返回TRUE則表明操作成功

 

  需要注意的是如果該函數(shù)因?yàn)槌瑫r(shí)而返回,那么返回值是TRUE。參數(shù)lpOverlapped在重疊操作時(shí)應(yīng)該指向一個(gè)OVERLAPPED結(jié)構(gòu),如果該參數(shù)為NULL,那么函數(shù)將進(jìn)行同步操作,而不管句柄是否是由FILE_FLAG_OVERLAPPED標(biāo)志建立的。

  當(dāng)ReadFile和WriteFile返回FALSE時(shí),不一定就是操作失敗,線程應(yīng)該調(diào)用GetLastError函數(shù)分析返回的結(jié)果。例如,在重疊操作時(shí)如果操作還未完成函數(shù)就返回,那么函數(shù)就返回FALSE,而且GetLastError函數(shù)返回ERROR_IO_PENDING。

  在使用重疊I/O時(shí),線程需要?jiǎng)?chuàng)建OVERLAPPED結(jié)構(gòu)以供讀寫函數(shù)使用。OVERLAPPED結(jié)構(gòu)最重要的成員是hEvent,hEvent是一個(gè)事件對(duì)象句柄,線程應(yīng)該用CreateEvent函數(shù)為hEvent成員創(chuàng)建一個(gè)手工重置事件,hEvent成員將作為線程的同步對(duì)象使用。如果讀寫函數(shù)未完成操作就返回,就那么把hEvent成員設(shè)置成無(wú)信號(hào)的。操作完成后(包括超時(shí)),hEvent會(huì)變成有信號(hào)的。

  如果GetLastError函數(shù)返回ERROR_IO_PENDING,則說(shuō)明重疊操作還為完成,線程可以等待操作完成。有兩種等待辦法:一種辦法是用象WaitForSingleObject這樣的等待函數(shù)來(lái)等待OVERLAPPED結(jié)構(gòu)的hEvent成員,可以規(guī)定等待的時(shí)間,在等待函數(shù)返回后,調(diào)用GetOverlappedResult。另一種辦法是調(diào)用GetOverlappedResult函數(shù)等待,如果指定該函數(shù)的bWait參數(shù)為TRUE,那么該函數(shù)將等待OVERLAPPED結(jié)構(gòu)的hEvent 事件。GetOverlappedResult可以返回一個(gè)OVERLAPPED結(jié)構(gòu)來(lái)報(bào)告包括實(shí)際傳輸字節(jié)在內(nèi)的重疊操作結(jié)果。

  如果規(guī)定了讀/寫操作的超時(shí),那么當(dāng)超過(guò)規(guī)定時(shí)間后,hEvent成員會(huì)變成有信號(hào)的。因此,在超時(shí)發(fā)生后,WaitForSingleObject和GetOverlappedResult都會(huì)結(jié)束等待。WaitForSingleObject的dwMilliseconds參數(shù)會(huì)規(guī)定一個(gè)等待超時(shí),該函數(shù)實(shí)際等待的時(shí)間是兩個(gè)超時(shí)的最小值。注意GetOverlappedResult不能設(shè)置等待的時(shí)限,因此如果hEvent成員無(wú)信號(hào),則該函數(shù)將一直等待下去。

  在調(diào)用ReadFile和WriteFile之前,線程應(yīng)該調(diào)用ClearCommError函數(shù)清除錯(cuò)誤標(biāo)志。該函數(shù)負(fù)責(zé)報(bào)告指定的錯(cuò)誤和設(shè)備的當(dāng)前狀態(tài)。

  調(diào)用PurgeComm函數(shù)可以終止正在進(jìn)行的讀寫操作,該函數(shù)還會(huì)清除輸入或輸出緩沖區(qū)中的內(nèi)容。

 

12.3.4 通信事件

  在Windows 95/NT中,WM_COMMNOTIFY消息已經(jīng)取消,在串行口產(chǎn)生一個(gè)通信事件時(shí),程序并不會(huì)收到通知消息。線程需要調(diào)用WaitCommEvent函數(shù)來(lái)監(jiān)視發(fā)生在串行口中的各種事件,該函數(shù)的第二個(gè)參數(shù)返回一個(gè)事件屏蔽變量,用來(lái)指示事件的類型。線程可以用SetCommMask建立事件屏蔽以指定要監(jiān)視的事件,表12.4列出了可以監(jiān)視的事件。調(diào)用GetCommMask可以查詢串行口當(dāng)前的事件屏蔽。

 

表12.4 通信事件

事件屏蔽

含義

EV_BREAK

檢測(cè)到一個(gè)輸入中斷

EV_CTS

CTS信號(hào)發(fā)生變化

EV_DSR

DSR信號(hào)發(fā)生變化

EV_ERR

發(fā)生行狀態(tài)錯(cuò)誤

EV_RING

檢測(cè)到振鈴信號(hào)

EV_RLSD

RLSD(CD)信號(hào)發(fā)生變化

EV_RXCHAR

輸入緩沖區(qū)接收到新字符

EV_RXFLAG

輸入緩沖區(qū)收到事件字符

EV_TXEMPTY

發(fā)送緩沖區(qū)為空

  WaitCommEvent即可以同步使用,也可以重疊使用。如果串口是用FILE_FLAG_OVERLAPPED標(biāo)志打開的,那么WaitCommEvent就進(jìn)行重疊操作,此時(shí)該函數(shù)需要一個(gè)OVERLAPPED結(jié)構(gòu)。線程可以調(diào)用等待函數(shù)或GetOverlappedResult函數(shù)來(lái)等待重疊操作的完成。

  當(dāng)指定范圍內(nèi)的某一事件發(fā)生后,線程就結(jié)束等待并把該事件的屏蔽碼設(shè)置到事件屏蔽變量中。需要注意的是,WaitCommEvent只檢測(cè)調(diào)用該函數(shù)后發(fā)生的事件。例如,如果在調(diào)用WaitCommEvent前在輸入緩沖區(qū)中就有字符,則不會(huì)因?yàn)檫@些字符而產(chǎn)生EV_RXCHAR事件。

  如果檢測(cè)到輸入的硬件信號(hào)(如CTS、RTS和CD信號(hào)等)發(fā)生了變化,線程可以調(diào)用GetCommMaskStatus函數(shù)來(lái)查詢它們的狀態(tài)。而用EscapeCommFunction函數(shù)可以控制輸出的硬件信號(hào)(如DTR和RTS信號(hào))。
為了使讀者更好地掌握本章的概念,這里舉一個(gè)具體實(shí)例來(lái)說(shuō)明問(wèn)題。如圖12.1所示,例子程序名為Terminal,是一個(gè)簡(jiǎn)單的TTY終端仿真程序。讀者可以用該程序打開一個(gè)串行口,該程序會(huì)把用戶的鍵盤輸入發(fā)送給串行口,并把從串口接收到的字符顯示在視圖中。用戶通過(guò)選擇File->Connect命令來(lái)打開串行口,選擇File->Disconnect命令則關(guān)閉串行口。

T12_1.tif (174388 bytes)

圖12.1 Terminal終端仿真程序

  當(dāng)用戶選擇File->Settings...命令時(shí),會(huì)彈出一個(gè)Communication settings對(duì)話框,如圖12.2所示。該對(duì)話框主要用來(lái)設(shè)置串行口,包括端口、波特率、每字節(jié)位數(shù)、校驗(yàn)、停止位數(shù)和流控制。

T12_2.tif (92960 bytes)

圖12.2 Communication settings對(duì)話框

 

  通過(guò)該對(duì)話框也可以設(shè)置TTY終端仿真的屬性,如果選擇New Line(自動(dòng)換行),那么每當(dāng)從串口讀到回車符(‘\r’)時(shí),視圖中的正文就會(huì)換行,否則,只有在讀到換行符(‘\n’)時(shí)才會(huì)換行。如果選擇Local echo(本地回顯),那么發(fā)送的字符會(huì)在視圖中顯示出來(lái)。

  終端仿真程序的特點(diǎn)是數(shù)據(jù)的傳輸沒有規(guī)律。因?yàn)殒I盤輸入速度有限,所以發(fā)送的數(shù)據(jù)量較小,但接收的數(shù)據(jù)源是不確定的,所以有可能會(huì)有大量數(shù)據(jù)高速涌入的情況發(fā)生。根據(jù)Terminal的這些特性,我們?cè)诔绦蛑袆?chuàng)建了一個(gè)輔助工作者線程專門來(lái)監(jiān)視串行口的輸入。由于寫入串行口的數(shù)據(jù)量不大,不會(huì)太費(fèi)時(shí),所以在主線程中完成寫端口的任務(wù)是可以的,不必另外創(chuàng)建線程。

  現(xiàn)在就讓我們開始工作。請(qǐng)讀者按下面幾步進(jìn)行:

用AppWizard建立一個(gè)名為Terminal的MFC應(yīng)用程序。在MFC AppWizard對(duì)話框的第1步選擇Single document,在第4步去掉Docking toolbar的選擇,在第6步把CTerminalView的基類改為CEditView。

在Terminal工程的資源視圖中打開IDR_MAINFRAME菜單資源。去掉Edit菜單和View菜單,并去掉File菜單中除Exit以外的所有菜單項(xiàng)。然后在File菜單中加入三個(gè)菜單項(xiàng),如表12.5所示。

 

表12.5 新菜單項(xiàng)

標(biāo)題

ID

Settings...

ID_FILE_SETTINGS

Connect

ID_FILE_CONNECT

Disconnect

ID_FILE_DISCONNECT

 

 

用ClassWizard為CTerminalDoc類創(chuàng)建三個(gè)與上表菜單消息對(duì)應(yīng)的命令處理函數(shù),使用缺省的函數(shù)名。為ID_FILE_CONNECT和ID_FILE_DISCONNECT命令創(chuàng)建命令更新處理函數(shù)。另外,用ClassWizard為該類加入CanCloseFrame成員函數(shù)。

用ClassWizard為CTerminalView類創(chuàng)建OnChar函數(shù),該函數(shù)用來(lái)把用戶鍵入的字符向串行口輸出。

新建一個(gè)對(duì)話框模板資源,令其ID為IDD_COMSETTINGS。請(qǐng)按圖12.2和表12.6設(shè)計(jì)對(duì)話框模板。

 

表12.6 通信設(shè)置對(duì)話框中的主要控件

控件

ID

屬性設(shè)置

Base options組框

缺省

標(biāo)題為Base options

Port組合框

IDC_PORT

Drop List,不選Sort,初始列表為COM1、COM2、COM3、COM4

Baud rate組合框

IDC_BAUD

Drop List,不選Sort,初始列表為300、600、1200、2400、9600、14400、19200、38400、57600

Data bits組合框

IDC_DATABITS

Drop List,不選Sort,初列表為5、6、7、8

Parity組合框

IDC_PARITY

Drop List,不選Sort,初列表為None、Even、Odd

Stop bits組合框

IDC_STOPBITS

Drop List,不選Sort,初列表為1、1.5、2

Flow control組框

缺省

標(biāo)題為Flow control

None單選按鈕

IDC_FLOWCTRL

標(biāo)題為None,選擇Group屬性

RTS/CTS單選按鈕

缺省

標(biāo)題為RTS/CTS

XON/XOFF單選按鈕

缺省

標(biāo)題為XON/XOFF

TTY options組框

缺省

標(biāo)題為TTY options

New line檢查框

IDC_NEWLINE

標(biāo)題為New line

Local echo檢查框

IDC_ECHO

標(biāo)題為L(zhǎng)ocal echo

 

 

打開ClassWizard,為IDD_COMSETTINGS模板創(chuàng)建一個(gè)名為CSetupDlg的對(duì)話框類。為該類加入OnInitDialog成員函數(shù),并按表12.7加入數(shù)據(jù)成員。

 

表12.7 CSetupDlg類的數(shù)據(jù)成員

控件ID

變量名

數(shù)據(jù)類型

IDC_BAND

m_sBaud

CString

IDC_DATABITS

m_sDataBits

CString

IDC_ECHO

m_bEcho

BOOL

IDC_FLOWCTRL

m_nFlowCtrl

int

IDC_NEWLINE

m_bNewLine

BOOL

IDC_PARITY

m_nParity

int

IDC_PORT

m_sPort

CString

IDC_STOPBITS

m_nStopBits

int

 

 

按清單12.6、12.7和12.8修改程序。清單12.6列出了CTerminalDoc類的部分代碼,清單12.7是CTerminalView的部分代碼,清單12.8是CSetupDlg類的部分代碼。在本例中使用了WM_COMMNOTIFY消息。雖然在Win32中,WM_COMMNOTIFY消息已經(jīng)取消,系統(tǒng)自己不會(huì)產(chǎn)生該消息,但Visual C++對(duì)該消息的定義依然保留。考慮到使用習(xí)慣,Terminal程序輔助線程通過(guò)發(fā)送該消息來(lái)通知視圖有通信事件發(fā)生。

 

清單12.6 CTerminalDoc類的部分代碼

// TerminalDoc.h : interface of the CTerminalDoc class

//

/////////////////////////////////////////////////////////////////////////////

 

 

#define MAXBLOCK 2048

#define XON 0x11

#define XOFF 0x13

 

UINT CommProc(LPVOID pParam);

 

class CTerminalDoc : public CDocument

{

protected: // create from serialization only

CTerminalDoc();

DECLARE_DYNCREATE(CTerminalDoc)

 

// Attributes

public:

 

CWinThread* m_pThread; // 代表輔助線程

volatile BOOL m_bConnected;

volatile HWND m_hTermWnd;

volatile HANDLE m_hPostMsgEvent; // 用于WM_COMMNOTIFY消息的事件對(duì)象

OVERLAPPED m_osRead, m_osWrite; // 用于重疊讀/寫

 

volatile HANDLE m_hCom; // 串行口句柄

int m_nBaud;

int m_nDataBits;

BOOL m_bEcho;

int m_nFlowCtrl;

BOOL m_bNewLine;

int m_nParity;

CString m_sPort;

int m_nStopBits;

 

 

// Operations

public:

 

BOOL ConfigConnection();

BOOL OpenConnection();

void CloseConnection();

DWORD ReadComm(char *buf,DWORD dwLength);

DWORD WriteComm(char *buf,DWORD dwLength);

// Overrides

. . .

};

 

/////////////////////////////////////////////////////////////////////////////

// TerminalDoc.cpp : implementation of the CTerminalDoc class

//

 

#include "SetupDlg.h"

 

CTerminalDoc::CTerminalDoc()

{

// TODO: add one-time construction code here

 

m_bConnected=FALSE;

m_pThread=NULL;

 

m_nBaud = 9600;

m_nDataBits = 8;

m_bEcho = FALSE;

m_nFlowCtrl = 0;

m_bNewLine = FALSE;

m_nParity = 0;

m_sPort = "COM2";

m_nStopBits = 0;

}

 

CTerminalDoc::~CTerminalDoc()

{

 

if(m_bConnected)

CloseConnection();

// 刪除事件句柄

if(m_hPostMsgEvent)

CloseHandle(m_hPostMsgEvent);

if(m_osRead.hEvent)

CloseHandle(m_osRead.hEvent);

if(m_osWrite.hEvent)

CloseHandle(m_osWrite.hEvent);

}

 

BOOL CTerminalDoc::OnNewDocument()

{

if (!CDocument::OnNewDocument())

return FALSE;

((CEditView*)m_viewList.GetHead())->SetWindowText(NULL);

 

// TODO: add reinitialization code here

// (SDI documents will reuse this document)

 

 

// 為WM_COMMNOTIFY消息創(chuàng)建事件對(duì)象,手工重置,初始化為有信號(hào)的

if((m_hPostMsgEvent=CreateEvent(NULL, TRUE, TRUE, NULL))==NULL)

return FALSE;

memset(&m_osRead, 0, sizeof(OVERLAPPED));

memset(&m_osWrite, 0, sizeof(OVERLAPPED));

// 為重疊讀創(chuàng)建事件對(duì)象,手工重置,初始化為無(wú)信號(hào)的

if((m_osRead.hEvent=CreateEvent(NULL, TRUE, FALSE, NULL))==NULL)

return FALSE;

// 為重疊寫創(chuàng)建事件對(duì)象,手工重置,初始化為無(wú)信號(hào)的

if((m_osWrite.hEvent=CreateEvent(NULL, TRUE, FALSE, NULL))==NULL)

return FALSE;

return TRUE;

}

 

void CTerminalDoc::OnFileConnect()

{

// TODO: Add your command handler code here

 

if(!OpenConnection())

AfxMessageBox("Can't open connection");

}

 

void CTerminalDoc::OnFileDisconnect()

{

// TODO: Add your command handler code here

 

CloseConnection();

}

 

void CTerminalDoc::OnUpdateFileConnect(CCmdUI* pCmdUI)

{

// TODO: Add your command update UI handler code here

 

pCmdUI->Enable(!m_bConnected);

}

 

void CTerminalDoc::OnUpdateFileDisconnect(CCmdUI* pCmdUI)

{

// TODO: Add your command update UI handler code here

 

pCmdUI->Enable(m_bConnected);

}

 

 

// 打開并配置串行口,建立工作者線程

BOOL CTerminalDoc::OpenConnection()

{

COMMTIMEOUTS TimeOuts;

POSITION firstViewPos;

CView *pView;

 

firstViewPos=GetFirstViewPosition();

pView=GetNextView(firstViewPos);

m_hTermWnd=pView->GetSafeHwnd();

 

if(m_bConnected)

return FALSE;

m_hCom=CreateFile(m_sPort, GENERIC_READ | GENERIC_WRITE, 0, NULL,

OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,

NULL); // 重疊方式

if(m_hCom==INVALID_HANDLE_VALUE)

return FALSE;

SetupComm(m_hCom,MAXBLOCK,MAXBLOCK);

SetCommMask(m_hCom, EV_RXCHAR);

 

// 把間隔超時(shí)設(shè)為最大,把總超時(shí)設(shè)為0將導(dǎo)致ReadFile立即返回并完成操作

TimeOuts.ReadIntervalTimeout=MAXDWORD;

TimeOuts.ReadTotalTimeoutMultiplier=0;

TimeOuts.ReadTotalTimeoutConstant=0;

/* 設(shè)置寫超時(shí)以指定WriteComm成員函數(shù)中的

GetOverlappedResult函數(shù)的等待時(shí)間*/

TimeOuts.WriteTotalTimeoutMultiplier=50;

TimeOuts.WriteTotalTimeoutConstant=2000;

SetCommTimeouts(m_hCom, &TimeOuts);

if(ConfigConnection())

{

m_pThread=AfxBeginThread(CommProc, this, THREAD_PRIORITY_NORMAL,

0, CREATE_SUSPENDED, NULL); // 創(chuàng)建并掛起線程

if(m_pThread==NULL)

{

CloseHandle(m_hCom);

return FALSE;

}

else

{

m_bConnected=TRUE;

m_pThread->ResumeThread(); // 恢復(fù)線程運(yùn)行

}

}

else

{

CloseHandle(m_hCom);

return FALSE;

}

return TRUE;

}

 

 

// 結(jié)束工作者線程,關(guān)閉串行口

void CTerminalDoc::CloseConnection()

{

if(!m_bConnected) return;

m_bConnected=FALSE;

 

//結(jié)束CommProc線程中WaitSingleObject函數(shù)的等待

SetEvent(m_hPostMsgEvent);

 

//結(jié)束CommProc線程中WaitCommEvent的等待

SetCommMask(m_hCom, 0);

 

//等待輔助線程終止

WaitForSingleObject(m_pThread->m_hThread, INFINITE);

m_pThread=NULL;

CloseHandle(m_hCom);

}

 

// 讓用戶設(shè)置串行口

void CTerminalDoc::OnFileSettings()

{

// TODO: Add your command handler code here

 

CSetupDlg dlg;

CString str;

 

dlg.m_bConnected=m_bConnected;

dlg.m_sPort=m_sPort;

str.Format("%d",m_nBaud);

dlg.m_sBaud=str;

str.Format("%d",m_nDataBits);

dlg.m_sDataBits=str;

dlg.m_nParity=m_nParity;

dlg.m_nStopBits=m_nStopBits;

dlg.m_nFlowCtrl=m_nFlowCtrl;

dlg.m_bEcho=m_bEcho;

dlg.m_bNewLine=m_bNewLine;

if(dlg.DoModal()==IDOK)

{

m_sPort=dlg.m_sPort;

m_nBaud=atoi(dlg.m_sBaud);

m_nDataBits=atoi(dlg.m_sDataBits);

m_nParity=dlg.m_nParity;

m_nStopBits=dlg.m_nStopBits;

m_nFlowCtrl=dlg.m_nFlowCtrl;

m_bEcho=dlg.m_bEcho;

m_bNewLine=dlg.m_bNewLine;

if(m_bConnected)

if(!ConfigConnection())

AfxMessageBox("Can't realize the settings!");

}

}

 

 

// 配置串行口

BOOL CTerminalDoc::ConfigConnection()

{

DCB dcb;

 

if(!GetCommState(m_hCom, &dcb))

return FALSE;

dcb.fBinary=TRUE;

dcb.BaudRate=m_nBaud; // 波特率

dcb.ByteSize=m_nDataBits; // 每字節(jié)位數(shù)

dcb.fParity=TRUE;

switch(m_nParity) // 校驗(yàn)設(shè)置

{

case 0: dcb.Parity=NOPARITY;

break;

case 1: dcb.Parity=EVENPARITY;

break;

case 2: dcb.Parity=ODDPARITY;

break;

default:;

}

switch(m_nStopBits) // 停止位

{

case 0: dcb.StopBits=ONESTOPBIT;

break;

case 1: dcb.StopBits=ONE5STOPBITS;

break;

case 2: dcb.StopBits=TWOSTOPBITS;

break;

default:;

}

// 硬件流控制設(shè)置

dcb.fOutxCtsFlow=m_nFlowCtrl==1;

dcb.fRtsControl=m_nFlowCtrl==1?

RTS_CONTROL_HANDSHAKE:RTS_CONTROL_ENABLE;

// XON/XOFF流控制設(shè)置

dcb.fInX=dcb.fOutX=m_nFlowCtrl==2;

dcb.XonChar=XON;

dcb.XoffChar=XOFF;

dcb.XonLim=50;

dcb.XoffLim=50;

return SetCommState(m_hCom, &dcb);

}

 

 

// 從串行口輸入緩沖區(qū)中讀入指定數(shù)量的字符

DWORD CTerminalDoc::ReadComm(char *buf,DWORD dwLength)

{

DWORD length=0;

COMSTAT ComStat;

DWORD dwErrorFlags;

ClearCommError(m_hCom,&dwErrorFlags,&ComStat);

length=min(dwLength, ComStat.cbInQue);

ReadFile(m_hCom,buf,length,&length,&m_osRead);

return length;

 

}

 

// 將指定數(shù)量的字符從串行口輸出

DWORD CTerminalDoc::WriteComm(char *buf,DWORD dwLength)

{

BOOL fState;

DWORD length=dwLength;

COMSTAT ComStat;

DWORD dwErrorFlags;

ClearCommError(m_hCom,&dwErrorFlags,&ComStat);

fState=WriteFile(m_hCom,buf,length,&length,&m_osWrite);

if(!fState){

if(GetLastError()==ERROR_IO_PENDING)

{

GetOverlappedResult(m_hCom,&m_osWrite,&length,TRUE);// 等待

}

else

length=0;

}

return length;

}

 

// 工作者線程,負(fù)責(zé)監(jiān)視串行口

UINT CommProc(LPVOID pParam)

{

OVERLAPPED os;

DWORD dwMask, dwTrans;

COMSTAT ComStat;

DWORD dwErrorFlags;

CTerminalDoc *pDoc=(CTerminalDoc*)pParam;

 

memset(&os, 0, sizeof(OVERLAPPED));

os.hEvent=CreateEvent(NULL, TRUE, FALSE, NULL);

if(os.hEvent==NULL)

{

AfxMessageBox("Can't create event object!");

return (UINT)-1;

}

while(pDoc->m_bConnected)

{

ClearCommError(pDoc->m_hCom,&dwErrorFlags,&ComStat);

if(ComStat.cbInQue)

{

// 無(wú)限等待WM_COMMNOTIFY消息被處理完

WaitForSingleObject(pDoc->m_hPostMsgEvent, INFINITE);

ResetEvent(pDoc->m_hPostMsgEvent);

// 通知視圖

PostMessage(pDoc->m_hTermWnd, WM_COMMNOTIFY, EV_RXCHAR, 0);

continue;

}

dwMask=0;

if(!WaitCommEvent(pDoc->m_hCom, &dwMask, &os)) // 重疊操作

{

if(GetLastError()==ERROR_IO_PENDING)

// 無(wú)限等待重疊操作結(jié)果

GetOverlappedResult(pDoc->m_hCom, &os, &dwTrans, TRUE);

else

{

CloseHandle(os.hEvent);

return (UINT)-1;

}

}

}

CloseHandle(os.hEvent);

return 0;

}

 

BOOL CTerminalDoc::CanCloseFrame(CFrameWnd* pFrame)

{

// TODO: Add your specialized code here and/or call the base class

 

SetModifiedFlag(FALSE); // 將文檔的修改標(biāo)志設(shè)置成未修改

return CDocument::CanCloseFrame(pFrame);

}

  毫無(wú)疑問(wèn),CTerminalDoc類是研究重點(diǎn)。該類負(fù)責(zé)Terminal的通信任務(wù),主要包括設(shè)置通信參數(shù)、打開和關(guān)閉串行口、建立和終止輔助工作線程、用輔助線程監(jiān)視串行口等等。

  在CTerminalDoc類的頭文件中,有些變量是用volatile關(guān)鍵字聲明的。當(dāng)兩個(gè)線程都要用到某一個(gè)變量且該變量的值會(huì)被改變時(shí),應(yīng)該用volatile聲明,該關(guān)鍵字的作用是防止優(yōu)化編譯器把變量從內(nèi)存裝入CPU寄存器中。如果變量被裝入寄存器,那么兩個(gè)線程有可能一個(gè)使用內(nèi)存中的變量,一個(gè)使用寄存器中的變量,這會(huì)造成程序的錯(cuò)誤執(zhí)行。

  成員m_bConnected用來(lái)表明當(dāng)前是否存在一個(gè)通信連接。m_hTermWnd用來(lái)保存是視圖的窗口句柄。m_hPostMsgEvent事件對(duì)象用于WM_COMMNOTIFY消息的允許和禁止。m_pThread用來(lái)指向AfxBeginThread創(chuàng)建的CWinThread對(duì)象,以便對(duì)線程進(jìn)行控制。OVERLAPPED結(jié)構(gòu)m_osRead和m_osWrite用于串行口的重疊讀/寫,程序應(yīng)該為它們的hEvent成員創(chuàng)建事件句柄。

  CTerminalDoc類的構(gòu)造函數(shù)主要完成一些通信參數(shù)的初始化工作。OnNewDocument成員函數(shù)創(chuàng)建了三個(gè)事件對(duì)象,CTerminalDoc的析構(gòu)函數(shù)關(guān)閉串行口并刪除事件對(duì)象句柄。

  OnFileSettings是File->Settings...的命令處理函數(shù),該函數(shù)彈出一個(gè)CSetupDlg對(duì)話框來(lái)設(shè)置通信參數(shù)。實(shí)際的設(shè)置工作由ConfigConnection函數(shù)完成,在OpenConnection和OnFileSettings中都會(huì)調(diào)用該函數(shù)。

  OpenConnection負(fù)責(zé)打開串行口并建立輔助工作線程,當(dāng)用戶選擇了File->Connect命令時(shí),消息處理函數(shù)OnFileConnect將調(diào)用該函數(shù)。該函數(shù)調(diào)用CreateFile以重疊方式打開指定的串行口并把返回的句柄保存在m_hCom成員中。接著,函數(shù)對(duì)m_hCom通信設(shè)備進(jìn)行各種設(shè)置。需要注意的是對(duì)超時(shí)的設(shè)定,將讀間隔超時(shí)設(shè)置為MAXDWORD并使其它讀超時(shí)參數(shù)為0會(huì)導(dǎo)致ReadFile函數(shù)立即完成操作并返回,而不管讀入了多少字符。設(shè)置超時(shí)就規(guī)定了GetOverlappedResult函數(shù)的等待時(shí)間,因此有必要將寫超時(shí)設(shè)置成適當(dāng)?shù)闹?,這樣如果不能完成寫串口的任務(wù),GetOverlappedResult函數(shù)會(huì)在超過(guò)規(guī)定超時(shí)后結(jié)束等待并報(bào)告實(shí)際傳輸?shù)淖址麛?shù)。

  如果對(duì)m_hCom設(shè)置成功,則函數(shù)會(huì)建立一個(gè)輔助線程并暫時(shí)將其掛起。在最后,調(diào)用CWinThread:: ResumeThread使線程開始運(yùn)行。

  OpenConnection調(diào)用成功后,線程函數(shù)CommProc就開始工作。該函數(shù)的主體是一個(gè)while循環(huán),在該循環(huán)內(nèi),混合了兩種方法監(jiān)視串行口輸入的方法。先是調(diào)用ClearCommError函數(shù)查詢輸入緩沖區(qū)中是否有字符,如果有,就向視圖發(fā)送WM_COMMNOTIFY消息通知其接收字符。如果沒有,則調(diào)用WaitCommEvent函數(shù)監(jiān)視EV_RXCHAR通信事件,該函數(shù)執(zhí)行重疊操作,緊接著調(diào)用的GetOverlappedResult函數(shù)無(wú)限等待通信事件,如果EV_RXCHAR事件發(fā)生(串口收到字符并放入輸入緩沖區(qū)中),那么函數(shù)就結(jié)束等待。

  上述兩種方法的混合使用兼顧了線程的效率和可靠性。如果只用ClearCommError函數(shù),則輔助線程將不斷耗費(fèi)CPU時(shí)間來(lái)查詢,效率較低。如果只用WaitCommEvent來(lái)監(jiān)視,那么由于該函數(shù)對(duì)輸入緩沖區(qū)中已有的字符不會(huì)產(chǎn)生EV_RXCHAR事件,因此在通信速率較高時(shí),會(huì)造成數(shù)據(jù)的延誤和丟失。

  注意到輔助線程用m_PostMsgEvent事件對(duì)象來(lái)同步WM_COMMNOTIFY消息的發(fā)送。在發(fā)送消息之前,WaitForSingleObject函數(shù)無(wú)限等待m_PostMsgEvent對(duì)象,WM_COMMNOTIFY的消息處理函數(shù)CTerminalView::OnCommNotify在返回時(shí)會(huì)把該對(duì)象置為有信號(hào),因此,如果WaitForSingleObject函數(shù)返回,則說(shuō)明上一個(gè)WM_COMMNOTIFY消息已被處理完,這時(shí)才能發(fā)下一個(gè)消息,在發(fā)消息前還要調(diào)用ResetEvent把m_PostMsgEvent對(duì)象置為無(wú)信號(hào)的,以供下次使用。

  由于PostMessage函數(shù)在消息隊(duì)列中放入消息后會(huì)立即返回,所以如果不采取上述措施,那么輔助線程可能在主線程未處理之前重復(fù)發(fā)出WM_COMMNOTIFY消息,這會(huì)降低系統(tǒng)的效率。

  可能有讀者會(huì)問(wèn),為什么不用SendMessage?該函數(shù)在發(fā)送的消息被處理完畢后才返回,這樣不就不用考慮同步問(wèn)題了嗎?是的,本例中也可以使用SendMessage,但該函數(shù)會(huì)阻塞輔助線程的執(zhí)行直到消息處理完畢,這會(huì)降低效率。如果用PostMessage,那么在函數(shù)立即返回后線程還可以干別的事情,因此,考慮到效率問(wèn)題,這里使用了PostMessage而不是SendMessage。

  函數(shù)ReadComm和WriteComm分別用來(lái)從m_hCom通信設(shè)備中讀/寫指定數(shù)量的字符。ReadComm函數(shù)很簡(jiǎn)單,由于對(duì)讀超時(shí)的特殊設(shè)定,ReadFile函數(shù)會(huì)立即返回并完成操作,并在length變量中報(bào)告實(shí)際讀入的字符數(shù)。此時(shí),沒有必要調(diào)用等待函數(shù)或GetOverlappedResult。在WriteComm中,調(diào)用GerOverlappedResult來(lái)等待操作結(jié)果,直到超時(shí)發(fā)生。不管是否超時(shí),該函數(shù)在結(jié)束等待后都會(huì)報(bào)告實(shí)際的傳輸字符數(shù)。

  CloseConnection函數(shù)的主要任務(wù)是終止輔助線程并關(guān)閉m_hCom通信設(shè)備。為了終止線程,該函數(shù)設(shè)置了一系列信號(hào),以結(jié)束輔助線程中的等待和循環(huán),然后調(diào)用WaitForSingleObject等待線程結(jié)束。
 

清單12.7 CTerminalView類的部分代碼

// TerminalView.h : interface of the CTerminalView class

/////////////////////////////////////////////////////////////////////////////

 

class CTerminalView : public CEditView

{

. . .

afx_msg LRESULT OnCommNotify(WPARAM wParam, LPARAM lParam);

DECLARE_MESSAGE_MAP()

};

 

 

// TerminalView.cpp : implementation of the CTerminalView class

//

BEGIN_MESSAGE_MAP(CTerminalView, CEditView)

. . .

ON_MESSAGE(WM_COMMNOTIFY, OnCommNotify)

END_MESSAGE_MAP()

 

 

LRESULT CTerminalView::OnCommNotify(WPARAM wParam, LPARAM lParam)

{

char buf[MAXBLOCK/4];

CString str;

int nLength, nTextLength;

CTerminalDoc* pDoc=GetDocument();

CEdit& edit=GetEditCtrl();

if(!pDoc->m_bConnected ||

(wParam & EV_RXCHAR)!=EV_RXCHAR) // 是否是EV_RXCHAR事件?

{

SetEvent(pDoc->m_hPostMsgEvent); // 允許發(fā)送下一個(gè)WM_COMMNOTIFY消息

return 0L;

}

nLength=pDoc->ReadComm(buf,100);

if(nLength)

{

nTextLength=edit.GetWindowTextLength();

edit.SetSel(nTextLength,nTextLength); //移動(dòng)插入光標(biāo)到正文末尾

for(int i=0;i<nLength;i++)

{

switch(buf[i])

{

case '\r': // 回車

if(!pDoc->m_bNewLine)

break;

case '\n': // 換行

str+="\r\n";

break;

case '\b': // 退格

edit.SetSel(-1, 0);

edit.ReplaceSel(str);

nTextLength=edit.GetWindowTextLength();

edit.SetSel(nTextLength-1,nTextLength);

edit.ReplaceSel(""); //回退一個(gè)字符

str="";

break;

case '\a': // 振鈴

MessageBeep((UINT)-1);

break;

default :

str+=buf[i];

}

}

edit.SetSel(-1, 0);

edit.ReplaceSel(str); // 向編輯視圖中插入收到的字符

}

SetEvent(pDoc->m_hPostMsgEvent); // 允許發(fā)送下一個(gè)WM_COMMNOTIFY消息

return 0L;

}

 

void CTerminalView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)

{

// TODO: Add your message handler code here and/or call default

 

CTerminalDoc* pDoc=GetDocument();

char c=(char)nChar;

 

if(!pDoc->m_bConnected)return;

pDoc->WriteComm(&c, 1);

if(pDoc->m_bEcho)

CEditView::OnChar(nChar, nRepCnt, nFlags); // 本地回顯

}

  CTerminalView是CEditView的派生類,利用CEditView的編輯功能,可以大大簡(jiǎn)化程序的設(shè)計(jì)。

  OnChar函數(shù)對(duì)WM_CHAR消息進(jìn)行處理,它調(diào)用CTerminalDoc::WriteComm把用戶鍵入的字符從串行口輸出。如果設(shè)置了Local echo,那么就調(diào)用CEditView::OnChar把字符輸出到視圖中。

  OnCommNotify是WM_COMMNOTIFY消息的處理函數(shù)。該函數(shù)調(diào)用CTerminalDoc::ReadComm從串行口輸入緩沖區(qū)中讀入字符并把它們輸出到編輯視圖中。在輸出前,函數(shù)會(huì)對(duì)一些特殊字符進(jìn)行處理。如果讀者對(duì)控制編輯視圖的代碼不太明白,那么請(qǐng)參見6.1.4。在函數(shù)返回時(shí),要調(diào)用SetEvent把m_hPostMsgEvent置為有信號(hào)。

 

清單12.8 CSetupDlg類的部分代碼

// SetupDlg.h : header file

//

class CSetupDlg : public CDialog

{

 

. . .

public:

BOOL m_bConnected;

. . .

};

 

 

// SetupDlg.cpp : implementation file

//

 

BOOL CSetupDlg::OnInitDialog()

{

CDialog::OnInitDialog();

 

// TODO: Add extra initialization here

 

GetDlgItem(IDC_PORT)->EnableWindow(!m_bConnected);

return TRUE; // return TRUE unless you set the focus to a control

// EXCEPTION: OCX Property Pages should return FALSE

}

  CSetupDlg的主要任務(wù)是配置通信參數(shù)。在OnInitDialog函數(shù)中,要根據(jù)當(dāng)前是否連接來(lái)允許/禁止Port組合框。因?yàn)樵诖蜷_一個(gè)連接后,顯然不能隨便改變端口。

 

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多