Разработка встроенных систем с помощью микроконтроллеров PIC

Автор данной статьи попытался создать подобное устройство из легко доступных элементов, возложив основную нагрузку по управлению на ПК. Основная идея здесь заключается в следующем. Выводы МК, как правило, двунаправленные, но во многих реальных устройствах используется лишь возможность работы их только в одном направлении. Поэтому если вместо управляющих выводов МК к схеме подключить, скажем, выводы параллельного порта ПК и написать программу, которая бы изменяла состояния этих выводов точно так же, как это бы делал МК, то в итоге действительно получится ВСЭ, правда с несколько урезанными возможностями. Следует правда отметить, что в этом случае можно использовать только цифровой ввод/вывод, однако если в данную схему добавить АЦП и ЦАП, то можно работать и с аналоговыми величинами.

Тим Уилмсхерст. Разработка встроенных систем с помощью микроконтроллеров PIC

Таким образом, сложностью разрабатываемого ВСЭ можно варьировать в зависимости от типа реального устройства, в котором он будет работать. Можно, например, изготовить карту дополнительного параллельного ввода/вывода на базе микросхемы КР580ВВ55А [2] и обеспечить уже двунаправленную передачу данных. Также можно изготовить подобную карту на базе какого-либо МК и получить возможность задавать для каждой линии ввода/вывода индивидуальное направление сигнала. Кроме того, современные параллельные порты ПК могут работать в нескольких режимах, например, стандартный Centronics, EPP, ECP. Последние два режима позволяют с большой скоростью обмениваться данными с внешними устройствами в двух направлениях. При этом все управляющие сигналы вырабатываются автоматически. Однако это иногда очень затрудняет разработку устройств сопряжения, так как они должны самостоятельно формировать необходимые управляющие сигналы и при том очень быстро (длительность цикла передачи информации для режима EPP, например, не должна превышать 15 мкс). Читателям, желающим подробнее познакомиться с режимами работы параллельного порта, можно посоветовать обратиться к книге [3].

В итоге аппаратная часть ВСЭ во многих случаях получается довольно простой, но всё управление ложится на программное обеспечение. Таким образом, мы подошли к самому главному - как заставить управляющую программу МК работать на ПК и тем более с его портами внешнего ввода/вывода. Сразу отметим, что сделать это можно только в том случае, если управляющая программа написана на языке высокого уровня, например, Паскаль или Си. Почему здесь не подходит ассемблер поясним ниже. Для запуска управляющей программы МК на ПК нужно просто временно заменить команды МК для работы с портами ввода/вывода на вызовы специальных подпрограмм, которые бы изменяли нужные разряды внешнего порта ПК (мы будем использовать параллельный порт). Остальная же часть программы остаётся без изменений, так как она будет одинаково работать на любом типе процессоров. В этом и состоит основное преимущество языков высокого уровня - один раз написанный и отработанный алгоритм будет работать правильно в любой микропроцессорной системе, если конечно сам компилятор не содержит ошибок. Здесь, правда, есть некоторые нюансы, которые касаются работы с вещественными числами и заключаются в том, что разные компиляторы для представления вещественных чисел используют разное число байтов, из-за этого точность вычислений может также отличаться.

Выше было сказано, что проводит отладку таким образом можно только в том случае, если управляющая программа написана на языках высокого уровня. Это обосновывается тем, что данные языки имеют стандартный синтаксис, практически не зависящий от среды программирования (особенно это касается языка Си, для которого даже был создан специальный стандарт ANSI C). Языки ассемблера же сильно привязаны к особенностям конкретных процессоров, и у них практически отсутствует свойство переносимости, присущее языкам высокого уровня.

До сих пор не утихают споры о том, на каком языке лучше писать управляющие программы МК. Некоторые разработчики утверждают, что язык Си и вообще языки высокого уровня подходит только для МК, имеющих большое число команд, например, МК семейства MCS-51. Стало быть, для МК серии PIC16 язык Си вообще не подходит, так как команд у них всего 35. В некотором смысле они правы - программы на ассемблере для этих МК получаются, как правило, меньшего размера, чем аналогичные программы на языке Си, но при грамотном написании последних эту разницу можно существенно сократить. Кроме того, по мнению автора уже прошло то время, когда нужно бороться за каждое слово памяти команд МК и за каждую микросекунду времени выполнения программы. В настоящее время цены на МК стремительно падают, и практически в любом случае оказывается возможным приобрести МК с большим объёмом памяти и более высоким быстродействием. Поэтому сейчас на языках высокого уровня можно писать почти любые программы, начиная от работы с каким-либо стандартным протоколом и заканчивая многозадачной средой и работой с различными внешними устройствами. Кроме того, языки высокого уровня позволяют без проблем переносить программы с одного МК на другой. И это очень важно, так как сейчас номенклатура МК постоянно расширяется, и не стоит ограничивать себя применением устройств только одного семейства. Главное идти в ногу с прогрессом. Сейчас интеграция вновь разрабатываемых МК идёт по пути всё более аппаратной адаптации с языками высокого уровня.

