суббота, 28 декабря 2013 г.

Светодиоды, кнопки и arduino uno

Еще одна статья о кнопках и светодиодах. Эта статья не содержит новых схемотехнических решений. А вот программная часть статьи будет необычна, даже в сравнении с прошлыми моими публикациями.


Прежде всего схема устройства


Кто читал мою предыдущую статью о светодиоде и кнопке не найдут в этой схеме ничего нового. И непонятного тоже. Я на это надеюсь.

Расскажу о том, как это все должно работать. При нажатии на первую кнопку должен загораться первый светодиод. Когда первая кнопка отпущена, первый светодиод гаснет. С второй кнопкой и вторым светодиодом все аналогично. С третей кнопкой и третьим светодиодом все тоже самое. И так далее. Простенько и без затей.

Программа для arduino (среда разработки ардуино)

////////////////////////
//
// Arduino UNO
//
////////////////////////
//
// Sketch: LED and BUTTON
//

const unsigned char LED1 = 8;
const unsigned char LED2 = 9;
const unsigned char LED3 = 10;
const unsigned char LED4 = 11;
const unsigned char LED5 = 12;
const unsigned char LED6 = 13;

const unsigned char BUTTON1 = 14;
const unsigned char BUTTON2 = 15;
const unsigned char BUTTON3 = 16;
const unsigned char BUTTON4 = 17;
const unsigned char BUTTON5 = 18;
const unsigned char BUTTON6 = 19;

void setup()
{
    pinMode(LED1, OUTPUT);
    pinMode(LED2, OUTPUT);
    pinMode(LED3, OUTPUT);
    pinMode(LED4, OUTPUT);
    pinMode(LED5, OUTPUT);
    pinMode(LED6, OUTPUT);

    pinMode(BUTTON1, INPUT_PULLUP);
    pinMode(BUTTON2, INPUT_PULLUP);
    pinMode(BUTTON3, INPUT_PULLUP);
    pinMode(BUTTON4, INPUT_PULLUP);
    pinMode(BUTTON5, INPUT_PULLUP);
    pinMode(BUTTON6, INPUT_PULLUP);
}

void loop()
{
    digitalWrite(LED1, digitalRead(BUTTON1));
    digitalWrite(LED2, digitalRead(BUTTON2));
    digitalWrite(LED3, digitalRead(BUTTON3));
    digitalWrite(LED4, digitalRead(BUTTON4));
    digitalWrite(LED5, digitalRead(BUTTON5));
    digitalWrite(LED6, digitalRead(BUTTON6));
}

//
// End
//
////////////////////////

Комментировать здесь нечего. Обо всех функциях встречающихся в этом скетче я уже писал. Ничего нового.

Программа для ATmega328 на C  (AVR Studio)

/*******************************************
Program                 : LED and BUTTON
Compiler                : AVR GCC
Chip type               : ATmega328
System Clock            : 16 MHz
*******************************************/

#include <avr/io.h>

int main( void )
{
    // Input/Output Ports initialization

    // leds
    DDRB = 0b11111111;
    PORTB = 0b11111111;

    // buttons
    DDRC = 0b00000000;
    PORTC = 0b11111111;

    while (1)
    {
        PORTB = PINC;
    }

    return 0;
}

/******************************************/

В этом примере демонстрируется возможность одновременного управления всеми ножками одного порта.

Весь порт B (цифровые выводы ардуино с 8 по 13) настраивается как выход. Для этого в регистр DDRB записывается число 255. Затем сразу на всех ножках устанавливаются высокие логические уровни. Для этого в регистр PORTB также записывается 255.

Чтобы все ножки порта C (аналоговые выводы ардуино с 14 по 19) стали входами, в регистр DDRC записывается 0. А подключение внутренних подтягивающих резисторов микроконтроллера выполняется записью 255 в регистр PORTC.

Как видите функции регистра PORTx зависят от того как настроен порт. Иначе говоря функции регистра PORTx зависят от числа записанного в DDRx. Если порт является выходом, то регистр PORTx управляет логическими уровнями на ножках. А вот если порт настроен как вход, то через этот регистр включают или отключают внутренние подтягивающие резисторы микроконтроллера.

Так как кнопки подключены к порту C, а светодиоды к порту B, то для правильной работы устройства достаточно скопировать значение регистра PINC в регистр PORTB. Одна короткая строчка кода решает поставленную задачу.

Скажу еще несколько слов о регистре PINx. Значение этого регистра зависит от того какие логические уровни установлены на ножках порта. Когда порт настроен как вход, то эти уровни зависят от состояния кнопок подключенных к порту. Если порт настроен как выход, то значение регистра PINx должно дублировать значение регистра PORTx. Если это не так, значит к порту микроконтроллера подключена слишком мощная нагрузка. В таком режиме микроконтроллер долго не протянет. Впрочем, сложно не заметить, что микросхема сильно нагрелась. И не нужно быть гением, чтобы понять, что это добром не кончится.

Программа для ATmega328 на ассемблере (AVR Studio)

;---------------------------------------------
; Program      : LED and BUTTON
; Compiler     : AVR Studio
; Chip type    : ATmega328
; System Clock : 16 MHz
;---------------------------------------------

.include "m328def.inc"

;---------------------------------------------

        .cseg
        .org 0x0000

