Простая программа для AVR микроконтроллера на языке Си
В прошлой статье мы разобрали строение программы на AVR Ассемблере, собрали несложную схему и выполнили прошивку микроконтроллера. К микроконтроллеру были подключены два светодиода, которые мы заставили попеременно мигать.
Здесь мы разберем пример программы для AVR микроконтроллера на языке Си (C), которая будет использовать ту же принципиальную схему что и в примере с программой на Ассемблере, так что для работы нам пригодится тот-же макет что и в прошлой статье. Мигать светодиоды мы заставим не просто попеременно, а немножко по другому и с дополнительными задержками по времени.
Содержание:
- Кратко о языке программирования Си
- Исходный код программы на языке Си
- Компиляция и прошивка программы в МК
- Документация по языку Си и AVR Си
- Заключение
Кратко о языке программирования Си
Язык Си является компилируемым статически типизированным языком программирования, который был разработан сотрудниками Bell Labs - Кеном Томпсоном (Ken Thompson) и Деннисом Ритчи (Dennis MacAlistair Ritchie) в начале 1970-х годов.
Си - универсальный язык программирования с современным набором операторов и типов, который также позволяет работать напрямую с памятью, адресами и минимальными единицами данных.
Изначально язык Си использовался в операционной системе Unix для написания приложений и самого ядра ОС. Позже он был портирован и на другие платформы, что принесло ему очень широкую популярность.
При разработке программ на языке Си для AVR микроконтроллеров используется набор библиотек avr-libc и компилятор avr-gcc, с установкой которых в Linux мы уже разобрались в одной из прошлых статей.
Исходный код программы на языке Си
Вполне может быть что вы никогда не писали программ на языке Си - в этом нет ничего страшного. Для того чтобы написать первую программу на AVR C и провести с ней эксперименты вполне достаточно базовых знаний по работе с консолью в Linux. Позже вы сами сможете найти недостающую информации и изучить все необходимое самостоятельно.
Приведенный ниже код программы на Си будет выполнять следующие действия (алгоритм):
- Зажечь светодиод 1 и погасить его с небольшой задержкой (два раза подряд);
- Выполнить более длительную задержку;
- Зажечь светодиод 2 и погасить его с небольшой задержкой (два раза подряд);
- Начать все сначала.
Вот исходный код программы который работает по приведенному выше алгоритму:
/* Светодиодная мигалка на микроконтроллере ATmega8
* https://ph0en1x.net
*/
#define F_CPU 1000000UL // укажем компилятору частоту ЦПУ
#include <avr/io.h> // Подключим файл io.h
#include <util/delay.h> // Подключим файл delay.h
void main(void) { // начало программы
// -- установим параметры --
int delay_ms_1 = 100; // задержка для светодиода
int delay_ms_2 = 300; // задержка между светодиодами
// -- настроим пины порта --
DDRD |= (1 << PD0); // пин PD0 порта DDRD на вывод
DDRD |= (1 << PD1); // пин PD1 порта DDRD на вывод
// -- основной цикл программы --
while (1) { // реализация бесконечного цикла
PORTD |= (1 << PD0); // на пине PD0 высокий уровень
_delay_ms(delay_ms_1); // задержка по времени 1
PORTD &= ~(1 << PD0); // на пине PD0 низкий уровень
_delay_ms(delay_ms_1);
PORTD |= (1 << PD0);
_delay_ms(delay_ms_1);
PORTD &= ~(1 << PD0);
_delay_ms(delay_ms_2); // задержка по времени 2
PORTD |= (1 << PD1); // на пине PD1 высокий уровень
_delay_ms(delay_ms_1); // задержка по времени 1
PORTD &= ~(1 << PD1); // на пине PD1 низкий уровень
_delay_ms(delay_ms_1);
PORTD |= (1 << PD1);
_delay_ms(delay_ms_1);
PORTD &= ~(1 << PD1);
}
}
Рассмотрим все в исходном коде более подробно.
Строки или части строк что начинаются с двух слешей "//", а также блоки текста что начинается с символов "/*" и заканчивается символами "*/" - это комментарии. В комментариях может размещаться полезная информация и примечания.
Строкой "#define F_CPU 1000000UL" мы объявляем константу, которая говорит компилятору что частота ЦПУ нашего микроконтроллера равна 1000000Гц (1МГц). Данное объявление необходимо для правильной работы некоторых функций, в нашей программе это функция "_delay_ms". В моем примере микроконтроллер ATmega8 без установки битов-фьюзов по умолчанию работает на внутреннем тактовом RC-генераторе с частотой 1МГц.
Строка "#include <avr/io.h>" производит подключение файла "io.h" к текущему файлу исходного кода, а строка "#include <util/delay.h>" - подключает файл "delay.h".
Узнать где размещаются данные файлы в Linux можно с помощью программы "locate". Установим ее и обновим индекс файлов для поиска:
sudo apt-get install locate
sudo updatedb
В качестве примера, выполним поиск путей где размещаются файлы "io.h" и оставим только те результаты, в которых содержится сочетание символов "avr":
locate io.h | grep avr
В результате получим список путей ко всем файлам где в имени встречается "io.h", а также путь содержит подстроку "avr":
/usr/lib/avr/include/stdio.h
/usr/lib/avr/include/avr/io.h
/usr/share/doc/avr-libc/avr-libc-user-manual/group__avr__io.html
/usr/share/doc/avr-libc/avr-libc-user-manual/group__avr__stdio.html
/usr/share/man/man3/io.h.3avr.gz
/usr/share/man/man3/stdio.h.3avr.gz
Здесь мы можем видеть что нужный нам файл находится по пути "/usr/lib/avr/include/avr/io.h". Посмотрев его содержимое можно увидить что он содержит в себе включение других файлов с определениями (AVR device-specific IO definitions), которые в свою очередь зависят от выбранного нами типа микроконтроллера.
Тип микроконтроллера (MCU Type) в даном случае указывается как параметр "-mmcu=atmega8" (для ATmega8) при вызове команды-компилятора "avr-gcc".
В моем случае для микроконтроллера ATmega8 через файл "io.h" подключается следующий файл - "iom8.h" (Input Output Mega8), в нем хранятся все определения таких переменных как PD0, PD1, PB8, DDRD, DDRB, RAMSTART, RAMEND и много всего другого.
Файлы с определениями IO (io*.h) для каждого типа МК хранятся в директории по адресу "/usr/lib/avr/include/avr/", рекомендую зайти туда и посмотреть что в ней творится для более глубокого понимания.
Полистать содержимое файла iom8.h можно в редакторе nano, для этого выполним команду:
nano /usr/lib/avr/include/avr/iom8.h
Для поиска в редакторе nano используйте комбинацию клавиш CTRL+W (для запоминания: where, где).
Также используя команду "cat" можно вывести только те строчки, которые содержат в файле указанное сочетание символов или слово:
cat /usr/lib/avr/include/avr/iom8.h | grep RAM
Данная команда выведет вот такой текст:
#define RAMSTART (0x60)
#define RAMEND 0x45F
#define XRAMEND RAMEND
Таким образом можно посмотреть какие есть константы и определения в библиотеке avr-gcc для работы с операциями ввода-вывода(Input-Output), их значения и многое другое для вашего типа микроконтроллера!
Файл "delay.h" содержит в себе определения функций задержки, в частности там содержится код функции "_delay_ms", которую мы будем использовать в примере. Для просчета временной задержки такие функции используют константу "F_CPU", которую мы объявили раньше в начале кода.
Строкой "void main(void) {" с левосторонней фигурной скобки начинается тело нашей программы и заканчивается оно правосторонней фигурной скобкой "}" в самом низу листинга. Таким образом мы объявили основную функцию "main" с которой начнется выполнение программы, тело функции взято в фигурные скобки, а ключевые слова "void" означают что функция не принимает и не возвращает никаких данных, в данном случае.
Важно знать что в языке Си символ точка с запятой ";" является специальным символом - пустым оператором (который ничего не выполняет) и используется для указанию компилятору что это конец команды.
В строчке "int delay_ms_1 = 100;" мы объявили новую переменную "delay_ms_1" с типом "int" (Integer, Целый тип, значения от -32768 до 32767) и присвоили ей значение 100. Служит она в нашей программе для установки задержки в миллисекундах при мелькании каждого из светодиодов.
В следующей строке "int delay_ms_2 = 300;" мы также выполнили инициализацию переменной, которая будет служить для установки времени задержки между мельканиями отдельных светодиодов - 300 миллисекунд.
Дальше идет команда "DDRD |= (1 << PD0);" которой мы настраиваем канал PD0 порта DDRD на вывод. Для этого, при помощи левостороннего сдвига битов (разряов) числа 1, создается битовая маска которую мы накладываем на содержимое регистра управления каналами порта DDRD при помощи битовой операции "|" (логическое ИЛИ).
Следующая команда в коде идентична предыдущей за исключением того что она устанавливает на вывод канал PD1 порта DDRD.
К каналам PD0 и PD1 (ножки 2 и 3 чипа) у нас подключены светодиоды, свечением которых мы и будем управлять.
Строкой "PORTD |= (1 << PD0);" мы выполняем установку бита с номером 0 в байте регистра для порта PORTD, чем мы подаем на канал PD0 высокий уровень и самым зажигаем подключенный к нему светодиод.
Также данную запись можно представить вот так:
PORTD = PORTD | (1 << PD0);
Здесь мы присваиваем переменной PORTD содержимое этой же переменной, применив к ней битовую операцию "|" (логическое ИЛИ), в качестве аргумента которой выступает результат выражения "1 << PD0", который в свою очередь представляет число один, биты которого сдвинуты на 0 (PD0 = 0) разрядов влево.
Дальше мы выполняем небольшую задержку по времени "_delay_ms(delay_ms_1);" вызывая функцию "_delay_ms" и передав ей в качестве аргумента переменную "delay_ms_1", которая уже содержит число 100.
Строкой "PORTD &= ~(1 << PD0);" мы выполняем записываем 0 в бит под номером 0 в байте регистра для порта PORTD и гасим светодиод, который подключен к каналу PD0.
Более развернуто данную строку можно записать так:
PORTD = PORTD & ~(1 << PD0);
Здесь мы выполняем запись в порт PORTD его начального значения, перед тем применив к последнему операцию "&" (логическое И), в качестве второго аргумента которой передаем результат комплексного выражения "~(1 << PD0)", которое в свою очередь представляет число один, сдвинутое на ноль (PD0 = 0) разрядов влево с применением к результату оператора инверсии "~" (смена значений всех бит на противоположные).
В следующих строках в коде мы снова выполняем установку (запись 1) и сброс (запись 0) бита PD0 в байте регистра для порта PORTD с установленной задержкой "delay_ms_1", чем мы заставляем светодиод подключенный к пину канала PD0 зажечься и погаснуть (мелькнуть, blink).
Строкой "_delay_ms(delay_ms_2);" выполняется более длительная задержка по времени с использованием значения переменной "delay_ms_2" которая выше получила значение 300 (задержка в 300 миллисекунд).
Дальше мы дважды производим установку и сброс бита PD1 (бит под номером 1 в байте регистра) в регистре порта PORTD, чем заставляем мелькать светодиод который подключен к пину канала PD1 порта PORTD микроконтроллера.
По завершению приведенных команд все начинается снова в бесконечном цикле "while (1)".
Самое сложное к пониманию здесь это, пожалуй, работа с установкой нужных битов в портах. Более подробно данная тема освещена в статье: Работа с регистрами AVR микроконтроллера на Си, битовые операции.
Компиляция и прошивка программы в МК
Для компиляции программы сохраним исходный код в файле под названием "leds_blinking.c". Если у вас уже настроена среда Geany то для компиляции достаточно нажать на панели инструментов кнопку "Compile".
Для компиляции файла с программой на Си в консоли нужно выполнить команду:
avr-gcc -mmcu=atmega8 -Os leds_blinking.c -o leds_blinking.o
В результате работы, если нет ошибок, получим объектный файл leds_blinking.o с которого нам нужно извлечь необходимые данные для прошивки нашего микроконтроллера (в моем случае ATmega8, параметр "-mmcu=atmega8").
Для извлечения данных и построения файла прошивки в формате Intel Hex нужно нажать в Geany кнопку "Build". Из консоли получить нужный файл можно при помощи команды:
avr-objcopy -j .text -j .data -O ihex leds_blinking.o leds_blinking.hex
Теперь, когда у нас есть файл с прошивкой в формате Intel HEX останется записать его содержимое (прошить) во флешь-памяти микроконтроллера, выполнить эту операцию можно нажав в подготовленной нами среде Geany кнопку "Run or view current file" (Execute).
В консоли выполнить прошивку можно при помощи avrdude командой (для ATmega8 параметр "-p m8", программатор USBAsp "-c usbasp"):
avrdude -c usbasp -p m8 -P usb -U flash:w:leds_blinking.hex
Сразу после прошивки на МК будет послана команда сброса(RESET) и программа начнет выполняться в кристалле, о чем будут свидетельствовать помигивающие светодиоды. Также RESET можно выполнить и вручную, переподключив для этого питание к микроконтроллеру.
Желательно выполнять все шаги (компиляция+построение hex-файла + прошивка) поочередно и вести наблюдение за информацией что появляется в консоли или на информационной панели Geany. Это поможет обнаружить ошибки и замечания если что-то не будет работать так как нужно.
Документация по языку Си и AVR Си
Брайан Керниган и Dennis Ritchie - Язык программирования C: brian-kernighan-and-dennis-ritchie-c-language.pdf.zip (2,1Мб, PDF).
Герберт Шилдт - Полный справочник по C: gerbert-shildt-c-complete-guide.zip (912Кб, HTML).
Это оцифрованные электронные версии книг с очень удобной навигацией, которые были найдены в сети. Все права на содержимое этих книг принадлежат их авторам.
По возможности купите себе хороший и свежий справочник по Си в бумажном виде для удобного обучения и работы.
Библиотека Си для AVR микроконтроллеров (AVR C Runtime Library) - https://savannah.nongnu.org/projects/avr-libc/
По приведенной выше ссылке можно почитать документацию (на английском языке) прямо на сайте или же скачать ее одним файлом в форматах HTML и PDF, там есть вся необходимая информация по использованию библиотеки avr-libc для программирования AVR микроконтроллеров.
Заключение
Добившись уверенной работы приведенного выше кода, попробуйте поэкспериментировать с ним. Например сделайте так чтобы каждый светодиод мелькал не по два раза, как в примере, а по три или четыре. Также поэкспериментируйте с задержками по времени.
В отличие от предыдущей программы на Ассемблере, здесь светодиоды мелькают немного по другому. Вот небольшая видео-демонстрация работы собранной схемы с прошитой программой на Си:
В этом видео программатор USBAsp уже отключен, а питание схемы на микроконтроллере осуществляется от батареи КРОНА с напряжением 9В через схему стабилизатора напряжения которая обеспечивает на выходе стабильные 5В.
Начало цикла статей: Программирование AVR микроконтроллеров в Linux на языках Asembler и C.
Не слишком ли сложно для новичков работать с портами побитно через сдвиги и логические операции?
Может проще для их понимания:
// -- настроим пины порта --
DDRD = 0x01; // пин PD0 порта DDRD на вывод
DDRD = 0x02; // пин PD1 порта DDRD на вывод
// -- основной цикл программы --
while (1) { // реализация бесконечного цикла
PORTD = 0x01; // на пине PD0 высокий уровень
_delay_ms(100); // задержка по времени 1
PORTD = 0x00; // на пине PD0 низкий уровень
_delay_ms(100);
PORTD = 0x01;
_delay_ms(100);
PORTD = 0x00;
Думаю что не слишком сложно, тем более что я привел ссылку на статью в которой простым языком расписал что такое байты, биты и какие операции для работы с ними существуют, сопровождая все это простыми примерами.
Чем раньше будущий разработчик программ для МК ознакомится с этой информацией - тем лучше, поскольку при программировании МК и периферии очень часто придется иметь дело с отдельными битами и не важно на каком языке пишется программа - на Си или Ассемблере.
Очень важно понимать что и откуда берется, это поможет написать более стабильный, уверенный код и в случае какого-то "бага" суметь его найти и исправить в кратчайший срок.
Хорошо начинать обучение программированию МК с основ - с Ассемблера, со структур данных, регистров, операторов, организации памяти...тем не менее, возможно кому-то будет проще начать с чего-то попроще, например с Си, а потом уже углубляться в более сложные основы.
Всёже для полных новичьков будет сложно понять как работать с битами через сдвиги, непонимая, того же асемблера, а здесь речь о Си. Статью нужно дополнить более низкоуровневыми примерами и подробными пояснениями как и что делает побитовая логика И(&) ИЛИ(|) НЕ(!) ИЛИНЕ(^), инверсия битов(~). Банально на примерах из одной байтных чисел. И я бы не советовал новичькам такую литературу как Язык программирования C (c) Керниган и Ричи, там материал подан для тех кто знает поднаготную С и просто хочет закрепить свою базу. В самом крайнем случае можно посоветовать литературу из разряда С для чайников (как по мне там уж больно для даунов все расписано) - она даст возможность постепенно въехать в курс.
Для серьйозной разработки софта для МК обязательно нужно знать асемблер и ещё какойнить высокоуровневый язык (в данном случае Си). Для ардуиншиков хватит и простого Си, ведь как я понял ардуино это всеголишь задефайненый Си?
Цель данной статьи - это скорее дать возможность "пощупать" как все работает, а по программированию сейчас доступно очень много литературы и материалов, было бы желание и время для изучения.
Ардуино - это такой себе удобный конструктор, набор для обучения, который состоит из микроконтроллера с минимально необходимой обвязкой + разнообразные платы расширения + удобная интегрированная среда разработки (IDE) для языка C++ (используется компилятор AVR-GCC) с набором разных подготовленных констант, макросов, библиотек и упрощений.
При разработке программ в среде Ардуино оболочка прячет от пользователя много разной нужной "черновой" работы, многое делается "за кулисами" чтобы обезопасить пользователя от ошибки и упростить процесс написания программы.
Возможно что для полных новичков Arduino будет более подходящим вариантом для знакомства чем изучение голого AVR + Asm/Си, поскольку последняя связка может немного спугнуть человека без опыта какого-либо программирования и разработки устройств, показаться очень сложной в работе и в понимании ее структуры.
"Для этого, при помощи левостороннего сдвига битов (разрядов) числа 1, создается битовая маска которую мы накладываем на содержимое регистра управления каналами порта DDRD при помощи битовой операции "|" (логическое ИЛИ). PORTD |= (1 << PD1); "
Вопрос новичка: для установки (скажем) бита с номером пять PD5, мы запишем число 0000001 в память, сдвинем его 5 раз, потом сделаем операцию ИЛИ с содержимым регистра PORTD. Итого около 7-8 тактов для Risk процессора. Неужели просто бит записать за один такт он не умеет и нет такой команды?
Следующие конструкции идентичны по результату, но разнятся по удобству и краткости записи:
Компилятор языка C (AVR GCC) трансформирует их так, что операция левостороннего сдвига числа 1 получит константный эквивалент "0b100000", поэтому исполняемый код после компиляции будет идентичен скомпилированному коду следующей команды:
Таким образом, бояться за растрату ресурсов МК сильно не стоит, компилятор AVR GCC выполняет множество различных оптимизаций. Тем не менее, иногда случается так что некоторые из этих оптимизаций могут быть не очень удачными в плане производительности.
Конструкции кода на Си, в которых выявлены большие потери ресурсов CPU, можно заменить более производительными вставками идентичного по назначению кода на языке Ассемблера. Пример вставки ассемблерного кода в Си, в котором вызываем оператор "nop" (No OPeration, утилизирует один такт CPU не выполнив никакой операции) :
Рассмотрим еще один пример. Имеются две конструкции, в которых устанавливаем значения четырех регистров порта:
Из первой конструкции легко понять какие регистры порта будут установлены, чего не скажешь о второй. Компилятор оптимизирует цепочку из команд "(1<<PD0) | (1<<PD3) | (1<<PD5) | (1<<PD8)", поскольку ее результат в программе всегда будет иметь постоянное значение (бинарные операции над числами и константами без переменных).
При использовании первой конструкции получаем:
В AVRStudio или консольном симуляторе simulavr можно поэкспериментировать с производительностью различных конструкций кода на Си и Ассемблере, можно понаблюдать за значениями счетчика циклов CPU, расчетным временем выполнения, а также за состоянием памяти, регистров и т.п.
Вот небольшая анимация, пример симуляции разных конструкций кода в AVRStudio:
Спасибо за подробные ответы. Очень академично.
Еще вопрос если PORTD часть выводов настроено на ввод, а часть на вывод это не влияет на маски, налагаемые на регистры?
Скажем PORTD |= (1<<PD0) | (1<<PD3) | (1<<PD5) | (1<<PD8);
При том, что PD0 на вывод PD3 на ввод итд.
Регистры для работы с портами:
Где "x" может быть: B, C, D...
Если пин канала PD0 порта DDRD настроен на вывод, а PD1 - на ввод, то при установке битов в регистре "PORTD |= (1<<PD0) | (1<<PD1)" высокий уровень появится только на пине с каналом PD0, причем содержимое регистра PORTD будет иметь следующий вид: 00000011 (два бита установлены).
Можете выполнить эксперимент с программой, которая приведена в статье: закомментируйте или удалите строчку "DDRD |= (1 << PD1);". Теперь пин с каналом PD1, к которому подключен один из светодиодов, будет настроен по умолчанию на ввод.
В цикле программы в регистре PORTD соответствующий бит этого канала будет то устанавливаться (запись значения 1) то сбрасываться (запись значения 0), но светодиод не будет мелькать поскольку канал этого пина в регистре DDRD сконфигурирован на ввод.
А зачем нужны эти логические элементы, если можно обойтись без них. Например выражение
PORTB |= (1 << PB1); можно записать так: PORTB = (1 << PB1);
а вот это PORTB &= ~(1 << PB1); можно записать так: PORTB = (0 << PB1);
Или это всё-таки неправильно?
den, ниже на примерах расскажу в чем разница и "магия" этих записей.
Если в микроконтроллере используется только один пин, привязанный к каналу порта PORTB (например PB1, к нему подключен светодиод), то можно обойтись записью:
Поскольку константа PB1 имеет значение 1, то выполнив левосторонний сдвиг числа 1 (00000001) на 1 позицию получим число 00000010, которое и будет записано в байт регистра для порта PORTB - светодиод засветится.
Если же в микроконтроллере задействованы несколько пинов в соответствии с каналами порта PORTB, то запись "PORTB = (1 << PB1);" принудительно установит в 0 (00000010) все кроме одного бита, что может быть неприемлемо если к другим пинам МК подключены исполнительные устройства, светодиоды и т.п.
В данном случае нам необходимо изменить состояние только одного бита в байте регистра порта (например PB1), а состояния всех остальных битов оставить без изменений. Здесь на помощь приходит логический оператор ИЛИ (OR), обозначаемый в языке Си как "|".
Допустим что значения битов в байте порта PORTB сейчас установлены как "10110101" и нужно изменить только состояние младшего первого бита (отсчет битов ведется с нуля), который настроен на вывод и привязан к каналу PB1 и соответствующему ему пину на корпусе МК.
В этом случае понадобится запись вида:
В ней мы сдвигаем число 1 (00000001) на 1 бит влево (00000010), а потом выполняем бинарную операцию ИЛИ над двумя операндами: текущим значением байта в регистре порта PORTB (10110101) и полученным после сдвига числом (00000010).
В результате операции получим число 10110111 (1 байт), которое и будет записано в регистр PORTB. В итоге нужный бит установлен, а состояние остальных битов сохранены!
По второму примеру, аналогично. Допустим что текущее значение байта в регистре порта PORTB - 10110111, все каналы настроены на вывод и к соответствующим им пинам на МК подключены светодиоды. В результате 6 светодиодов сейчас светятся.
Применим следующую конструкцию из кода:
В регистр PORTB будет записано число 00000000, все ранее установленные биты будут установлены в 0, все светодиоды погаснут, в том числе и нужный нам светодиод что подключен к пину PB1.
А теперь применим вторую конструкцию (помним что регистр PORTB изначально содержит число 10110111):
Здесь в 0 устанавливается только один бит в байте регистра для порта PORTB - погаснет только один светодиод, который подключен к пину в соответствии каналу PB1, остальные будут светиться (без изменений).
Разбор логики и вычислений по порядку:
Как видим, в байте регистра для PORTB изменился только один из младших битов, он установлен в 0 и привязан к пину что соответствует каналу PB1, подключенный к нему светодиод погаснет.
Краткий итог: с помощью "магических" операторов "& ~ |" можно создавать конструкции кода, который будет изменять только один или несколько нужных битов в байте.
Спасибо за подробное объяснение, проверил, действительно всё так и есть.
А как пишут сложные программы для контроллеров? Некоторые вещи сложно блоками описать, не то что на Си. Получается только мигать светодиодом?
Сложные программы для AVR-микроконтроллеров, как правило, разбиваются на множество отдельных файлов с исходным кодом, которые могут размещаться в древовидной файловой структуре (с каталогами).
Часто используемые шаблоны кода оформляются в отдельные функции, а подборки похожих по назначению функций собираются в библиотеки. Наборы констант и макроопределений также по возможности выносятся в отдельные файлы.
Таким образом, для работы с различной периферией к основной программе подключаются уже написанные и отлаженные библиотеки с функциями и макросами.
Программист сам решает какой будет архитектура его программы, здесь многое зависит от сложности проекта и количества используемых в нем компонентов.
Помигать светодиодом - это простейший пример программы для AVR микроконтроллера, приведенный здесь для доступного понимания как все работает. В этой простой программе также можно сделать некоторые оптимизации для уменьшения и улучшения структуры кода, например вот так:
Этот пример по функционалу аналогичен тому, который приведен в статье.
Здравствуйте, спасибо за труд. А можете рассказать, как мне прошить МК через Geany настроенный по вашему примеру, если у меня есть проект написанный в Atmel Studio в котором функции оформлены в отдельные модули и использованы заголовочные файлы? Открытый main.c не компилируется
Здравствуйте. Если проект состоит из нескольких файлов с исходным кодом на Си, то при вызове компилятора avr-gcc нужно в аргументах ему передать все файлы с расширением *.c которые должны быть включены в проект.
Например:
Более подробно все рассмотрено в статье: AVR Си - выносим функции и переменные в модули, компиляция нескольких файлов в avr-gcc, Makefile
Также, при переносе проекта из AVR Studio могут возникнуть ошибки касающиеся стандартизации, использования устаревших директив и переменных - здесь нужно смотреть на лог ошибок и поочередно их исправлять.