Глава 18 Персональный компьютер и системы на МК
Обычный программист пишет код, чтобы отработать свою зарплату. Великий хакер готов заниматься программированием для собственного удовольствия, поэтому его искренне радует, когда кто-то готов за это заплатить.
Пол Грэхем
Если вы умеете паять и знаете, с какого конца отсчитываются выводы у микросхемы — в современной электронике это даже не полдела. В лучшем случае вы сможете повторить часть тех конструкций, что описаны в этой книге, публикуются в журнале «Радио» или размещаются на сайте shema.ru. Но ничего серьезного вам создать не удастся, пока вы не научитесь самостоятельно писать «верхние» программы, т. е. такие, с помощью которых персональный компьютер (ПК) «общается» с вашим устройством.
Каналом связи с ПК имеет смысл снабжать практически все конструкции — я уже отмечал, что огромная доля современной электроники модифицируется простой заменой микропрограммы («перешивкой»), это стало даже стандартным маркетинговым приемом. Но дело даже не в этом: возьмите довольно простые часы, которые мы разбирали в главе 14. Не правда ли, они бы ничего не потеряли, если бы были снабжены дополнительной возможностью установки времени через ПК, а не только кнопочками? Нажал экранную кнопку «Установить», и время из компьютера переписалось в часы. Секундное дело — вместо того, чтобы вручную «ковыряться» с кнопками.
Беда в том, что для подобных вещей почти невозможно приспособить какую-то готовую программу. «Почти», потому что далее я предложу вам программу, с помощью которой можно работать со всеми устройствами, упоминающимися в этой книге. Однако, во-первых, мне пришлось эту программу написать самому — за вас. Во-вторых, будучи универсальной, она, как водится, для каждого конкретного случая неудобна (лучше всего эта программа подходит для отладки, с какой целью и создавалась). В-третьих, я ее создавал «под себя», и она не может охватить всех возможных вариантов, например, она не работает с девятибитовыми посылками с проверкой четности или с синхронными протоколами. Во всех этих случаях вам придется писать программу самому, причем, как правило, каждый раз заново.
Правда, в этом деле есть два смягчающих обстоятельства. Во-первых, создание таких программ заметно проще, чем написание офисных или веб-приложений. Вам не потребуется знание SQL, Java или Ajax — достаточно общего понимания того, как работает Windows. С другой стороны, вам и не удастся обойтись стандартными компонентами Delphi или Visual Basic — придется привлекать нестандартные компоненты или использовать функции Windows API, обращение с которыми может вызвать приступы «истерического бешенства» похуже, чем любая Java. Но в целом создание типовых приложений для обслуживания электронных приборов — все же сравнительно несложная и к тому же достаточно консервативная область искусства программирования.
Начнем с того, что разберем технические аспекты взаимодействия ПК с микроконтроллерными устройствами.
Соединение ПК и МК
Я уже упоминал о том, что средой обмена данными между UART служит RS-232, где логика отрицательная (логическая единица есть низкий уровень напряжения, см. рис. 16.1), а сами уровни двуполярные, причем с большим допуском: низкий уровень представлен напряжением от -12 до -3 В, а высокий от +3 до +12 В. В этих пределах любая линия передачи по этому стандарту надежно работает.
Как мы говорили в главе 16, приемник RS-232 дополнительно снабжают схемой, которая фиксирует уровень не один раз за период действия бита, а трижды, при этом за окончательный результат принимается уровень двух одинаковых из трех полученных состояний линии (мажоритарная схема), таким образом удается избежать случайных помех. И хотя длина линии связи по стандарту не должна превышать 15 м, но на практике это могут быть много большие величины. Если скорость передачи не выбирать слишком высокой, то RS-232 может уверенно работать на расстояниях в десятки и даже сотни метров (автору этих строк удавалось без дополнительных ухищрений наладить обмен с компьютером на скорости 4800 но кабелю, правда, довольно толстому, длиной около полукилометра). В табл. 18.1 приведены ориентировочные эмпирические данные по длине неэкранированной линии связи для различных скоростей передачи.
Эти данные ни в коем случае не могут считаться официальными — слишком много влияющих факторов (уровень помех, толщина проводов, их взаимное расположение в кабеле, фактические уровни напряжения, выходное/входное сопротивление портов и т. п.). Для экранированного кабеля длину можно увеличить примерно в полтора-два раза.
Замечание
В правильно построенной экранированной линии экран не должен быть одним из токоведущих проводов, т. е. контакты GND соединяются отдельным проводом в кабеле, а экран либо соединяется с GND только на одной стороне — там, где имеется более качественное настоящее заземление, либо — в случае, если сигнальная «земля» GND является «плавающей» относительно «настоящей» земли — вообще только с заземлением.
При «несанкционированной» длине кабеля связи, особенно на больших скоростях передачи, следует применять меры по дополнительной проверке целостности данных: контроль четности, и/или программные способы (вычисление контрольных сумм и т. п.).
RS-232 также стандартизирует всем известные разъемы типа DB. Полный список всех контактов для двух стандартных разъемов типа DB (9- и 25-контактного) приведен в табл. 18.2. Нумерация контактов DB-разъема обычно приведена прямо на нем. Отметим, что 25-контактный разъем был создан первоначально в расчете на развитие стандарта, но позднее стало ясно, что развития не предвидится, и создали 9-контактный, которого достаточно для всех нужд, и в настоящее время практически только он и используется. Кабельная часть для RS-232 обычно представляет собой гнездовую часть разъема («маму»), а приборная — штыревую («папу»). Потому, к примеру, разъем LPT, который тоже есть 25-контактный DB, перепутать с СОМ-портом невозможно: для LPT на компьютере установлена гнездовая часть, а для СОМ-порта — штыревая.
Смысл дополнительных линий в том, что они могут применяться для организации различных синхронных протоколов обмена (протоколов с handshakes — «рукопожатием»). В «чистый» UART они не входят, в контроллере их организуют выводами обычных портов (но они входят в отдельные микросхемы UART для реализации полного протокола RS-232, а также в USART). Большинство устройств их не задействует. Однако любое устройство, использующее «рукопожатия», можно подключить к устройству, не имеющему этой функции (потеряв, конечно, возможности синхронизации), если соединить на каждой стороне между собой выводы RTS и CTS, а также выводы DSR, DCD и DTR.
Для нормальной совместной работы приемника и передатчика выводы RxD и TxD, естественно, нужно соединять накрест — TxD одного устройства с RxD второго, и наоборот (то же относится и к RTS, CTS и т. д.). Кабели RS-232, которые устроены именно таким образом, называются еще нуль-модемными (в отличие от простых удлинительных). Их стандартная конфигурация показана на рис. 18.1. В варианте рис. 18.1, в дополнительные выводы подключены именно так, как описано ранее, для возможности соединения устройств с «рукопожатием».
Рис. 18.1. Схемы нуль-модемных кабелей RS-232:
а, б — различные полные варианты, в — минимальный вариант
Выходные линии RTS и DTR иногда могут служить и для «незаконных» целей — питания устройств, подсоединенных к COM-порту. Именно так устроены, например, компьютерные мыши, работающие через СОМ. Разумеется, для работы такого устройства требуется установить эти линии в нужное состояние. Тех, кто интересуется этим вопросом, я отсылаю к моей книге [11], а здесь мы на этом не будем останавливаться, т. к. сейчас подобных устройств никто не проектирует. Мы займемся более актуальным вопросом: как обеспечить преобразование уровней UART в уровни RS-232 со стороны микроконтроллерного устройства?
Преобразователи уровней UART в уровни RS-232
Простейшая схема преобразователя уровня показана на рис. 18.2. В ней мы учли отмеченный ранее факт, что линия TxD со стороны компьютера большую часть времени пребывает в состоянии низкого уровня, и мы запасаем это напряжение на конденсаторе через диод, а потом расходуем его при передаче. Это несколько снижает входное сопротивление линии RxD устройства (и повышает выходное сопротивление TxD), но в принципе прекрасно работает, даже если байты идут туда-сюда сплошным потоком.
Рис. 18.2. Простейший вариант самодельного преобразователя уровней RS-232 — UART при соединении контроллера с компьютером
«Официальный» путь состоит в том, чтобы применять специальные микросхемы приемопередатчиков RS-232 (правильнее их было бы называть преобразователями уровня), это, например, МАХ202, МАХ232, ADM202 и подобные, которые содержат внутри преобразователь — инвертор напряжения, подобный тому, что мы применяли для питания измерителя в автономном режиме (см. главу 17). Вариант построения такой схемы показан на рис. 18.3. Выходные уровни вывода TxD здесь при интенсивном обмене составят не менее ±7 В.
Рис. 18.3. Вариант одноканального преобразователя уровней RS-232 — UART на микросхеме МАХ202
Рис. 18.3. Вариант одноканального преобразователя уровней RS-232 — UART на микросхеме МАХ202
Одной из этих схем следует дополнить наш измеритель температуры и давления, чтобы получить возможность соединения его с ПК. Если выбран разъем DB-9M (штыревая часть, как на самом ПК), спроектированный для установки на плату (т. е. типа DRB), то вы сможете соединить ваше устройство с компьютером только одним способом: с помощью симметричного нуль-модемного кабеля, который имеет на обоих концах гнездовые части. Удлинительный кабель RS-232, в котором линии передачи не перекрещиваются, имеет на одном из концов гнездовую, на другом — штыревую часть, и с его помощью подсоединить компьютер не удастся. Можно, конечно, спроектировать устройство в расчете на удлинительный кабель (тогда надо поставить разъем DRB-9F и поменять местами выводы RxD и TxD).
Применение таких приемопередатчиков не решает одной проблемы — гальванической развязки устройства с COM-портом. А это очень даже может понадобиться, поскольку на корпусе компьютера «висит» обычно вполне приличный потенциал.
Заметки на полях
По этой причине, кстати, нужно внешнее металлическое обрамление разъемов DB-9 соединять с «землей» и со стороны компьютера, и со стороны прибора — оно первое входит в соприкосновение и потенциалы выравниваются до того, как успевают соприкоснуться контакты разъема. Заставлять пользователей подключать устройства исключительно при выключенном компьютере — «прошлый век».
Автор этих строк однажды чуть не убил одно несчастное животное (до сих пор в кошмарах вспоминается), когда проектировал прибор для измерения внутричерепного давления у обезьян. Главная причина всей этой «катавасии» в отсутствии, разумеется, нормального заземления в наших постройках, но даже при его наличии развязка все равно не помешает.
Один из вариантов такой развязки, реализованный на относительно быстродействующем оптроне типа 6N139, показан на рис. 18.4. Верхняя часть схемы (оптрон D1) служит для передачи сигналов от контроллера к компьютеру. Сигнал TxD с контроллера должен иметь положительный уровень не ниже 4,5 В под нагрузкой, в противном случае следует увеличить номинал резистора R1.
Рис. 18.4. Вариант одноканального преобразователя уровней RS-232 — UART с гальванической развязкой
Приемная часть построена на оптроне D2. Ток через входной светодиод оптрона идет во время положительного уровня напряжения на линии TxD COM-порта, а диод VD3 защищает этот светодиод от обратного напряжения.
Здесь питание той части схемы, которая подает сигнал к компьютеру, обеспечивается преобразователем напряжения TMA0505D фирмы TRACO, имеющим гальваническую развязку между входом и выходом. Он обеспечивает преобразование положительного напряжения +5 В в два напряжения ±5 В.
Схема спроектирована в расчете на то, что оптоизолягор выполнен в форме удлинительного кабеля для COM-порта. Разъем Х2 типа DRB и остальные детали, кроме разъема XI, монтируют на макетной плате размерами 30x60 мм. С противоположной от разъема стороны распаивают трехжильный плоский кабель примерно 0,5 м длиной, закрепляют его на плате и соединяют с разъемом XI. Разъем XI может быть не только DB, а любого удобного типа, например, IDC. После проверки плату затягивают в отрезок термоусадочного кембрика подходящего диаметра. Разъем Х2 вместе с платой соединяется прямо с COM-портом, ответная часть разъема X1 располагается на плате устройства.
Для того чтобы вывести напряжение +5 В, пришлось тянуть отдельную линию (контакт 1 разъема X1). Поэтому, если вы собираетесь подключать ПК к компьютеру таким способом, вам придется использовать только этот кабель. В большинстве случаев вместо этого проще будет установить всю схему на плату устройства. Тогда, естественно, разъем XI следует исключить, но учтите, что у нас в схеме линии RxD и TxD уже перекрещиваются и соединять придется не нуль-модемным, а удлинительным кабелем (что и обеспечивает разъем DRB-9F). Если есть желание унифицировать соединение (нуль-модемные кабели более распространены, чем удлинительные), то следует взять разъем DRB-9M, а линии RxD и TxD в нем поменять местами.
Подключение через USB
Еще не так давно считалось, что протокол обмена данными по USB настолько сложен, что его реализовать под силу только далеко не рядовым специалистам. Но спрос рождает предложение. Вероятно, самое удобное на сегодняшний день решение для эмуляции COM-порта через USB предлагает английская (точнее шотландская) фирма Future Technology Devices Intl. Limited — FTDI. Читатель, несомненно, в курсе того, что подобные готовые «переходники» СОМ/USB имеются в продаже вместе с уже настроенным драйвером, но мы немного остановимся на том, как спроектировать такой интерфейс самому и как с ним обращаться.
Наилучший для практики способ построения последовательного порта через USB-интерфейс — на микросхеме FT232BM[16]. С возможностями ее и других USB-микросхем этой фирмы можно ознакомиться из хорошей подборки статей на сайте компании «ЭФО» [12]. Самое главное преимущество этой микросхемы — наличие драйверов для Windows (притом бесплатно распространяемых), которые обеспечат, в том числе, полную эмуляцию последовательного COM-порта со скоростями до 1 Мбит в секунду. На рис. 18.5 без лишних объяснений я привожу схему устройства сопряжения USB/RS-232, которая без изменений честно заимствована из фирменной документации FTDI [13]. В русифицированном варианте эта схема приведена в [14], где она также заимствована из размещенной на сайте екатеринбургской фирмы «Институт радиотехники» (http://wwwJnstitute-rt.ru/conrnion/statyi/convl/index.html) статьи А. Лысенко, Р. Назмутдинова, И. Малыгина в журнале «Радио» (2002, № 6 и 7).
Согласно уверениям производителя, если вы просто припаяете микросхему FT232BM без дополнительного программирования внешней EEPROM (микросхема 93С46 на схеме), в которой должны храниться идентификаторы устройства и прочая служебная информация, и даже вообще без нее, то устройство все равно будет работать, хотя могут возникнуть сложности с подключением других подобных устройств. Если же есть желание EEPROM запрограммировать, то специально этим заниматься не требуется, при установке драйвера типа D2XX (как указано далee) это можно сделать прямо на готовой плате через специальную фирменную утилиту EditEEPROM. Есть, по слухам, некоторые особенности с обеспечением скоростного режима этих микросхем, но вдаваться в подробности в рамках этой книги не имеет смысла.
Имейте в виду, что максимальную скорость обмена здесь ограничивает не интерфейс, а применяемые компоненты. Так, в схеме по рис. 18.5 преобразователь МАХ213 или ADM213 могут обеспечить 115 кбод, микросхема SP213 — 500 кбод, а 1 Мбод вы получите только при выборе МАХ3245. Правда, при этом встанет необходимость еще и запрограммировать как UART, так и виртуальный COM-порт на такие скорости. На самом деле схема, приведенная на рис. 18.5, целесообразна только для устройств с уже готовым интерфейсом RS-232. Если вы устройство целиком проектируете самостоятельно, то нет никакого смысла преобразовывать уровни UART в уровни RS-232 и обратно, дважды устанавливая приемопередатчик — в этом случае его из схемы на рис. 18.5 нужно исключить, а вместо него линии RxD и TxD подсоединить прямо к контроллеру. Остальные линии можно не подключать, вывод CTS# микросхемы FT232BM при этом следует заземлить.
Рис. 18.5. Рекомендуемая производителем схема преобразователя USB-RS-232 с использованием микросхемы FT232BM
Как я уже говорил, к устройствам FTDI прилагаются бесплатные и свободно распространяемые драйверы под все основные ОС, в том числе и под Windows семейств NT и 9х. Разновидностей таких драйверов две — это VCP и D2ХХ-драйверы.
VCP означает Virtual Communication Port, этот драйвер просто-напросто транслирует все стандартные функции Win32 API, которые мы будем использовать далее, в необходимые команды для USB, и через микросхему FT232BM или аналогичные ей, находящиеся в устройстве, опять преобразует их в битовые последовательности UART. При подключении устройства в системе возникает новый виртуальный COM-порт, и все описанные далее программы без переделок будут работать через него. Это касается и случая, когда у вас покупной «шнурок» USB/RS-232, и когда сами «изобретаете» интерфейс по схеме рис. 18.5. Мало того, в Windows ХР такой драйвер уже встроен, и там ничего вообще устанавливать не надо. Единственная разница между таким подключением и обычным в том, что виртуальный СОМ, в отличие от реального, не будет виден в системе, пока вы свое устройство не подключите к USB.
Один вопрос, впрочем, может возникнуть в случае применения VCP-драйверов, и касается он упомянутых скорости передачи — если уж USB, то хочется реализовать хоть малую долю возможностей этой шины. Между тем стандартно COM-порт может иметь скорость максимум в 256 кбод. Есть приемы установки для COM-порта нестандартной скорости обмена, но вопрос о том, насколько это корректно для данного случая, лично для меня, ввиду новизны ситуации, пока открыт (официальная документация не слишком уверенно утверждает, что допустимы только стандартные скорости). Второй — менее серьезный — недостаток драйверов VCP заключается в том, что вы не можете через него работать со встроенной в FT-устройство EEPROM, в которой записан идентификатор устройства и прочие необходимые для USB «прибамбасы».
В этих случаях следует выбрать 02ХХ-драйвер, работа с которым отличается, и похожа на работу с разобранным далее компонентом AfComPort. Программы, конечно, придется переписывать. С установкой Э2ХХ-драйвера связана одна совершенно «идиотская» трудность (так и хочется написать — «характерная для продуктов Microsoft») — как указывалось ранее, в ХР VCP-драйвер уже есть, и для установки Э2ХХ-драйвера нужно устраивать некоторые «пляски с бубном», цитирую из русского переложения фирменных рекомендаций по этому поводу (http://www.efo.ru/doc/Ftdi/Ftdi.pI71046):
«Немного сложнее обстоит дело в случае операционной системы Windows ХР, которая уже имеет в своем составе сертифицированные VCP-драйверы FTDI. При попытках присоединить к компьютеру новое USB-устройство со стандартными идентификаторами FTDI (например, любой DLP-модуль) система по умолчанию, не спрашивая пользователя, самостоятельно установит VCP-драйверы. Пользователю, желающему работать с D2XX-драйверами, необходимо в этот момент вспомнить, что очень полезно воспитывать в себе терпение и воспользоваться утилитой ftxprcvr.exe, входящей в состав дистрибутива В2ХХ-драйверов для Windows ХР. Утилита ftxprcvr.exe, используя установившиеся по умолчанию VCP-драйверы, перепрограммирует внешнюю EEPROM в присоединенном устройстве и задаст новые значения идентификаторов (VID=0403 и PID=6006). После этого необходимо повторить процедуру установки В2ХХ-драйверов с начала, т. е. отключить и снова присоединить устройство. Теперь система даст возможность пользователю указать директорию для установки D2XX-драйверов.»
Перепрограммировать EEPROM в устройстве USB на основе микросхем FT232BM через свою программу не обязательно, как уже упоминалось, для этого есть фирменная утилита EditEEPROM (http://www.efo.nl/doc/Ftdi/Ftdi.pl7798). Приводить примеры программ для работы с D2ХХ-драйвером я не вижу никакого смысла, т. к. фирменная документация уже не однажды переписана и переведена на русский, см., например, цитированную ранее статью или книгу [14]. На сайте екатеринбургской фирмы «Институт радиотехники» есть, в том числе, работающий пример проекта на Delphi (http://www.institute-rt.ru/ftdi/d2xxappl.zip). А вот сами драйверы с этого сайта скачивать не следует, на момент написания этих строк там лежит устаревший вариант, лучше обратиться к первоисточнику (http://www.ftdichip.com) или на упоминавшийся сайт компании «ЭФО» (http://www.efo.ru/doc/Ftdi/Ftdi.pl7784).
На этом мы с вопросом эмуляции СОМ через USB закончим и обратимся непосредственно к программам, которые взаимодействуют с СОМ-портом, неважно настоящим или эмулированным, но через стандартные функции API.
Программа СОМ2000
Обещанная универсальная программа для доступа к микроконтроллерным устройствам через COM-порт под названием СОМ2000 находится на моей домашней страничке по адресу: http://revich.lib.ru/comcom.zip. Устанавливать ничего не требуется, просто распакуйте содержащий два файла архив в любую папку. Сама программа содержится в файле com2000.exe. Файл помощи help2000.htm можно открыть как изнутри программы (через меню со знаком вопроса или клавишей <F1>), так и обычным способом в браузере, что удобнее. Собственно, в этом файле все рассказано, здесь я только немного подробнее опишу основные возможности программы.
На рис. 18.6 представлено основное и единственное окно программы СОМ2000. Когда-то подобные программы называли эмуляторами терминала (сейчас это название забыто вместе с самим понятием «терминалы»). Основная их функциональность заключается в постоянном ожидании приема данных по заданному порту с заданной скоростью (на рис. 18.6 установлен порт СОМ1 и скорость 9600, см. статусную строку внизу). Принятые данные побайтно выводятся на экран, причем отображение их может осуществляться тремя различными способами (в соответствии с выбором из показанного на рис. 18.6 меню в пункте Receive): в шестнадцатеричной форме, в десятичной и в виде текстового символа, соответствующего значению принятого байта. К последней возможности нужно относиться с осторожностью — Windows не «любит» встречать в текстовых компонентах несуществующие символы (вроде символа с номером 0) и программа может «рухнуть». Так что текстовый режим следует выбирать только, если вы ожидаете именно текст.
Рис. 18.6. Программа COM2000
На рис. 18.6 показан пример приема байтов в шестнадцатеричной форме в ответ на посланные команды (во втором случае, видном на экране полностью, это команда $Е2). Посылать команды можно выбором из меню Send Byte(s) также одной из трех возможностей: с клавиатуры (пункт Keyboard. <Ctrl>+<K>), непосредственным вводом значений (Value, <Ctrl>+<V>) или из файла (From file, <Ctrl>+<F>). Посылка с клавиатуры означает то же, что и прием в текстовой форме: при нажатии буквенной клавиши посылается ее код в виде байта с соответствующим значением. В Windows с ее путаницей в отношении виртуальных кодов клавиш эта возможность почти потеряла значение, но до сих пор встречаются устройства, в инструкции к которым команды записаны именно в виде символов (а не их номеров в таблице ASCII). Для совместимости с этими устройствами и сохранена такая возможность.
Если вы выбрали ввод с клавиатуры, то внизу в статусной строке надпись Keyboard Off сменится надписью Keyboard On. Не забудьте обратиться к пункту меню Keyboard или нажать <Ctrl>+<K> еще раз, чтобы выключить отсылку символов с клавиатуры после ввода, иначе они будут отсылаться и дальше при любом нажатии клавиш.
В обычном режиме используются две другие возможности, в основном вторая — посылка байтов с конкретным значением. При обращении к меню Send Byte(s)|Value (<Ctrl>+<V>) вы вызовете на экран однострочный редактор, в поле которого можно ввести нужное значение байтов, причем сразу много (до 32). Байты можно вводить в десятичном или шестнадцатеричном виде (с предваряющим знаком $) вперемешку, разделяя их пробелами. В выпадающем списке редактора запоминаются ранее отосланные вами строки (в том числе там есть несколько значений по умолчанию, для образца). После ввода значений нужно либо нажать на <Enter>, либо дважды щелкнуть мышью в окне редактора с введенной строкой значений. Обратите внимание, что проверка значения не производится, и при превышении диапазона посылаемый байт усекается до 8 разрядов, например значение 257 будет послано, как 257–256 = 1. Проверяется только корректность записи, например, при попытке послать ОА без предваряющего $, вам будет выдано сообщение об ошибке.
Аналогично осуществляется посылка из файла, которая хороша тогда, когда нужно послать много байтов, и вводить их в однострочный редактор неудобно. Тогда следует создать текстовый файл, в котором содержится строка со значениями, составленная по точно таким же правилам, что действуют для непосредственной посылки, и выбрать этот файл через меню Send Byte(s)|From file (<Ctrl>+<F>).
Заметим, что непрерывный прием можно отключить, если выбрать пункт меню Disable (он изменится на Enable и для включения его следует нажать еще раз). Это полезно, когда устройство (вроде GPS-навигатора) выдает данные непрерывно, и не хочется «забивать» экран ненужными данными. Только будьте внимательны: если режим непрерывного приема отключен, вы можете забыть об этом и подумать, что прибор внезапно перестал работать. Пункт меню Clear предназначен для очистки экрана.
Важная особенность программы — непрерывное ведение log-файла, который создается при первом запуске и далее только дополняется. В него записывается все, что отображается на экране, плюс при каждом запуске программы пишется еще текущая дата и время, log-файл полезен, если вы хотите сохранить принятые данные. Со временем он увеличивается до «неподъемных» размеров, и чтобы удалить ненужные данные, просто сотрите com.log, и он создастся заново при следующем запуске.
В программе можно, естественно, выбирать COM-порт (от СОМ1 до COM4) и скорость обмена (пункт меню СОМ). Кроме этого, можно менять оформление программы (цвет фона и надписей) через пункт меню Receive|Colors. Оформление и заданные режимы запоминаются к следующему сеансу.
Программа СОМ2000 очень удобна для отладки, о чем рассказывалось в разделе главы 16 «Отладка программ с помощью UART». Вы соединяете компьютер через один COM-порт с программатором, в свою очередь, соединенным со схемой, а через второй — с выходом UART схемы. Держа на экране открытыми одновременно три окна (редактор, программу для загрузки через программатор и СОМ2000), вы получаете возможность почти в реальном времени править программу и немедленно проверять ее работоспособность, временно расставляя контрольные вызовы функции out_com в нужных местах.
Работа с COM-портом в Delphi
В подавляющем большинстве случаев для создания даже таких относительно «навороченных» программ, как СОМ2000, можно обойтись стандартным Турбо Паскалем под DOS. Это было бы целесообразно, однако пользование DOS-программами на современном ПК крайне неудобно и порождает множество проблем совместимости (не говоря уж о многозадачности — конечно, работать в режиме немедленной проверки в DOS далеко не так удобно, как в Windows). Потому мы остановимся на Delphi, как одном из самых популярных в нашей стране средств быстрого создания приложений (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<CR><LF>» (65 84 13 10). В ответ мы от модема должны получить строку «АТ<CR><LFxCR><LF>OK<CR><LF>» (65 84 13 10 13 10 79 75 13 10), но все такие подробности нам не требуются, подозреваю, что строка для разных модемов может немного отличаться. Но в любом случае в ней должны содержаться символы «ОК», если модем, конечно, свободен (с занятым модемом я предоставляю читателю разобраться самостоятельно). В последнем операторе, если порт инициализировался нормально, выводим в Label номер порта и скорость.
Эту процедуру мы выполним сразу при запуске (для СОМ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
procedure TForm1.ButtonlClick(Sender: TObject);
begin {запрос}
if (hCQM=0) or (hCOM=INVALID_HANDLE_VALUE) then exit;
{если порт еще не инициализирован — выход}
PurgeComm(hCOM,PURGE_RXCLEAR); {очищаем буфер}
xb:=$А2;
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('Устройство не обнаружено','Error',MB_OK);
exit;
end;
if xn<>6 then
begin
Application.MessageBox('Неправильный формат данных', 1 Error',MB_OK);
exit;
end;
StaticText1.Caption:=IntToHex(ab[1],2)
StaticText2.Caption:=IntToHex(ab[2],2)
StaticText3.Caption:=IntToHex(ab[3],2)
StaticText4.Caption:=IntToHex(ab[4],2)
StaticText5.Caption:=IntToHex(ab[5],2)
StaticText6.Caption:=IntToHex(ab[6],2)
end else {если не сработало}
begin
Application.MessageBox('COM сломался','Error',MB_0K);
exit;
end;
end;
Результат будет выглядеть примерно так, как показано на рис. 18.7.
Рис. 18.7. Результат приема значений времени из часов-измерителя
Здесь процедура PurgeComm нужна для очистки приемного буфера, на случай, если там случайно задержались какие-то байты (те, кто работал с СОМ-портом в DOS, часто об этом забывают, т. к. там никаких буферов не было). Второй момент, который нужно прокомментировать, связан с возможным отсутствием нужного устройства на втором конце линии (или его неработоспособностью — включить забыли). Мы здесь, как видите, все делаем очень просто: читаем системное время до и после вызова функции ReadFile, и выясняем — если прошло более 1 с (a timeout у нас задан в 2 с), то «устройство не обнаружено». Опыт показывает, что это самый надежный метод. Если же связь каким-то образом прервется посередине посылки, то мы получим меньше байтов, чем заказывали, и программа выдаст сообщение «Неправильный формат данных». Не забудьте, что при работе с функциями времени в Delphi надо добавить ссылку на модуль DateUtils.
По той же схеме можно организовать взаимодействие с измерителем для всех наших команд, кроме команды чтения данных из flash-памяти. С ней сложности возникнут оттого, что мы точно не знаем, сколько данных нам придет, и когда пора заканчивать. В принципе можно обойтись тем же приемом с проверкой времени, т. к. данные поступают сплошным потоком, и когда поток этот прервется, то можно заканчивать. Организация процедуры тогда в некотором роде напоминает использование сторожевого таймера в МК: мы задаем в процедуре чтения максимальное количество принимаемых байтов за один прием (например, 1024 — столько придет примерно за секунду), и пока данные идут, в цикле принимаем их, «скидываем» временно в какой-нибудь массив (потом будем обрабатывать) и обнуляем разницу между told и ttime. Если она не обнулилась вовремя и превысила 1 с — прием окончен.
Это требует довольно тонкой организации процесса и вообще не универсально: а как быть, если мы не знаем точно момента прихода данных? Ну, например, пишем программу для взаимодействия с упоминавшимся GPS-навигатором, который выдает данные (и точно неизвестно, сколько именно) каждые несколько секунд?
Для этой цели можно организовать прием данных в отдельном потоке. Мы здесь не будем разбирать этот способ, потому что из-за «заумности» соответствующих функций API самое простое, что там приходится делать, — это создавать сам параллельный поток (по сути очень простое действие, которое почему-то вызывает дрожь у новичков, в значительной степени из-за неудобства этого процесса в Delphi). А вот собственно прием данных и их передача в основную программу там получается неоправданно сложной и запутанной процедурой. Потому я не буду на этом останавливаться (интересующихся подробностями отсылаю опять же к книге [11]), а сразу покажу, как можно создать подобную программу на основе дополнительного стороннего компонента, специально созданного для приема через COM-порт. Именно такой способ используется в программе СОМ2000.
Использование драйвера AsyncFree
Компонентов для работы с COM-портом довольно много, есть среди них платные и бесплатные. Мы будем использовать один из самых удачных и профессионально сделанных компонентов для СОМ-порта — свободно распространяемый AsyncFree некоего Петра Вониса (Petr Vones), судя по электронному адресу из Чехии. Компонент доступен бесплатно, с исходными кодами, и скачанный архив включает в себя, в том числе, и файлы dpk, что упрощает процедуру установки до предела — нужно просто щелкнуть мышью на том из dpk-файлов, который соответствует имеющейся у вас версии Delphi, и компонент установится самостоятельно, без утомительных процедур ручной инсталляции. Хотя к самому компоненту приложена ссылка на (отличный, кстати) сайт Delphree Open Source Initiative (http://delphree.clexpert.com), однако на нем я нашел только старую версию AsyncFree под Delphi 5, а скачивать последние версии лучше отсюда: http://sourceforge.net/project/ showfiles.php?group_id=20226.
Принцип работы компонента заключается как раз в создании параллельного потока, в котором байты принимаются по мере их поступления и накапливаются в буфере независимо от деятельности основной программы. Специально следить за приходом данных не требуется, поскольку все делается автоматически, нам остается только отловить принятые данные. Недостаток этого способа в том, что данные принимаются кучей в одной процедуре, и приходится отдельно разбираться, что же мы приняли в данный момент, и где заканчиваются одни данные и начинаются другие. Но при использовании параллельного потока это пришлось бы делать в любом случае, а тут все организовано за вас, «ручками» ничего делать не приходится.
После установки компонент будет находиться в палитре компонентов на вкладке AsyncFree. На самом деле там образуется много компонентов, но нам требуется только самый первый из них под названием AfComPort. Установим его на форму. В перечень переменных добавим:
…
FlagCOM:boolean=False;
FlagSend:integer=0;
tall:integer;
…
Переменные told и ttime нам здесь не понадобятся. На форму добавим Label7, в который будем выводить установленный порт и скорость передачи, и Timer. Проверьте, чтобы у таймера интервал составлял 1000 мс (так по умолчанию). Кроме этого, установим компонент comboBox (выпадающий список), у которого в свойстве Items сразу запишем строки для выбора СОМ-порта («СОМ1», «COM2» и т. п.). Аналогичный список можно создать для выбора скорости передачи, но если речь идет о конкретном приборе, то это необязательно (а вот СОМ выбирать, скорее всего, придется).
Начнем с того, что перепишем процедуру IniCoM, что иллюстрирует листинг 18.4 (как и в предыдущем случае, к моменту ее вызова в stcom должна находиться строка с номером порта, например, «СОМ1»).
Листинг 18.4
procedure IniCOM;
var i, err: integer;
begin
FlagCOM:=False;
Form1.Label7.Caption:='COM?';
{инициализация COM — номер в строке stcom}
Form1.AfComPort1.Close; {закрываем старый COM, если был}
val(stcom[length(stcom)],i,err); {извлекаем номер порта}
if err=0 then Form1.AfComPort1.ComNumber:=i else exit;
{здесь требуется число, а не строка}
Form1.AfComPortl.BaudRate:=br9600; {скорость 9600}
try
Form1.AfComPortl.Open ;{пробуем открыть}
except
if not Form1.AfComPort1.Active then {если не открылся}
begin
st:=stcom+' does not be present or occupied.';
Application.MessageBox(Pchar(st),'Error',MB_OK);
exit {выход из процедуры — неудача}
end;
end;
ab[1]:=ord('A'); {будем посылать инициализацию модема}
ab[2]:=ord('Т');
ab[3]:=13; {CR}
ab[4]:=10; {LF}
for i:=1 to 4 do Form1.AfComPort1.WriteData(ab[i],1);
{ответ не сразу:}
Form1.Timer1.Enabled:=True;
tall:=0;
while tall<1 do application.ProcessMessages; {пауза в 1 с}
Form1.Timer1.Enabled:=False;
st:=Form1.AfComPort1.ReadString; {ответ модема 10 знаков}
if pos(1 OK',st)<>0 then {модем}
begin
st:=stcom+' занят модемом';
Application.MessageBox(Pchar(st),'Error',MB_OK);
exit;
end else {все нормально, COM открыт}
begin
Form1.Label7.Caption:=stcom+' 96001;
FlagCOM:=True;
end;
end;
Как видим, процедура создания порта много понятнее, чем в случае прямого обращения к API — все через привычную установку свойств компонента. FlagCOM играет у нас роль индикатора, доступен порт или нет. Если он остался при значении False, то процедуру следует повторить с другим значением в строке stcom (каковую мы задаем с помощью ComboBox, см. далее). При определении модема применен хитрый способ задания паузы — вместо обычного оператора Sleep, который тормозит программу, мы использовали таймер. Чтобы это сработало, надо в обработчике события OnTimer: все время увеличивать переменную tall. Полностью процедура по таймеру приводится далее, т. к. tall нам понадобится не только для этого.
Как только мы обратились к процедуре AfComPort1.open, у нас немедленно будет создан параллельный поток и весь прием пойдет через него. Поэтому, чтобы при определении модема принятые байты не обрабатывались, нужно не забыть добавить в процедуру приема выход по условию FiagCOM=Faise.
Для создания этой процедуры обычным способом — через инспектор объектов — создадим обработчик события AfComPort1DataRecived[17] (листинг 18.5).
Листинг 18.5
procedure TForm1.AfComPort1DataRecived(Sender:TObject; Count:Integer);
{чтение очередного байта по сообщению wmCOMPORT}
var i: integer;
begin
if FlagCOM=False then exit; {если модем еще не опрошен}
if count<>0 then {если что-то принято}
begin
AfComPort1.ReadData(ab,count); {читаем буфер в массив}
хn:=xn+count; {число принятых байт}
tall:=0; {обнуляем время}
end;
end;
На самом деле условие count<>0 не требуется, оно введено просто ради порядка (иначе бы процедура просто не была бы вызвана). По выходу из процедуры в переменной хn будет накапливаться количество принятых байтов. Осталось только дописать остальные процедуры (листинг 18.6).
Листинг 18.6
procedure TForm2.Button2Click(Sender:TObject);
begin {запрос}
if FlagCOM=False then exit;
{если порт еще не инициализирован — выход}
AfComPortl.PurgeRX; {очищаем буфер порта на всякий случай}
xb:=$А2;
AfComPort1.WriteData(xb,1); {посылаем команду}
FlagSend:=$А2; {обозначаем посылку запроса времени}
tall:=0; {обнуляем время}
хn:=0; {счетчик принятых байтов}
Timer1.Enabled:=True; {запускаем таймер}
end;
procedure TForm1.FormCreate(Sender:TObject);
begin
{инициализация COM1 при запуске}
stcom:='COM1';
IniCOM;
end;
procedure TForm1.ComboBox1Select(Sender: TObject);
begin
stcom:=ComboBox1.Text; {устанавливаем порт COM1,2,3,4}
IniCOM;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
AfComPort1.Close; {закрытие порта}
end;
Теперь нам осталось разобраться с тем, что мы там напринимали. Это позволит сделать установленное нами значение FiagSend и таймер. В таймере переменную tall мы будем увеличивать на единицу, а в процедуре приема мы все время ее обнуляем, так что пока она равна нулю, можно полагать, что прием еще не закончился. Как только она станет больше единицы (прошло более секунды с момента последнего принятого байта или прием вообще не происходил), мы начинаем что-то делать, но только в том случае, если флаг FlagCOM установлен (True), иначе это вообще был не прием, а опрос модема. Сказанное иллюстрирует листинг 18.7.
Листинг 18.7
procedure TForm1.Timer1Timer(Sender:TObject);
var i: integer;
begin {таймер}
inc tall
if FlagCOM=False then exit;
if tall>1 then
begin
Timer1.Enabled:=False; {выключаем таймер}
if xn=0 then {если счетчик = 0, то ничего не принято}
begin
Application.MessageBox('Устройство не обнаружено 1,'Error',МВ_ОК);
exit {выход из процедуры — неудача}
end else
begin {иначе обрабатываем данные}
if FlagSend=$A2 then {если был запрос времени}
begin
if xn<>6 then
begin
Application.MessageBox('Неправильный формат данных','Error',MB_OK);
exit;
end;
StaticText1.Caption:=IntToHex(ab[1],2); // часы
StaticText2.Caption:=IntToHex(ab[2],2); // минуты
StaticText3.Caption:=IntToHex(ab[3],2); // секунды
StaticText4.Caption:=IntToHex(ab[4],2); // дата
StaticText5.Caption:=IntToHex(ab[5],2); //месяц
StaticText6.Caption:=IntToHex(ab[6],2); // год
end;
end;
end;
end; {конец таймера)
По аналогии вы легко добавите процедуры, соответствующие всем остальным командам, предусмотренным в программе нашего измерителя. Пользуясь функцией DateTime, легко создать процедуру, которая будет загружать из компьютера точное время (только С форматом TDateTime придется немного попотеть, см. по этому поводу [15] и [16]). Не забывайте принимать и анализировать возвращаемые байты для процедур записи. При длинной процедуре приема данных из flash, когда число байтов заранее неизвестно, суммарное значение счетчика хn покажет, сколько именно байтов принято. Причем если это число не кратно четырем, то можно смело утверждать, что целостность данных была нарушена. И не забудьте увеличить размер массива ab, если у вас энергонезависимая память большей емкости!
Наряду с такой программой для обслуживания прибора имеет смысл также написать отдельное приложение, которое обрабатывает скачанные данные по температуре и давлению, переводит их в физические величины и представляет их в вид масштабируемого графика, отфильтровывая и записывая отдельно для просмотра байты сбоев с соответствующими датами.
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОК