Добавить в избранное

Программирование AVR на чистом C: регистры, GPIO и первая прошивка без Arduino

Чтение ≈ 4 мин

Полный гид по программированию микроконтроллеров Atmel AVR на чистом C: установка avr-gcc и avrdude, регистры DDRx/PORTx/PINx, мигание светодиодом, кнопки с подавлением дребезга, битовые макросы и Makefile — всё на примере ATmega328P.

Зачем писать на чистом C, если есть Arduino

Arduino-обёртки удобны, но скрывают главное — как на самом деле работает микроконтроллер. digitalWrite(13, HIGH) превращается в десятки машинных инструкций, тогда как запись бита в регистр PORTB — это одна инструкция sbi за один такт. Код на чистом C с avr-libc:

  • работает в 10–50 раз быстрее Arduino-функций для GPIO;
  • занимает в разы меньше Flash — мигающий светодиод укладывается в 150–200 байт вместо ~1 КБ;
  • даёт полный контроль над таймерами, прерываниями и спящими режимами;
  • переносится на любой AVR — от ATtiny13 до ATmega2560 — без переделок.

Все примеры ниже рассчитаны на ATmega328P (он же стоит в Arduino Uno), но почти без изменений работают на ATmega8/16/32 и с минимальной правкой имён регистров — на ATtiny85/2313.

Установка инструментария

На Debian/Ubuntu всё ставится одной командой:

sudo apt install gcc-avr avr-libc avrdude make

Проверяем версии:

avr-gcc --version
avrdude --version

На Windows проще всего поставить toolchain из состава Microchip Studio или MSYS2-пакет mingw-w64-avr-gcc. На macOS — brew install avr-gcc avrdude.

Три регистра GPIO: DDRx, PORTx, PINx

Каждый порт AVR (PORTB, PORTC, PORTD…) управляется тремя 8-битными регистрами. Каждый бит регистра соответствует одной ножке:

  • DDRx (Data Direction Register) — направление: 1 = выход, 0 = вход. После сброса все ножки — входы.
  • PORTx — для выхода: уровень на ножке (1 = высокий, 0 = низкий). Для входа: 1 = включён внутренний подтягивающий резистор pull-up, 0 = hi-Z.
  • PINx — чтение фактического уровня на ножке. Запись 1 в бит PINx на современных AVR инвертирует соответствующий бит PORTx — удобно для toggle за одну инструкцию.

Мнемоника для ATmega328P: PB0..PB7 — порт B, PC0..PC6 — порт C, PD0..PD7 — порт D. Ножка Arduino «13» — это PB5.

Мигание светодиодом: минимальная прошивка

#define F_CPU 16000000UL
#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    DDRB |= (1 << PB5);        // PB5 (Arduino D13) — выход

    while (1) {
        PORTB |= (1 << PB5);   // высокий уровень
        _delay_ms(500);
        PORTB &= ~(1 << PB5);  // низкий уровень
        _delay_ms(500);
    }
}

F_CPU обязан быть определён до включения util/delay.h — от него считаются задержки. 1 << PB5 — маска бита 5; то же самое делает макрос _BV(PB5).

Инверсия через запись в PIN — ещё короче:

while (1) {
    PINB = (1 << PB5);   // toggle PB5 одной инструкцией
    _delay_ms(500);
}

Битовые макросы: база всей работы с регистрами

Определите их один раз в заголовке проекта — код станет читаемее:

#define BIT(n)          (1U << (n))
#define SET_BIT(reg, n)   ((reg) |= BIT(n))
#define CLR_BIT(reg, n)   ((reg) &= ~BIT(n))
#define TGL_BIT(reg, n)   ((reg) ^= BIT(n))
#define READ_BIT(reg, n)  (((reg) >> (n)) & 1U)

Использование:

SET_BIT(DDRB, PB5);     // PB5 — выход
SET_BIT(PORTB, PB5);    // зажечь
CLR_BIT(PORTB, PB5);    // погасить
TGL_BIT(PORTB, PB5);    // переключить

if (READ_BIT(PIND, PD2)) {
    // на PD2 высокий уровень
}

Кнопка: вход с pull-up и дребезг

Кнопку подключают между ножкой и GND, включают внутреннюю подтяжку — внешний резистор не нужен:

DDRD &= ~(1 << PD2);    // PD2 — вход
PORTD |= (1 << PD2);    // включить pull-up

if (!(PIND & (1 << PD2))) {
    // кнопка нажата (вход притянут к GND)
}