И язык Си здесь занимает далеко не последнее место. Например, перед разработчиками МК серии PIC18 стояла задача создания такой архитектуры, которая бы позволила получать эффективный код после компиляции программы на языке Си. В заключение отметим, что тем читателям, которые заинтересуются проблемой выбора языка программирования для МК, рекомендуется прочитать материалы конференции, проходящей на сайте MicroChip(раздел "HT-PIC & MPLAB"). Кроме того, материалы этого раздела можно найти на ftp-сервере журнала Радио.Читателям, желающим изучить язык Си, рекомендуется обратиться к [4], где в доступной форме и на многочисленных примерах изложены основы данного языка. Теперь перейдём от теории к практической реализации вышеописанного способа отладки. Для этого рассмотрим следующей пример: пусть необходимо написать программу, которая бы управляла светодиодом - включала и выключала бы его с частотой 2 Гц. Исходный текст такой программы, написанный в среде программирования Hi-Tech C представлен ниже:

 

 

     В этой, казалось бы простой, программе, уже используется несколько макроопределений и одна подпрограмма DelayMS, которая формирует задержку на заданное количество миллисекунд. Макрос DelayUS формирует задержку на заданное количество микросекунд. И вот здесь начинается самое главное. Дело в том, что этот макрос в зависимости от системы программирования, в которой компилируется программа, будет определён по-разному. Это обеспечивается директивами условной компиляции #ifdef, #ifndef, #else и #endif. Например, в системе программирования BC++ изначально определён идентификатор __BORLANDC__, и это можно использовать для того, чтобы сама программа могла фактически определить в какой среде она сейчас компилируется. Таким образом, в BC++ будут определены ещё переменная B, хранящая текущее состояние регистра данных параллельного порта ПК, и подпрограмма LED, которая будет вызываться вместо команды МК для работы с внешними портами ввода/вывода.

В результате, если запустить эту программу в BC++, то для управления светодиодом в нашем случае нужно будет использовать вывод D0 параллельного порта. Если же эту программу откомпилировать в Hi-Tech C, а затем <прошить> программатором в практически любой МК, то для управления светодиодом будет использоваться вывод RB7. Оговорка практически любой здесь не случайна - ведь не все МК имеют столько линий ввода/вывода. Так, например, у МК серии PIC12Cxxx их всего 6. В этом случае в нашей программе нужно изменить лишь макроопределение #define LED RB7 (скажем на #define LED GP0, то есть будем использовать нулевой разряд порта ввода/вывода) и инициализацию портов (вместо TRISB7 = 0 написать TRIS = 0b111110). Здесь следует сделать одно пояснение. Строка TRISB7 = 0 устанавливает 7-ой бит регистра TRISB в 0, то есть настраивает вывод RB7 как выход. При этом остальные биты регистра TRISB не изменяются. Эта строка эквивалентна строке: TRISB = TRISB & 0b0111111 (или короче TRISB &= 0b0111111), но использование определений TRISBx (где x может принимать значения от 0 до 7) является более удобным. Однако такие определения отсутствуют для МК серии PIC12, , так как у них регистр TRIS доступен только для записи.

Как видите, наша программа удивительно легко работает на любом МК и даже на ПК. Но в последнем случае есть один нюанс. Дело в том, что запущенную программу можно остановить только одним способом - нажать комбинацию клавиш Ctrl+Break (так как в ней используется бесконечный цикл). Поэтому лучше строку while (1) заменить на while (!kbhit()), где kbhit - функция возвращающая 1 при нажатии любой клавиши на клавиатуре (для её использования нужно подключить стандартную библиотеку conio.h). Таким образом, работу программы можно завершить нажатием любой клавиши, правда при условии, что внутри этого цикла while не будет других бесконечных циклов.

Это позволяет проводит очень быструю и удобную отладку с помощью встроенных средств системы программирования (например, трассировщика и процессора точек останова). Следует отметить, что программу написанную в среде программирования Hi-Tech C, можно отлаживать в системе MPLAB. Для этого необходимо сделать следующее:

1.в меню "Project" системы MPLAB выбрать пункт "Install Language Tool", после чего в появившемся диалоговом окне следует задать следующие параметры: "Language Suite": HI-TECH, "Tool Name": PIC-C Compiler, "Executable": <путь к файлу picc.exe>, "Command-line" - галочка;

2.проделать то же самое, но только в строке "Tool Name" указать PIC-C Linker;

