Назад в библиотеку  

Вы еще не программируете микроконтроллеры? Тогда мы идем к вам!

  • Описание: Введение в разработку под платформу AVR

    Источник: https://habr.com/ru/post/205972/

  • Введение

    Здравствуйте, уважаемые Хабражители! В этой статье я хочу рассказать о том, как однажды решил начать программировать микроконтроллеры, что для этого понадобилось и что в итоге получилось. Тема микроконтроллеров меня заинтересовала очень давно, году этак в 2001. Но тогда достать программатор по месту жительства оказалось проблематично, а о покупке через Интернет и речи не было. Пришлось отложить это дело до лучших времен. И вот, в один прекрасный день я обнаружил, что лучшие времена пришли не выходя из дома можно купить все, что мне было нужно. Решил попробовать. Итак, что нам понадобится:

    1. Программатор

    На рынке предлагается много вариантов — от самых дешевых ISP (In-System Programming) программаторов за несколько долларов, до мощных программаторов-отладчиков за пару сотен. Не имея большого опыта в этом деле, для начала я решил попробовать один из самых простых и дешевых — USBasp. Купил в свое время на eBay за $12, сейчас можно найти даже за $3-4. На самом деле это китайская версия программатора от Thomas Fischl. Что могу сказать про него? Только одно — он работает. К тому же поддерживает достаточно много AVR контроллеров серий ATmega и ATtiny. Под Linux не требует драйвера.


    Рисунок 1 – Фото программатора

    Для прошивки надо соединить выходы программатора VCC, GND, RESET, SCK, MOSI, MISO с соответствующими выходами микроконтроллера. Для простоты я собрал вспомогательную схему прямо на макетной плате:


    Рисунок 2 – Подключение контроллера к компьютеру через программатор

    2. Микроконтроллер

    С выбором микроконтроллера я особо не заморачивался и взял ATmega8 от Atmel — 23 пина ввода/вывода, два 8-битных таймера, один 16-битный, частота — до 16 Мгц, маленькое потребление (1-3.6 мА), дешевый ($2). В общем, для начала — более чем достаточно.

    При разработке собственного гаджета часто встает проблема его подключения к компьютеру. Надо сказать, что порты LPT и COM — теперь экзотика на материнских платах ПК, не говоря о ноутбуках, у которых эти интерфейсы исчезли давным-давно. Поэтому у современных компьютеров практически не осталось альтернатив интерфейсу USB.

    Если ресурсы чипа используются на все сто процентов, то лучше сразу смотреть в сторону устройств с аппаратной поддержкой универсальной последовательной шины (такие микроконтроллеры присутствуют в линейке любого производителя). В остальных случаях можно использовать софтовый USB.

    Под Linux для компиляции и загрузки прошивки на контроллер отлично работает связка avr-gcc + avrdude. Установка тривиальная. Следуя инструкции, можно за несколько минут установить все необходимое ПО. Единственный ньюанс, на который следует обратить внимание — avrdude (ПО для записи на контроллер) может потребовать права супер-пользователя для доступа к программатору. Выход — запустить через sudo (не очень хорошая идея), либо прописать специальные udev права. Синтаксис может отличаться в разных версиях ОС, но в моем случае (Linux Mint 15) сработало добавление следующего правила в файл /etc/udev/rules.d/41-atmega.rules:

        SUBSYSTEM=="usb", ATTR{idVendor}=="16c0", ATTR{idProduct}=="05dc", GROUP="plugdev", MODE="0666"
            

    После этого, естественно, необходим перезапуск сервиса

                service udev restart
            

    Компилировать и прошивать без проблем можно прямо из командной строки (кто бы сомневался), но если проектов много, то удобнее поставить плагин AVR Eclipse и делать все прямо из среды Eclipse. Под Windows придется поставить драйвер. В остальном проблем нет. Ради научного интереса попробовал связку AVR Studio + eXtreme Burner в Windows. Опять-таки, все работает на ура.

    Начинаем программировать

    Программировать AVR контроллеры можно как на ассемблере (AVR assembler), так и на Си. Тут, думаю, каждый должен сделать свой выбор сам в зависимости от конкретной задачи и своих предпочтений. Лично я в первую очередь начал ковырять ассемблер. При программировании на ассемблере архитектура устройства становится понятнее и появляется ощущение, что копаешься непосредственно во внутренностях контроллера. К тому же полагаю, что в особенно критических по размеру и производительности программах знание ассемблера может очень пригодиться. После ознакомления с AVR ассемблером я переполз на Си.

    После знакомства с архитектурой и основными принципами, решил собрать что-то полезное и интересное. Тут мне помогла дочурка, она занимается шахматами и в один прекрасный вечер заявила, что хочет иметь часы-таймер для партий на время. БАЦ! Вот она — идея первого проекта! Можно было конечно заказать их на том же eBay, но захотелось сделать свои собственные часы, с блэк… эээ… с индикаторами и кнопочками. Сказано — сделано!

    В качестве дисплея решено было использовать два 7-сегментных диодных индикатора. Для управления достаточно было 5 кнопок — “Игрок 1”, “Игрок 2”, “Сброс”, “Настройка” и “Пауза”. Ну и не забываем про звуковую индикацию окончания игры. Вроде все. На рисунке ниже представлена общая схема подключения микроконтроллера к индикаторам и кнопкам. Она понадобится нам при разборе исходного кода программы:


    Рисунок 3 – Подключение контроллера к семисегментному индикатору

    Разбор полета

    Начнем, как и положено, с точки входа программы — функции main. На самом деле ничего примечательного в ней нет — настройка портов, инициализация данных и бесконечный цикл обработки нажатий кнопок. Ну и вызов sei() — разрешение обработки прерываний, о них немного позже.

        int main(void)
        {
            init_io();
            init_data();
            sound_off();
            sei();
    
            while(1)
            {
                handle_buttons();
            }
            return 0;
        }
            

    Рассмотрим каждую функцию в отдельности.

        void init_io()
        {
            // set output
            DDRB = 0xFF;
            DDRD = 0xFF;
    
            // set input
            DDRC = 0b11100000;
    
            // pull-up resistors
            PORTC |= 0b00011111;
    
            // timer interrupts
            TIMSK = (1<<OCIE1A) | (1<<TOIE0);
    
            TCCR0 |= (1 << CS01) | (1 << CS00);
    
            TCCR1B = (1<<CS12|1<<WGM12);
    
            //OCRn =  (clock_speed / prescaler) * seconds - 1
            OCR1A = (F_CPU / 256) * 1 -1;
        }
            

    Настройка портов ввода/вывода происходит очень просто — в регистр DDRx (где x — буква, обозначающая порт) записивается число, каждый бит которого означает, будет ли соответствующий пин устройством ввода (соответствует 0) либо вывода (соответствует 1). Таким образом, заслав в DDRB и DDRD число 0xFF, мы сделали B и D портами вывода. Соответственно, команда DDRC = 0b11100000; превращает первые 5 пинов порта C во входные пины, а оставшиеся — в выходные. Команда PORTC |= 0b00011111; включает внутренние подтягивающие резисторы на 5 входах контроллера. Согласно схеме, к этим входам подключены кнопки, которые при нажатии замкнут их на землю. Таким образом контроллер понимает, что кнопка нажата.

    Далее следует настройка двух таймеров, Timer0 и Timer1. Первый мы используем для обновления индикаторов, а второй — для обратного отсчета времени, предварительно настроив его на срабатывание каждую секунду. Подробное описание всех констант и метода настройки таймера на определенноый интервал можно найти в документации к ATmega8.

    Обработка прерываний

        ISR (TIMER0_OVF_vect)
        {
        	display();
    
        	if (_buzzer > 0)
        	{
        		_buzzer--;
        		if (_buzzer == 0)
        			sound_off();
        	}
        }
    
        ISR(TIMER1_COMPA_vect)
        {
        	if (ActiveTimer == 1 && Timer1 > 0)
        	{
        		Timer1--;
        		if (Timer1 == 0)
        			process_timeoff();
        	}
    
        	if (ActiveTimer == 2 && Timer2 > 0)
        	{
        		Timer2--;
        		if (Timer2 == 0)
        			process_timeoff();
        	}
        }
            

    При срабатывании таймера управление передается соответствующему обработчику прерывания. В нашем случае это обработчик TIMER0_OVF_vect, который вызывает процедуру вывода времени на индикаторы, и TIMER1_COMPA_vect, который обрабатывает обратный отсчет.

    Вывод на индикаторы

        void display()
        {
        	display_number((Timer1/60)/10, 0b00001000);
        	_delay_ms(0.25);
    
        	display_number((Timer1/60)%10, 0b00000100);
        	_delay_ms(0.25);
    
        	display_number((Timer1%60)/10, 0b00000010);
        	_delay_ms(0.25);
    
        	display_number((Timer1%60)%10, 0b00000001);
        	_delay_ms(0.25);
    
        	display_number((Timer2/60)/10, 0b10000000);
        	_delay_ms(0.25);
    
        	display_number((Timer2/60)%10, 0b01000000);
        	_delay_ms(0.25);
    
        	display_number((Timer2%60)/10, 0b00100000);
        	_delay_ms(0.25);
    
        	display_number((Timer2%60)%10, 0b00010000);
        	_delay_ms(0.25);
    
        	PORTD = 0;
        }
    
        void display_number(int number, int mask)
        {
            PORTB = number_mask(number);
            PORTD = mask;
        }
            

    Обработка нажатий

        void handle_buttons()
        {
        	handle_button(KEY_SETUP);
        	handle_button(KEY_RESET);
        	handle_button(KEY_PAUSE);
        	handle_button(KEY_PLAYER1);
        	handle_button(KEY_PLAYER2);
        }
    
        void handle_button(int key)
        {
        	int bit;
        	switch (key)
        	{
        		case KEY_SETUP: 	bit = SETUP_BIT; break;
        		case KEY_RESET: 	bit = RESET_BIT; break;
        		case KEY_PAUSE: 	bit = PAUSE_BIT; break;
        		case KEY_PLAYER1: 	bit = PLAYER1_BIT; break;
        		case KEY_PLAYER2: 	bit = PLAYER2_BIT; break;
        		default: return;
        	}
    
        	if (bit_is_clear(BUTTON_PIN, bit))
        	{
        		if (_pressed == 0)
        		{
        			_delay_ms(DEBOUNCE_TIME);
        			if (bit_is_clear(BUTTON_PIN, bit))
        			{
        				_pressed |= key;
    
        				// key action
        				switch (key)
        				{
        					case KEY_SETUP: 	process_setup(); break;
        					case KEY_RESET: 	process_reset(); break;
        					case KEY_PAUSE: 	process_pause(); break;
        					case KEY_PLAYER1: 	process_player1(); break;
        					case KEY_PLAYER2: 	process_player2(); break;
        				}
    
        				sound_on(15);
        			}
        		}
        	}
        	else
        	{
        		_pressed &= ~key;
        	}
        }
            

    Эта функция по очереди опрашивает все 5 кнопок и обрабатывает нажатие, если таковое случилось. Нажатие регистрируется проверкой bit_is_clear(BUTTON_PIN, bit), т.е. кнопка нажата в том случае, если соответствующий ей вход соединен с землей, что и произойдет, согласно схеме, при нажатии кнопки. Задержка длительностью DEBOUNCE_TIME и повторная проверка нужна во избежание множественных лишних срабатываний из-за дребезга контактов. Сохранение статуса нажатия в соответствующих битах переменной _pressed используется для исключения повторного срабатывания при длительном нажатии на кнопку. Функции обработки нажатий достаточно тривиальны и полагаю, что в дополнительных комментариях не нуждаются.

    Заключение

    Потратив $20-25 на оборудование и пару вечеров на начальное ознакомление с архитектурой микроконтроллера и основными принципами работы, можно начать делать интересные DIY проекты. Статья посвящается тем, кто, как и я в свое время, думает, что начать программировать микроконтроллеры — это сложно, долго или дорого. Поверьте, начать намного проще, чем может показаться. Если есть интерес и желание — пробуйте, не пожалете!