пятница, 4 января 2013 г.

Maihe, MCU, how-to

Продолжим игры с контроллером.
Первая часть здесь, вкратце напомню - используем четыре ноги как вход, в зависимости от состояния на входных ногах на некоторые выходы включаем / выключаем 1 / 0, на других используем шим.

Микросхемка используется Atmega8A-PU в корпусе DIP.
Питание 2.7-5.5 В, внешний кварц до 16 МГц.





Сохранил datasheet на нее на всякий случай.










Программы для всего этого дела, часть платные, но очень классные, часть бесплатные

  • Atmel studio для написания самой программы - freeware
  • Набор программ для симуляции реального устройства - freeware
  • Proteus ISIS - программа для моделирования - proprietary software
  • Fritzing - программа для трассировки платы - freeware
Работать будем в Atmel Studio 6, начнем File - New - Project.

 Первый шаг - выбираем GCC C Executable Project
 Шаг второй - выбираем Atmega8
Вот это наша первая программа, все что внутри while(1) будет крутиться постоянно, по сути сюда надо обработчики ставить.
Все будет на языке C.





Прежде всего небольшая ремарка - подход к программированию - этой мой подход. Он может быть полон ошибок, не оптимизирован, но этой мой подход.
Все программирование atmega - по сути работа с регистрами. Например, хотим использовать ножки 2 и 4 как выходы (это ножки порта PORTD), нужно в специальный регистр этого порта DDRD записать значение 1 напротив бита 0 (ножка 2 - PORTD.0) и 1 напротив 2 (ножка 4 - PORTD.2), или в простой форме DDRD = 0b00000101. Нумерация справа налево, запись 0b - значит в двоичной форме. Эту же запись можно вот так написать DDRD = 5 (00000101 в десятичной форме). Т.е. для работы с портом D нам необходимы три регистра, прежде всего DDRD - побитно можно установить режим работы соответствующей ноги (0 вход, 1 выход), PIND для чтения состояния определенной ноги, PORTD для установки состояния.
Идем далее. Мы захотели установить на ножке 2 (PD0) уровень 1. Для этого воспользуемся битовой операцией ИЛИ. Запись примет вид PORTD = PORTD || 0b00000001, или в извращенной сокращенной форме PORTD |= 1 (0b00000001 в десятичной). Число справа от составного оператора |= называется маской. В этом примере маска изменит только нулевой бит, так как там 1, а неизвестное ИЛИ один всегда будет один, в остальных битах значения не изменятся.
Теперь про прерывания - есть такая классная штука. Прерывания можно настроить через регистры на всякие события - например, появление 1 на ноге 4 (PD2, INT0), переполнение специального регистра на внутреннем таймере и т.д. При этом программа прыгнет в функцию обработчик прерывания, выполнит там все и вернется обратно.
В нашем случае будет использован внешний тактовый кристалл на 10 МГц, то есть atmega будет 10 млн операций в секунду выполнять (грубо), слишком много. Вместо этого сделаем таким образом - настроим прерывание на одном из трех таймеров (два других будут использованы для шим), чтобы оно возникало 100 раз в секунду (10 миллисекунд) - введем глобальную переменную frameNumber, каждый раз при возникновении прерывания будем увеличить ее на один и опрашивать входы. Потом, ориентируясь на номер фрейма, сможем управлять режимами нажатий - долго, кратковременно и т.д. Плюс к этому всему сразу опишем режимы работы светосигнального оборудования и изменения шим.

Итак, поехали. Создаем новый проект, примерно такой вид будет в начале программы

/*
 * GccApplication4.c
 *
 * Created: 04.01.2013 22:05:18
 *  Author: Me
 */ 


#include <avr/io.h>

int main(void)
{
    while(1)
    {
        //TODO:: Please write your application code 
    }
}

Создаем переменные для хранения значений по логике программы и добавляем перед определением типа волшебное слово volatile - это не позволит оптимизатору кода чудить с этой переменной (а то потом будете блох ловить как я)), вот пример моей переменной на текущий номер кадра

volatile unsigned short currentFrame = 1;

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

Порты

Несложно. Главное везде использовать // примечания, чтобы не тупить затем.
Битами в регистре DDR[BCD] устанавливаем как ноге работать - 0 на вход, 1 на выход. Регистром PORT[BCD] можно еще режим на вход поменять - сделать, например, что там всегда 1 (от внутреннего напряжения контроллера) или 0 с большим сопротивлением. Все есть в ds.

// PB0 - линия 5 ССО
// PB1 - источник шим для направления
// PB2 - источник шим для движения
// PB3 PB4 - переключение движения
PORTB = 0;
DDRB = 0b00011111; // 00 01 11 11
 
