Главная » Delphi » Прием и передача одного или нескольких байтов

0

В самом идеальном случае организации обмена, как мы уже говорили, программа посылает запрос, в ответ на который устройство отвечает фиксированным и заранее известным количеством байтов. Рассмотрим простейший вариант такой процедуры. Как вы увидите, если делать все, как надо, то он окажется не таким уж и "простейшим" (а кто сказал, что в DOS было все так уж и просто? сами убедитесь, что в некоторых отношениях здесь даже удобнее), но зато в дальнейшем он послужит основой для более сложных протоколов. К большому сожалению, вы, скорее всего, не сможете сразу проверить работу этой программы у себя. Хотя я и размещаю на диске, как обычно, полный проект, но изучать вам его придется теоретически — "часы", которые служат в качестве подключенного устройства для этого примера, я приложить не могу, и описание устройства, которое я исиользовш1 здесь в качестве "часов", выходит далеко за рамки этой книги. Впрочем, один выход у вас имеется (кроме, конечно, как соорудить подобное устройство самому) — "часы" можно эмулировать вторым компьютером, соединив у них СОМ-поргы через нуль-модемный кабель (см. приложение 4) и установив на него программу, аналогичную описанному далее "эмулятору терминала", измененную так, чтобы она воспринимала команды и посылала нужный набор байтов. Еще показательней было бы использовать один и тот же компьютер с двумя портами, которые и соединить между собой. Если вы внимательно изучите эту главу, то сделаете все это без труда и заодно приобретете бесценный опыт.

Итак, предположим, имеется некое устройство (часы), которое может передать нам через СОМ-порт текущее время. Для его получения мы обязаны отправить запрос— байт со значением $А2. После получения такого запроса часы немедленно выдадут нам время в виде последовательности 6 байтов (Sec: Min:Ilour: Date: Month: Year). Все эти значения выдаются в упакованном BCD-формате (обычный формат для электронных часов, см. приложение /). Обмен идет со скоростью 9600 бод, в соответствии с формулой "8n I" (см. приложение 4).

Создадим пробный проект под названием COMproba (расположен в папке Glava21\l). Иа форме расставим компоненты Label и staticText в количестве 6 штук каждый. Чтобы форма выглядела более эстетично, для всех staticText установим свойство Color в clNavy (темно-синий), а свойство Feat в полужирный 10 кегля с цветом clAqua (голубой). Заголовки static:ext очистим, а в Labej. напишем Число, Месяц, Год, Час, к>ш и Сек. Всем StaticText для удобства придадим соответствующие названия StaticTextMin, StaricTextSek и т. п. (причем в случае "sck" не сочтите меня неграмотным — для значения секунд в идентификаторах я часто использую обозначение Sek, а не Sec, т. к. последнее, например, есть идентификатор функции секанса). Ниже поставим еще один Label для отображения реально установленного порта и компонент ComboBox для выбора порта. В ComboBox в свойство items запишем построчно наименования портов (без пробелов!): comi, сомг. сомз и com4. Еще ниже поставим кнопку Buttonl Запрос (см. рис. 20.1). Разместим также в папке с проектом модуль Ariphm.

Собственно передача и прием данных через СОМ-порт неоднократно описаны во множестве публикаций и теоретически в них ничего сложного нет. Однако не все так просто. А что вы будете делать, если:

?      устройство отключено;

?      это не то устройство;

?      выбран несуществующий СОМ;

?      на выбранном порту находится модем.

В общем, в этой процедуре главное — обработка исключительных ситуации, причем тут это жизненно необходимая вещь, если вы сделаете что-то не так, то Delphi за вас ничего не поправит. И, как вы увидите, подводных камней тут достаточно много.

Один из них— определение имеющихся в системе последовательных портов и их характера. Сама Windows, конечно, точно знает, сколько у нее портов и каких— это отображается в Диспетчере устройств. Однако, как ни странно, адекватной процедуры для получения их перечня не имеется. 11о- следовательные порты должны быть перечислены в ключах реестра Windows 98:

HKEY_LOCAL_MAC! I1NE \На rdware \Dev.i. ceMap\Seria ICoiran HKEY_LOCAL_MACHINE\System\CurrGntControlSet\Services\Class\Ports HKEY_LOCAL_MACHINE\Enum

Для ХР аналогичные ключи выглядят так:

HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM

HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\serenum\Enum

