Komunikace pomocí sériového portu 1.díl (MFC a C++)

V dnešním článku si povíme něco o tom, jak propojit dva počítače pomocí sériového portu a jak potom mezi nimi přenášet data. První věc, co musíte udělat, je zhotovit si příslušný kabel a na něm spojit země (GND), spojit výstupy TxD s vstupy RxD, výstupy RTS spojit s vstupy CTS a výstupy DTR spojit se vstupy DSR (pokud nerozumíte, přečtěte si článek o ovládání přístrojů pomocí portu COM). Pokud tedy máte tento kabel, stačí už pouze propojit počítače a můžeme se vrhnout na samotné programování. V tomto příkladu si vytvoříme jednoduchou aplikaci typu chat, která bude mezi počítači přenášet krátké textové zprávy.

Zapouzdření

Jelikož chci, aby celý program byl přehledný rozhodl jsem se vytvořit třídu CComPort, která bude pomocí členských funkcí zprostředkovávat všechny operace s portem. Třída bude mít tuto podobu:

class CComPort  
{
public:
  BOOL ReadData(char* buffer,int size);		//Čtení dat z portu
  BOOL WriteData(char* buffer,int size);	//Zápis dat na port
  BOOL IsConnected();				//Je port připojen?
  BOOL GetComHandle(HANDLE* handle);		//Vrátí handle portu
  BOOL CloseCom();				//Zavře port
  BOOL OpenCom(int ComNumber,HWND parent);	//Otevře port
  CComPort();					//Konstruktor
  virtual ~CComPort();				//Destruktor

private:
  void Chyba(LPCTSTR text);			//Chybový výpis
  HWND m_Parent;				//Handle rodičovského okna
  HANDLE m_ComHandle;				//Handle portu
  OVERLAPPED m_OLRead,m_OLWrite;		//Struktury pro čtení a zápis
  BOOL m_conect;				//Určuje, zda je připojeno

  CCriticalSection m_cs;			//Proměnná pro synchronizaci vláken

};

Možná vás napadlo, proč se předává handle rodičovského okna, ale je to pouze proto, abychom mohli ve funkci Chyba zobrazit modální messagebox. Funkce chyba má tuto podobu:

void CComPort::Chyba(LPCTSTR text)
{
  if(m_Parent!=NULL)
    MessageBox(m_Parent,text,"Chyba",MB_ICONERROR);
  else
    MessageBox(NULL,text,"Chyba",MB_ICONERROR);
}

Otevření a nastavení portu

První věc, kterou musíme udělat, je otevřít si vybraný port a nastavit věci jako jsou přenosová rychlost, typ synchronizace, počet bitů atd.. Je jasné, že pokud bychom měli jeden port nastavený na určitou rychlost a druhý port zase na jinou, tak by to asi nefungovalo správně.

Otevření portu se provádí funkcí CreateFile, která má tyto parametry:

HANDLE CreateFile(
  LPCTSTR lpFileName,		//Jméno souboru - použijeme COM1, COM2 ...
  DWORD dwDesiredAccess,	//Typ přístupu (čtení, zápis)
  DWORD dwShareMode,		//Typ sdílení
  LPSECURITY_ATTRIBUTES lpSA,	//Bezpečnostní nastavení
  DWORD dwCreationDisp,		//Udává, jestli chceme soubor vytvořit
  DWORD dwFlagsAndAttrib,	//Další příznaky souboru
  HANDLE hTemplateFile		//Šablonový soubor (nepoužito)
);

Pokud tedy chceme otevřít port COM1, zavoláme funkci s těmito parametry:

m_ComHandle=CreateFile("COM1",GENERIC_READ | GENERIC_WRITE,0,NULL,
  OPEN_EXISTING,FILE_FLAG_OVERLAPPED,NULL);

Tímto jsme tedy otevřeli port, ze kterého budeme moci číst i do něj budeme moci zapisovat, a tyto dvě operace budou moci probíhat současně díky parametru (FILE_FLAG_OVERLAPPED).

Další funkce, kterou použijeme, bude SetCommState. Tato funkce má dva parametry, první určuje handle portu a druhý adresu struktury DCB, pomocí které nadefinujeme nastavení portu. Jelikož je tato struktura opravdu rozsáhlá, nebudu ji zde vypisovat, protože stejně ji najdete v MSDN.

Kompletní příklad

Celá funkce pro otevření portu je celkem rozsáhlá, takže si ji nejdříve pozorně pročtěte, potom ji popíši:

