RAD, Rapid Application Development). Можно использовать и Visual Basic (в некоторых отношениях это даже более удобно, т. к. в силу происхождения он лучше интегрирован с Windows), однако мой личный опыт общения с этим продуктом отрицательный — значительную часть времени приходится тратить на то, чтобы бороться с внутренними проблемами самого VB. Язык Delphi (Object Pascal) проще и понятнее новичку. Для тех, кто привык к С, можно рекомендовать Borland C++ Builder — это почти то же самое, что и Delphi, даже с общей библиотекой компонентов, только язык другой.
Мы будем ориентироваться на Delphi 7 — последнюю версию для Win32. Программы работают без оговорок во всех версиях Windows (начиная с 98-й). Насколько программы для этой платформы работоспособны в Vista — на момент написания этих строк мне еще не удалось проверить, но, по слухам, все более-менее в порядке (правда, установление этого порядка все требует некоторых «плясок с бубном»), а на крайний случай там имеется режим совместимости с более ранними версиями, который работает, говорят, лучше, чем в Windows ХР. Потому, надеюсь, с изучением платформы. NET можно повременить.
В дальнейшем я буду предполагать, что читатель имеет некоторые навыки работы в Delphi, поскольку рассмотрение данного вопроса выходит за рамки этой книги. Остальным я рекомендую обратиться к [15] и [16], а также к моей книге [11].
Работа через функции Win32 API
Собственно передача и прием данных через COM-порт неоднократно описаны во множестве публикаций и теоретически в них ничего сложного нет. На практике, однако, могут возникнуть проблемы различного уровня сложности, особенно касающиеся непрерывного приема данных в реальном времени. Далее я описываю только проверенные способы работы (многие публикации в Интернете содержат ошибки, и к тому же не описывают ситуацию полностью).
Начнем мы с самого главного — с отдельной процедуры инициализации порта, которую назовем iniCOM. COM-порт представляется с точки зрения системы в виде файла, потому его сначала нужно открыть («создать файл»). Но этого недостаточно — необходимо проверить его работоспособность, и факт, не является ли запрошенный COM-порт модемом.
Объявим следующие переменные:
var
Form1:TForm1;
hCOM:hFile=0;
pDCB:TDCB;
comt ime:TCOMMTIMEOUTS;
xb:byte;
xn:dword;
ab:array[1..32768] of byte;
st,stcom:string;
ttime,told:TDateTime;
…
Размер буфера ab может быть произвольным в зависимости от количества ожидаемых данных (в данном случае он подогнан для чтения данных из используемой нами внешней flash-памяти). К началу выполнения процедуры iniCOM у нас в строке stcom должно содержаться название порта, например, «СОМ1». Задаваемые параметры: прием по схеме 8n1, скорость 9600. Текст процедуры приведен в листинге 18.1.
Листинг 18.1
procedure IniCOM;
var i:integer;
begin
{инициализация СОМ — номер в строке stcom}
hCOM:=CreateFile(Pchar(stcom),
GENERIC_READ+GENERIC_WRITE,0,nil,OPEN_EXISTING,0,0);
if (hCom = INVALID_HANDLE_VALUE) then
begin
st:=stcom+'не найден';
Application.MessageBox(Pchar(st),'Error',MB_OK);
exit;
end;
if GetCommState(hCOM,pDCB)
then st:=stcom+': baud=9600 parity=N data=8 stop=l';
if BuildCommDCB(Pchar(st), pDCB) then SetCommState(hCOM,pDCB)
else
begin
st:=stcom+' занят или заданы неверные параметры';
Application.MessageBox(Pchar(st),'Error',MB_OK);
exit;
end;
GetCommTimeouts(hCom,comtime); {устанавливаем задержки:}
comtime.WriteTotalTimeoutMultiplier:=1;
comtime.WriteTotalTimeoutConstant:=10;
comtime.ReadlntervalTimeout:=10;
comtime.ReadTotalTimeoutMultiplier:=1;
comtime.ReadTotalTimeoutConstant:=2 000; {ждем чтения 2 с}
SetCommTimeouts(hCom,comtime);
ab[1]:=ord('A'); {будем посылать инициализацию модема}
ab[2]:=ord('T');
ab[3]:=13 ;{CR}
ab[4]:=10 ;{LF}
WriteFile(hCOM,ab,4,xn,nil);
if ReadFile(hCOM,ab,10,xn,nil) then {ответ модема 10 знаков}
begin
st:=»;
for i:=1 to 10 do st:=st+chr(ab[i]);
if pos('OK',st)<>0 then
begin
st:=stcom+' занят модемом';
Application.MessageBox(Pchar(st),'Error',MB_OK);
CloseHandle(hCOM); hCOM:=0;
Forml.Label7.Caption:='COM?';
exit;
end;
end;
Form1.Labe17.Caption:=s tcom+' 9600 1;
end;
Сначала текст тут мало отличается от того, что описано во всех стандартных рекомендациях по программированию порта. Единственный момент, который несколько выходит за рамки стандарта: мы не устанавливаем поля структуры DCВ напрямую, а используем функцию BuildCommDCB. Я это делаю отчасти потому, что структура DCB в Delphi транслируется из API не полностью (сравните ее описание в Windows.pas и в Win32.hip), и хотя для данного случая, разумеется, все нужные поля имеются, но функция BuildCommDCB все равно удобнее.
После стандартных установок мы сразу выполняем два действия, о которых упоминают далеко не всегда. Во-первых, мы устанавливаем все возможные задержки (timeout) для разных вариантов приема и передачи. В параметрах, которые заканчиваются на «Multiplier» можно для простоты всегда ставить «1» (если больше, то процедуры чтения/записи будут отслеживать еще и скорость поступления байтов, что нам не требуется). А остальные из этих параметров делают следующее: если задержка посылки через порт больше, чем writeTotaiTimeoutConstant (в миллисекундах), то будет прервана передача, а при задержке между поступающими байтами больше, чем ReadIntervaiTimeout, и при задержке всей процедуры чтения (в данном случае — самый главный параметр) больше, чем ReadTotalTimeoutConstant, будет прерван прием. Последний параметр мы установили равным двум секундам. При выборе этих параметров следует иметь в виду, что один байт при скорости 9600 передается/принимается примерно за 1 мс. Если эти параметры вообще не устанавливать (оставить их в значении «0», как по умолчанию), то при отсутствии принимаемых байтов процедура чтения через ReadFile просто зациклится и «повесит» всю программу.
Во-вторых, мы определяем, не является ли установленный нами порт модемом. Так как мы рассматриваем «чистый» RS-232, то для нас модемный порт все равно как бы занят. Определяем модем мы очень просто: посылаем в выбранный порт символьный код инициализации, который одинаков для всех модемов: «AT
Эту процедуру мы выполним сразу при запуске (для СОМ1). Заодно напишем процедуру (листинг 18.2) закрытия порта (ведь порт все время занят, пока программа запущена).
Листинг 18.2
procedure TForm1.FormCreate(Sender:TObject);
begin
{инициализация COM1 при запуске}
stcom:='COM1';
IniCOM;
end;
procedure TForm1.FormDestroy(Sender:TObject);
begin {уничтожаем COM}
CloseHandle(hCOM);
end;
Кроме этого, конечно, следует создать меню для выбора порта по ходу работы с программой (аналогичное меню СОМ в моей программе СОМ2000, но мы не будем здесь на этом останавливаться, т. к. на основе приведенной процедуры его создать несложно.
Итак, порт открыт, инициализирован с нужными параметрами — что дальше? Далее все очень непросто, потому что мы здесь не можем, как в DOS, зациклить программу в ожидании поступившего байта или нажатия клавиши. Windows и сама представляет собой бесконечный цикл в ожидании сообщений (событий) — в точности такой же, как мы организовывали у себя в контроллере в ожидании прерываний. И вот этими сообщениями мы и должны ее снабдить.
Сначала разберем простейший способ обмена данными: когда мы точно знаем, что нам придет в ответ и в каком количестве. Например, посмотрим, как можно организовать процедуру приема значения часов в ответ на команду $А2 (описание того, как это работает в контроллере, см. главу 16). Расставим на форме следующие компоненты: кнопку Button1, а также Label и напротив него StaticText, и то и другое в количестве 6 штук. В компоненты Label запишем, соответственно, по порядку: «Часы», «Минуты», «Секунды», «Дата», «Месяц», «Год» (в таком же порядке их выдает наш контроллер). Листинг 18.3 иллюстрирует, как будет выглядеть сама процедура, когда по нажатию кнопки Button1 в МК выдается команда $А2, а потом принятые значения выводятся в Static Text (т. к. они в BCD-формате, то приходится переводить в НЕХ-представление).
Листинг 18.3