int21h

Komunikace mezi počítači

Po delší době Vám opět přináším pokračování našeho seriálu (na způsob "Jak si udělat hru"). Dostal jsem jeden postřeh, že se v mých článcích o grafice lidé trochu ztrácejí. Z toho si nic nedělejte, mám stejný problém :-). Mým cílem není napsat kompletní jednotky. Chci jen, abyste byli na základě toho, že pochopíte principy, schopni napsat si vlastní (nebo upravit ty stávající). Ve dvou dílech se budeme nyní věnovat komunikaci mezi dvěma a více počítači. A začneme tím, co dneska už pomalu mizí.

Paralelní a sériový kabel

Dříve hojně používané řešení, dneska se už docela prodražuje. Navíc moderní počítače mají jen jeden sériový port (což u PS/2 myší nevadí, ale můžete používat ještě něco jiného), což znesnadňuje hlavně programování pro více než 2 počítače komunikující současně. Dnes je výhodnější opravdu postavit síť (rychlejší, snadnější, na delší vzdálenosti i levnější). Protože dříve ještě neexistovaly obousměrné paralelní porty (LPT), hrálo se většinou jen přes sériový port (COM). Z toho důvodu vezmeme LPT jen stručně (velmi).

Mohlo by se zdát, že s použitím těchto prostředků mohou hrát jen dva hráči najednou. Není tomu tak. Pokud tomu Vaši hru přizpůsobíte, může hrát tolik hráčů, kolik dokážete propojit do kaskády počítačů (pokud budete chtít 4 hráče, musíte mít PC1-PC2-PC3-PC4, přičemž 2 a 3 musí mít 2 sériové porty!). Počítače s 2 kabely budou zajišťovat přenos dat na ostatní (pokud přijdou data z jednoho portu a nejsou určena pro něj, pošle je na druhý port). Takto se dá napsat i jednoduchá emulace sítě, např. sdílení souborů, atd. (ale to už vyžaduje, abyste si sami napsali vlastní protokoly, jak se budou Vaše počítače připojovat (vše je pouze o tom, jakou sekvenci bytů si pošlete, nic jiného v tom není - stále se jen posílají data), v jakém formátu si sdělí, která data jsou k dispozici (něco jako příkaz DIR), jak se budou přenášet), atd. Ale to budete potřebovat i pro 3 a více počítačů, kdy se bude posílat např. sekvence 32 bytů najednou, přičemž 1. byte bude určovat, komu jsou data směrována. Je možné kombinovat kabely paralení a sériové, ale u sériových máte snadnější obsluhu (poté, co si napíšete program).

Zase je tu omezení rychlosti. Paralení port dokáže rychlost až 115.2 kbaudů (víceméně 115.200 bitů za vteřinu), zatímco sériový port většinou pouze 57.600 (novější zvládají většinou už i několika násobek, třeba až přes 300kb/s). Může se to zdát rychlé, ale kb/s nejsou kB/s! Skutečný počet bytů (ne bitů) bude 8x nižší, tj. 7200B/s (7.03kB/s). Toto by ovšem měla být dnes minimální rychlost, nikoliv maximální. Naproti tomu i obyčejná stará ISA síťová karta s 10Mb Ethernetem zvládá přenášet data v průměrné (!) rychlosti přes 400 kB/s. A co teprve dnešní 100Mb nebo 1Gb karty! Ale ruku na srdce: budete dělat takovou hru? Stejně jako u sériového přenosu totiž síťový přenos (alespoň pod DOSem a také i ISA :-D) vyžaduje obsluhu přerušení, které bývá i dnes pomalé. Takže se jdeme podívat na to, co Vás čeká (nebudu se zabývat HW realizací, že potřebujete křížené kabely, tzv. lap-linky, atd.). Kdyby Vám tento článek nestačil, můžete se podívat na moje stránky (www.volny.cz/martinlux), kde v sekci Programování, a Periférie (úplně vpravo) najdete článek nejen o sériovém přenosu (který probíráme nyní), ale i o IPX sítích (které budeme brát příště), a také zvukových kartách 8 bitů a 16 bitů (které budeme brát jako poslední).

Paralelní kabel