BOOL CComPort::OpenCom(int ComNumber,HWND parent)
{
  m_cs.Lock();	//Uzamčít kritickou sekci
  m_Parent=parent;	//Nastavit rodiče

  if(m_conect)	//Test zda už nejsme připojeni
  {
    Chyba("Už je připojeno!");
    m_cs.Unlock();
    return FALSE;
  }

  m_OLRead.hEvent=::CreateEvent(NULL,TRUE,FALSE,NULL);	//Vytvořit události
  m_OLWrite.hEvent=::CreateEvent(NULL,TRUE,FALSE,NULL);

  if((m_OLRead.hEvent==NULL) || (m_OLWrite.hEvent==NULL))	//Podařilo se?
  {
    Chyba("Nelze vytvořit objekty událostí!");
    m_cs.Unlock();
    return FALSE;
  }

  CString name;
  name.Format("COM%d",ComNumber);	//Vytvořit celé jméno portu
	
  m_ComHandle=CreateFile(name,GENERIC_READ | GENERIC_WRITE,0,NULL,
    OPEN_EXISTING,FILE_FLAG_OVERLAPPED,NULL);	//Otevřít port
  if(m_ComHandle==INVALID_HANDLE_VALUE)	//Kontrola otevření
  {
    Chyba("Nelze otevřít port!");
    m_cs.Unlock();
    return FALSE;
  }

  m_conect=TRUE;	//Nyní jsme připojeni

  if(!::SetCommMask(m_ComHandle,EV_RXCHAR))	//Maska událostí
  {
    Chyba("Nelze nastavit masku!");
    m_cs.Unlock();
    return FALSE;
  }

  if(!::SetupComm(m_ComHandle,4096,4096))	//Nastavit velikosti bufferů
  {
    Chyba("Nelze nastavit velikost bufferů!");
    m_cs.Unlock();
    return FALSE;
  }

  if(!PurgeComm(m_ComHandle,PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | 
    PURGE_RXCLEAR))		//Vyprázdnit buffery
  {
    Chyba("Nelze vyprázdnit buffery!");
    m_cs.Unlock();
    return FALSE;
  }

  COMMTIMEOUTS t;
  t.ReadIntervalTimeout=0;
  t.ReadTotalTimeoutConstant=0;
  t.ReadTotalTimeoutMultiplier=0;
  t.WriteTotalTimeoutConstant=0;
  t.WriteTotalTimeoutMultiplier=0;

  if(!SetCommTimeouts(m_ComHandle,&t))	//Nastavit časové prodlevy
  {
    Chyba("Nelze nastavit časové prodlevy!");
    m_cs.Unlock();
    return FALSE;
  }

  DCB c;
  c.BaudRate=CBR_115200;
  c.ByteSize=8;
  c.DCBlength=sizeof(c);
  c.EofChar=0;
  c.ErrorChar=0;
  c.EvtChar=0;
  c.fAbortOnError=FALSE;
  c.fBinary=TRUE;	
  c.fDsrSensitivity=FALSE;
  c.fDtrControl=DTR_CONTROL_DISABLE;
  c.fDummy2=0;
  c.fErrorChar=FALSE;
  c.fInX=FALSE;	
  c.fNull=FALSE;
  c.fOutX=FALSE;
  c.fOutxCtsFlow=TRUE;			
  c.fOutxDsrFlow=FALSE;			
  c.fParity=FALSE;
  c.fRtsControl=RTS_CONTROL_ENABLE;
  c.fTXContinueOnXoff=FALSE;
  c.Parity=NOPARITY;
  c.StopBits=ONESTOPBIT;
  c.wReserved=0;
  c.wReserved1=0;
  c.XoffChar=0;
  c.XoffLim=0;
  c.XonChar=0;
  c.XonLim=0;

  if(!SetCommState(m_ComHandle,&c))		//Nastavit konfiguraci
  {
    Chyba("Nelze nastavit konfiguraci portu!");
    m_cs.Unlock();
    return FALSE;
  }

  m_cs.Unlock();

  return TRUE;
}

Jak vidíte, tato funkce je opravdu rozsáhlá, takže si podrobně popíšeme, co jednotlivé části provádějí. Poté co otestujeme, jestli už port není připojen, je vytvoření objektů událostí, které se používají při čtení a při zápisu na port. Tyto události umožňují dohromady číst i zapisovat, takže nemusíme čekat, až se jedna akce dokončí, abychom mohli vykonat jinou. Potom si vytvoříme z čísla portu celé jméno portu (např z 1 se vytvoří COM1) a pokusíme se port otevřít. Dále nastavíme masku událostí. My jsme nastavili příznak EV_RXCHAR, to znamená, že chceme být informování vždy, když na port přijdou nějaká data. Dále si nastavíme dostatečnou velikost vstupního a výstupního bufferu a tyto buffery vyprázdníme. Potom si nastavíme strukturu DCB. Jako první je rychlost; ta, co je v tomto kódu je jedna z nejvyšších, takže pokud by později port zlobil, doporučuji rychlost snížit. V této struktuře se mj. nastavuje synchronizace portu pomocí obvodů RTS/CTS - tato vlastnost musí být na obou PC nastavena stejně. Úplně nakonec se potom vrátí TRUE na znamení, že funkce proběhla úspěšně.

Uzavření portu

Po skončení práce s portem je slušností ho zavřít a to se provádí členskou funkcí CloseCom, která má tuto podobu:

BOOL CComPort::CloseCom()
{
  m_cs.Lock();

  if(!m_conect)
  {
    Chyba("Není připojeno!");
    m_cs.Unlock();
    return FALSE;
  }

  m_conect=FALSE;

  if(!::SetCommMask(m_ComHandle,0))	//Vymazat masku
  {
    Chyba("Nelze nastavit masku!");
    m_cs.Unlock();
    return FALSE;
  }

  if(!PurgeComm(m_ComHandle,PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | 
    PURGE_RXCLEAR))		//Vymazat buffery
  {
    Chyba("Nelze vyprázdnit buffery!");
    m_cs.Unlock();
    return FALSE;
  }

  if(!CloseHandle(m_ComHandle))	//Zavřít port
  {
    Chyba("Nelze uzavřít port!");
    m_cs.Unlock();
    return FALSE;
  }

  m_ComHandle=NULL;
  m_Parent=NULL;

  ::CloseHandle(m_OLRead.hEvent);	//Vymazat události
  ::CloseHandle(m_OLWrite.hEvent);

  m_cs.Unlock();

  return TRUE;
}

Myslím, že tato funkce žádný další popis nepotřebuje, je to pouze opak funkce pro otevření portu.

To je tedy pro dnešek vše. V dalším článku najdete další členské funkce třídy CComPort. Pokud máte nějaký problém s portem, tak neváhejte a napište mi na e-mail.

UPOZORNĚNÍ: Jedná se o archiv článků z let 2003 - 2005, uvedené technologie či postupy již mohou být neaktuální.