// PC0 PC1 - переключение направления
// PC2 PC3 PC4 PC5 - линии 1,2,3,4 ССО
PORTC = 0;
DDRC = 0b00111111; // 00 11 11 11
 
// PD5 PD6 PD7 - линии 6,7,8 ССО
// PD0 PD1 PD2 PD3 - входы
PORTD = 0;
DDRD = 0b11100000; // 11 10 00 00

Здесь я не использую замудренную запись с регистровым сдвигом <<, а просто пишу установи в регистр двоичное число, например для регистра DDRB, 0b00011111. Можно это же реализовать как DDRB=31.

Как я реализовал опрос входов контроллера на частоте 100 Гц.

Для этого использовал третий из доступных таймеров -TIMER_COUNTER_2. Первый таймер может генерировать прерывание по переполнению, второй и третий могут по переполнению и сравнению. Мне нужен таймер со сравнением (ниже объясню) поэтому не первый. Второй используется на шим, остается третий.

Регистры третьего таймера в Atmel Studio 6.
TCNT2 - регистр увеличивается на один с периодичностью частота процессора / предделитель
OCR2 - сюда пишем число с чем сравнивать будем (если нужно)
TCCR2 - битами 2-1-0 (CS22-CS21-CS20) ставим режим предделителя, битами 6 и 3 (WGM20 WGM21) - ставим режим работы
Все есть в datasheet



У таймеров есть два режима работы.
Первый - по переполнению - есть регистр у таймера (у первого и третьего 8-ми битный, у второго 16-и битный), есть его предделитель. Предделителем устанавливаем когда добавлять +1 в регистр относительно тактов процессора. Например, предделитель 64 - это означает, что каждые 64 такта процессора в регистр уходит единица. Если регистр 8-ми битный, то после 64*256 (2 в степени 8) тактов регистр заполнится и сработает прерывание по переполнению (если все это настроено конечно, по умолчанию все выключено), затем регистр обнулится. И круг за кругом.
Второй - по сравнению (первый таймер у atmega8 его не поддерживает, только второй и третий) - есть регистр, куда пишем число, есть регистр, куда каждый раз добавляется +1, есть предделитель относительно тактов процессора. Например, предделитель 1024, число для сравнения 97. Каждые 1024 такта процессора в регистр таймера идет единица, когда значение регистра сравняется с нашим числом - произойдет прерывание по сравнению, затем регистр обнулится. И опять круг за кругом.

Теперь конкретный пример для этого изделия. Задача получить прерывание каждые 10мс, или 100 раз в секунду при частоте кварца в 10 МГц. Решаем так - ставим режим "по сравнению", предделитель на 1024, число сравнения 97 (значений будет 98, 0-97). То есть, каждые 100352 (1024*98) такта процессора будет происходить прерывание. Тактов у нас 10 млн в секунду, отсюда прерывание будет происходить 100352 / 10 млн, или каждую 0.0100352 секунду, или наши искомые 100 раз с погрешностью 3.5 мс на одну секунду.
Вот так это выглядит в коде.

TCCR2 |= (1 << WGM21); // воткнули 1 в бит 3, в 6 стоит по умолчанию 0, по таблице из DS это режим CTC - прерывание по сравнению
OCR2 = 97; //число сравнения
TCCR2 |= (1<<CS22)|(1<<CS21)|(1<<CS20); //предделитель на 1024
TIMSK |= (1 << OCIE2); // включение прерывания по сравнению для TIMER_COUNTER_2

Установкой предделителя таймер запускается. А вот так выглядит функция обработчик для этого события (только надо не забыть подключить заголовок прерываний)

#include <avr/interrupt.h>
...
ISR(TIMER2_COMP_vect){
   //тут идет какой-то код
}
После выполнения тела функции программа вернется в то самое место где ее застало событие прерывания. Это надо иметь в виду. С этим все, далее шим.

ШИМ на два канала

Здесь тоже не очень сложно, опять все упирается в регистры.

// ШИМ
OCR1A = 0x00; // начальный шим, ножка PB1
OCR1B = 0x00; // начальный шим, ножка PB2
TCCR1A |= (1 << COM1A1); // неинвертированный режим
TCCR1A |= (1 << COM1B1); // неинвертированный режим
TCCR1A |= (1 << WGM11) | (1 << WGM10);
TCCR1B |= (1 << WGM12); // быстрый шим, 10-bit 
TCCR1B |= (1 << CS10); // установка делителя 1 (001), старт шим


Теперь на двух ногах можно устанавливать независимые значения шим. Это вот два источника шим с 50% скважностью.









Самое главное - процесс идет, голова пухнет, код растет. Скоро изделие поедет и будет огоньками светить.