Структура программы для ATmega (Microchip Studio)
В этой статье мы рассмотрим рекомендуемую структуру программы для микроконтроллеров семейства ATmega, написанную в среде Microchip Studio. Такой подход улучшает читаемость, поддержку и расширение кода. Ниже приведена подробная схема организации исходного файла .c
.
У меня есть друг, московский инженер-схемотехник и программист уровня "Сеньор" - так вот, он может без затей объявить какую-нибудь глобальную переменную хоть бы даже посреди "простыни" всей программы, ему похуй. Я хоть сам по жизни распиздяй, но считаю, что такое поведение крайне вредно в том смысле, что надо делать код читаемым для твоих коллег на тот случай, если тебя нахуй уволят или если у тебя появятся миньоны, которым можно делегировать какие-то рутинные задачи. Если не придерживаться определенных условных правил, которые все понимают, то они ведь потом просто заебут тебя вопросами! Так ведь? Да-да, именно так!!! Поэтому разбираем типичный шаблон, пытаемся прояснить для себя где тут что.
Общая структура программы:
// === 1. Указание частоты CPU === #define F_CPU 16000000UL // === 2. Подключение заголовков === #include <avr/io.h> #include <util/delay.h> #include <avr/interrupt.h> // === 3. Остальные #define === #define LED_PIN PB0 #define BUTTON_PIN PD2 // === 4. Глобальные переменные === volatile uint8_t led_state = 0; volatile uint32_t milliseconds_1 = 0; // Переменная для отсчёта миллисекунд // === 5. Прототипы функций === void io_init(void); void interrupts_init(void); void toggle_led(void); void setup(void); void loop(void); int main(void); uint32_t millis(void); // Прототип функции millis() // === 6. Реализация функций === void toggle_led(void) { PORTB ^= (1 << LED_PIN); } uint32_t millis(void) { uint32_t temp; cli(); // Отключаем прерывания для безопасного чтения temp = milliseconds_1; sei(); // Включаем прерывания обратно return temp; } // === 7. Блок настроек IO/Прерываний === void io_init(void) { DDRB |= (1 << LED_PIN); // LED как выход PORTB &= ~(1 << LED_PIN); // LED выключен DDRD &= ~(1 << BUTTON_PIN); // Кнопка как вход PORTD |= (1 << BUTTON_PIN); // Подтягивающий резистор } void interrupts_init(void) { // Настройка внешнего прерывания INT0 EICRA |= (1 << ISC01); // Прерывание по спадающему фронту EIMSK |= (1 << INT0); // Включить INT0 // Настройка Timer0 для прерывания каждую миллисекунду TCCR0A |= (1 << WGM01); // Режим CTC TCCR0B |= (1 << CS01) | (1 << CS00); // Предделитель 64 OCR0A = 249; // 16 МГц / 64 / 1000 = 250 - 1 TIMSK0 |= (1 << OCIE0A); // Включить прерывание по совпадению sei(); // Разрешить глобальные прерывания } // === 8. Блок базовых настроек === void setup(void) { io_init(); interrupts_init(); } void loop(void) { while (1) { _delay_ms(100); // Имитация основной задачи } } // === 9. Точка входа в программу === int main(void) { setup(); loop(); return 0; // Для соответствия стандарту, хотя никогда не выполнится } // === 10. Обработчики прерываний === ISR(INT0_vect) { toggle_led(); led_state = !led_state; } ISR(TIMER0_COMPA_vect) { milliseconds_1++; // Увеличиваем счётчик миллисекунд }
Пояснение к структуре
- F_CPU — макрос, определяющий частоту работы МК, обязателен при использовании
_delay_ms()
и особенно это важно при работе с интерфейсами, которые напрямую зависят от таймеров... Вообще, тактовая частота МК прямо влияет на всю математику любых задержек. Это краеугольный камень всего программирования под микроконтроллеры, это даже важнее чем побитовые операции, о которых говорил сеньер Жуков, хотя сеньер Жуков далеко не дурак и он, разумеется, тоже по-своему прав. - #include — подключаем системные библиотеки. А может и несистемные. Я, например, иногда подключаю свои собственные, с блэкджеком и шлюхами.
- #define — определяем пины, константы и упрощаем читабельность.
- Глобальные переменные — переменные, которые используются в нескольких частях программы или в прерываниях.
- Прототипы функций — объявления функций для удобства расположения кода. Без прототипов можно обойтись. Но в таком случае из вас не получится сеньора Жукова, вы не сможете писать свои функции где попало, придется соблюдать определенный порядок, "понятный" компилятору. Впрочем, лично я за то, чтобы этот порядок соблюдать в любом случае.
- Функции — основная логика: работа с портами, обработка прерываний, функции включения/выключения устройств и т.д.
- setup() — инициализация портов, таймеров, периферии (без прерываний).
- interrupts_init() — настройка и включение прерываний.
- loop() — главный цикл, где выполняются основные задачи.
- main() — точка входа, где вызываются
setup()
иloop()
. - Обработчики прерываний — ISR-блоки, в которых описана реакция на события.
Заключение
Такое разделение кода делает программу понятной и масштабируемой. Вы в любой момент можете перенести реализацию функций в отдельные файлы, создать заголовочные файлы, добавить отладку — всё это становится проще, если структура кода изначально аккуратная.
Автор: Андрей, madmentat.ru