А какую конструкцию на основе МК хотите создать ВЫ?
Устроим, так сказать, небольшой конкурс идей. Рассказывайте свои задумки, а я выберу самую интересную, с точки зрения программной реализации, конструкцию (а может даже и не одну) и мы попробуем ее создать!
Список всех частей:
Грызём микроконтроллеры. Урок 1. Моргаем 8-ю светодиодами. CodeVision, Proteus, ISIS
Грызём микроконтроллеры. Урок 2. CodeVision и С
Грызём микроконтроллеры. Урок 3. Циклы, прерывания и массивы
Грызём микроконтроллеры. Урок 4. Мерим температуру или напряжение
Грызём микроконтроллеры. Урок 5. Кодовый замок
Грызём микроконтроллеры. Урок 6. Прошиваем МК
Грызём микроконтроллеры. Урок 7. Подключение к МК кнопок, клавиатуры, энкодера
Грызём микроконтроллеры. Урок 8. Программирование кнопок, клавиатуры, энкодера
Грызём микроконтроллеры. Урок 9. Клавиатура вглубину
Начну я с дополнения предыдущей статьи. В ней остались не рассмотрены циклы, прерывания и массивы.
Массивы.
Массив – это простейший способ хранения однотипных данных.
Массивы бывают одномерными (строка), и многомерными (двумерный, трехмерный и т.д.)
Объявление:
char massiv[10];
создает одномерный массив из 10 элементов.
char massiv[10]={0,1,2,3,4,5,6,7,8,9};
то же самое, но мы сразу присваиваем значения элементов.
Объявление многомерных массивов происходит так же, только нужно указать несколько размерностей
char massiv[10][10];
Квадратная матрица 10х10
char massiv[10][10][10];
Кубическая 10х10х10
Обращение к элементам массива происходит по номеру элемента, причем нумерация начинается с “0” (нуля)
char massiv[10][10];
massiv[0][0]=0; // обращение к первому элементу массива
massiv[9][9]=5; // обращение к последнему элементу массива
Оператор цикла FOR
Синтаксис: for(операция_перед_началом;условие;операция_после_итерации)
Например:
char i, sum=0;
for(i=1;i<10;i++)
{
sum=sum+I;
}
В самом начале выполнения цикла выполняется команда i=1, после чего первый раз выполняются все действия, заключенные в фигурные скобки. Каждый такой проход называется итерацией.
ИТЕРАЦИЯ (от лат. iteratio - повторение), повторное применение какой-либо математической операции.
После каждой итерации выполняется действие, описанное как операция_после_итерации. То есть, после каждого прохода в нашем случае i будет увеличиваться на единицу (i++).
Цикл будет продолжаться до тех пор, пока условие будет истинным, т.е. пока i не станет равно 10.
Таким образом, в нашем цикле вычисляется сумма всех чисел от 1 до 9.
Оператор цикла WHILE
Перевод – «до тех пор, пока»
Синтаксис: while(условие)
Пример:
char a=0;
while(a<15)
{
a++;
}
Цикл будет выполняться до тех пор, пока условие не примет значение лжи, т.е., пока a не достигнет значения 15.
Прерывания
Для быстрой обработки каких-либо внешних сигналов или внутренних событий микроконтроллеров, разработчики добавили в МК функции прерываний
.
С аппаратной точки зрения, прерывание - это сигнал, вызывающий остановку выполнения основной программы и переход к программе обработки этого прерывания.
С программной точки зрения – это сама функция обработки прерывания. То есть, это те команды, которые должен выполнить МК при возникновении сигнала этого самого прерывания.
О названиях и применении прерываний я расскажу дальше вместе с примерами.
По теоретическим основам программирования вроде закончили, пора перейти к практике!
Как я и обещал, будем дорабатывать программу для нашей мигалки.
Начнем с записи данных во все выводы порта сразу.
Так как наш порт является 8-и битным, то мы спокойно можем записывать в него (присваивать ему при помощи операции “=”) значение какой-либо переменной или числа.
Заменим в нашей программе строки
PORTB.0=1;
delay_ms(500);
…
PORTB.7=0;
delay_ms(500);
На следущее:
PORTB=0b00000001;
delay_ms(500);
PORTB=0b00000010;
delay_ms(500);
PORTB=0b00000100;
delay_ms(500);
PORTB=0b00001000;
delay_ms(500);
PORTB=0b00010000;
delay_ms(500);
PORTB=0b00100000;
delay_ms(500);
PORTB=0b01000000;
delay_ms(500);
PORTB=0b10000000;
delay_ms(500);
Результат работы такой же, а вот программа стала в 3 раза меньше и гораздо симпатичнее.
А если использовать тут цикл и операцию побитового сдвига, то в программе останется вообще три строчки (не читая фигурных скобок)
for(i=0;i<8;i++)
{
PORTB= 0b00000001 << i;
delay_ms(500);
}
главное не забыть добавить в начало функции main() инициализацию переменной i
void main(void)
{
// Declare your local variables here
int i;
…
Всё это – простейшие методы реализации “бегущего огня”.
А если мы захотим задать иную последовательность? Да без проблем!
Опишем все варианты в массиве. Пусть у нас будет две точки, бегущих друг другу на встречу и обратно.
void main(void)
{
// Declare your local variables here
int i;
char migalka[8]={
0b10000001,
0b01000010,
0b00100100,
0b00011000,
0b00100100,
0b01000010};
…
while (1)
{
for(i=0;i<6;i++)
{
PORTB= migalka[i];
delay_ms(1000);
}
};
Теперь добавим в схему кнопку, которой будем переключать режим работы.
Сами режимы зададим двумерным массивом.
void main(void)
{
// Declare your local variables here
char i,j=0;
char migalka[3][8]={
{0b00000001,0b00000010,0b00000100,0b00001000,0b00010000,0b00100000,0b01000000,0b10000000},
{0b10000000,0b01000000,0b00100000,0b00010000,0b00001000,0b00000100,0b00000010,0b00000001},
{0b01010101,0b10100110,0b11001101,0b00001111,0b11110000,0b00111110,0b11111110,0b11011101}};
Дополним и программу
while (1)
{
if(PIND.0==0)
{
j++;
if(j>2)j=0;
}
for(i=0;i<8;i++)
{
PORTB= migalka[j][i];
delay_ms(1000);
}
};
Обратите внимание на то, что для записи в порт мы используем слово PORT, а для чтения состояния входов нужно использовать PIN. Нередкая ошибка, и как результат – неправильная работа программы, в том, что при попытке определить состояние входа, обращаются к регистру PORT, а в нем содержится информация о том, что выводится в порт, если он настроен как выход. А при настройке его на вход, определяет, включены ли подтягивающие резисторы, находящиеся внутри микроконтроллера.
Подтягивающие резисторы нужны для уменьшения количества элементов обвязки микроконтроллера.
Например, мы можем написать в своей программе
PORTD=0xff;
или
PORTD=0b11111111;
что по сути одно и то же, просто в разных системах счисления. Тогда включатся все потягивающие резисторы на входах D и мы можем подключать кнопки без резисторов (см. схему выше)
А теперь обратите внимание на саму программу. В ней мы проверяем состояние входа PIND.0, и, если она нажата (PIND.0==0), то увеличиваем значение переменной j, отвечающей за выбор элементов массива migalka[], на единицу. Чем и меняем режим работы нашей мигалки. Но т.к. у нас всего три режима, то добавлена строчка
if(j>2) j=0;
чтобы значения j менялись по кругу 0 -> 1 -> 2 -> 0.
Хотя, у этой программы есть и свои недостатки. Состояние кнопки проверяется один раз за цикл работы программы, а учитывая что, в цикле for у нас 8 итераций длительностью по одной секунде (delay_ms(1000)), то эта проверка происходит с частотой 1 раз в 8 секунд.
Неудобно, особенно если нужно оперативное переключение режимов.
Вот тут мы и подходим к понятию “прерывание” и осознанию всех плюсов в его использовании
Создадим новый проект:
Настроим порты ввода-вывода.
Для входов справа можно указать включены или нет подтягивающие резисторы.
Если установить P (PullUp) – резисторы включены, а если T (Tristate) – выключены, тогда входы как-бы «болтаются» в воздухе.
А теперь перейдем на вкладку External IRQ (внешние прерывания) и включим прерывание INT0 в режиме Falling Edge (по заднему фронту), чтобы прерывание вызывалось в момент нажатия кнопки.
Сгенерируем код и сохраним проект.
Теперь посмотрим на то, что сгенерировал нам CVAvr:
Первое, что бросается в глаза – новая функция
// External Interrupt 0 service routine
interrupt [EXT_INT0] void ext_int0_isr(void)
{
// Place your code here
}
Это и есть функция обработки прерывания.
Вставим в ее тело нашу функцию выбора режима, но уже без условия, т.к. эти команды и так будут выполняться только при нажатии кнопки:
interrupt [EXT_INT0] void ext_int0_isr(void)
{
j++;
if(j>2)j=0;
}
А в основном цикле программы оставим только строки
while (1)
{
for(i=0;i<8;i++)
{
PORTB= migalka[j][i];
delay_ms(1000);
}
};
Но теперь переменная j используется не только в функции main(), но и в обработчике прерывания, поэтому мы должны убрать ее инициализацию в основной функции и описать ее как глобальную.
Массив migalka[] тоже советую определить как глобальную переменную. Иначе, компилятор выдаст вам сообщение о переполнении стека. Стек – это хранилище данных, в которое записываются переменные текущей функции, если вызывается другая функция. Делается это для того, чтобы не потерять данные текущей функции. Но наш массив сравнительно большой и просто туда не поместится. А глобальные переменные являются общими для всех функций и помещать их в стек нет никакой необходимости.
В общем, должно получиться примерно так:
…
char j=0;
char migalka[3][8]={
{0b00000001,0b00000010,0b00000100,0b00001000,0b00010000,0b00100000,0b01000000,0b10000000},
{0b10000000,0b01000000,0b00100000,0b00010000,0b00001000,0b00000100,0b00000010,0b00000001},
{0b01010101,0b10100110,0b11001101,0b00001111,0b11110000,0b00111110,0b11111110,0b11011101}};
// External Interrupt 0 service routine
interrupt [EXT_INT0] void ext_int0_isr(void)
…
void main(void)
{
// Declare your local variables here
char i;
…
Компилируем и, если нет ошибок, идем дальше!
Теперь подправим схему, подключив кнопку к выводу внешнего прерывания INT0 (PORTD.2).
Укажем только что созданный нами файл прошивки и запустим программу.
Светодиоды сразу начнут мигать по первой программе, а при нажатии кнопки, программа будет незамедлительно меняться.
Вот она, прелесть прерываний!
Что же, не будем останавливаться на достигнутом!
Попробуем использовать таймер.
Таймер – очень удобная штука, если мы хотим выполнять одни и те же действия через одинаковые промежутки времени.
Снова создаем проект
Но теперь, ко всему прочему, заходим на вкладку Timers -> Timer 1 и устанавливаем тактовую частоту таймера и прерывание при его переполнении (Timer 1 Overflow)
При наших настройках этот таймер будет считать по кругу от 0 до 65535. И каждый раз при переходе от максимального значения к нулю (переполнении), будет вызываться функция прерывания.
Сохраняем, смотрим.
Заметили что-то новенькое?
// Timer 1 overflow interrupt service routine
interrupt [TIM1_OVF] void timer1_ovf_isr(void)
{
// Place your code here
}
Это функция обработки прерывания при переполнении таймера 1.
Давайте объявим в программе глобальные переменные i, j, migalka[3][8] и вставим код в функции прерываний:
Chip type : ATtiny2313
Clock frequency : 1,000000 MHz
Memory model : Tiny
External SRAM size : 0
Data Stack size : 32
*****************************************************/
#include <tiny2313.h>
#include <delay.h>
char i, j=0;
char migalka[3][8]={
{0b00000001,0b00000010,0b00000100,0b00001000,0b00010000,0b00100000,0b01000000,0b10000000},
{0b10000000,0b01000000,0b00100000,0b00010000,0b00001000,0b00000100,0b00000010,0b00000001},
{0b01010101,0b10100110,0b11001101,0b00001111,0b11110000,0b00111110,0b11111110,0b11011101}};
// External Interrupt 0 service routine
interrupt [EXT_INT0] void ext_int0_isr(void)
{
j++;
if(j>2)j=0;
}
// Timer 1 overflow interrupt service routine
interrupt [TIM1_OVF] void timer1_ovf_isr(void)
{
PORTB= migalka[j][i];
i++;
if(i>7)i=0;
}
void main(void)
{
….
Теперь основной цикл программы у нас остается совсем пустым и мы можем вписать туда всё, что душе угодно. Например, программу управления каким-либо дополнительным устройством или так всё и оставить, ведь мы достигли поставленной цели – мигалка работает!
Думаю, на сегодня информации для экспериментов и размышлений достаточно.
Домашнее задание:
- проверить все написанные примеры и при обнаружении каких-либо ошибок и неточностей сообщить автору;
- придумать свое устройство на микроконтроллере и рассказать о нем мне.
В следующий раз мы изучим принцип работы АЦП (аналого-цифрового преобразователя), встроенного в большинство современных МК. И разберемся, как можно измерить при помощи него напряжение, ток, мощность или температуру.
До скорых встреч!
Камрад, рассмотри датагорские рекомендации
🌼 Полезные и проверенные железяки, можно брать
Опробовано в лаборатории редакции или читателями.