Arduino по-взрослому: Си и иже с ним. Первая программа. Порты ввода-вывода
ВИШ RosCanSat Junior. Лекция 2.
Препроцессор, функции/процедуры, цикл while, задержка, DDR, PORT, PIN, необходимые логические операции.
Часть информации, рассказанной в видеолекции содержится в статье 1.
Содержание
Теория
Каждый микроконтроллер имеет некоторое количество портов ввода-вывода. Их функционал, назначение и состояние определяется некоторыми регистрами. Вообще, все в микроконтроллерах определяется регистрами, и в этом нет ничего страшного. Все выводы объединены в группы по 8 штук. В нашем случае, это группы B, С и D. Для взаимодействия с портами используются регистры DDR, PORT и PIN. Соответственно, DDRB для группы B, DDRC для группы C и так далее.
У регистра DDRx очень простая роль – он определяет, что есть выход. То есть, если мы подаем логическую единицу на 0-й бит регистра DDRD, то ножка PD0 будет выходом, а если логический ноль — входом.
В регистре PINx хранится состояние тех ножек, которые являются входами. То есть, если на ножку PD1, которая осталась входом, подадим извне логическую единицу, то в регистре PIND первый бит будет единицей.
Самый интересный регистр – регистр PORTx. С первого взгляда покажется, что у него самая простая функция – устанавливать логический уровень. Собственно, это так и есть, пока Вы управляете ножкой, которая настроена как выход. Ну а если же в регистре DDRx установлен ноль, и ножка является входом, то регистр управляет включением и отключением подтягивающего резистора к плюсу питания. Следовательно, пока нога «висит в воздухе», на ней может быть установлена логическая единица, ну а если что-то более сильное «прижмет» линию к нулю, то наш микроконтроллер не будет сопротивляться и логический уровень линии будет равен нулю.
Более полно и достаточно понятно теория рассказана в Лекции 2 (вводная МК) ВИШ Юниор.
Практика на языке Си
В программировании считается, что каждый, кто хочет стать программистом, должен в первую очередь написать программу, которая выводит на экран надпись «Hello World». В аналоговой электронике вместо этого принято собирать схему симметричного мультивибратора. В цифровой технике своеобразным «хелловорлдом» значится так называемый проект «Blink» — мигающий светодиод. Что же, прежде чем написать в своём резюме «Инженер-разработчик робототехнических систем с применением искусственного интеллекта», давайте сделаем этот самый Блинк.
#define F_CPU 16000000 //Программу желательно начинать с определения константы F_CPU - тактовой частоты #include <avr/io.h> //Библиотека ввода-вывода #include <util/delay.h> //Библиотека задержек //Тут можно объявлять переменные int main(void) //Начало основного алгоритма { while(1) //Основной цикл { } }
Строки, начинающиеся на # — команды препроцессора. Эти команды подготавливают текст программы к компиляции. Пользовательские константы препроцессора, объявленные однажды и используемые один или несколько раз подставляются в нужные места, выполняется проверка условий препроцессора. То есть, фрагмент кода
#define MY_CONSTATNT 5 ... _delay_ms(MY_CONSTANT); #if 0 do_something(); #endif #if 1 do_another_algotithm(); #endif
преобразуется в
_delay_ms(5); do_another_algotithm();
Строка #define F_CPU 16000000 определяет константу F_CPU, равную 16 000 000 (Гц) — тактовая частота микроконтроллера на большинстве плат Arduino. Эта константа важна в расчетах задержек и частот.
Строки #include <avr/io.h> и #include <util/delay.h> подключают соответствующие файлы библиотек, в которых описаны некоторые функции.
int main(void) — объявление процедуры. Строка объявления процедуры строится так: тип_возвращаемого_значения название_процедуры (тип_аргумента_1 значение_аргумента_1) { … }. Одной процедуре можно передавать один, несколько аргументов, или не передавать вообще. Процедура может возвращать одно значение или не возвращать вовсе. Если процедура не принимает аргументов, в скобках ставится ключевое слово void — ничего. Если процедура ничего не возвращает, то перед названием ставится ключевое слово void — ничего. Внутри фигурных скобок записываются команды, из которых состоит процедура.
Конструкцию while(условие) { команды } можно перевести на русский язык как «Пока_выполняется (условие) {выполнять команды}«.
Теперь напишем программу, которая будет попросту мигать светодиодом:
#define F_CPU 16000000 //Программу желательно начинать с определения константы F_CPU - тактовой частоты #include <avr/io.h> //Библиотека ввода-вывода #include <util/delay.h> //Библиотека задержек //Тут можно объявлять переменные int main(void) //Начало основного алгоритма { DDRB |= 0b00100000; while(1) //Основной цикл { PORTB |= (1<<5); _delay_ms(1000); PORTB &=~ (1<<5); _delay_ms(1000); } }
Видно, что в начале основного алгоритма я установил бит 5 регистра DDRB в 1.
Затем в бесконечном цикле я что-то делаю с регистром PORTB, затем жду 1000 мс (1 с), затем снова что-то с регистром PORTB, и опять 1000 мс. И дальше по кругу. Да, функция _delay_ms() обеспечивает задержку выполнения на указанное количество миллисекунд. Также существует похожая функция _delay_us() — задержка в микросекундах.
Что же именно я делаю с регистром PORTB? Конструкция |= означает «логическое ИЛИ с присваиванием«. По-русски можно сказать так: если определенный бит левого числа (операнда) равен нулю, а этот же бит в правом — единица, то в левом тоже поставить единицу, сохраняя все остальные биты. Конструкция &=~ означает «логическое ИЛИ-НЕ с присваиванием«. По-русски можно сказать так: если определенный бит левого числа (операнда) равен единице, этот же бит в правом — единица, то в левом поставить ноль, сохраняя все остальные биты. Конструкция (1<<5) читается как «единица, сдвинутая влево на пять«. Конечно, имеется в виду двоичный сдвиг. Также существует конструкция ^= . Эта конструкция инвертирует биты в соответствии с правым операндом в качестве маски и сохраняет значение в регистр. Покажу нагляднее на примерах:
x = 0b01011010; //х равен 0b01011010 x |= 0b00000011; //х станет равен 0b01011011 x &=~ 0b00000011; //х станет равен 0b01011001 x ^= 0b00001100; //х станет равен 0b01010101 x = (1<<3); //х равен 0b00001000 x = (1<<5)|(1<<3)|(1<<2); //х равен 0b00101100
Тот же пример можно записать короче:
#define F_CPU 16000000 //Программу желательно начинать с определения константы F_CPU - тактовой частоты #include <avr/io.h> //Библиотека ввода-вывода #include <util/delay.h> //Библиотека задержек //Тут можно объявлять переменные int main(void) //Начало основного алгоритма { DDRB |= 0b00100000; while(1) //Основной цикл { PORTB ^= (1<<5); _delay_ms(1000); } }
Учебные проекты
В конце лекции я хочу предложить несколько проектов с использованием информации, приведенной в этом и предыдущем уроках.
- Подключи светодиод к выводу PB0 и измени программу из урока для работы со светодиодом на PB0.
- Подключи два светодиода к разным выводам и напиши программу для поочерёдного мигания обоими
- Подключи три светодиода к разным портам и напиши программу «Светофор», которая будет поочередно переключать светодиоды, аналогично автодорожному светофору.