После этого рабочий проект создаётся также, как и для ассемблера - с помощью выбора пункта "New Project", меню "Project". Отладка программы на языке Си также не отличается от отладки ассемблерной программы. Но стоит отметить, что возможности отладки в системе MPLAB весьма скудные. Более широкие предоставляются системой BC++. Здесь тоже можно исполнять программу по шагам с помощью клавиш F7 и F8 и расстанавливать точки останова. Но, кроме того, очень удобным средством является наблюдение переменных, позволяющее просмотреть текущее значение одной или нескольких переменных в процессе выполнения программы. При этом значения переменных могут представляться в том виде, в котором их хотел бы видеть разработчик программы: в десятичном, шестнадцатеричном, в виде строки символов. Для вещественных чисел можно указать количество отображаемых знаков после запятой. Если переменная имеет тип char*, то отладчик выдаст не значение указателя, а соответствующую строку символов. Для добавления просматриваемой переменной следует выбрать пункт "Add Watch" подменю "Watch" меню "Debug". После этого в появившемся диалоговом окне нужно задать имя необходимой переменной или даже выражение. Отладчик откроет окно "Watch", в котором будет показана переменная или выражение и соответствующее значение. Можно продолжить добавлять переменные в окно "Watch". Также можно продолжить выполнение программы, в процессе чего значения в этом окне будут автоматически изменяться. Если переменная глобальная, то её значение доступно в любом месте программы. Если же переменная локальная, то её значение доступно лишь в области видимости переменной. Если переменная недоступна, то в окне "Watch" вместо значения выдаётся соответствующее предупреждение.

При просмотре выражений в окне "Watch" есть два ограничения. Во-первых, в выражении запрещён вызов функций. Во-вторых, в выражении не могут применяться макросы, определённые с использованием директивы #define.

Отладчик BC++ позволяет осуществлять форматный вывод наблюдаемых значений. Для задания формата используется следующая форма: <выражение>, <код формата> Список кодов формата задан в табл.1.

Таблица 1
C В виде символа
D Десятичное число
F(n) Число с плавающей точкой
H или X Шестнадцатеричное число
M Показать память (dump)
P Указатель
R Структуры: вывести имена и значения полей
S Вывести управляющие символы

В формате F можно указать число значащих цифр после запятой, например: i, F2. Если формат не указан, отладчик сам подбирает соответствующий тип формата. Кроме использования встроенного отладчика можно осуществлять вывод на экран монитора какой-либо информации во время выполнения программы, например, значений переменных: printf("Текущее значение переменной i = %d",i). К сожалению, нельзя следить за изменениями состояний внутренних регистров МК, но это и понятно - ведь программа компилируется на совершенно другом процессоре, и регистры здесь совершенно другие. Следует отметить, что у данного способа отладки есть один существенный в некоторых случаях недостаток - программа здесь работает в масштабе времени, отличном от реального, так как, во-первых, микропроцессоры ПК имеют большее быстродействие, чем МК, а во-вторых, информация, выводимая во внешний порт, в большинстве случаев аппаратно или с помощью операционной системы буферизируется, вследствие чего, её поток может быть непостоянным. Первое обстоятельство приводит к тому, что подпрограмма формирования задержки потребует корректировки на каждом компьютере (коэффициент 50 в нашем случае задан для ПК на базе процессора Pentium с тактовой частотой 133 МГц). В пакете BС++ предусмотрена функция delay, реализующая выдержку в заданное число миллисекунд. В стандартном описании сказано, что её работа на зависит от быстродействия компьютера, так как подпрограмма калибровки включается в компилируемую программу автоматически и вызывается при каждом её запуске. Автор взял на себя смелость на двух компьютерах (первый имел процессор Pentium 133 МГц, а второй - Celeron 1,2 ГГц) измерить время выполнения команды delay(10000) с помощью следующей несложной программы:

 

 Для точного измерения времени здесь использовался встроенный таймер, генерирующий 55 мс импульсы. Результаты оказались неожиданными: на первом компьютере процедура delay (10000) вместо 10 секунд выполнялась 8,88 сек, на втором - 5,06 сек (в два разе меньше, чем необходимо!). Таким образом, доверять функции delay особо не стоит.

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

Предложенным методом была практически полностью отлажена программа для микроконтроллерного таймера, описанного в предыдущей статье автора. Причём отладка заняла всего пару дней (объём откомпилированного при этом составлял 7524 слов!). В программе использовались некоторые алгоритмы, заимствованные из опыта программирования для ПК типа IBM PC. Все они прекрасно работали и на МК PIC16F876.

Нужно сказать ещё об одной важной проблеме. Дело в том, что в реальном МК существует большое количество встроенных узлов: таймеры, последовательные каналы связи (RS232C, CAN, USB, I2C, SPI и др.), АЦП, ЦАП, аналоговые компараторы и источники опорного напряжения. Работу аналоговых узлов, а также некоторых последовательных интерфейсов типа RS232C, SPI и I2C можно довольно просто смоделировать, используя специализированные микросхемы, управление которыми может производиться через тот же параллельный порт. Пример организации АЦП и интерфейса I2C был показан на рис.1. Другие примеры мы рассмотрим позже.