Логика инвертированная: нажата = 0. Механические контакты дребезжат 5–20 мс, поэтому программный антидребезг обязателен:

uint8_t button_pressed(void)
{
    if (PIND & (1 << PD2))
        return 0;            // не нажата

    _delay_ms(20);           // ждём окончания дребезга

    if (PIND & (1 << PD2))
        return 0;            // ложное срабатывание

    while (!(PIND & (1 << PD2)))
        ;                    // ждём отпускания
    _delay_ms(20);
    return 1;
}

Компиляция и прошивка

Компилируем под конкретный чип и оптимизируем — -Os обязателен, без него _delay_ms считает неправильно и код раздувается:

avr-gcc -mmcu=atmega328p -DF_CPU=16000000UL -Os -o main.elf main.c
avr-objcopy -O ihex -R .eeprom main.elf main.hex

Прошиваем через USBasp:

avrdude -c usbasp -p m328p -U flash:w:main.hex:i

Или через Arduino Uno как ISP-программатор / через bootloader по UART:

avrdude -c arduino -p m328p -P /dev/ttyUSB0 -b 115200 -U flash:w:main.hex:i

Makefile для проекта

MCU     = atmega328p
F_CPU   = 16000000UL
PROG    = usbasp
TARGET  = main

CC      = avr-gcc
CFLAGS  = -mmcu=$(MCU) -DF_CPU=$(F_CPU) -Os -Wall -Wextra

all: $(TARGET).hex

$(TARGET).elf: $(TARGET).c
	$(CC) $(CFLAGS) -o $@ $<

$(TARGET).hex: $(TARGET).elf
	avr-objcopy -O ihex -R .eeprom $< $@
	avr-size --format=avr --mcu=$(MCU) $<

flash: $(TARGET).hex
	avrdude -c $(PROG) -p m328p -U flash:w:$<:i

clean:
	rm -f *.elf *.hex

avr-size покажет, сколько Flash и SRAM заняла прошивка — приятно видеть «Program: 178 bytes» там, где Arduino дала бы килобайт.

Бегущий огонь на 8 светодиодах

Восемь светодиодов на PORTD (через резисторы 220–330 Ом на каждый!):

#define F_CPU 8000000UL
#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    DDRD = 0xFF;             // весь порт D — выходы

    while (1) {
        for (uint8_t i = 0; i < 8; i++) {
            PORTD = (1 << i);
            _delay_ms(80);
        }
        for (int8_t i = 6; i > 0; i--) {
            PORTD = (1 << i);
            _delay_ms(80);
        }
    }
}

Здесь пишем в PORTD целиком — за один такт обновляются все 8 ножек. В Arduino потребовалось бы 8 вызовов digitalWrite.

Чтение целого порта и маски

Настройка смешанного порта: PD0–PD3 выходы, PD4–PD7 входы с подтяжкой:

DDRD  = 0b00001111;          // младшие 4 — выходы
PORTD = 0b11110000;          // на выходах 0, на входах pull-up

uint8_t high_nibble = PIND >> 4;    // состояние PD4..PD7

Префикс 0b — расширение GCC, удобно для наглядности. Стандартный вариант — шестнадцатеричный 0x0F.

Частые ошибки новичков

  • Забыли -Os — delay-петли оптимизируются не так, код не влезает во Flash.
  • F_CPU не совпадает с реальной частотой — задержки и UART «уплывут» во столько же раз. Заводские чипы идут с внутренним RC 1 МГц (8 МГц / CKDIV8), а не 16 МГц.
  • Нет конденсаторов по питанию — 100 нФ керамика на каждую пару VCC/GND обязательна, иначе чип будет сбрасываться.
  • Ножка RESET подтянута «плавающе» — 10 кОм на VCC, иначе случайные ресеты.
  • Светодиод без резистора — ножка AVR даёт максимум ~20 мА, без резистора сожжёте и пин, и диод.
  • Запись = вместо |=/&= — PORTB = 0x20 заодно сбрасывает все остальные биты порта.

Что дальше

Освоив GPIO на регистрах, логично двигаться к таймерам (CTC, ШИМ — без них не сделать ни точных задержек в фоне, ни управление сервами), внешним прерываниям INT0/PCINT, UART для отладки и АЦП для аналоговых датчиков — по каждой теме у нас есть отдельная статья с примерами кода.

Поделиться
Понравился материал?

Комментарии

Свернуть форму

Необходимо Войти или Зарегистрироваться, чтобы оставлять комментарии