Вы здесь: Главная » микроконтроллеры. Урок 3

микроконтроллеры. Урок 3

микроконтроллеры. Урок 3


Эту статью я начну с провокационного вопроса…
А какую конструкцию на основе МК хотите создать ВЫ?
Устроим, так сказать, небольшой конкурс идей. Рассказывайте свои задумки, а я выберу самую интересную, с точки зрения программной реализации, конструкцию (а может даже и не одну) и мы попробуем ее создать!

Начну я с дополнения предыдущей статьи. В ней остались не рассмотрены циклы, прерывания и массивы.

Массивы.

Массив – это простейший способ хранения однотипных данных.
Массивы бывают одномерными (строка), и многомерными (двумерный, трехмерный и т.д.)

Объявление:
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)
{
….


Теперь основной цикл программы у нас остается совсем пустым и мы можем вписать туда всё, что душе угодно. Например, программу управления каким-либо дополнительным устройством или так всё и оставить, ведь мы достигли поставленной цели – мигалка работает!


Думаю, на сегодня информации для экспериментов и размышлений достаточно.


Домашнее задание:
- проверить все написанные примеры и при обнаружении каких-либо ошибок и неточностей сообщить автору;
- придумать свое устройство на микроконтроллере и рассказать о нем мне.



В следующий раз мы изучим принцип работы АЦП (аналого-цифрового преобразователя), встроенного в большинство современных МК. И разберемся, как можно измерить при помощи него напряжение, ток, мощность или температуру.
До скорых встреч!