Высокий уровень программирования. Пишем на Си под AVRАлексей «Cluster» Авдюхин (clusterrr@clusterrr.com) Программисты микроконтроллеров всегда делились на тех, кто пишет на ассемблере, и тех, кто предпочитает высокоуровневые языки программирования. Обе группы готовы бесконечно доказывать, что их язык лучше. И каждая права по-своему. Ассемблер обычно используют профессионалы, которые давно знакомы с микроконтроллерами. Но зачем учить язык тем, кто только начинает осваивать новую архитектуру? Я расскажу, как программировать на Си под микроконтроллеры AVR. Выбор компилятораСуществует несколько компиляторов Си под AVR, включая официальный от Atmel. Я предпочитаю использовать WinAVR (произносится, как «whenever»). Этот пакет представляет набор бесплатных утилит под винду, включая компилятор AVR-GCC, консольный отладчик и даже софт для прошивки, который умеет работать с самыми разными программаторами. Далее я буду рассматривать именно WinAVR, поэтому рекомендую установить его. Домашняя страничка проекта находится по адресу: http://winavr.sourceforge.net, но ты можешь взять свежую версию на нашем диске. Обрати внимание, что почти все утилиты в этом пакете консольные. Если ты уже знаком с GCC, то тебе не привыкать. Можно использовать для него соответствующую оболочку-редактор. Именно для таких целей в комплекте идет Programmer's Notepad. Я же предпочитаю писать код в обычном текстовом редакторе и компилировать его командой «make». Это дело вкуса и привычки. Регистры, регистры, регистры...Неоспоримое преимущество Си в переносимости. Один и тот же код зачастую можно легко скомпилировать под разные платформы. Однако программе нужно как-то взаимодействовать с внешним миром, которым является операционная система или аппаратная часть. В случае с софтом под Windows и *nix для этого используются вызовы системных функций. В микроконтроллерах же нет операционной системы, и мы будем напрямую взаимодействовать с железом. Делается это с помощью регистров, которые, по сути, являются ячейками в памяти микроконтроллера. Так, если ты хочешь работать с модулями микроконтроллера, будь то таймеры, компараторы, USART или просто ноги, тебе придется писать данные в регистры. Этим я хочу сказать, что если ты уже сейчас без проблем кодишь на Си, то единственное, что тебе осталось изучить - это регистры микроконтроллера. У каждой АВРки они свои. Их список и подробное описание ты сможешь найти в соответствующем даташите. Однако базовые принципы для каждой модели очень похожи. Давай рассмотрим основные регистры, с которыми ты, наверняка, будешь играться в первую очередь: DDRx, PORTx и PINx. Отвечают они непосредственно за управление ногами микроконтроллера. Немного поясню для тех, кто не в теме. В каждой АВРке все ноги делятся на порты, которые обозначаются латинскими буквами A, B, C и т.д. Каждый может содержать до восьми ног, которые номеруются цифрами от 0 до 7. Скачай даташит к своему микроконтроллеру. На первых страницах ты найдешь рисунок с распиновкой, где подписано, какая нога за что отвечает. Обрати внимание на надписи типа PB0, PC1 и т.п. Именно они указывают букву порта и номер ноги. Например, строка «PA7» означает, что мы имеем дело с седьмой ногой на порту A. Существует набор регистров DDRx, PORTx и PINx, которые у каждого порта свои. Соответственно, вместо «x» нужно подставить соответствующую букву. Например, для управления портом «B» используются регистры DDRB, PORTB и PINB. Каждый из них размером всего в 1 байт. Нетрудно догадаться, что каждый бит при этом отвечает за соответствующую ногу. Итак, DDRx регистры определяют направление данных - ввод или вывод. PORTx позволяют задать состояние ноги - логическая единица или логический ноль, а также включать подтягивающий резистор. Регистры PINx рассчитаны только на чтение и позволяют определить уровень на ноге, когда она работает на ввод. Моргаем светодиодом
В первой строке определяется тактовая частота, на которой будет работать микроконтроллер. Это необходимо для корректной работы некоторых функций. Далее мы подключаем модули. WinAVR идет с очень широким набором заголовочных файлов, в которых описывается все, что может понадобиться при работе с самыми разными АВРками. Файл «io.h» содержит список всех регистров для соответствующего микроконтроллера, модель которого обычно определяется в файле «makefile». Надеюсь, что ты уже умеешь писать мейкфайлы. Но на самом деле, это необязательно, ведь можно без проблем пользоваться готовым шаблоном, который ты найдешь в примерах. Заголовочный файл «delay.h» содержит функции задержек. Функция «main» уже должна быть знакома тем, кто пишет на Си. Именно тут начинается наша программа. Обрати внимание, что параметры командной строки не передаются. Да и откуда им взяться? :) Теперь начинается самое интересное. Строка «DDRB |= (1 << 2)» выставляет второй бит (отсчет идет от нуля!) в регистре DDRB равным единице. Знающие Си не увидят тут ничего сложного. Для остальных поясню, что оператор «<<» выполняет битовый сдвиг влево, то есть выражение «1 << 2» равно числу 4, которое в двоичной системе выглядит как «100». Строку можно записать таким образом: DDRB = DDRB | 0b00000100. Выполняет она следующее: берет значение регистра DDRB, производит логическое сложение его с числом 4 и записывает результат назад в DDRB. После этой простой операции в регистре изменяется только второй бит; его значение становится равным единице. Первоначальная запись выглядит красивее, если ты знаешь язык. В этом и заключается прелесть Си – коротким выражением можно записать достаточно хитрый код (надо знать этот язык, если хочешь такое понимать). Но не будем углубляться в уроки Си. Логическая единица во втором бите регистра DDRB означает, что вторая нога порта «B» теперь будет работать на вывод. Далее по коду идет бесконечный цикл, в котором мы работаем уже с регистром PORTB, изменяя его второй бит, а вместе с ним – и напряжение на второй ноге порта «B». Можешь смело взять светодиод, подключить его к PB2 и проверить – он будет мигать каждые 100 миллисекунд. Этим простым примером я хочу показать, что, если ты знаешь Си, то сможешь уже сейчас легко написать свою первую программу для AVR, которая будет мигать светодиодом. Наверное, именно с такой «дискотеки» все и начинали; это своеобразный «Hello World» в мире микроконтроллеров. И он очень радует. ПрерыванияПри прямой работе с железом без прерываний не обойтись. В свое время эта тема казалась мне весьма сложной, а слово «прерывание» даже немного пугало, ведь я не понимал его значения. На деле, все очень просто. Это часть программы, которая автоматически вызывается в заданный момент. Когда пишем на Си, прерывания выглядят почти как обычные функции. Полный их список, значения и инструкции по использованию ты всегда можешь найти в даташите. В качестве простейшего примера возьмем получение данных из USART порта. Чтение данных Функция-прерывание обозначается словом «ISR», в скобках после которого идет имя вектора прерывания. В примере это «USART_RXC», он вызывается, когда завершено получение данных из USART-порта. Однако прерывание не будет вызываться, если микроконтроллеру этого не указать. В нашем случае для этого используется бит RXCIE в регистре UCSRB. В итоге, инициализация USART порта делается следующим образом:
Много новых и непонятных слов? Смотри их описание в даташите! Код инициализирует USART-порт для работы на скорости в UART_BAUD бод, соответственно, нужно заранее задать эту константу. И да, каждый бит в регистре тоже имеет свое название для удобства работы с ним. Нас особенно интересуют последние две строки. Простой код «UCSRB |= (1 << RXCIE)» делает то, о чем я написал выше – устанавливает бит RXCIE равным единице. Именно это говорит микроконтроллеру о том, что нужно вызывать прерывание. Функция sei() дает процессору команду о том, что теперь надо обрабатывать прерывания. Фактически sei() просто включает все прерывания. Ее обязательно надо вызвать, чтобы они начали работать. Существует и обратная функция - cli(). Использовать ее стоит, если ты хочешь отключить сразу все прерывания. Иногда это может быть полезно (аналогичные команды существуют в ассемблере, cli и sei, – Прим. dlinyj). Обрати внимание, если ты не опишешь функцию для прерывания и выполнишь USART_init(), в которой оно разрешается, результат будет непредсказуемым. Так делать нельзя. Подключаем LCD-дисплейЧто ж, давай уже сделаем что-то полезное! Разработаем и соберем устройство с текстовым LCD-дисплеем, которое будет выводить произвольные данные, получаемые с COM-порта компьютера. Я взял для этого микроконтроллер ATmega8, но ты легко сможешь адаптировать код под любую АВРку, главное, чтобы количества выводов хватило. Схема получается достаточно простая. В меге8 уже есть встроенный USART-порт. Чтобы подружить его с COM-портом компьютера, необходимо использовать конвертер уровней, например, старый добрый MAX232. Из дисплеев же можно выбрать любой на контроллере HD44780; их существует огромное множество. Я взял Winstar WH2404, – у него две строки по 24 символа, есть поддержка кириллических букв, подсветка, да и стоит он недорого. Для подключения дисплея к микроконтроллеру я решил использовать семь проводов и выделил под него отдельный порт. Не буду глубоко вдаваться в принципы работы с дисплеем. Это широкая тема, поэтому мы возьмем один из готовых модулей. Общая схема работы программы будет достаточно простая... Получаем данные от компьютера и заносим их в буфер. Если пришел символ конца строки, то сразу же выводим содержимое буфера на экран. Определения констант Тут мы определили тактовую частоту, на которой будет работать микроконтроллер, скорость USART-порта и ноги, куда подключен дисплей. Все эти данные ты можешь отредактировать на свое усмотрение. Никогда не бойся экспериментировать, именно так обычно и учатся. Перейдем к основному модулю программы. Начинается он, как обычно, с подключения заголовочных файлов: Подключение заголовочных файлов Файл «defines.h» подключается самым первым, так как следующие хедеры могут требовать соответствующие константы, например, F_CPU. Далее идет «io.h» - стандартный заголовочный файл для работы с АВРками. Мы используем прерывания, поэтому подключаем и «interrupt.h». Хедер «hd44780.h» нужен для работы с дисплеем. В нем описаны прототипы функций обмена данными с контроллером HD44780. Рекомендую изучить их самостоятельно; посмотри также файл «hd44780.c», в котором описаны сами функции. Это и есть готовый модуль, о котором я писал выше. В статье нет места, чтобы рассказывать, как он работает, но на диске ты его найдешь. Открой даташит по дисплею, сравни с функциями и пойми, как они устроены. Определим глобальные переменные: Определяем глобальные переменные Массив «buf» – это буфер, куда складываем получаемую строку; «bufsize» – это длина строки (увеличиваем эту переменную с каждым полученным байтом). А «ready» - это просто флаг, который будет указывать, получена ли строка полностью. Но я не просто так выбрал именно этот пример! Тут есть один хитрый момент. Обрати внимание на директиву «volatile». Сейчас я тебе расскажу про очень популярную ошибку среди новичков, чтобы ты ее не повторял. Дело в том, что у компилятора Си очень хитрый оптимизатор, но им надо уметь пользоваться. Если в коде встречается условие, которое ну никак не выполнится, по мнению компилятора, то оптимизатор легко может вырезать его из программы. Такое условие, например, – изменение переменной, которая не меняется в коде программы. Но ведь эта глобальная переменная может изменяться по прерыванию! Вот директива «volatile» и указывает именно на то, что переменная может непроизвольно изменяться внешними силами. Далее я покажу пример именно такого кода. Опишем функцию-прерывание. Обработчик прерывания Здесь ты уже должен все понимать. Получаем один байт данных. Если это не символ завершения строки, то заносим его в массив и увеличиваем счетчик длины строки; иначе пишем в переменную «ready» единицу, что указывает основной программе, что строка получена полностью. Перейдем к самому главному – функция «main», с которой начинается выполнение программы: Основная подпрограмма
Сначала инициализация дисплея и USART, – код этих функций смотри на диске. Далее по коду идет главный бесконечный цикл «while(1)»; программа в микроконтроллере должна выполняться бесконечно, из нее нет выхода. Строка «return 0» в конце только для того, чтобы компилятор не ругался; фактически она никогда не будет выполнена. Разберемся, что же у нас так бесконечно выполняется. Строка «while (!ready);» не делает абсолютно ничего. Программа будет стоять на этом месте, пока «ready» равно нулю. Ты не забыл про наше прерывание? Именно оно изменит переменную, когда получит строку целиком. Если бы мы не использовали директиву «volatile», то программа на этом месте просто зависала бы. В таком случае компилятор считает, что переменная «ready» не может измениться сама собой. Итак, строка получена, и дальше никаких хитростей нет. Очищаем экран, возвращаем курсор и выводим текст. Обрати внимание, что перед каждой командой дисплею необходимо ожидать его готовности. Ну и в конце не забываем обнулить переменные. Прошивка готова! ФьюзыНадеюсь, ты уже знаешь что такое «фьюзы» (FUSES)? Это два байта в специальной памяти АВРки, где хранятся настройки. Эти байты можно изменить только с помощью программатора. В случае с мегой8 у меня младший и старший фьюзы равны, соответственно, 0xE4 и 0xD9. Это значит, что у нас включены следующие конфигурационные биты: CKSEL0, CKSEL1, CKSEL3, SUT0, SPIEN, BOOTSZ0, BOOTSZ1. Для их вычисления существует множество утилит, а их значения есть в даташите. Самое главное – это выбор тактовой частоты. В данном случае – 8МГц со встроенным источником синхронизации. ПроверяемКомпилируем и прошиваем микроконтроллер получившейся прошивкой. Подключаем готовое устройство к COM-порту компьютера. Очень надеюсь, что этот порт у тебя есть, иначе используй специальный PCI или USB контроллер (в качестве последнего можешь взять шнурок от мобильника). Думаю, что с такими простыми задачами ты справишься. Запускай на компе любой эмулятор терминала, настраивай его на соответствующий порт и скорость; в моем случае это 9600 бод, один стоповый бит, проверки четности нет, контроля передачи данных тоже нет. Сделал? Набирай на клавиатуре произвольный текст, жми <Enter> – набранное будет выведено на дисплей. Полагаю, ты уже понял, как это использовать. Например, в Linux достаточно простой команды вроде «echo “Hello world!” > /dev/ttyS0» для вывода соответствующей надписи. Ты легко сможешь написать скрипт для отображения загрузки процессора, загруженности канала… да чего угодно! Только учти, что в примере реализуется работа только с первой строкой. А еще в этих дисплеях очень необычная кодировка, поэтому русский текст надо конвертировать. Доверяю тебе самому решить эти проблемы. Наверняка, справишься. ИтогЦель статьи – дать понять, что если ты знаешь Си, но являешься полным чайником в микроконтроллерах, то тебе ничто не мешает уже сейчас легко писать простейшие программы. Я постарался преподнести тебе основы и показать парочку примеров. Многие, вероятно, будут критиковать меня и говорить, что проще подключить дисплей к LPT-порту. А что делать, если LPT-порта на компе нет? Или если это вообще не компьютер? О том, как такое возможно и зачем нужно, я расскажу тебе в следующий раз. RTFM!Никогда не ленись читать даташиты. В технической документации всегда подробно описано, как пользоваться соответствующим устройством. Там ты найдешь и регистры для АВРок, и команды для дисплея со всеми соответствующими примерами и описаниями. Это действительно очень подробная и ценная информация; осталось только согласовать ее с тем, что ты уже знаешь. Не бойся экспериментировать! ДискНа диске ты найдешь все исходники, а также сам WinAVR, с помощью которого они компилируется. |