Здравствуйте, уважаемые Хабражители!
В этой статье я хочу рассказать о том, как однажды решил начать программировать микроконтроллеры, что для этого понадобилось и что в итоге получилось.
Тема микроконтроллеров меня заинтересовала очень давно, году этак в 2001. Но тогда достать программатор по месту жительства оказалось проблематично, а о покупке через Интернет и речи не было. Пришлось отложить это дело до лучших времен. И вот, в один прекрасный день я обнаружил, что лучшие времена пришли не выходя из дома можно купить все, что мне было нужно. Решил попробовать. Итак, что нам понадобится:
1. Программатор
На рынке предлагается много вариантов - от самых дешевых ISP (In-System Programming) программаторов за несколько долларов, до мощных программаторов-отладчиков за пару сотен. Не имея большого опыта в этом деле, для начала я решил попробовать один из самых простых и дешевых - USBasp. Купил в свое время на eBay за $12, сейчас можно найти даже за $3-4. На самом деле это китайская версия программатора от Thomas Fischl . Что могу сказать про него? Только одно - он работает. К тому же поддерживает достаточно много AVR контроллеров серий ATmega и ATtiny. Под Linux не требует драйвера.Для прошивки надо соединить выходы программатора VCC, GND, RESET, SCK, MOSI, MISO с соответствующими выходами микроконтроллера. Для простоты я собрал вспомогательную схему прямо на макетной плате:
Слева на плате - тот самый микроконтроллер, который мы собираемся прошивать.
2. Микроконтроллер
С выбором микроконтроллера я особо не заморачивался и взял ATmega8 от Atmel - 23 пина ввода/вывода, два 8-битных таймера, один 16-битный, частота - до 16 Мгц, маленькое потребление (1-3.6 мА), дешевый ($2). В общем, для начала - более чем достаточно.Под Linux для компиляции и загрузки прошивки на контроллер отлично работает связка avr-gcc + avrdude. Установка тривиальная. Следуя инструкции , можно за несколько минут установить все необходимое ПО. Единственный ньюанс, на который следует обратить внимание - avrdude (ПО для записи на контроллер) может потребовать права супер-пользователя для доступа к программатору. Выход - запустить через sudo (не очень хорошая идея), либо прописать специальные udev права. Синтаксис может отличаться в разных версиях ОС, но в моем случае (Linux Mint 15) сработало добавление следующего правила в файл /etc/udev/rules.d/41-atmega.rules:
# USBasp programmer SUBSYSTEM=="usb", ATTR{idVendor}=="16c0", ATTR{idProduct}=="05dc", GROUP="plugdev", MODE="0666"
После этого, естественно, необходим перезапуск сервиса
service udev restart
Компилировать и прошивать без проблем можно прямо из командной строки (кто бы сомневался), но если проектов много, то удобнее поставить плагин и делать все прямо из среды Eclipse.
Под Windows придется поставить драйвер. В остальном проблем нет. Ради научного интереса попробовал связку AVR Studio + eXtreme Burner в Windows. Опять-таки, все работает на ура.
Начинаем программировать
Программировать AVR контроллеры можно как на ассемблере (AVR assembler), так и на Си. Тут, думаю, каждый должен сделать свой выбор сам в зависимости от конкретной задачи и своих предпочтений. Лично я в первую очередь начал ковырять ассемблер. При программировании на ассемблере архитектура устройства становится понятнее и появляется ощущение, что копаешься непосредственно во внутренностях контроллера. К тому же полагаю, что в особенно критических по размеру и производительности программах знание ассемблера может очень пригодиться. После ознакомления с AVR ассемблером я переполз на Си.После знакомства с архитектурой и основными принципами, решил собрать что-то полезное и интересное. Тут мне помогла дочурка, она занимается шахматами и в один прекрасный вечер заявила, что хочет иметь часы-таймер для партий на время. БАЦ! Вот она - идея первого проекта! Можно было конечно заказать их на том же eBay, но захотелось сделать свои собственные часы, с блэк… эээ… с индикаторами и кнопочками. Сказано - сделано!
В качестве дисплея решено было использовать два 7-сегментных диодных индикатора. Для управления достаточно было 5 кнопок - “Игрок 1” , “Игрок 2” , “Сброс” , “Настройка” и “Пауза” . Ну и не забываем про звуковую индикацию окончания игры. Вроде все. На рисунке ниже представлена общая схема подключения микроконтроллера к индикаторам и кнопкам. Она понадобится нам при разборе исходного кода программы:
Разбор полета
Начнем, как и положено, с точки входа программы - функции main . На самом деле ничего примечательного в ней нет - настройка портов, инициализация данных и бесконечный цикл обработки нажатий кнопок. Ну и вызов sei() - разрешение обработки прерываний, о них немного позже.Int main(void)
{
init_io();
init_data();
sound_off();
sei();
while(1)
{
handle_buttons();
}
return 0;
}
Рассмотрим каждую функцию в отдельности.
Void init_io()
{
// set output
DDRB = 0xFF;
DDRD = 0xFF;
// set input
DDRC = 0b11100000;
// pull-up resistors
PORTC |= 0b00011111;
// timer interrupts
TIMSK = (1< Настройка портов ввода/вывода происходит очень просто - в регистр DDRx (где x - буква, обозначающая порт) записивается число, каждый бит которого означает, будет ли соответствующий пин устройством ввода (соответствует 0) либо вывода (соответствует 1). Таким образом, заслав в DDRB и DDRD число 0xFF, мы сделали B и D портами вывода. Соответственно, команда DDRC = 0b11100000; превращает первые 5 пинов порта C во входные пины, а оставшиеся - в выходные. Команда PORTC |= 0b00011111; включает внутренние подтягивающие резисторы на 5 входах контроллера. Согласно схеме, к этим входам подключены кнопки, которые при нажатии замкнут их на землю. Таким образом контроллер понимает, что кнопка нажата. Далее следует настройка двух таймеров, Timer0 и Timer1. Первый мы используем для обновления индикаторов, а второй - для обратного отсчета времени, предварительно настроив его на срабатывание каждую секунду. Подробное описание всех констант и метода настройки таймера на определенноый интервал можно найти в документации к ATmega8. Обработка прерываний
ISR (TIMER0_OVF_vect)
{
display();
if (_buzzer > 0)
{
_buzzer--;
if (_buzzer == 0)
sound_off();
}
}
ISR(TIMER1_COMPA_vect)
{
if (ActiveTimer == 1 && Timer1 > 0)
{
Timer1--;
if (Timer1 == 0)
process_timeoff();
}
if (ActiveTimer == 2 && Timer2 > 0)
{
Timer2--;
if (Timer2 == 0)
process_timeoff();
}
}
При срабатывании таймера управление передается соответствующему обработчику прерывания. В нашем случае это обработчик TIMER0_OVF_vect, который вызывает процедуру вывода времени на индикаторы, и TIMER1_COMPA_vect, который обрабатывает обратный отсчет. Вывод на индикаторы
Void display()
{
display_number((Timer1/60)/10, 0b00001000);
_delay_ms(0.25);
display_number((Timer1/60)%10, 0b00000100);
_delay_ms(0.25);
display_number((Timer1%60)/10, 0b00000010);
_delay_ms(0.25);
display_number((Timer1%60)%10, 0b00000001);
_delay_ms(0.25);
display_number((Timer2/60)/10, 0b10000000);
_delay_ms(0.25);
display_number((Timer2/60)%10, 0b01000000);
_delay_ms(0.25);
display_number((Timer2%60)/10, 0b00100000);
_delay_ms(0.25);
display_number((Timer2%60)%10, 0b00010000);
_delay_ms(0.25);
PORTD = 0;
}
void display_number(int number, int mask)
{
PORTB = number_mask(number);
PORTD = mask;
}
Функция display использует метод динамической индикации. Дело в том, что каждый отдельно взятый индикатор имеет 9 контактов (7 для управления сегментами, 1 для точки и 1 для питания). Для управления 4 цифрами понадобилось бы 36 контактов. Слишком расточительно. Поэтому вывод разрядов на индикатор с несколькими цифрами организован по следующему принципу: Напряжение поочередно подается на каждый из общих контактов, что позволяет высветить на соответствующем индикаторе нужную цифру при помощи одних и тех же 8 управляющих контактов. При достаточно высокой частоте вывода это выглядит для глаза как статическая картинка. Именно поэтому все 8 питающих контактов обоих индикаторов на схеме подключены к 8 выходам порта D, а 16 управляющих сегментами контактов соединены попарно и подключены к 8 выходам порта B. Таким образом, функция display с задержкой в 0.25 мс попеременно выводит нужную цифру на каждый из индикаторов. Под конец отключаются все выходы, подающие напряжение на индикаторы (команда PORTD = 0;). Если этого не сделать, то последняя выводимая цифра будет продолжать гореть до следующего вызова функции display, что приведет к ее более яркому свечению по сравнению с остальными. Обработка нажатий
Void handle_buttons()
{
handle_button(KEY_SETUP);
handle_button(KEY_RESET);
handle_button(KEY_PAUSE);
handle_button(KEY_PLAYER1);
handle_button(KEY_PLAYER2);
}
void handle_button(int key)
{
int bit;
switch (key)
{
case KEY_SETUP: bit = SETUP_BIT; break;
case KEY_RESET: bit = RESET_BIT; break;
case KEY_PAUSE: bit = PAUSE_BIT; break;
case KEY_PLAYER1: bit = PLAYER1_BIT; break;
case KEY_PLAYER2: bit = PLAYER2_BIT; break;
default: return;
}
if (bit_is_clear(BUTTON_PIN, bit))
{
if (_pressed == 0)
{
_delay_ms(DEBOUNCE_TIME);
if (bit_is_clear(BUTTON_PIN, bit))
{
_pressed |= key;
// key action
switch (key)
{
case KEY_SETUP: process_setup(); break;
case KEY_RESET: process_reset(); break;
case KEY_PAUSE: process_pause(); break;
case KEY_PLAYER1: process_player1(); break;
case KEY_PLAYER2: process_player2(); break;
}
sound_on(15);
}
}
}
else
{
_pressed &= ~key;
}
}
Эта функция по очереди опрашивает все 5 кнопок и обрабатывает нажатие, если таковое случилось. Нажатие регистрируется проверкой bit_is_clear(BUTTON_PIN, bit) , т.е. кнопка нажата в том случае, если соответствующий ей вход соединен с землей, что и произойдет, согласно схеме, при нажатии кнопки. Задержка длительностью DEBOUNCE_TIME и повторная проверка нужна во избежание множественных лишних срабатываний из-за дребезга контактов. Сохранение статуса нажатия в соответствующих битах переменной _pressed используется для исключения повторного срабатывания при длительном нажатии на кнопку. Полный текст программы
#define F_CPU 4000000UL
#include Прототип был собран на макетной плате. AVR-микроконтроллеры предоставляют пользователю несколько различных интерфейсов для программирования. Это последовательное программирование при высоком напряжении, последовательное программирование при низком напряжении через SPI, параллельное программирование при высоком напряжении и программирование по интерфейсу JTAG. Первый тип программирования встречается только в моделях AVR семейства ATtiny, последний - доступен некоторым моделям старшего семейства. Модели ATmega с наиболее развитой периферией могут поддерживать до трех различных интерфейсов программирования. Подавляющее большинство AVR-микроконтроллеров обладают также способностью самопрограммирования, благодаря чему содержимое памяти программ можно модифицировать непосредственно из пользовательской программы. Кроме этого FLASH-память может быть перепрограммирована в режиме отладки через однопроводной интерфейс dW, имеющийся в ряде моделей ATmega и во всех новых моделях ATtiny. Программирование при высоком напряжении (параллельное и последовательное) требует значительного числа выводов микроконтроллера и дополнительного источника напряжения 12 В. По этой причине конструкция программаторов достаточно сложна. При высоковольтном программировании достигается наибольшая скорость записи и предоставляется максимальный доступ к ресурсам AVR. Чаще всего этот вид программирования применяется при крупносерийном заводском производстве. Интерфейс JTAG очень удобно использовать в тех случаях, когда необходимо вести программирование и отладку в одном цикле разработки. К сожалению JTAG имеется далеко не во всех моделях AVR, а фирменные программаторы стоят значительных денег. Низковольтное последовательное программирование через SPI, наиболее распространено. Это способ стоит признать основным при программировании AVR-микроконтроллеров. Его поддерживают все модели с ядром AVR, за исключением двух устаревших представителей младшего семейства ATtiny11x и ATtiny28x. В данном разделе будет приведено описание двух программаторов работающих в подобном режиме. Первый из них можно рекомендовать для быстрого старта. Он имеет простую конструкцию и работает под управлением популярной радиолюбительской программы . Второй, намного более совершенный, является функциональным аналогом AVR ISP фирмы ATMEL. Этот программатор интегрируется с и позволяет реализовать алгоритмы программирования с максимально возможной точностью. Для взаимодействия программатора с микроконтроллером при последовательном низковольтном программировании используется аппаратный модуль SPI. Это очень практичное решение, позволяющее использовать минимальное число выводов и изменять алгоритмы работы устройства предварительно запаянного на плату. В виду последней причины программирование через SPI называют также еще внутрисхемным программированием или ISP (In System Programming). Внутрисхемное программирование потребует задействовать у микроконтроллера в общей сложности 5 выводов. Это 3 линии модуля SPI (MISO, MOSI, SCK), вывод RESET и общий провод GND. В моделях семейства ATmega, имеющих на борту 64 и более кбайт FLASH-памяти, вместо MISO, MOSI используются выводы PDO и PDI, соответственно. В случае если программатор и микроконтроллер получают питание от одного источника, то дополнительно понадобится также вывод VCC, соединяющий шины питания. Перевод микроконтроллера в режим программирования осуществляется подачей низкого логического уровня на линию RESET. Длина шлейфа, соединяющего программатор с устройством, не должна превышать 15…20 см. На рис.1а показана схема соединения программатора с AVR-микроконтроллером, при программировании через ISP. Для более надежной работы последовательно линиям MISO, MOSI, SCK рекомендуется включать сопротивления небольшого номинала. Напряжение питания программатора и устройства не должно иметь больших различий. Внутрисхемное программирование двух и более микроконтроллеров также возможно (рис.1б). В этом случае необходимо помнить об одном важном условии: в момент программирования на шине должен находиться только один активный микроконтроллер. Поэтому при проектировании платы заранее нужно предусмотреть переключатели (джампера J1, J2 на рис.1б), с помощью которых можно выборочно подавать напряжение на каждый программируемый микроконтроллер. После программирования модуль SPI или линии ввода-вывода, совпадающие с MISO, MOSI и SCK, могут быть использованы по своему прямому назначению. При внутрисхемном программировании для чтения и записи доступны FLASH–память программ, EEPROM-память данных, биты защиты и управляющие FUSE–биты. Кроме этого могут быть считаны калибровочные ячейки и ячейки идентификатора. Изменения некоторых FUSE–битов необходимо производить с большой осторожностью. Особенно если демонтировать микроконтроллер уже не представляется возможным. Главным образом это касается битов RSTDISBL и DWEN (если таковые имеется). Сброс любого из них в дальнейшем сделает невозможным использование линии RESET микроконтроллера. При RSTDISBL=0 вывод RESET настраивается как линия порта ввода-вывода, а при DWEN=0 – вход RESET служит однопроводным отладочным интерфейсом dW. Естественно, что в обоих случаях работа программатора с микроконтроллером будет заблокирована. Кроме того во время внутрисхемного программирования микроконтроллеры AVR должны работать от собственного источника тактовой частоты, выбор которого осуществляется битами CKSEL3:CKSEL0. Если их настройка произведена некорректно (например, вместо внутреннего RC-генератора, выбран внешний кварцевый резонатор), то устройство может вообще отказаться работать. Еще один FUSE–бит, о котором следует помнить, - это SPIEN. SPIEN не доступен во время последовательного низковольтного программирования. Однако его установка при программировании в каком-либо другом режиме запретит работу модуля SPI. Напомним, что активизированным FUSE–битам соответствует состояние лог.0. Всем привет. Как и обещал, с сегодняшнего дня начинаем изучать программирования AVR микроконтроллеров (на примере Atmega8). Тем же читателям, которым интересно программирование платы ардуино, не волнуйтесь, статьи по данному направлению будут продолжаться 🙂 .
Можно задать логичный вопрос, почему из ряда других микроконтроллеров (далее — МК) в качестве подопытного выбран именно МК AVR
. На это есть несколько причин: Как говорил ранее, в качестве подопытного будем использовать МК Atmega8
. Почему именно его? Данный микроконтроллер может похвастаться наличием 3 портов ввода/вывода. Кроме этого он довольно дешевый. Под портами, понимают шины данных, которые могут работают в двух противоположных направлениях (то бишь на вывод и на ввод). У Atmega8
3 порта. Порт B состоит из 8 ножек-выводов (нумерация 0,1,2,3,4,5,6,7). Порт С состоит из 7 ножек-выводов (нумерация 0,1,2,3,4,5,6). Порт D состоит из 8 ножек-выводов (нумерация 0,1,2,3,4,5,6,7). Запитывать микроконтроллер можно от 3,3 и 5 В. При напряжении питания 5 В максимальная частота тактирования составляет 16 МГц, а при напряжении питания 3,3 В – максимальная частота тактирования 8 МГц. Пока не будем заморачиваться относительно частот тактирования. Питания подаётся на 7 ножку-вывод, а «земля» подводится к 8 ножке. Скачивается бесплатно. Скачали, установили, запустили 🙂 Первое, с чего следует начать знакомство с Atmel Studio – это создание проекта. Выбираем File -> new -> project . Откроется окно выбора. Выбираем папку «Browse», в которой будем сохранять написанные проекты. Папку для проектов создал заранее. Присваиваем имя проекту, в моём случае lesson_avr_1 Обратите внимание на галочку «create directory for solution». Если отметка стоит, то в той папке, которую мы выбрали для сохранения проектов, будет создана отдельная папка под текущий проект. На этом всё – проект создан. Займемся настройкой созданного нами проекта. Нажимаем Projest -> lesson_avr_1 properties или (alt+F7) Переходим на вкладку Tool. Выбираем – симулятор. Совершенные нами действия сделают возможным отлаживать написанный код. Сохраняем изменения. Можно сохранить изменения в одном (текущем) файле или же во всех файлах проекта сразу. Закрываем настройки. Министерство
образования и науки Российской Федерации Государственное
образовательное учреждение высшего
профессионального образования «САНКТ-ПЕТЕРБУРГСКИЙ
ГОСУДАРСТВЕННЫЙ МОРСКОЙ ТЕХНИЧЕСКИЙ
УНИВЕРСИТЕТ» Е. В. Коротицкий, Ю. Е.
Коротицкая Учебное пособие Санкт-Петербург Универсальный язык С
был разработан как инструмент для
написания операционной среды UNIX. Язык С поддерживает
процедурно-ориентированную парадигму
программирования, т.е. парадигма –
взаимосвязанный набор процедур. Для языка Си характерны
лаконичность, современный набор
конструкций управления потоком
выполнения, структур данных и
обширный набор операций. Препроцессор
(макропроцессор) - это составная часть
языка Си, которая обрабатывает исходный
текст программы до того, как он пройдет
через компилятор. Препроцессор читает
строки текста и выполняет действия,
определяемые командными строками. Если
первым символом в строке, отличным от
пробела, является символ #, то такая
строка рассматривается препроцессором
как командная. Командные строки называются
директивами
препроцессора. Препроцессор компилятора
CodeVisionAVRимеет несколько
директив. В Табл. 1 даётся их краткое
описание. Табл. 1 –Директивы
препроцессора компилятора CodeVisionAVR Директива Назначение Используется
для включения в программу другого
файла Используется
для замены одних лексических единиц
языка Си на другие, а также для генерации
макросов Используется
для отмены действия директивы #define Используются
для условной компиляции Используется
для изменения встроенных макросов
_LINE_и_FILE_ Позволяет
остановить компиляцию и отобразить
сообщение об ошибках Используются
для включения в исходную программу
ассемблерного кода Разрешает
специальные директивы компилятора ВСЕ директивы
препроцессора начинаются со знака #.
После директив препроцессора точка с
запятой НЕ СТАВИТСЯ. Пример:
Директива #
include
#include
"имя_файла"
и #include <имя_файла> Имя_файла
состоит
из имени файла. Директива #
include
широко используется для включения в
программу так называемых заголовочных
файлов (файлы с расширением.
h
),
содержащих определения периферийных
устройств и векторов прерываний
используемого микроконтроллера,
прототипы библиотечных функций, прототипы
функций, определённых пользователем,
и т. д. #include "имя_файла.h" Директива #
define
служит для замены часто использующихся
одних лексических единиц языка Си
(констант, ключевых слов, операторов
или выражений) на другие, так называемыеидентификаторы.
Идентификаторы,
заменяющие текстовые или числовые
константы, называютименованными
константами.
Идентификаторы,
заменяющие фрагменты программ, называютмакроопределениями,
причём
макроопределения могут иметь аргументы. Директива #
define
имеет две синтаксические формы: #define идентификатор
текст #define идентификатор
(список параметров) текст Перед компиляцией
программы препроцессор в соответствии
с директивой #
define
заменит все идентификаторы, встречающиеся
в программе, на соответствующий им
текст. Пример:
#define А 15 #define В (А+20) //
Эти директивы заменят в тексте программы каждый идентификатор
А на число 15, а каждый идентификатор В
на выражение (15+20) вместе с окружающими
его скобками. Пример:
#define X(a,b,c) ((а)*(b)-(с)) //
Препроцессор в соответствии с этой
директивой заменит фрагмент Y=X(k+m,k-m,n);
на фрагмент Я не раз и не два говорил, что изучение МК надо начинать с ассемблера. Этому был посвящен целый курс на сайте (правда он не очень последовательный, но постепенно я его причесываю до адекватного вида) . Да, это сложно, результат будет не в первый день, но зато ты научишься понимать что происходит у тебя в контроллере. Будешь знать как это работает, а не по обезьяньий копировать чужие исходники и пытаться понять почему оно вдруг перестало работать. Кроме того, Си намного проще натворить быдлокода, который вылезет вилами в самый неподходящий момент. К сожалению все хотят результат немедленно. Поэтому я решил пойти с другой стороны — сделать обучалку по Си, но с показом его нижнего белья. Хороший программист-эмбеддер всегда крепко держит свою железку за шкварник, не давая ей ни шагу ступить без разрешения. Так что будет вначале Си код, потом то что родил компилятор и как все это работает на самом деле:) С другой стороны у Си сильная сторона это переносимость кода. Если, конечно, писать все правильно. Разделяя алгоритмы работы и их железные реализации в разные части проекта. Тогда для переноса алгоритма в другой МК достаточно будет переписать только интерфейсный слой, где прописано все обращение к железу, а весь рабочий код оставить как есть. И, конечно же, читаемость. Сишный исходник проще понять с первого взгляда (хотя.. мне, например, уже пофигу на что фтыкать — хоть си, хоть асм:)), но, опять же, если правильно все написать. Этим моментам я тоже буду уделять внимание. В качестве подопытной железки на которой будет ставиться львинная доля всех примеров будет моя отладочная плата . Первая программа на Си для AVR
Выбор компилятора и установка среды
Вторым идет WinAVR GCC
— мощный оптимизирующий компилятор. Полный опенсорц, кроссплатформенный, в общем, все радости жизни. Еще он отлично интегрируется в AVR Studio позволяя вести отладку прямо там, что адски удобно. В общем, я выбрал его. Также есть CodeVision AVR C
— очень популярный компилятор. Стал популярен в связи со своей простотой. Рабочую программу в нем получить можно уже через несколько минут — мастер стартового кода этом сильно способствует, штампуя стандартыне инициализации всяких уартов. Честно говоря, я как то с подозрением к нему отношусь — как то раз приходилось дизасмить прогу написаную этим компилером, каша какая то а не код получалась. Жуткое количество ненужных телодвижений и операций, что выливалось в неслабый обьем кода и медленное быстродействие. Впрочем, возможно тут была ошибка в ДНК писавшего исходную прошивку. Плюс он хочет денег. Не так много как IAR, но ощутимо. А в деморежиме дает писать не более чем 2кб кода. Еще есть Image Craft AVR C
и MicroC
от микроэлектроники. Ни тем ни другим пользоваться не приходилось, но вот SWG
очень уж нахваливает MicroPascal
, мол жутко удобная среда программирования и библиотеки. Думаю MicroC не хуже будет, но тоже платный. Как я уже сказал, я выбра WinAVR
по трем причинам: халявный, интегрируется в AVR Studio и под него написана просто прорва готового кода на все случаи жизни. Так что качай себе инсталяху WinAVR с и AVR Studio. Далее вначале ставится студия, потом, сверху, накатывается WinAVR и цепляется к студии в виде плагина. Настоятельно рекомендую ставить WinAVR по короткому пути, что то вроде C:\WinAVR тем самым ты избежишь кучи проблем с путями. Cоздание проекта
Открывается рабочее поле с пустым *.c файлом. Теперь не помешает настроить отображение путей в закладках студии. Для этого слазь по адресу: Настройка проекта
Тыкай в кнопку с шестеренкой. Это настройки твоего проекта, а точнее настройки автоматической генерации make файла. На первой странице надо всего лишь вписать частоту на которой будет работать твой МК. Это зависит от фьюз битов, так что считаем что частота у нас 8000000Гц. Следующим шагом будет настройка путей. Первым делом добавь туда директорию твоего проекта — будешь туда подкладывать сторонние библиотеки. В списке появится путь «.\» Make файл сгенерирован, его ты можешь поглядеть в папке default в своем проекте, просто пробегись глазами, посмотри что там есть. На этом пока все. Жми везде ОК и переходи в исходник. Постановка задачи
Работать будет так: Собираем схему
Получится, в итоге вот такая схема: Подключение остальных выводов, питания, сброса не рассматриваю, оно стандартное Пишем код
Сразу оговорюсь, что я не буду углубляться конкретно в описание самого языка Си. Для этого существует просто колоссальное количество материала, начиная от классики «Язык программирования Си» от K&R и заканчивая разными методичками. Одна такая метода нашлась у меня в загашнике, я когда то именно по ней изучал этот язык. Там все кратко, понятно и по делу. Я ее постепенно верстаю и перестаскиваю на свой сайт. Там правда еще не все главы перенесены, но, думаю, это ненадолго. Вряд ли я опишу лучше, поэтому из учебного курса, вместо подробного разьяснения сишных тонкостей, я буду просто давать прямые линки на отдельные страницы этой методички. Добавляем библиотеки.
#include Этот файл находится в папке WinAVR
и в нем содержится описание всех регистров и портов контроллера. Причем там все хитро, с привязкой к конкретному контроллеру, который передается компилятором через make
файл в параметре MCU
и на основании этой переменной в твой проект подключается заголовочный файл с описанием адресов всех портов и регистров именно на этот контроллер. Во как! Без него тоже можно, но тогда ты не сможешь использовать символические имена регистров вроде SREG или UDR и придется помнить адрес каждого вроде «0xC1», а это голову сломать. Сама же команда #include <имя файла>
позволяет добавить в твой проект содержимое любого текстового файла, например, файл с описанием функций или кусок другого кода. А чтобы директива могла этот файл найти мы и указывали пути к нашему проекту (директория WinAVR там уже по дефолту прописана). Главная функция.
Все значения передаваемые и возвращаемые обязаны быть какого либо типа, в зависимости от данных. Любая программа на Си должна содержать функцию main
как точку входа в главную прогрмму, иначе это нифига не Си:). По наличию main в чужом исходнике из миллиона файлов можно понять, что это и есть головная часть программы откуда начинается все. Вот и зададим: int main(void)
{
return 0;
} Все, первая простейшая программа написана, не беда что она ничего не делает, мы же только начали. Разберем что же мы сделали. Конечно, в микроконтроллере main
ничего вернуть в принципе не может и по идее должна быть void main(void)
, но GCC изначально заточен на PC и там программа может вернуть значение операционной системе по завершении. Поэтому GCC на void main(void)
ругается Warning’ом. Это не ошибка, работать будет, но я не люблю варнинги. void
это тип данных которые мы передаем в функцию, в данном случае main
также не может ничего принять извне, поэтом void
— пустышка. Заглушка, применяется тогда когда не надо ничего передавать или возвращать. Вот такие вот { }
фигурные скобочки это программный блок, в данном случае тело функции main
, там будет распологаться код. return
— это возвращаемое значение, которое функция main отдаст при завершении, поскольку у нас int, то есть число то вернуть мы должны число. Хотя это все равно не имеет смысла, т.к. на микроконтроллере из main нам выходить разве что в никуда. Я возвращаю нуль. Ибо нефиг. А компилятор обычно умный и на этот случай код не генерит. Сделали, поехали дальше. Добавим переменную, она нам не особо нужна и без нужны вводить переменные не стоит, но мы же учимся. Если переменные добавляются внутри тела функции — то они локальные и существуют только в этой функции. Когда из функции выходишь эти переменные удаляются, а память ОЗУ отдается под более важные нужды. . int main(void)
{
unsigned char i;
return 0;
} unsigned
значит беззнаковый. Дело в том, что в двоичном представлении у нас старший бит отводится под знак, а значит в один байт (char) влазит число +127/-128, но если знак отбросить то влезет уже от 0 до 255. Обычно знак не нужен. Так что unsigned
. Теперь надо проинициализировать порты и UART
. Конечно, можно взять и подключить библиотеку и вызвать какой нибудь UartInit(9600); но тогда ты не узнаешь что же произошло на самом деле. Делаем так: int main(void)
{
unsigned char i;
#define XTAL 8000000L
#define baudrate 9600L
#define bauddivider (XTAL/(16*baudrate)-1)
#define HI(x) ((x)>>8)
#define LO(x) ((x)& 0xFF)
UBRRL = LO(bauddivider);
UBRRH = HI(bauddivider);
UCSRA = 0;
UCSRB = 1< Страшна? На самом деле реалного кода тут всего пять последних строк. Все что #define
это макроязык препроцессора. Почти та же ботва, что и в Ассемблере, но синтаксис несколько иной. Они облегчат твои рутинные операции по вычислении нужных коэффициентов. В первой строке мы говорим что вместо XTAL
можно смело подставлять 8000000, а L
— указание типа, мол long — это тактовая частота процессора. То же самое baudrate
— частота передачи данных по UART. bauddivider
уже сложней, вместо него будет подставлятся выражение вычисленное по формуле из двух предыдущих. Так что все что сделано как #define
можно смело выкинуть, а нужные числа подсчитать на калькуляторе и сразу же вписать их в строки UBBRL = …. и UBBRH = ….. Можно. Но! Делать этого КАТЕГОРИЧЕСКИ НЕЛЬЗЯ
! Работать будет и так и эдак, но у тебя в программе появятся так называемые магические числа
— значения взятые непонятно откуда и непонятно зачем и если ты через пару лет откроешь такой проект то понять что это за значения будет чертовски трудно. Да и сейчас, захочешь ты изменить скорость, или поменяешь частоту кварца и все придется пересчитывать заново, а так поменял пару циферок в коде и все само. В общем, если не хочешь прослыть быдлокодером, то делай код таким, чтобы он легко читался, был понятен и легко модифицировался. Дальше все просто:
Запись вида 1< Готово, пора бы посмотреть что получилось. Жми на компиляцию и запуск эмуляции (Ctrl+F7). Отладка
Дело в том, что изначально, на самом деле, она стояла на строке UBRRL = LO(bauddivider); Ведь то что у нас в define это не код, а просто предварительные вычисления, вот симулятор немного и затупил. Но теперь он осознал, первая инструкция выполнена и если ты залезешь в дерево I/O View
, в раздел USART и поглядишь там на байт UBBRL то увидишь, что там значение то уже есть! 0х33. Сделай еще один шаг. Погляди как изменится содержимое другого регистра. Так прошагай их все, обрати внимание на то, что все указаные биты выставляются как я тебе и говорил, причем выставляются одновременно для всего байта. Дальше Return дело не пойдет — программа кончилась. Вскрытие
Вначале будет ботва из серии: 00000000: 940C002A JMP 0x0000002A Jump
+00000002: 940C0034 JMP 0x00000034 Jump
+00000004: 940C0034 JMP 0x00000034 Jump
+00000006: 940C0034 JMP 0x00000034 Jump
+00000008: 940C0034 JMP 0x00000034 Jump
+0000000A: 940C0034 JMP 0x00000034 Jump
+0000000C: 940C0034 JMP 0x00000034 Jump
+0000000E: 940C0034 JMP 0x00000034 Jump
+00000010: 940C0034 JMP 0x00000034 Jump
+00000012: 940C0034 JMP 0x00000034 Jump
+00000014: 940C0034 JMP 0x00000034 Jump
+00000016: 940C0034 JMP 0x00000034 Jump
+00000018: 940C0034 JMP 0x00000034 Jump
+0000001A: 940C0034 JMP 0x00000034 Jump
+0000001C: 940C0034 JMP 0x00000034 Jump
+0000001E: 940C0034 JMP 0x00000034 Jump
+00000020: 940C0034 JMP 0x00000034 Jump
+00000022: 940C0034 JMP 0x00000034 Jump
+00000024: 940C0034 JMP 0x00000034 Jump
+00000026: 940C0034 JMP 0x00000034 Jump
+00000028: 940C0034 JMP 0x00000034 Jump Это таблица векторов прерываний. К ней мы еще вернемся, пока же просто посмотри и запомни, что она есть. Первая колонка — адрес ячейки флеша в которой лежит команда, вторая код команды третья мнемоника команды, та самая ассемблерная инструкция, третья операнды команды. Ну и автоматический коммент. 0000002B: BE1F OUT 0x3F,R1 Out to I/O location Запись этого нуля по адресу 0x3F, Если ты поглядишь в колонку I/O view, то ты увидишь что адрес 0x3F это адрес регистра SREG — флагового регистра контроллера. Т.е. мы обнуляем SREG, чтобы запустить программу на нулевых условиях. 0000002C: E5CF LDI R28,0x5F Load immediate
+0000002D: E0D4 LDI R29,0x04 Load immediate
+0000002E: BFDE OUT 0x3E,R29 Out to I/O location
+0000002F: BFCD OUT 0x3D,R28 Out to I/O location Это загрузка указателя стека. Напрямую грузить в I/O регистры нельзя, только через промежуточный регистр. Поэтому сначала LDI в промежуточный, а потом оттуда OUT в I/O. О стеке я тоже еще расскажу подробней. Пока же знай, что это такая динамическая область памяти, висит в конце ОЗУ и хранит в себе адреса и промежуточные переменные. Вот сейчас мы указали на то, откуда у нас будет начинаться стек. 00000032: 940C0041 JMP 0x00000041 Jump Прыжок в сааааамый конец программы, а там у нас запрет прерываний и зацикливание наглухо само на себя: 00000041: 94F8 CLI Global Interrupt Disable
+00000042: CFFF RJMP PC-0x0000 Relative jump Это на случай непредвиденых обстоятельств, например выхода из функции main. Из такого зацикливания контроллер можно вывести либо аппаратным сбросом, либо, что вероятней, сбросом от сторожевой собаки — watchdog. Ну или, как я говорил выше, подправить это мест в хекс редакторе и ускакать куда нам душе угодно. Также обрати внимание на то, что бывает два типа переходов JMP и RJMP первый это прямой переход по адресу. Он занимает четыре байта и может сделать прямой переход по всей области памяти. Второй тип перехода — RJMP — относительный. Его команда занимает два байта, но переход он делает от текущего положения (адреса) на 1024 шага вперед или назад. И в его параметрах указывается смещение от текущей точки. Используется чаще, т.к. занимает в два раза меньше места во флеше, а длинные прееходы нужны редко. 00000034: 940C0000 JMP 0x00000000 Jump А это прыжок в самое начало кода. Перезагрузка своего рода. Можешь проверить, все вектора прыгают сюда. Из этого вывод — если ты сейчас разрешишь прерывания (они по дефолту запрещены) и у тебя прерывание пройзойдет, а обработчика нет, то будет программный сброс — программу кинет в самое начало. Функция main. Все аналогично, даже можно и не описывать. Посмотри только что в регистры заносится уже вычисленное число. Препроцессор компилятора рулит!!! Так что никаких «магических» чисел! 00000036: E383 LDI R24,0x33 Load immediate
+00000037: B989 OUT 0x09,R24 Out to I/O location
15: UBRRH = HI(bauddivider);
+00000038: BC10 OUT 0x20,R1 Out to I/O location
16: UCSRA = 0;
+00000039: B81B OUT 0x0B,R1 Out to I/O location
17: UCSRB = 1< А вот тут косяк: 0000003E: E080 LDI R24,0x00 Load immediate
+0000003F: E090 LDI R25,0x00 Load immediate
+00000040: 9508 RET Subroutine return Спрашивается, для чего это компилятор добавляет такую ботву? А это не что иное, как Return 0, функцию то мы определили как int main(void) вот и просрали еще целых четыре байта не пойми на что:) А если сделать void main(void) то останется только RET, но появится варнинг, что мол у нас функция main ничего не возвращает. В общем, поступай как хошь:) Сложно? Вроде бы нет. Пощелкай пошаговое исполнение в режиме дизассемблера и позырь как процессор выполняет отдельные инструкции, что при этом происходит с регистрами. Как происходит перемещение по командам и итоговое зацикливание. Продолжение следует через пару дней … Offtop:
Функции обработки нажатий достаточно тривиальны и полагаю, что в дополнительных комментариях не нуждаются. Особенности последовательного низковольтного программирования
внутрисхемном программировании одного микроконтроллера
Рис.1а Схема подключения программатора при
внутрисхемном программировании 2-х и более микроконтроллеровОсновы языка си для микроконтроллеров avr
1. Основы языка Си для микроконтроллеров avr
Препроцессор языка Cи его команды
Директива #include
Директивы #define, #undef
Для AVR существует множество разных компиляторов Си:
В первую очередь это IAR AVR C
— почти однозначно признается лучшим компилятором для AVR, т.к. сам контроллер создавался тесном сотрудничистве Atmel и спецов из IAR. Но за все приходится платить. И этот компилятор мало того, что является дорогущим коммерческим софтом, так еще обладает такой прорвой настроек, что просто взять и скомпилить в нем это надо постраться. У меня с ним правда не срослось дружбы, проект загнивал на странных ошибках на этапе линковки (позже выяснил, что это был кривой кряк).
Кряк конечно есть, но если уж воровать, так миллион, в смысле IAR:)
Итак, студия поставлена, Си прикручен, пора бы и попробовать что нибудь запрограммировать. Начнем с простого, самого простого. Запускай студию, выбирай там новый проект, в качестве компилятора AVR GCC и вписывай название проекта.
Меню Tools — Options — General — FileTabs и выбираем в выпадающем списке «Filename Only». Иначе работать будет невозможно — на вкладке будет полный путь файла и на экране будет не более двух трех вкладок.
Вообще, классическим считается создание make файла в котором бы были описаны все зависимости. И это, наверное, правильно. Но мне, выросшему на полностью интегрированных IDE вроде uVision
или AVR Studio
этот подход является глубоко чуждым. Поэтому буду делать по своему, все средствами студии.
Также обрати внимание на строку оптимизации. Сейчас там стоит -Os это оптимизация по размеру. Пока оставь как есть, потом можешь попробовать поиграться с этим параметром. -O0 это отстутсвие оптимизации вообще.
Чистый лист так и подмывает воплотить какую нибудь хитрую задумку, так как банальное мигание диодом уже не вставляет. Давай уж сразу брать быка за рога и реализуем связь с компом — это первым делом что я делаю.
При приходе по COM порту единички (код 0х31) будем зажигать диодик, а при приходе нуля (код 0х30) гасить. Причем сделано будет все на прерываниях, а фоновой задачей будет мигание другого диода. Простенько и со смыслом.
Нам надо соединить модуль USB-USART конвертера с выводами USART микроконтроллера. Для этого берем перемычку из двух проводков и накидывам на штырьки крест накрест. То есть Rx контроллера соединяем с Tx конвертера, а Tx конвертера с Rx контроллера.
Первым делом мы добавляем нужные библиотеки и заголовки с определениями. Ведь Си это универсальный язык и ему надо обьяснить что мы работаем именно с AVR, так что вписывай в исходник строку: 1
#include
Программа на языке Си вся состоит из функций. Они могут быть вложенными и вызываться друг из друга в любом порядке и разными способами. Каждая функция имеет три обязательных параметра: 1
2
3
4
5
int
main(void
)
{
return
0
;
}
int
это тип данных которая функция main возвращает.
Хотя, если извратиться, то из main
на МК выйти можно — например вывалиться в секцию бутлоадера и исполнить ее, но тут уже потребуется низкоуровневое ковыряние прошивки, чтобы подправить адреса перехода. Ниже ты сам увидишь и поймешь как это сделать. Зачем? Вот это уже другой вопрос, в 99.999% случаев это нафиг не надо:) 1
2
3
4
5
6
int
main(void
)
{
unsigned
char
i;
return
0
;
}
i
— это всего лишь имя переменной. Не более того. 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int
main(void
)
{
unsigned
char
i;
#define XTAL 8000000L
#define baudrate 9600L
#define bauddivider (XTAL/(16*baudrate)-1)
#define HI(x) ((x)>>8)
#define LO(x) ((x)& 0xFF)
UBRRL =
LO(bauddivider)
;
UBRRH =
HI(bauddivider)
;
UCSRA =
0
;
UCSRB =
1
<<
RXEN|
1
<<
TXEN|
1
<<
RXCIE|
0
<<
TXCIE;
UCSRC =
1
<<
URSEL|
1
<<
UCSZ0|
1
<<
UCSZ1;
}
Ну, а LO
и HI
из этого результата возьмут младший и старший байты, т.к. в один байт оно явно может не влезть. В HI
делается сдвиг икса (входной параметр макроса) восемь раз в вправо, в результате от него останется только старший байт. А в LO
мы делаем побитовое И с числом 00FF, в результате останется только младший байт.
Все эти «UBRRL и Со» это регистры конфигурации UART передатчика с помощью которого мы будем общаться с миром. И сейчас мы присвоили им нужные значения, настроив на нужную скорость и нужный режим.
Пробежали всякие прогресс бары, студия переменилась и возле входа в функцию main появилась желтая стрелочка. Это то где процессор в текущий момент, а симуляция на паузе.
Теперь сбрось симуляцию в ноль. Нажми там Reset (Shift+F5)
. Открывай дизассемблированный листинг, сейчас ты увидишь что происходит в контроллере в самом деле. View -> Disassembler
. И не ЫЫАААА!!! Ассемблер!!! УЖОС!!! А НАДО. Чтобы потом, когда что то пойдет не так, не тупил в код и не задавал ламерских вопросах на форумах, а сразу же лез в потроха и смотрел где у тебя затык. Ничего там страшного нет. 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+00000000: 940C002A JMP 0x0000002A Jump
+00000002: 940C0034 JMP 0x00000034 Jump
+00000004: 940C0034 JMP 0x00000034 Jump
+00000006: 940C0034 JMP 0x00000034 Jump
+00000008: 940C0034 JMP 0x00000034 Jump
+0000000A: 940C0034 JMP 0x00000034 Jump
+0000000C: 940C0034 JMP 0x00000034 Jump
+0000000E: 940C0034 JMP 0x00000034 Jump
+00000010: 940C0034 JMP 0x00000034 Jump
+00000012: 940C0034 JMP 0x00000034 Jump
+00000014: 940C0034 JMP 0x00000034 Jump
+00000016: 940C0034 JMP 0x00000034 Jump
+00000018: 940C0034 JMP 0x00000034 Jump
+0000001A: 940C0034 JMP 0x00000034 Jump
+0000001C: 940C0034 JMP 0x00000034 Jump
+0000001E: 940C0034 JMP 0x00000034 Jump
+00000020: 940C0034 JMP 0x00000034 Jump
+00000022: 940C0034 JMP 0x00000034 Jump
+00000024: 940C0034 JMP 0x00000034 Jump
+00000026: 940C0034 JMP 0x00000034 Jump
+00000028: 940C0034 JMP 0x00000034 Jump
Так вот, если ты посмотришь, то тут сплошные переходы. А код команды JMP четырех байтный, в нем содержится адрес перехода, записанный задом наперед — младший байт по младшему адресу и код команды перехода 940C 1
2
3
4
+0000002C: E5CF LDI R28,0x5F Load immediate
+0000002D: E0D4 LDI R29,0x04 Load immediate
+0000002E: BFDE OUT 0x3E,R29 Out to I/O location
+0000002F: BFCD OUT 0x3D,R28 Out to I/O location
1
2
+00000041: 94F8 CLI Global Interrupt Disable
+00000042: CFFF RJMP PC-0x0000 Relative jump
1
+00000034: 940C0000 JMP 0x00000000 Jump
1
2
3
4
5
6
7
8
9
10
11
12
< 1
2
3
+0000003E: E080 LDI R24,0x00 Load immediate
+0000003F: E090 LDI R25,0x00 Load immediate
+00000040: 9508 RET Subroutine return
Alexei78
сварганил плагинчик для файрфокса облегчающий навигацию по моему сайту и форуму.
Обсуждение и скачивание,