Самое сложное - это смоделировать таймеры реального времени. Вся проблема заключается в том, что в ПК IBM PC доступных устройств подобного типа просто нет. Правда в одной из БИС материнской платы находится узел, аналогичный по функционированию микросхеме i8253 (отечественный аналог - КР580ВИ53) и имеющий три программируемых канала таймера. Но все они предназначены для выполнения специальных функций: 0-ой - для регенерации оперативной памяти, 1-ый - для отсчёта системного времени (в 55 мс интервалах), 2-ой - для генерации звука через встроенный динамик. Из этих трёх каналов в принципе можно использовать первый, но изменение его настроек приведёт к искажению системного времени, что во многих случаях недопустимо. Выходом из положения является добавление дополнительной платы расширения, реализующей работу таймера реального времени, но здесь довольно сложно реализовать не только аппаратную часть (которая в данном случае будет насчитывать не одну микросхему), но и программную. Если кто из читателей заинтересуется проектированием подобных устройств, то может обратиться к [5], где материал изложен очень понятно и подробно, но, к сожалению, электрические и структурные схемы грешат избыточностью. Также тем, кто всё таки захочет использовать первый канал встроенного в ПК таймера, рекомендуем обратиться к литературе [6, с. 172-180].

Автором данной статьи были предложены два других способа. Первый является полностью программным и особой оригинальностью не отличается. Однако следует отметить, что во многих случаях он несильно уступает аппаратному способу. Суть его заключается в следующем. В большинстве случаев в программе используется прерывание от таймера через заданный интервал времени. Это прерывание может возникнуть во время выполнения практически любого участка программы. Однако на ПК такое прерывание вообще никогда не появится, так как отсутствует реальный источник прерывания. Поэтому для имитации данного механизма по всей программе разбрасываются вызовы процедуры псевдообработки прерывания от таймера, подобно тому, как это делается при сбросе сторожевого таймера. В идеале вызов данной процедуры необходимо осуществлять после выполнения каждой команды, но на практике, как правило, достаточно всего нескольких вызовов, которые должны присутствовать во всех циклах и во всех процедурах осуществления задержки. Так сделано, например, в упомянутой выше программе для микроконтроллерного таймера. При этом отчёт реального времени производился довольно точно, и динамическая индикация также осуществлялась корректно.

Структура такой процедуры имеет следующий вид:

 

Как именно осуществляется подсчёт микросекунд этой функцией автору, к сожалению, узнать не удалось - даже в документации на это прерывание не раскрывались принципы его работы. Но одно можно сказать с уверенностью - отсчёт времени здесь производится довольно точно, причём независимо от быстродействия ПК. Более подробно ознакомиться с этим и другими прерываниями, а также аппаратной частью ПК типа IBM PC можно в электронном справочнике TechHelp!, который находится на сервере журнала Радио".

Следует отметить, что вместо вышеописанной ассемблерной вставки, можно использовать стандартную функцию intr системы программирования BC++. В этом случае вместо оператора asm {:} нужно вставить следующие строки:

reg.r_es = FP_SEG(&PS);

reg.r_bx = FP_OFF(&PS);

reg.r_ax = 0x8300;

reg.r_cx = 0;

reg.r_dx = 1500;

intr(0x15,R);

Однако использование этой функции не очень удобно, так как нужно вводить ещё одну дополнительную переменную reg, описание которой (static struct REGPACK reg;) следует разместить в начале функции Timer0.

При каждом вызове процедуры псевдообработки прерывания Timer0 осуществляется проверка установки 7-го бита переменной PS в единицу. Если это произошло, то выполняется основная часть процедуры обработки прерывания и перезапуск таймера. Здесь нужно отметить один нюанс, который заключается в том, что после установки 7-го бита, то есть по истечению заданного времени, нельзя тут же вызывать данное прерывание для отсчёта нового интервала - необходимо, чтобы прошло какое-то время (порядка нескольких десятков микросекунд). Скорее всего, это связано с программными особенностями операционной системы.

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

 

Оно подключается к одному из COM-портов ПК и для питания использует напряжения на его выводах. Для подачи питания на схему нужно программным способом выставить положительный уровень на выводе RTS. Суть метода эмуляции заключается в следующем. Микроконтроллер DD1 выдаёт на линию CTS импульсы переполнения своего внутреннего таймера-счётчика TMR0. Так как в ПК имеется возможность генерации прерывания по изменению состояния входных линий, то мы получаем возможность реализации <настоящей> процедуры обработки прерывания от таймера, которая действительно может быть вызвана в любом месте программы. При этом период генерации этого прерывания полностью синхронизирован с периодом переполнения таймера реального МК. Кроме того, данный период можно изменять из программы (по умолчанию задан период в 2 мс: PSA = 0; PS0 = 0; PS1 = 1; PS2 = 0; TMR0 = -250;). Для этого необходимые значения регистра TMR0 и битов PSA, PS0-PS2 регистра OPTION_REG передаются в МК последовательным кодом со скоростью 19200 бод через COM-порт. В МК DD1 реализован программный приёмник асинхронного последовательного кода. Управляющая программа МК написана в среде программирования Hi-Tech C 8.01PL1. Коды прошивки МК DD1 приведены в табл.2.