Tento způsob se dnes skoro vůbec nepoužívá. Existuje sice přerušení BIOSu INT 17h, jenže to není v ATHelpu popsané lépe než pro odesílání znaků na tiskárnu (tedy o jejich příjmu si tímto způsobem budete muset najít informace sami). Takže budeme muset používat port přímo. Takže budeme potřebovat adresy portu. Existují standardní hodnoty, ale lepší je přečíst si je od BIOSu. Adresy pro první 4 paralení porty najdete po 2 bytech od adresy 0:408h (z této adresy tedy přečteme WORD pro LPT1, který by měl být standardně $378 a vede až do $f na posledním niblu (pokud používáte port i jako ECP nebo EPP, najdete ho ještě na adresách $778 až $77b, viz. Windows a Systém). Pokud se Vám do systému podařilo vložit ještě ISA kartu MDA, tak ta má standardně svůj vlastní port, který se nacpe na místo LPT1 (obsadí ale adresu standardně $3bc) a tedy Váš port bude až LPT2!

Standardní řešení spočívá v tom, že pokud chcete něco odeslat, jednoduše pošlete daný byte na danou bázovou adresu ($378). Problém je, jak ho na druhé straně přečíst. ATHelp uvádí, že čtením adresy tam najdete jen to, co jste poslali, ale u novějších portů a PC se Vám může poštěstit najít data, která Vám poslal druhý počítač. Používat $778 nemůžete, jelikož je to mimo Váš dosah (novější desky Vám však umožní přístup k těmto portům, ovšem pravděpodobně pouze v chráněném režimu). Na portu $379 a $37a můžete ovládat stavové vodiče kabelu, a tak si např. se vzdáleným systémem vyměňovat informace o synchronizaci (pošlete znak a čekáte, až Vám systém např. nastaví linku ACK, kterou mu vy potvrdíte např. PE :-), on si ACK shodí a bude čekat na další znak, který může přečíst, až Vy zase shodíte např. BUSY, atd.). Nebo můžete vyžadovat, aby Vám každý znak poslal zpět, nebo posílal nějaké znamení (např. na střídačku 0 a 255 byty). Kdybyste chtěli obsluhovat port pomocí přerušení, tak budete potřebovat 7, někdy i 5.

Sériový kabel

Sériový port nám už umožňuje standardně tři způsoby obsluhy (i když, pravda, ten první, pokud jej dovedete do důsledku s využitím přerušení, je to samé, co ten druhý). První, nejjednodušší na naprogramování, ale horší na obsluhu, je přímý zápis na port. Na adrese 0:$400 nám opět BIOS poskytuje v 8 bytech adresu až 4 COM portů. COM1 má většinou $3f8, COM2 pak $2f8 (a opět až do $xxf). COM3 a COM4 sdílí standardně stejné adresy jako port X-2, takže musíte rozlišovat, co Vám tam vlastně přišlo! Jednodušší je porty přenastavit (BIOS totiž inicializuje standardně jen první dva)! Dva porty totiž nesmí používat dvě stejné adresy. Posílat a příjímat data po celých bytech (!) můžete tak, že je prostě zapíšete na bázovou adresu daného portu:

procedure Send(Adresa : word; Data : byte); assembler;
asm
	mov	dx,adresa
	mov	al,data
	out	dx,al
end;


function Receive(Adresa : word) : byte; assembler;
asm
	mov	dx,adresa
	in	al,dx
end;

Posílat data můžete kdykoliv, ale jak poznáte, že Vám data přišla? K tomu slouží bit 0 na portu (registru) $3fd (platí pro COM1, pro COM2 si to přepočtěte odečtením $100), který se automaticky nuluje, pokud data přečtete:

function PrislaData : boolean; assembler;
asm
	mov	dx,$3fd
	in	al,dx
	and	al,1
end;

Jen pro informaci, testováním na AND 2 a pak SHR 1 dostanete informaci, že Vám přišel znak, ale není ho kam uložit, protože už v bufferu jeden je a Vy jste ho ještě nezpracovali. Znaky samozřejmě nemůžete střílet na port v libovolné rychlosti, musíte nejprve počkat, až bude znak odeslán. To se provede takto:

function MoznoPoslat : boolean; assembler;
asm
	mov	dx,$3fd
	in	al,dx
	and	al,32
	shr	al,5
end;

