Мы дошли до точки, где даже библиотеки и компиляторы C++ становятся «прослойкой». Сегодня на ARDUINOHACKS мы погружаемся в самое ядро микроконтроллера — в мир инструкций процессора. Это уровень, на котором один такт имеет значение, а размер программы считается в байтах.
Зачем спускаться на уровень ассемблера?
Когда вы пишете digitalWrite(13, HIGH), компилятор превращает это в несколько десятков инструкций ассемблера. Вы теряете контроль над каждым тактом. Ассемблер дает вам:
- Максимальную скорость: инструкция
SBI(Set Bit in I/O) выполняется за 1 такт - Минимальный размер: программа Blink на C++ занимает ~1 КБ, на ассемблере — 44 байта
- Точный контроль времени: идеально для бит-бэнга и протоколов реального времени
Архитектура ATmega328P: карта памяти и регистры
Прежде чем писать код, нужно понять, как устроена память микроконтроллера :
| Область | Адрес | Размер | Назначение |
|---|---|---|---|
| Регистры R0-R31 | 0x0000 — 0x001F | 32 байта | Рабочие регистры, подключены напрямую к АЛУ |
| I/O регистры | 0x0020 — 0x005F | 64 байта | DDRx, PORTx, PINx и др. — доступны через IN/OUT |
| Расширенные I/O | 0x0060 — 0x00FF | 160 байт | Таймеры, АЦП, UART — доступны через LDS/STS |
| SRAM | 0x0100 — 0x08FF | 2048 байт | Оперативная память для данных |
Ключевое знание: Регистры 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, b | Set Bit — устанавливает бит в 1 | SBI DDRB, 5 | 1 |
CBI A, b | Clear Bit — устанавливает бит в 0 | CBI PORTB, 5 | 1 |
SBIC A, b | Skip if Bit in I/O Cleared | SBIC PIND, 2 | 1-2 |
SBIS A, b | Skip if Bit in I/O Set | SBIS PIND, 2 | 1-2 |
IN Rd, A | Input from I/O | IN R16, PIND | 1 |
OUT A, Rr | Output to I/O | OUT PORTB, R16 | 1 |
Хак: Инструкция 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
Лучшие практики для ассемблерного кода
- Используйте символические имена —
DDRBчитается лучше, чем0x04 - Документируйте каждый шаг — через неделю вы не вспомните, что делает этот код
- Считайте такты — каждая инструкция имеет известное время выполнения
- Используйте
__SFR_OFFSET 0x00— чтобы компилятор правильно подставлял адреса I/O регистров