Исходный текст программы можно найти на ftp-сервере журнала "Радио". Программа постоянно генерирует прерывания по переполнению таймера и кроме того обрабатывает байты, полученные через COM-порт. Для установки нового периода таймера нужно отправить МК пакет из трёх байтов:

  • заголовочный байт – всегда равен 0xFD;
  • значение регистра TMR0 – от 0 до 255. Можно и со знаком минус – в этом случае компилятор автоматически переведёт это значение в дополнительный код, то есть заменит его на 256 – <значение>. Последнее очень удобно. Пусть мы хотим, чтобы прерывания от таймера возникали каждые 250 мкс (при кварце на 4 МГц и отключенном предделителе). Тогда, записав в него число –250, на самом деле инициализируем его значением 256 – 250 = 6. Таким образом, действительно понадобиться 250 мкс (так как таймер переполнится на 256-ом импульсе), чтобы возникло прерывание;
  • байт, содержащий значения битов PSA и PS0-PS2 (битам PS0-PS2 соответствуют разряды 0-2, а биту PSA – разряд 3).

Таким образом, в разрабатываемую управляющую программу МК, которая должна компилироваться в среде BC++, должны быть включены подпрограммы настройки COM-порта и передачи через него информации в МК DD1. Такие подпрограммы были разработаны автором данной статьи и объединены в заголовочный файл realtmr.h, который можно найти по указанному выше адресу в Интернете. Исходный текcт этого файла представлен ниже:

Теперь приведём исходный текст описанной нами ранее программы, осуществляющей мигание светодиода с частотой 2 Гц, но уже с использованием прерывания по переполнению таймера.

Таким образом, для запуска управляющей программы МК, содержащей обработку прерываний от таймера TMR0, на ПК нужно заменить операторы инициализации регистра TMR0 и битов PSA, PS0-PS2 регистра OPTION_REG следующими вызовами подпрограмм:
InitInterrupt();
PutUART(0xFD);
PutUART([значение регистра TMR0]);
PutUART([значения битов PSA и PS0-PS2]);

В подпрограмме обработки прерывания следует заменить строки T0IF = 0 и TMR0 = [значение] на inportb(BASE + 2) и inportb(BASE + 6) соответственно. Кроме того, в самый конец этой подпрограммы нужно вставить следующие две команды: outportb(0xA0,0x20) и outportb(0x20,0x20). В конце основной программы надо осуществить вызов функции DoneInterrupt.

Обработка аппаратных прерываний на ПК имеет свои особенности, которые необходимо учитывать, так как в противном случае может произойти «зависание» программы со всеми вытекающими отсюда последствиями. Практически структуру подпрограммы обработки прерывания выбирают, исходя из конкретных условий

Процессор ПК, получив запрос на прерывание, сохраняет в стеке значение своего регистра флагов и адрес прерванной команды (пара регистров CS : IP), а также сбрасывает бит IF регистра флагов, тем самым запрещая новые прерывания. Поэтому подпрограмма обработки прерывания не может быть прервана внешними сигналами.

Структура такой подпрограммы показана на рис.4,а. Здесь команда iret выполняется автоматически (для этого в заголовке процедуры обработки прерывания должен присутствовать модификатор interrupt) при выходе из процедуры обработки прерывания и её явное указание программистом не требуется.

Часто в начале этой процедуры осуществляют вызов функции enable(), разрешающей все прерывания, чтобы не задерживать обработку прерываний от более приоритетных устройств, в частности, таймера. Приказ конца прерывания EOI (End Of Interrupt) посылается в контроллер прерываний в самом конце процедуры, чтобы полностью исключить вложенные прерывания от запросов того же уровня (рис.4,б). Однако сигнал прерывания того же (или более низкого) уровня может придти между командой outportb(0x20,0x20) и командой iret. Поскольку блокировка нижележащих уровней в контроллере уже снята, возникнет вложенное прерывание и повторный вход в ту же процедуру. Чтобы избежать этого, перед командой EOI выполняют команду запрета всех прерываний disable(). В результате вложенные прерывания запрещаются до выхода из обработчика. Можно подумать, что прерывания останутся запрещёнными навсегда, однако это не так. Команда iret восстанавливает не только адрес возврата в регистрах CS : IP, но и содержимое регистра флагов на момент прерывания. Если при этом содержимом возникло прерывание, значит бит IF был установлен в единицу. Получается, что команда iret в процедуре обработки аппаратного прерывания всегда восстанавливает установленное состояние флага IF, то есть разрешает прерывания.