Až Váš znak odejde, můžete to zjistit pomocí AND AL,64 a SHR AL,6. Oba dva příznaky se resetují automaticky. Tímto způsobem si můžete posílat data mezi dvěma počítači. Zahájení přenosu můžete udělat tak, že např. pošlete druhému systému nějaký znak a počkáte, až Vám odpoví nějakým jiným znakem (mezitím můžete samozřejmě dělat něco jiného). Port pracuje na určité rychlosti. Tu můžete nastavit tak, že na bázovou adresu pošlete hodnotu dělitele (ovšem musíte nastavit určitý bit na 1 a po změně rychlosti ho zase vrátit). Hodnoty najdete v ATHelpu, takže např. pro 9600 tam pošlete 12, pro 19200 jen 6, pro 38400 hodnotu 3, pro 57600 číslo 2 a pro 115200 už 1. Pro vyšší rychlosti můžete zkusit buď 0 nebo $ffff. Rychlost by měla být na obou stranách stejná, jinak to nemusí fungovat:

procedure NastavRychnost(Delitel : word); assembler;
asm
	mov	dx,$3fb
	in	al,dx
	or	al,128
	out	dx,al
	mov	ax,delitel
	mov	dx,$3f8
	out	dx,ax	(* pošle se na dva porty 8 a 9 po sobě *)
	mov	dx,$3fb
	in	al,dx
	and	al,127
	out	dx,al
end;

V registru $3fb můžete také nastavovat paritu, počet bitů na byte (mělo by být 8) a počet stop bitů. Hodnoty pro správné nastavení si uložte do nějaké tabulky podle ATHelpu a vždy je pak vyberte jako parametry pro následující proceduru:

procedure NastavPort(Bitaz,ParitaAN,Parita : byte); assembler;
asm
	mov	dx,$3fb
	mov	al,bitaz	(* max. 3 bity *)
	mov	bl,paritaAN	(* 1 bit *)
	shl	bl,3
	or	al,bl
	mov	bl,parita	(* max. 2 bity *)
	shl	bl,4
	or	al,bl
	out	dx,al
end;

Standardní hodnoty by mohly být: Bitaz=011 (8 bitů a 1 stop bit), ParitaAN = 1, Parita = 00 (lichá). Jak zpracovat paritu nebudeme řešit (kdyby Vás to zajímalo, tak stačí povolit zpracování přerušení (viz. dále) i při chybě, a pak se dozvíte podle bitů, zda přišla data nebo zda došlo k chybě, mezi jinými třeba i parity). Pokud jste si jisti, že máte spolehlivý kabel, dejte ParitaAN na 0 a trochu tím zrychlíte přenos. Stejně tak, pokud posíláte pouze ASCII znaky bez horních 128 znaků, můžete dát přenášet jen 7 bitů místo 8 (nevhodné pro binární přenosy). Poslední bit tak bude nevyužit a přenos bude o 12.5% rychlejší. Obdobným způsobem můžete nastavení portu i číst (i jeho rychlost), takže můžete po skončení Vašeho programu obnovit původní hodnoty:

function ZjistiStav : byte; assembler;
asm
	mov	dx,$3fb
	in	al,dx
end;

A toto by nám mohlo stačit. Toto řešení umožňuje ale i využívání přerušení, což je vůbec nejideálnější. COM1 má standardně IRQ 4, COM2 má 3. COM3 a COM4 mohou mít ještě 5 nebo 7. Dva porty mohou mít stejné IRQ, protože se dá čtením jejich registrů zjistit, který obsahuje data a tedy nás volal. My si můžeme nastavit port tak, aby volal přerušení vždy, když přijdou data. To provedeme takto (funkce umožňuje toto přerušení i vypnout, což musíte udělat, když už s portem nebudete chtít pracovat nebo ukončíte svůj program!):

procedure Preruseni(Zapnout : boolean); assembler;
asm	(* zapisujte pouze, když nenastavujete rychlost portu! *)
	mov	dx,$3f9
	mov	al,zapnout
	out	dx,al
end;	(* je vhodné nejprve přečíst původní stav *)

Pokud má Váš sériový port čip UAT 16550 (dneska 99%), tak obsahuje i 16 bytovou frontu. Ta je vhodná v tom, že nemusíte volat přerušení po každém přijatém znaku, ale vždy po určité době, čímž urychlíte zpracování. Zde např. nastavíme, aby se přerušení volalo až po 14 přijatých znacích (tedy 13x vyhodíme volání INT a RET), tj. předáme funkci parametr s hodnotou 3:

procedure NastavFIFO(Znaku : byte); assembler;
asm	(* Znaku: 0=1B, 1=4B, 2=8B, 3=14B *)
	mov	dx,$3fa
	mov	al,1
	mov	bl,znaku
	shl	bl,6
	or	al,bl
	out	dx,al