HKEY_L0CAL_MACHINE\SYSTEM\ConcrolSet001\Services\sei:ial\Enum

Соединив сведения из [33, 36] и MSDN, я попробовал поковыряться в реестре своего компьютера (два обычных COM I и COM2, а также модем иа COM3) для обеих установленных у меня систем (Windows 98 и ХР), но ю- нял, что разобраться в логике устройства этих веток с налета не удастся. Например, в первой из перечисленных веток для Windows 98 перечислено почему-то целых пять идентичных портов от СОМ1 до СОМ5, в то время как для Windows ХР в том же ключе записано три порта, правда, без намеков на то, какой из них модем. Сопоставляя сведения из всех этих веток можно, наверно, разобраться, но ведь есть еще функция EnumPorts, которая, согласно описанию, должна возвращать описания всех портов в системе? Процедура ее использования с легкой руки автора [33] растиражирована по Интернету. Однако, как показывает простой эксперимент, корректно она работает только в Windows 98. А вот в Windows ХР она возвращает полную чушь:

С0М1:     Локальный порт

COM2:     Локальный порт COM3: Локальный порт COM4: Локальный порт

Откуда она берет все эти данные, ума не приложу, но факт, что использовать эту функцию в Windows ХР нельзя даже для того, чтобы определить количество реально действующих LPT-портов (как прямо рекомендует MSDN), ибо она мне вернула их аж целых три штуки, что в природе до сих пор не случалось (я только один раз в жизни встретил компьютер, в котором было больше одного LPT-порта). У меня есть один LPT, никакого другого, даже виртуального типа PDF или PostScript, в системе совершенно точно нет.

Мне кажется, что наиболее надежным способом определять количество последовательных портов в системе будет все же простой перебор их с попыткой инициализации каждого. Как при этом отличить модем от простого последовательного порта, я покажу далее.

Итак, приступим. Для начала установим следующие переменные:

var

Forml: TForml;

hCCW:hFile=0;

pDCB:TDCB;

comt ime:TCOMMTIMEOUTS;

xb:byte;

xrudword;

ab: array(1..6] of byte; st,stcom:string; ttinve, told:TDateTime;

Начнем мы с самого главного — с отдельной процедуры инициализации порта, которую назовем iniCOM. Вот ее текст:

procedure IniCOM; var i:integer; begin

/инициализация COM – номер в строке stcoml hCOM:=CreateFile(Pchar(stcom), GENERIC_READ+GENERIC_WRITE,0,nil,OPEN_EXISTING,0,0); if (hCom = X NVALID_HANDLE_VALUE) then begin

st:=stcam+’ не найден';

Applicat ion.МеъяадеВох(Pchar (st), ‘Errci.1 ,M3_0K); exit; end;

if GetCommState{hCOM,pDCB)

then st: -stcom+’ : baud-9600 parity-N data^B stop^l';

if BuildCornmDCB(Pchar (st) ,рПСВ) then SetCcmmState (hCOM,рПСВ)

else

begin

st:=stcoin+ ‘ занят нпи заданы неверные параметры'; ApplicatLon.Messag^Box(Pchar(st),’Error’,MB_OK); exi t; end;

GetConimTirrieouts (hCom, cointime); {устанавливаем задержки:)

comtime.Wri teTotalTimeoul’Mul с ip.) ier:-! ;

cornt ime. W r i t «Tota IT imeou tCoris t an t: -10 ;

comtime. ReadlritervalTimeout :=10;

comf ime.ReadTctalTirneoutMu] t ip1, ifir:« i;

comtime.ReadTotalTiir.eoutConstani: :->2000; {ждем чтения 2 сек) SetCommTimeouts(л Com,comtime);