Следует отметить, что в ПК IBM PC на базе микропроцессора i80286 или выше (и совместимых с ними) существует два контроллера прерываний – ведущий и ведомый. Причём они устроены так, что запросы на прерывания, поступающие в ведущий контроллер (уровни IRQ2…IRQ7) блокируют только его. Однако запросы на прерывания, поступающие в ведомый контроллер (уровни IRQ8…IRQ15) блокируют не только уровни низших приоритетов, но и уровни IRQ2…IRQ7 в ведущем контроллере. Поэтому в обработчиках аппаратных прерываний уровней 8…15 следует предусматривать посылку команд EOI в оба контроллера (ведущий имеет адрес 0x20, ведомый – 0xA0). В нашем случае работе с COM-портами используются линии IRQ3 (для COM2) и IRQ4 (для COM1), поэтому достаточно послать команду EOI только в ведущий контроллер.

Более подробно о контроллере аппаратных прерываний можно прочитать в [6, с. 187-192].
Как видите, использование даже такого простого блока (рис.2) позволяет осуществлять практически 100-процентную эмуляцию прерываний от встроенного в МК таймера. Недостатком этого устройства является невозможность чтения текущего значения таймера. Первоначально предполагалось с помощью специальной команды, передаваемой через COM-порт, заставить МК выдать текущее значение регистра таймера последовательным кодом. Но несложные расчёты показали, что даже, работая на предельной скорости в 115200 бод, для этого потребуется почти 200 мкс, что очень много, так как за это время таймер может даже переполниться. Единственным выходом из этого положения является, как уже говорилось в начале статьи, создание дополнительной платы расширения для ПК, реализующей функцию таймера и выдающей данные в параллельном коде. Однако, опыт показывает, что прямое чтение регистра таймера используется намного реже, чем прерывание по его переполнению.

Читателям, заинтересовавшимся разработанным блоком, можно предложить реализовать эмуляцию не только таймера TMR0, но и TMR1 и TMR2. Для этого вместо МК PIC16F84 лучше использовать PIC16F628, в котором есть помимо этих трёх таймеров ещё и модуль USART, позволяющий упростить управляющую программу за счёт замены программного варианта приёмника последовательного кода аппаратным. Пример программы, использующей модуль USART этого МК и выполняющей те же действия, что и программа для МК PIC16F84, можно найти на ftp-сервере журнала Радио.

На этом рассмотрение эмуляции таймера мы закончим и кратко опишем способы имитации некоторых других встроенных узлов МК. Например, в упомянутом выше МК PIC16F628 имеется два аналоговых компаратора. Каждый из них можно заменить узлом, показанным на рис.5. Для используемого компаратора К554СА3 в таком включении справедливы следующие соотношения:
при U4>U3;U9?0В?лог.0
при U4< компаратора. 9 выводе на напряжение – U9 Здесь>

 

 

Если |U4-U3|?Unum?K, где K=150?10? – коэффициент усиления операционного усилителя (ОУ) компаратора, то напряжение на его выходе может оказаться ни «единичным», ни "нулевым". Это режим переходного процесса, который не должен быть длительным, а тем более – статичным.

Выход компаратора в данной схеме подключен ко входу ERROR параллельного порта ПК, поэтому для чтения состояния компаратора можно воспользоваться следующей функцией, написанной на языке Си и возвращающей 1 при U4

unsigned char GetCMP (void)
 {
  return (inportb(0x378 + 1) & 8) > 0;
 };

Кстати, хотя выход компаратора К554СА3 выполнен и с «открытым коллектором», тем не менее его выходной сигнал будет нормально считываться с входных линий параллельного порта ПК, так как все они имеют "подтягивающие" резисторы.
Довольно часто в качестве компаратора используют обычный ОУ. Особенно удобным в этом случае оказывается ОУ типа КР140УД1208, имеющий малые габариты и способный работать на микротоках (рис.6 ). Его "компараторный" режим следующий:
при U2>U3; Uвых<0,5В?лог.0
при U2Unum-0,5В?лог.1

Как видно, в этом ОУ минимальное и максимальное выходное напряжение заметно отличаются от и (но всё-таки остаются в "цифровых" пределах). Исследуя какой-либо ОУ на предмет его использования в качестве компаратора, на это нужно обратить особое внимание. Если же "нули" и "единицы" ОУ выходят из цифровых пределов, то их требуется преобразовать, как, например, показано на рис.6 штриховыми линиями.