end;

Opět předpokládám, že chápete vhodnost nejprve změněný registr přečíst a před skončením Vašeho programu ho uvedete do původního stavu, stejně jako všechny ostatní, které hodláte měnit. Protože obsluhujeme přerušení, které se volá pouze při příchodu dat, máme jistotu, že pokud jsme zavoláni, nějaká data přišla. Pokud ovšem stejné přerušení sdílí více portů, musíme si ověřit, zda jsou data určena nám (to zjistíme tak, že z naší bázové adresy přečteme jistý údaj - pokud nebude nastaven, byla data určena pro jinou adresu a tedy ne pro nás). Pokud ne, musíme volat původní obsluhu portu, aby si data převzal jiný program:

procedure NaseData : boolean; assembler;
asm	(* volejte jen v obsluze, jinak bit0=0 znamená, že nic nepřišlo *)
	mov	dx,$3fa
	in	al,dx
	and	al,2
	shr	al,1
end;

My toto zatím řešit nebudeme, ale danou funkci můžeme využívat i pro FIFO, zda ještě zbývají ve frontě nějaká data. Předtím, než aktivujeme přerušení, musíme ale připravit nějakou funkci, která nám bude tato data z přerušení číst (funkci můžeme napsat jako obsluhu přerušení, jako zde, nebo bez ní, ovšem budeme si muset sami uschovat registry, které budeme používat - je to výhodnější, protože uschováváte jen to, co skutečně potřebujete; ale také ji pak musíme ručně na konci ukončit přes IRET). Zároveň budeme ale potřebovat sami i nějakou frontu, kam budeme ukládat přišlá data, než si je vyzvedne hlavní program. Toto se bude velmi podobat obsluze klávesnice. Opět budeme mít informace o Startu a Konci, které na začátku nastavíme na 0 (my si je pojmenujeme jako Zapis (tj. konec fronty) a Cteni (začátek fronty)):

const	Max = 1024;
var	Buffer : array[0..Max-1] of byte;
	Zapis,Cteni : word;


procedure ObsluhaIRQ; interrupt; assembler;
asm
	mov	ax,SEG @DATA	(* celkem zbytečné *)
	mov	ds,ax
@cykl:	call	NaseData	(* data už nejsou nebo nejsou naše? *)
	or	al,al
	jz	@konec
	mov	bx,offset Buffer
	add	bx,zapis
	mov	dx,$3f8
	in	al,dx
	mov	[bx],al
	inc	zapis
	cmp	zapis,max	(* CMP VAR a CONST *)
	jnz	@cykl
	mov	zapis,0
	jmp	@cykl
@konec:	mov	al,$20
	out	$20,al
{	out	$a0,al}		(* jen pro IRQ > 7 *)
(* vhodné ještě zavolat původní obsluhu *)
end;

Jak vidíte, řešíme zde zpracování dat pomocí fronty FIFO, kdy čteme frontu tak dlouho, dokud není prázdná. Neřešíme ovšem to, že je už plná naše fronta o velikosti 1024 bytů. Náš program by ji měl tedy zpracovávat co nejrychleji (nebo ji udělejte větší). Přiřazení obsluhy se provede opět procedurou SetIntVec (a nezapomeňte si uschovat jeho původní hodnotu přes GetIntVec a při skončení programu svoji obsluhu opět odinstalovat). Aby bylo ovšem Vaše přerušení vyvoláno, musíte sdělit řadiči, aby jej povolil. To se provede následující funkcí (řeší pouze přerušení 0..7):

procedure IRQ_On(Cislo : byte); assembler;
asm
	mov	cl,cislo
	mov	dx,$21
	mov	ah,1
	shl	ah,cl
	not	ah
	cli
	in	al,dx
	and	al,ah
	out	dx,al
	sti
end;

Po skončení programu bych měli přerušení vrátit do původní hodnoty. Tj. zda bylo či nebylo zapnuté zjistíme následující funkcí (pokud bylo vypnuté, použijte hned další funkci):

function IRQ(Cislo : byte) : boolean; assembler;
asm
	mov	cl,cislo
	mov	dx,$21
	in	al,dx
	shr	al,cl
	not	al
	and	al,1
end;