;---------------------------------------------
; Steck initialization

        ldi R16, Low(RAMEND)
        out SPL, R16
        ldi R16, High(RAMEND)
        out SPH, R16

;---------------------------------------------
; Input/Output Ports initialization

        ; leds
        ldi R16, 0b11111111
        out DDRB, R16
        ldi R16, 0b11111111
        out PORTB, R16

        ; buttons
        ldi R16, 0b00000000
        out DDRC, R16
        ldi R16, 0b11111111
        out PORTC, R16

;---------------------------------------------
; main loop

main:
        in R16, PINC
        out PORTB, R16

        rjmp main

;---------------------------------------------

В этой программе есть несколько команд про которые я еще не рассказывал.

Команда ldi записывает число от 0 до 255 в один из старших регистров общего назначения. У этой команды два параметра. Первый это название регистра. Я использовал регистр R16, но с этой командой можно использовать любой регистр начиная с R16 и заканчивая R31. Вторым параметром команды является число.

Команда out используется для загрузки числа хранящегося в регистре общего назначения в регистр ввода/вывода. У этой команды также два параметра. Первый параметр должен быть адресом регистра ввода/вывода. Адрес не должен быть больше 63. Можно использовать названия регистров ввода/вывода определенные в файле m328def.inc Вторым параметром должно быть название регистра общего назначения. С этой командой можно использовать все регистры общего назначения (с R0 до R31).

Команда in загружает содержимое регистра ввода/вывода в регистр общего назначения. У этой команды два параметра. Они аналогичны параметрам команды out. Только порядок их другой. Первым должно быть название регистра общего назначения, а вторым должен быть адрес или название регистра ввода/вывода.

Команда rjmp обеспечивает переход к метке. У этой команды один параметр. Это название метки. Метка к которой осуществляется переход должна быть размещена в программе по адресу плюс/минус 2000 слов (4 килобайта) от команды.

Думаю, про использованные в этой программе команды ассемблера AVR сказано достаточно.

Изучение ассемблера помогает понять, как работает микроконтроллер. Но это не все. Знание ассемблера AVR может быть использовано и при написании скетча.

Программа для arduino uno (среда разработки ардуино)

////////////////////////
//
// Arduino UNO
//
////////////////////////
//
// Sketch: LED and BUTTON
//

void setup()
{
    // leds
    DDRB = 0b11111111;
    PORTB = 0b11111111;

    // buttons
    DDRC = 0b00000000;
    PORTC = 0b11111111;
}

void loop()
{

asm volatile (

"loop_%=: " "in r16, %0"          "\n\t"
            "out %1, r16"         "\n\t"
            "rjmp loop_%="        "\n\t"
            :
            : "I" (_SFR_IO_ADDR(PINC)),
              "I" (_SFR_IO_ADDR(PORTB))
            : "r16"
);

}

//
// End
//
////////////////////////

Не часто можно увидеть скетчи с ассемблерными вставками :-)

Попробую пояснить некоторые различия между программой на ассемблере показанной выше и ассемблерной вставкой в скетче.

Название меток должно заканчиваться специальной последовательностью символов. Команды ассемблера имеют те же имена и выполняют те же функции. Но в ассемблерной вставке параметры команд указываются иначе. Они перечисляются в конце ассемблерной вставки, разделяемые двоеточием в соответствии с их назначением. Параметры сходные по назначению разделяются запятой. Непосредственно в ассемблерных командах указывается номер параметра. Нумеруются они с нуля. Таким образом, под %0 подразумевается адрес PINC, а под %1 подразумевается адрес PORTB. Порты ввода/вывода также записываются иначе. Последними перечисляются используемые регистры общего назначения. В командах они указываются по названиям.

В рамках этой статьи я не планировал давать исчерпывающую информацию по всем правилам ассемблерных вставок. Однако, надеюсь, некоторое представление об этом читатели смогли получить.

Теперь самое интересное. Во что превратит этот код компилятор? Ответ на этот вопрос можно получить с помощью дизассемблера AVR Studio.

Ниже находится фрагмент дизассемблированной прошивки

+00000059:   930F        PUSH      R16            Push register on stack
+0000005A:   B106        IN        R16,0x06       In from I/O location
+0000005B:   B905        OUT       0x05,R16       Out to I/O location
+0000005C:   CFFD        RJMP      PC-0x0002      Relative jump
+0000005D:   910F        POP       R16            Pop register from stack

Поясню немного. В первом столбике указаны адреса памяти программ (FLASH). Во втором столбике содержатся машинные коды записанные по соответствующим адресам. В третьем столбике указаны ассемблерные команды. В четвертом столбике перечислены параметры соответствующих ассемблерных команд. Пятый столбик содержит комментарии.

Из этого фрагмента видно, что компилятор сохранил значение регистра R16 в стеке. Затем загрузил значение PINC (адрес этого регистра 0x06) в регистр R16. А потом значение регистра R16 загружается в PORTB (адрес этого регистра 0x05). Следующим выполняется переход на две команды вверх. И снова повторяется загрузка значения PINC в R16. И дальше все повторяется. И так снова и снова.

В последней строке из стека восстанавливается значение регистра R16. Компилятор не знает, что из описанного ранее цикла нет выхода. Он делает все, что по его мнению необходимо для правильной работы программы.