Теперь рассмотрим как можно имитировать работу модуля последовательного синхронно-асинхронного приёмо-передатчика (USART), входящего в состав некоторых МК, в том числе и PIC16F628. В принципе он по своему функционированию аналогичен контроллеру интерфейса RS232C. Поэтому для имитации можно использовать один из COM-портов ПК. Правда здесь необходимо согласовать уровни управляющих сигналов. Это можно сделать с помощью специализированных микросхем, например, MAX220, MAX232 или MAX232A. Схема включения у них (рис.7) практически одинакова – отличаются только номиналы конденсаторов (табл.3).

Следует отметить, что применение подобных микросхем целесообразно в основном при высоких скоростях передачи информации (38400 бод и выше) и при необходимости использования большого числа управляющих сигналов. Однако во многих случаях достаточно только двух сигнальных линий – передачи и приёма.

В этом случае проще использовать схему, показанную на рис.8. Здесь в качестве преобразователей уровня используются транзисторы VT1 и VT2. На линии RTS необходимо выставить уровень положительного напряжения. Нужно отметить, что схема не использует отрицательного напряжения, что, вообще говоря, не соответствует стандарту RS232C, но она отлично работает на всех современных COM-портах. Кроме того, почти все компьютерные "мыши" работают с такими уровнями, и недостатков в их работе не замечается.

Схема работает следующим образом. При подаче на вывод RxD разъёма XS2 сигнала лог. 0, транзистор VT2 будет закрыт, а на линию RxD COM-порта через резистор R4 будет поступать положительное напряжение, что соответствует лог. 0 (в интерфейсе RS232C используется негативная логика). При подаче же на вывод RxD разъёма XS2 лог. 1, транзистор VT2 откроется, и линия RxD будет соединена с "землёй", что соответствует так называемой зоне неопределённости входных напряжений COM-порта. Но при этом из его регистра будет считываться значение лог.1., что нам и необходимо. Аналогично работает каскад на транзисторе VT1, но в этом случае на линии TxD относительно его эмиттера будет либо отрицательное (при передаче лог. 1), либо положительное (при передаче лог. 0) рабочее напряжение COM-порта. В первом случае транзистор будет закрыт, а на его коллекторе и соответственно на выводе TxD разъёма будет напряжение +5 В (лог. 1), которое должно подаваться с отлаживаемого устройства. Во втором случае транзистор будет открыт, а вывод TxD соединён с общим проводом, что соответствует лог. 0.

 

Для передачи и приёма через блок сопряжения информации можно использовать следующие подпрограммы:


// Инициализация контроллера COM-порта
void InitUART (unsigned int rate, unsigned char parity, unsigned char stop)
 {
  outportb(BASE + 4,9); // DTR = 1 (-12 В), RTS = 0 (+12 В)
  outportb(BASE + 3,0x80);
  outportb(BASE,rate);
  outportb(BASE,rate >> 8);
  outportb(BASE + 3,0x03 | parity | stop);
 };


Параметр rate задаёт скорость передачи (табл.4)

Значение rate Скорость передачи, бод Значение rate Скорость передачи, бод
1040 110 24 4800
768 150 12 9600
384 300; 6 19200
192 600 3 38400
96 1200 2 57600
48 2400 1 115200

Параметр parity задаёт контроль чётности: 0x00 – контроля чётности нет, 0x18 – контроль на чётность, 0x08 – контроль на нечётность, 0x20 – фиксация бита чётности. Параметр stop задаёт количество стоповых битов: 0x00 – один стоповый бит, 0x04 – два стоповых бита.

// Передача байта через COM-порт
void PutUART (unsigned char b)
 {
  // Ожидание окончания передачи предыдущего байта
  while ((inportb(BASE + 5) & 32) == 0);

  // Передача нового байта
  outportb(BASE,b);
 };


// Приём байта через COM-порт
unsigned char GetUART (void)
 {
  // Ожидание окончания приёма байта
  while ((inportb(BASE + 5) & 1) == 0);

  return inportb(BASE);
 };

Во всех этих подпрограммах переменная BASE задаёт базовый адрес используемого COM-порта: 0x3F8 – для COM1 и 0x2F8 – для COM2.

Необходимо заметить, что в приведённых выше подпрограммах передачи и приёма данных не осуществляется обнаружение ошибок. Читателям, которые захотят добавить эту возможность, можно посоветовать обратиться к [5], где дано множество рекомендаций по работе с последовательным портом ПК. Кроме того, в ПК при задании контроля чётности контрольный бит формируется аппаратно, в МК же его нужно вычислять каждый раз программно, например, с помощью следующей функции (осуществляет контроль по чётности):

unsigned char EVEN (unsigned char b)
 {
  unsigned char n = 0; // Для контроля по нечётности следует написать n = 1

  while (b > 0)
   {
    n ^= (b & 1);
    b >>= 1;
   }; 

  return n;
 };

