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

от автора

Здравствуйте, уважаемые Хабражители!

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

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

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

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

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

image

Слева на плате — тот самый микроконтроллер, который мы собираемся прошивать.

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

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

image

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

# USBasp programmer 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”, “Сброс”, “Настройка” и “Пауза”. Ну и не забываем про звуковую индикацию окончания игры. Вроде все. На рисунке ниже представлена общая схема подключения микроконтроллера к индикаторам и кнопкам. Она понадобится нам при разборе исхлдного кода программы:

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

Начнем, как и положено, с точки входа программы — функции 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; } 

Функция display использует метод динамической индикации. Дело в том, что каждый отдельно взятый индикатор имеет 9 контактов (7 для управления сегментами, 1 для точки и 1 для питания). Для управления 4 цифрами понадобилось бы 36 контактов. Слишком расточительно. Поэтому вывод разрядов на индикатор с несколькими цифрами организован по следующему принципу:

Напряжение поочередно подается на каждый из общих контактов, что позволяет высветить на соответствующем индикаторе нужную цифру при помощи одних и тех же 8 управляющих контактов. При достаточно высокой частоте вывода это выглядит для глаза как статическая картинка. Именно поэтому все 8 питающих контактов обоих индикаторов на схеме подключены к 8 выходам порта D, а 16 управляющих сегментами контактов соединены попарно и подключены к 8 выходам порта B. Таким образом, функция display с задержкой в 0.25 мс попеременно выводит нужную цифру на каждый из индикаторов. Под конец отключаются все выходы, подающие напряжение на индикаторы (команда PORTD = 0;). Если этого не сделать, то последняя выводимая цифра будет продолжать гореть до следующего вызова функции display, что приведет к ее более яркому свечению по сравнению с остальными.

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

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 используется для исключения повторного срабатывания при длительном нажатии на кнопку.
Функции обработки нажатий достаточно тривиальны и полагаю, что в дополнительных комментариях не нуждаются.

Полный текст программы

#define F_CPU 						4000000UL  #include <avr/io.h> #include <util/delay.h> #include <avr/interrupt.h>   #define DEBOUNCE_TIME 					20  #define BUTTON_PIN 					PINC #define SETUP_BIT 					PC0 #define RESET_BIT 					PC1 #define PAUSE_BIT 					PC2 #define PLAYER1_BIT 					PC3 #define PLAYER2_BIT 					PC4  #define KEY_SETUP					0b00000001 #define KEY_RESET					0b00000010 #define KEY_PAUSE					0b00000100 #define KEY_PLAYER1					0b00001000 #define KEY_PLAYER2					0b00010000   volatile int ActiveTimer = 0; volatile int Timer1 = 0; volatile int Timer2 = 0;  volatile int _buzzer = 0; volatile int _pressed = 0;   // function declarations  void init_io(); void init_data(); int number_mask(int num); void handle_buttons(); void handle_button(int key); void process_setup(); void process_reset(); void process_pause(); void process_timeoff(); void process_player1(); void process_player2(); void display(); void display_number(int mask, int number); void sound_on(int interval); void sound_off();  // interrupts  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(); 	} }   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; }  void init_data() { 	Timer1 = 0; 	Timer2 = 0; 	ActiveTimer = 0; }  int number_mask(int num) { 	switch (num) 	{ 		case 0 : return 0xC0; 		case 1 : return 0xF9; 		case 2 : return 0xA4; 		case 3 : return 0xB0; 		case 4 : return 0x99; 		case 5 : return 0x92; 		case 6 : return 0x82; 		case 7 : return 0xF8; 		case 8 : return 0x80; 		case 9 : return 0x90; 	};  	return 0; }  void process_setup() { 	Timer1 += 60; 	Timer2 += 60;  	// overflow check (5940 seconds == 99 minutes) 	if (Timer1 > 5940 || Timer2 > 5940) 	{ 		Timer1 = 0; 		Timer2 = 0; 	} }  void process_reset() { 	init_data(); }  void process_timeoff() { 	init_data();  	sound_on(30); }  void process_pause() { 	ActiveTimer = 0; }  void process_player1() { 	ActiveTimer = 2; }  void process_player2() { 	ActiveTimer = 1; }  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; 	} }  void handle_buttons() { 	handle_button(KEY_SETUP); 	handle_button(KEY_RESET); 	handle_button(KEY_PAUSE); 	handle_button(KEY_PLAYER1); 	handle_button(KEY_PLAYER2); }  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 sound_on(int interval) { 	_buzzer = interval;  	// put buzzer pin high 	PORTC |= 0b00100000; }  void sound_off() { 	// put buzzer pin low 	PORTC &= ~0b00100000; } 

Прототип был собран на макетной плате:

После тестирования прототипа пришло время все это добро разместить в корпусе, обеспечить питание и т.д.

Ниже показан окончательный вид устройства. Часы питаются от 9-вольтовой батарейки типа “Крона”. Потребление тока — 55 мА.

Заключение

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

Удачного всем программирования!

P.S. Ну и напоследок, небольшая видео-демонстрация прототипа:

ссылка на оригинал статьи http://habrahabr.ru/post/205972/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *