Часть 13: Ассемблер AVR — пишем код на уровне процессора

Мы дошли до точки, где даже библиотеки и компиляторы C++ становятся «прослойкой». Сегодня на ARDUINOHACKS мы погружаемся в самое ядро микроконтроллера — в мир инструкций процессора. Это уровень, на котором один такт имеет значение, а размер программы считается в байтах.

Зачем спускаться на уровень ассемблера?

Когда вы пишете digitalWrite(13, HIGH), компилятор превращает это в несколько десятков инструкций ассемблера. Вы теряете контроль над каждым тактом. Ассемблер дает вам:

  • Максимальную скорость: инструкция SBI (Set Bit in I/O) выполняется за 1 такт 
  • Минимальный размер: программа Blink на C++ занимает ~1 КБ, на ассемблере — 44 байта 
  • Точный контроль времени: идеально для бит-бэнга и протоколов реального времени 

Архитектура ATmega328P: карта памяти и регистры

Прежде чем писать код, нужно понять, как устроена память микроконтроллера :

ОбластьАдресРазмерНазначение
Регистры R0-R310x0000 — 0x001F32 байтаРабочие регистры, подключены напрямую к АЛУ
I/O регистры0x0020 — 0x005F64 байтаDDRx, PORTx, PINx и др. — доступны через IN/OUT
Расширенные I/O0x0060 — 0x00FF160 байтТаймеры, АЦП, UART — доступны через LDS/STS
SRAM0x0100 — 0x08FF2048 байтОперативная память для данных

Ключевое знание: Регистры R16-R31 — «особенные». Только они могут работать с непосредственными значениями через LDI (Load Immediate). Регистры R0-R15 такой возможности не имеют .

Первая программа на ассемблере: моргаем светодиодом

Добавляем в Arduino IDE новый файл с именем main.S (обратите внимание: буква S обязательно заглавная!) 

#define __SFR_OFFSET 0x00
#include <avr/io.h>

.global main

main:
    ; Шаг 1: Настраиваем пин 13 (PB5) как ВЫХОД
    SBI DDRB, 5          ; Set Bit in I/O — записываем 1 в бит 5 DDRB
                         ; Это одна инструкция — 1 такт процессора!

loop:
    ; Шаг 2: Включаем светодиод
    SBI PORTB, 5         ; Устанавливаем бит 5 PORTB в 1 (HIGH)
    CALL delay           ; Вызываем подпрограмму задержки

    ; Шаг 3: Выключаем светодиод
    CBI PORTB, 5         ; Clear Bit in I/O — записываем 0 в бит 5 PORTB
    CALL delay

    RJMP loop            ; Бесконечный цикл

delay:
    ; Простая задержка на 16 МГц
    LDI R18, 82          ; Загружаем 82 в R18 (внешний цикл)
d1: LDI R17, 0           ; Загружаем 0 в R17 (средний цикл)
d2: LDI R16, 0           ; Загружаем 0 в R16 (внутренний цикл)
d3: DEC R16              ; Уменьшаем R16
    BRNE d3              ; Если R16 != 0, переходим на d3
    DEC R17
    BRNE d2
    DEC R18
    BRNE d1
    RET                  ; Возврат из подпрограммы

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

  • SBI и CBI — самые быстрые инструкции для работы с I/O. Они работают только с адресами 0x00-0x1F (первые 32 I/O регистра). К счастью, DDRB и PORTB туда входят .
  • LDI (Load Immediate) — загружает константу в регистр. Работает только с R16-R31 .

Управление пинами через ассемблер: полная таблица

ИнструкцияОписаниеПримерТактов
SBI A, bSet Bit — устанавливает бит в 1SBI DDRB, 51
CBI A, bClear Bit — устанавливает бит в 0CBI PORTB, 51
SBIC A, bSkip if Bit in I/O ClearedSBIC PIND, 21-2
SBIS A, bSkip if Bit in I/O SetSBIS PIND, 21-2
IN Rd, AInput from I/OIN R16, PIND1
OUT A, RrOutput to I/OOUT PORTB, R161

Хак: Инструкция SBIC позволяет читать пин и делать условный переход за 1-2 такта. Сравните с digitalRead(), который тратит ~50 тактов.

Микс C++ и ассемблера: два в одном

Вы можете вызывать функции, написанные на ассемблере, прямо из Arduino-скетча. Это позволяет писать критичный код на ассемблере, а логику верхнего уровня — на C++ .

Файл .ino (C++):

extern "C" {
    void setup_asm();   // Объявляем функцию из ассемблера
    void loop_asm();
}

void setup() {
    setup_asm();        // Вызываем ассемблерную инициализацию
}

void loop() {
    loop_asm();         // Вызываем ассемблерный цикл
}

Файл main.S (ассемблер):

#define __SFR_OFFSET 0x00
#include <avr/io.h>

.global setup_asm
.global loop_asm

setup_asm:
    SBI DDRB, 5          ; Пин 13 на выход
    RET

loop_asm:
    SBI PORTB, 5         ; Включить LED
    CALL my_delay
    CBI PORTB, 5         ; Выключить LED
    CALL my_delay
    RET

Лучшие практики для ассемблерного кода

  1. Используйте символические имена — DDRB читается лучше, чем 0x04 
  2. Документируйте каждый шаг — через неделю вы не вспомните, что делает этот код
  3. Считайте такты — каждая инструкция имеет известное время выполнения
  4. Используйте __SFR_OFFSET 0x00 — чтобы компилятор правильно подставлял адреса I/O регистров 

Вам также может понравиться

About the Author: ардуинчиков

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

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