procedure IRQ_Off(Cislo : byte); assembler;
asm
	mov	cl,cislo
	mov	dx,$21
	mov	ah,1
	cli
	in	al,dx
	or	al,ah
	out	dx,al
	sti
end;

Jak vidíte, funkce jsou si dost podobné. Dost se také doporučuje vypínat IRQ v době, kdy měníte jeho obsluhu. Teď už nám chybí jen funkce, která zjistí, zda přišla nějaká data (resp. zda jsou njaká v naší frontě) a další funkce, která je z fronty přečte (můžete si samozřejmě napsat jen jednu funkci, která bude vypadat např. takto):

function CtiData(var Data : byte;) : boolean;
var Stav : boolean;
begin	(* Vrací TRUE, pokud není fronta prázdná *)
 Stav := Zapis = Cteni;
 if Stav then
 begin
  Data := Buffer[Cteni];
  Inc(Cteni);
  if Cteni = Max then Cteni := 0;
 end;
 CtiData := Stav;
end;  

A to je vše. Pokud byste chtěli názornější příklad něčeho, co je vyzkoušené, podívejte na mé stránky do již zmiňované kapitoly. Tam najdete popsanou jednotku od Guenthera Klaminga (SERIAL.PAS), která je napsána kompletně v assembleru pro nejvyšší rychlost, obsahuje čtení stavu UART, využití FIFO, počítá s tím, že naše přerušení může sdílet ještě jiný port (můžete číst dva porty současně, pokud si jednotku upravíte tak, aby používala zdvojená data), obsahuje funkce pro vyprázdnění fronty či obsluhu stavových linek modemu. Není to ovšem nic, co byste si nenapsali s pomocí ATHelpu také (konec konců, toto píšu na zelené louce jen s pomocí ATH, takže to není zase až tak těžké pro pochopení (před 5 lety jsem ovšem to nechápal, ani když mi někdo dal hotový program :-D)).

Pokud je toto na Vás příliš složité a nepotřebujete žádné obsluhy přerušení, přenášení velkého množství dat (např. obsluhujete se svojí 286 :-) jen nějaké vláčky a vyhýbky), můžete využít ještě funkce, které Vám nabízí BIOS na INT 14h. Funkce jsou jednoduché. Nejprve musíte port inicializovat:

function Init(port,data : byte) : byte; assmbler;
asm
	mov	dx,port
	mov	al,data
	int	14h
end;  

Funkce vlatně nedělá nic jiného, než že daná data posílá na port, takže jsou podobně formátovaná. Do DX dáte 0 nebo 1 (COM1 nebo COM2), do AL dáte formátovaná data (viz. ATHelp). Např. pro maximální rychlost 9600 baudů (více z toho touto funkcí nedostanete) po 8 bitech bez parity s 1 stop bitem nastavte DATA na 11100011, tj. $e3. Funkce vrací stav linky (viz. ATHelp). Přijímat a vysílat znaky můžete pomocí těchto funkcí:

function Posli(Port,Data : byte) : byte; assembler;
asm
	mov	dx,port
	mov	al,data
	int	14h
end;


function Cti(Port) : word; assembler;
asm
	mov	dx,port
	int	14h
end;	(* pro získání znaku proveďte CTI(x) AND $f *)  

Obě funkce vracejí jako parametr kód chyby (funkce čtení ho vrací jako vyšší byte). Např. bit 0 určuje, zda jsou data připravena nebo ne; bit 2 = 1 je chyba parity, atd. Funkce nepodporují přerušení, takže musíte počkat, dokud si přijímač Vaše data nevyzvedne (např. tak, že Vám pošle vyslaný znak zpět nebo Vám nastaví nějaký vodič na lince, atd.). Funkcí je na INT 14h hromada. Některé z nich umožňují i rychlejší přenos (19200), podporu bufferu, řízení toku a korekci dat (XON/XOFF, aj.), změnu a čtení bitů linky, atd. Ale pokud to budete potřebovat, tak si prostě pročtěte AT Help. Upozorňuji, že pomocí sériového portu můžete ovládat i modem, protože to se provádí prostě tak, že na něj pošlete tzv. AT řetězce pro jeho inicializaci a další činnosti, takže můžete dva modemy propojit přímo telefonním kabelem a nastavit oba do spojení. A to je pro tento díl všechno (moc jsem se nepředřel, pravda).

2006-11-30 | Martin Lux
Reklamy:
„O chytré ženské je nouze. Konečně o chytré mužské zrovna tak.“ Jan Werich