ab [ 11 : =ord (‘ A’) ; {будем посылать ншщпалпзатао модема) ab[2J:=ord(‘Т’); ab(3]:=13;{СВj ab(4|:=10;{LFj

WrileFile (hCOM, ab, 4, >:n, nil) ;

if ReadFile(hC0M,ab,10,xn,nil) then {ответ модема 10 знаков) begin st: = ";

for i:-1 to 10 do st:=st+chr(ab[i]);

if pos(‘OK’,stK>0 then

begin

st:=stcom+’ эанят модемом';

Application.MessageBox(Pchar(st),’Error’,MB_CK);

CloseHandle(hCCM);

hCQM:=0;

Forml. Label’/.Caption: =’ COM? ‘ ; exit; end; end;

Forml.Label7.Caption:=stcomr’ 9600′; end;

К началу выполнения этой процедуры у нас в строке stcom должно содержаться название порта, например, сом1. Сначала текст тут мало отличается от того, что описано во всех стандартных рекомендациях по программированию порта. Единственный момент, который несколько выходит за рамки стандарта— мы не устанавливаем поля структуры dcb напрямую, а используем функцию BuiidCommDCB. Я это делаю отчасти потому, что структура пев в Delphi транслируется из API не полностью (сравните ее описание в Windows.pas и в Win32.hlp), и хотя для данного случая, разумеется, все нужные поля имеются, но использование BuiidcoimvDCB все равно удобнее.

Заметки на полях

Любой преподаватель программирования, глядя на этот вопиющий случай использования глобальных переменных для передачи параметров (имеется в виду строка stcom), немедленно сделал бы мне замечание. И по правилам структурного программирования, как говорилось в главе 1, в данном случае, конечно, следовало бы завести локальную переменную — неизвестно, где и когда мы еще будем использовать описанную ранее процедуру. Но я уже предупреждал в главе 1, и повторю — традиционно я стараюсь локальных переменных избегать. Это, конечно, развращающее влияние ассемблера, где для организации локальных переменных требуется много комвнд push…pop, так загромождающих программу, что проще использовать ячейку памяти в качестве еще одной глобальной переменной. Все мое существо восстает против, когда я начинаю представлять себе, сколько лишних команд придется выполнить компьютеру только потому, что кому-то боязно забыть о своевременной установке некой переменной. Что, разумеется, есть рассуждение совершенно "ламер- ское": современные программы делают так много лишнего, что еще один уровень стека никакой роли сыграть не может, зато гарантий от ошибок больше.

После стандартных установок мы сразу делаем две вещи, о которых упоминают далеко не всегда. Во-первых, мы устанавливаем все возможные Timeout для разных вариантов приема и передачи. В параметрах, которые заканчиваются на Multiplier, можно для простоты всегда ставить I (если больше, то процедуры чтения/записи будут отслеживать еще и скорость поступления байтов, что нам не надо). А остальные делают следующее: если задержка посылки через порт больше, чем WriteTotaiTimeoutConstant (в миллисекундах), то будет прервана передача, а при задержке между поступающими байтами больше, чем ReadintervaiTimeout, и при задержке всей процедуры чтения (в данном случае — самый главный параметр) больше, чем ReadTotaiTimeoutconstant, будет прерван прием. Последний параметр мы установили равным 2 с. При выборе этих параметров следует иметь в виду, что один байт при скорости 9600 передается/принимается примерно за I мс. Если эти параметры вообще не устанавливать (оставить их в значении 0, как по умолчанию), то при отсутствии принимаемых байтов процедура чтения через ReadFile просто зациклится и "повесит" всю программу.

Заметки на полях

Среди коммуникационных функций [31] есть некоторые, которые работают с состоянием "обрыаа" линии — могут как устанавливать линию TxD в это состояние (см. приложение 4), так и определять "обрыа" на линии RxD. Если честно, то я не вижу никакой необходимости в манипуляции этим состоянием. Оно могло бы пригодиться для определения того, что устройство не подключено к компьютеру (или внеэвпно было отключено), но какая нам разница — подключено оно или нет, если мы так или инвче не получвем с него ни одного байта? Намного надежнее устанавливать факт подключения по получению от устройства посылки либо автоматически, либо в ответ нв посланную (возможно даже — специальную для этой цели) команду.

Наконец, в самую последнюю очередь мы определяем, не является ли установленный нами порт модемом. Так как мы рассматриваем "чистый" RS-232, то для нас модемный порт все равно как бы занят. Если же вы соберетесь программировать сам модем, то, во-первых, в Windows это лучше делать не напрямую через UART, а через TAPI ("телефонные" API), а во-вторых, и в этом случае процедура его определения вам пригодится. Определяем модем мы очень просто: посылаем в выбранный порт символьный код инициализации, который одинаков для всех модемов: at<crxlf> (65 84 13 10). В ответ мы от модема должны получить строку at<cr><lf><cr><lf>ok<cr><lf> (65 84 13 10 13 10 79 75 13 10), но все такие подробности нам не требуются, подозреваю, что строка для разных модемов может немного отличаться, но в любом случае в ней должны содержаться символы ок (если модем, конечно, свободен — с занятым модемом я предоставляю читателю разобраться самостоятельно). В последнем операторе, если порт инициализировался нормально, выводим в Label номер порта и скорость.

Эту процедуру мы выполним сразу при запуске (для СОМ1). Заодно напишем процедуру закрытия порта (ведь порт все время занят, пока программа запущена):

prooedure TForml.FormCreate(Sender: TObject); begin

Iинициализация CQM1 при запускеI stcom:=’COMl'; IniCOM; end;

prooedure TForml.FormDestroy(Sender: TObject); begin /уничтожаем COM)

CloseHandle(hCOM); end;

Сразу же напишем и процедуру установки нового порта при обращении к

ComboBox:

prooedure TForml.ComboBoxlSelect(Sender: TObject); begin

CloseHandle(hCOM); Iзакрываем старый COM}

stcom:=ComboBoxl.Text; {устанавливаем порт ССМ1,2,3,4)

IniCOM;

end;

Заметки на полях

Если у читателя возникнет такая необходимость, то он может при запуске программы заполнить список в ComboBox именвми реально действующих СОМ- портов, выполнив примерно вот такой цикл их опроса (здесь мы перебираем порты от СОМ1 до СОМ8):

for i:=1 to 8 do begin

stcom:=’COM’+IntToStr(i) ; hCOM:=CreateFile(Pchar(stcom),

GENERIC_READ+GENERIC_WRITE,0,nil, OPENJSXISTING, 0,0); if (hCom=INVALID_HANDLE_VALUE) then continue; ComboBox.Items.Add(stcom); CloseHandle(hCOM); end;

Hy и, наконец, главная процедура запрос/ответ по нажатию кнопки Buttoni:

procedure TForml.ButtonlClick(Sender: TObject); begin {запрос)

if (hCOM=0) or (hCOM=INVALID_HANDLE_VALUE) then exit;

{если порт еще не инициализирован – выход) PurgeComm(hCOM,PURGE_RXCLEAR); {очищаем буфер) xb:=$A2;

WriteFile (hCOM, xb, 1, xn,nil) ; told:=Time;

if ReadFile(hCOM,ab,6,xn,nil) then {читаем 6 байтов в массив ab) begin ttime:=Time;

if SecondsBetween(told,ttime)>0 then begin

Application.MessageBox(1 Устройство не обнаружено’,’Error’,MB_OK);

exit; end;

if xn<>6 then begin

Application.MessageBox(‘Неправильный формат данных’,’Error’,MB_OK);

exit; end;

StaticTextYear.Caption:-hexb(ab|6] ) ; StaticTextMonth.C’aptioni^hexbiabfSl) ; StaticTextDate.Capr.ion:=hexb(ab[4 1) ; St:aticTextKouc.Capcion:=he:<b(ab[3]) ; StaticTextMin.CapLion:-hexbiab[2) i ; S CaC i cText Se*. Capt i op. : -hexb (ab 11)) ; end else {не сработалоI begin

Application.MessageBox(‘COM сломался’,’Error’,MB_OK); exit; end; end;

Несколько моментов в этой процедуре требуют пояснений. Во-первых, нужна ли процедура очистки приемного буфера PurgeComm и вообще что это за буфер? Напомню, что в DOS никакого приемного буфера у UART не было (но крайней мере, если мы не открывали СОМ-порт, как файл), его приходилось организовывать самостоятельно. Но Windows (семейств и 9л- и NT) создают по умолчанию системный буфер, равный 128 байтам. Если мы его не очист им перед чтением, то можем попасть в дурацкую ситуацию, если, например, устройство не только выполняет посылку данных по команде, но и все время выдает что-то самостоятельно. То же самос будет и в случае, если посылка длиннее 6 байт— лишние байты будут считаны в следующем сеансе, а "хвост" в буфере еще больше увеличится. Поэтому команду очистки лучше на всякий случай добавлять пред запросом, тогда мы уверены, что получим именно то, что запрашивали.

И еще такой вопрос возникает— а много это или мало, 128 байт приемной) буфера? Скажем так — если мы не ведем прием сплошного потока данных (как. например, голосового потока через voiee-модсм) или просто не собираемся принимать больших массивов, то этого достаточно, главное вовремя данный буфер очищать, а последнее требуется при любом объеме буфера. Если мы при чтении произвольного потока очищаем буфер прямо сразу при поступлении очередного байта или их цепочки (как мы это будем делать позже), то величина его в принципе вообще значения не имеет— но на практике он все же требуется для страховки на случай "торможения" от самой Windows. Заведомо достаточной будет величина буфера примерно такая: сколько байт в секунду поступает при заданной скорости, т. е. для скорости 9600 это 1024 байта. Установить величину буфера для данного порта но умолчанию можно ручным редактированием секции [386Enh] файла systcm.ini, в которой нужно добавить строку (на примере СОМ 1):

Coml: ComlBuf ier-8192

Динамически буфер устанавливать также очень просто: через функцию setupComm, параметрами которой и являются величины выходного и входного буфера. Например, вот так можно установить указанное ранее значение 8192 (сохранив при этом для выходного буфера значение 128):

SetupComm(hCOM,8192,128);

Последний вопрос о буфере — а какое значение имеет выходной буфер? Отвечаем: если мы не посылаем в устройство сразу большой массив данных, то никакого, пусть гак и равным 128 байтам и остается.

Второй момент, который нужно прокомментировать: обнаружение устройства. Мы здесь, как видите, все делаем очень просто: читаем системное время до и после вызова функции Readme и выясняем—• если прошло более 1 секунды (a Timeout у нас задан в 2 секунды), то "устройство не обнаружено". Опыт показывает, что это самый надежный метод. Если же связь каким- то образом прервется посередине посылки, то мы получим меньше байтов, чем заказывали, и программа выдаст сообщение "Неправильный формат данных". Не забудьте, что надо добавить ссылку на модуль DateUtils.

Третий момент — предвкушаю недоумение внимательного читателя, который, без сомнения, обратил внимание на утверждение о том, что "значения выдаются в упакованном BCD-формате". И что, их не надо распаковывать? Нет, не надо — ведь мы с ними не производим никаких операций, а легко сообразить, что, будучи представлен в НЕХ-форме, упакованный BCD-формат даст нам отображение времени в привычном виде. Легко, кстати, на основе строковых преобразований перевести BCD-данные в обычные числа без всякой распаковки, достаточно применить к нему обратное преобразование в число:

var st:string;

xb:byte;

(xb=59k, в BCD-формате это булет просто 59, в десятичном вило 89) st:=hexb(xb) ; {St. =’59’)

xb: =StrToInt (st); I xb*=59 я десятичной форме)

Теперь уже можно производить с данными и всякие операции. Напоминаю, что вместо моих процедур из модуля Ariphm (см. приложение 1) в последних версиях Delphi можно использовать функцию intToiiex.

И. наконец, четвертый момент: что это за бредовое сообщение "СОМ сломался"? Все дело в том, что вернуть значение False процедура ReadFi Те при установленных нами в данном случае параметрах в функции crftat.eFi.ie просто не может. Ошибка могла бы возникнуть, например, если бы мы осуществляли проверку на четность. Еще более важный момент связан с возможным возвратом значения raise, если бы CreateFile вызывалась с флагом fil?_flag_overlapped и соответствующими дополнительными установками (определение события и т. д.). Механизм, связанный со структурой overlapped, направлен на то, чтобы освободить вызывающий поток и сам "файл" (т, е. порт в данном случае) на время ожидания операций чтения/записи, которые в принципе могут быть весьма длительны. При обычных операциях с СОМ-портом, когда данные передаются туда-сюда в час по чайной ложке, возиться с механизмом "оверлапинга" (overlapping) абсолютно ни к чему, т. к. большинство коммуникационных приложений основное время проводят впустую в ожидании поступления очередного байта и синхронное общение по принципу запрос/ответ тут ничуть не замедляет процесс. А вот когда мы точно не знаем, когда именно должен поступить очередной байт данных— другое дело, и позже мы как раз об этом и будем разговаривать. Здесь же именно поэтому я и охарактеризовал единственный случай, когда теоретически может быть возвращено значение False, как неисправность СОМ. А точнее не COM, a UART— если в процессе чтения порт просто "сгорит", то это обычно происходит с внешним преобразователем уровней, который UART не затрагивает, и мы получим в этом случае, скорее всею, сообщение "Устройство не обнаружено".

Результат работы программы COMproba при взаимодействии с моими "часами" см. на рис. 20.1.

Рис. 20.1. Результат работы профаммы COMproba

Источник: Ревнч Ю. В.  Нестандартные приемы программирования на Delphi. — СПб.: БХВ-Петербург, 2005. — 560 е.: ил.

По теме:

  • Комментарии