Структура программы для 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-блоки, в которых описана реакция на события.

Заключение

Такое разделение кода делает программу понятной и масштабируемой. Вы в любой момент можете перенести реализацию функций в отдельные файлы, создать заголовочные файлы, добавить отладку — всё это становится проще, если структура кода изначально аккуратная.

Пример подходит для большинства базовых проектов на ATmega328/168 и схожих чипах. Обратите внимание, что настройка таймеров, UART, SPI и других модулей может потребовать расширения структуры.

Автор: Андрей, madmentat.ru