Следует отметить, что у этой функции один существенный недостаток – большое время обработки, которое к тому же зависит от значения параметра b. Для b = 0 время обработки минимально и составляет 16 мкс при кварце на 4 МГц, для b = 0xFF – максимально (80 мкс). Среднее время: (16 + 80) / 2 = 48 мкс сравнимо с длительностью одного битового интервала при скоростях 19200 бод и более. Поэтому при передаче последовательности байтов МК может не успеть обработать бит контроля чётности до начала нового стартового бита, вследствие чего возможно появление ошибок. В этих ситуациях вместо использования данной функции нужно создать массив из 256 элементов, каждый из которых будет содержать значение контрольного бита для значения байта, равного соответствующему индексу. То есть, элементы этого массива вычисляются следующим образом: M [i] = EVEN(i). Исходный текст такого массива для контроля по чётности показан ниже:

const unsigned char EVEN [256] = 
      { 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
        0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
        0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
        1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
        0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
        1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
        1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
        0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
        0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
        1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
        1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
        0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
        1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
        0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
        0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
        1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1
      };


Очевидно, что для контроля по нечётности элементы этого массива нужно проинвертировать.

На этом описание имитации периферийных устройств мы закончим. В заключение приведём подпрограмму, считывающее значение из АЦП, показанного на рис.1:

unsigned char GetADC (void)
 {
  unsigned char b = inportb(0x378) & ~3, n, r = 0;

  outportb(0x378,b);

  for (n = 0; n <= 7; n++)
   {
    outportb(0x378,b | 1);
    asm { nop; nop; nop; };
    r |= (inportb(0x378) & 128) >> n;
    outportb(0x378,b);
    asm { nop; nop; nop; };
   };

  outportb(0x378,b | 2);
  
  return r;
 };


АЦП в данной схеме используется 8-разрядный, но при необходимости его можно заменить 10-разрядным TLC1549, который имеет точно такое же расположение выводов. Единственное отличие заключается в том, что в протоколе связи используется 10 бит данных вместо 8. Поэтому для него следует использовать другую подпрограмму:


unsigned int GetADC10 (void)
 {
  unsigned char b = inportb(0x378) & ~3, n;
  unsigned int  r = 0;

  outportb(0x378,b);

  for (n = 0; n <= 9; n++)
   {
    outportb(0x378,b | 1);
    asm { nop; nop; nop; };
    r |= ((inportb(0x378) & 128) << 2) >> n;
    outportb(0x378,b);
    asm { nop; nop; nop; };
   };

  outportb(0x378,b | 2);
  
  return r;
 };


Вообще же можно использовать и другие АЦП, в том числе со встроенными источниками опорного напряжения.

Таким образом, мы рассмотрели основные принципы отладки управляющих программ МК с помощью самодельного ВСЭ, который в простейшем случае (только при цифровом вводе/выводе) будет представлять собой лишь набор проводов, соединяющих выводы панельки под МК на плате разрабатываемого устройства с линиями параллельного порта ПК. Если же к нему подключить дополнительные блоки, имитирующие работу встроенных узлов МК, то появится возможность практически полной эмуляции всего МК. Правда у такого ВСЭ имеется один серьёзный недостаток – он работает в масштабе времени, отличном от реального. Но для большого числа применений это особой роли не играет и с лихвой окупается простотой реализации и низкой стоимостью подобного устройства.

СПИСОК ЛИТЕРАТУРЫ

  1. Гёлль П. Как превратить персональный компьютер в измерительный комплекс. – М.: «ДМК», 1999
  2. Васильев Н. Расширитель интерфейса PC. – Радио, 1994, №6, с. 20, 21
  3. Гук М. Интерфейсы ПК. Справочник. – С.-Пб.: «Питер», 1999
  4. Березин Б. И., Березин С. Б. Начальный курс C и C++. – М.: «ДИАЛОГ-МИФИ», 1999
  5. Новиков Ю. В., Калашников О. А., Гуляев С. Э. Разработка устройств сопряжения для персонального компьютера типа IBM PC. – М.: «ЭКОМ», 2000
  6. Рудаков П. И., Финогенов К. Г. Программируем на языке ассемблера IBM PC – Изд. 3-е. – Обнинск: «Принтер», 1999

От редакции. Первоначально эту статью планировалось опубликовать на страницах журнала "Радио". Однако ее объем оказался слишком велик (как минимум, восемь журнальных полос). Исходя из того, что при сокращении статьи до приемлемого размера пришлось бы опустить ряд второстепенных, но тем не менее интересных для начинающих (в области микроконтроллерной техники) деталей, а также учитывая тот факт, что конструирование и отладка микропроцессорных устройств предполагает наличие у радиолюбителя персонального компьютера (а многие владельцы ПК имеют выход в Интернет), было решено (с согласия автора) поместить статью в полном объеме на нашем сайте в Интернете.