Перейти к содержанию

Arduino и AVR: что скрывает ядро Arduino

Скетч Arduino для Uno, Nano или Mega — это программа на C++ для микроконтроллера AVR, к которой ядро Arduino добавляет функцию main(), настройку таймеров и АЦП, обработчик прерывания для millis() и таблицы выводов. Разбираем по исходникам ядра и даташиту ATmega328P, что именно происходит за каждым вызовом и как перейти от Arduino к регистрам.

Обновлено Редакция GAW

Плата Arduino Uno — это микроконтроллер ATmega328P на 16 МГц с загрузчиком, а скетч — программа на C++, которую собирает avr-gcc. Между скетчем и кристаллом стоит ядро Arduino: оно добавляет функцию main(), настраивает таймеры и АЦП, считает время в прерывании и переводит номера выводов Arduino в порты микроконтроллера. Ниже — что именно делает ядро AVR по его исходникам и почему это важно, когда скетча уже не хватает.

Коротко

  • main() ядра: init() → setup() → бесконечный loop().
  • init(): все таймеры с делителем 64, Timer0 — Fast PWM и прерывание по переполнению для millis(), остальные — Phase Correct PWM; АЦП — частота ÷ 128.
  • Время: переполнение Timer0 каждые 1024 мкс; micros() — с шагом 4 мкс при 16 МГц.
  • ШИМ: 976,6 Гц на Timer0 и 490,2 Гц на остальных таймерах.
  • АЦП: 125 кГц, 104 мкс на преобразование.
  • Загрузчик: Optiboot на 512 байт у Uno, stk500v2 у Mega 2560.

Платы и микроконтроллеры

Какой микроконтроллер стоит на плате и как его прошивать, ядро описывает в файле boards.txt:

ПлатаМикроконтроллерЧастотаСкетч, не большеОЗУЗагрузчикСкорость загрузки
Arduino UnoATmega328P16 МГц32 256 байт2048 байтOptiboot115 200 бит/с
Arduino NanoATmega328P16 МГц30 720 байт2048 байтOptiboot (новый)115 200 бит/с
Arduino Mega 2560ATmega256016 МГц253 952 байта8192 байтаstk500v2115 200 бит/с

У Nano в меню Arduino IDE есть ещё вариант «ATmega328P (Old Bootloader)» — для плат со старым загрузчиком ATmegaBOOT, который принимает скетч на 57 600 бит/с. Какой вывод платы какому порту микроконтроллера соответствует, показывает карта выводов Arduino.

Что происходит до setup()

Функцию main() в скетч добавляет ядро:

int main(void)
{
    init();          // timers, ADC, interrupts
    initVariant();   // board-specific, empty for Uno, Nano and Mega
    setup();
    for (;;) {
        loop();
        if (serialEventRun) serialEventRun();
    }
}

init() из файла wiring.c разрешает прерывания и настраивает периферию так, чтобы работали функции Arduino:

  • Timer0 — режим Fast PWM, делитель 64, прерывание по переполнению. На нём millis(), micros(), delay() и ШИМ на двух выводах.
  • Timer1 и Timer2 (у Mega ещё Timer3, Timer4 и Timer5) — 8-битный Phase Correct PWM, делитель 64. Для analogWrite().
  • АЦП — делитель 128 при частоте от 16 МГц, то есть 125 кГц, и включение (бит ADEN).
  • USART0 — отключается от выводов 0 и 1 (UCSR0B = 0): их подключает загрузчик, а Serial.begin() подключит снова.

Всё это занимает таймеры, которые иначе были бы свободны. Если скетчу нужен таймер с другой частотой, его перенастраивают через регистры после init() — например, в setup() — и теряют analogWrite() на его выводах.

Время: millis(), micros() и delay()

Timer0 считает такты с делителем 64 и переполняется каждые 256 отсчётов, то есть каждые 64 · 256 = 16 384 такта — 1024 мкс при 16 МГц. Обработчик прерывания прибавляет к счёту миллисекунд 1, а лишние 24 мкс копит в дробной части; когда набирается целая миллисекунда, прибавляет ещё 1. Поэтому millis() в среднем точна, но примерно раз в 42 мс перескакивает через значение.

micros() складывает число переполнений и текущее значение счётчика TCNT0, так что её шаг — 64 такта, 4 мкс при 16 МГц. delay() крутится в цикле, сравнивая micros(), — процессор всё это время занят. Внутри обработчиков прерываний millis() не растёт: пока выполняется один обработчик, прерывание по переполнению Timer0 ждёт.

Выводы: pinMode() и digitalWrite()

Номер вывода Arduino — индекс в таблицах файла pins_arduino.h: по нему digitalWrite() находит порт, бит и таймер. Если вывод подключён к таймеру, функция сначала отключает ШИМ, затем при запрещённых прерываниях меняет бит в регистре PORT. Та же операция на регистрах:

// digitalWrite(13, HIGH) on the Uno: pin 13 is PB5
DDRB |= (1 << 5);    // pinMode(13, OUTPUT)
PORTB |= (1 << 5);   // HIGH
PORTB &= ~(1 << 5);  // LOW

Регистровая версия не ищет порт по таблицам и не проверяет таймер, зато привязана к микроконтроллеру: на Mega тот же вывод 13 — уже PB7. Соответствие для каждого вывода и готовый код даёт карта выводов Arduino.

ШИМ: analogWrite()

analogWrite() не настраивает таймер — это уже сделала init(). Функция переводит вывод в режим выхода, подключает к нему выход сравнения таймера (бит COMnx1 в регистре TCCRnA) и записывает скважность в регистр OCRnx. Значения 0 и 255 она выводит как постоянный уровень через digitalWrite(). Частота задаётся режимом таймера и делителем 64:

fFast PWM=16 МГц64⋅256=976,6 Гц,fPhase Correct=16 МГц64⋅510=490,2 Гц.f_\text{Fast PWM} = \frac{16\ \text{МГц}}{64 \cdot 256} = 976{,}6\ \text{Гц}, \qquad f_\text{Phase Correct} = \frac{16\ \text{МГц}}{64 \cdot 510} = 490{,}2\ \text{Гц}.

У Uno и Nano на Timer0 выводы 5 и 6, у Mega 2560 — 4 и 13. Нужна другая частота — перенастройте таймер через регистры; делители и период под заданную частоту считает калькулятор таймера.

АЦП: analogRead()

analogRead() выбирает канал и опорное напряжение в регистре ADMUX, запускает преобразование битом ADSC и ждёт его окончания. Опорное напряжение по умолчанию — питание AVCC; analogReference(INTERNAL) переключает ATmega328P на внутренний источник 1,1 В. У Mega каналы 8–15 выбирает ещё бит MUX5 регистра ADCSRB.

Для полных 10 бит АЦП нужен такт от 50 до 200 кГц, и ядро делит 16 МГц на 128 — получается 125 кГц. Преобразование длится 13 тактов АЦП, 104 мкс, а первое после включения АЦП — 25 тактов. Значит, analogRead() даёт около 9600 измерений в секунду, и всё это время процессор ждёт результата.

Serial

Serial — это USART0 микроконтроллера с кольцевыми буферами на приём и передачу. Ядро делает их по 64 байта, если ОЗУ у микроконтроллера не меньше 1 КБ, и по 16 байт — у кристаллов с меньшим ОЗУ. Приём и передача идут в прерываниях: Serial.print() возвращается сразу, пока данные помещаются в буфер, и ждёт, когда буфер полон. У Mega есть ещё Serial1, Serial2 и Serial3 на USART1–USART3. Делитель скорости и ошибку для любой частоты считает калькулятор UART.

Загрузчик и фьюзы

Скетч по USB принимает загрузчик — небольшая программа в конце флеш-памяти, с которой микроконтроллер стартует после сброса. Под него фьюзами отведена загрузочная область, а lock-биты 0x0F запрещают скетчу её перезаписать. Значения из boards.txt:

ПлатаLowHighExtendedЗагрузочная область
Arduino Uno0xFF0xDE0xFD512 байт
Arduino Nano0xFF0xDA0xFD2 КБ
Arduino Mega 25600xFF0xD80xFD8 КБ

Low 0xFF выбирает внешний кварц без деления частоты на 8, Extended 0xFD включает детектор пониженного питания на 2,7 В. Расшифровку каждого бита даёт калькулятор фьюзов AVR. Если прошить плату через ISP-программатор, загрузчик сотрётся вместе со скетчем; вернуть его можно командой «Записать загрузчик» в Arduino IDE.

От Arduino к регистрам

Переходить на регистры стоит там, где ядро мешает: нужна другая частота ШИМ, точные интервалы без дрожания millis(), быстрые переключения вывода, прерывания по изменению уровня на любом выводе (PCINT) или режимы сна. Ничто не мешает смешивать оба подхода в одном скетче:

void setup() {
  Serial.begin(115200);        // the core keeps working
  // Timer1: CTC mode, 1 kHz on OC1A (pin 9 of the Uno), 16 MHz / (2 * 8 * 1000) - 1 = 999
  DDRB |= (1 << 1);            // PB1 = pin 9 as output
  TCCR1A = (1 << COM1A0);      // toggle OC1A on compare match
  TCCR1B = (1 << WGM12) | (1 << CS11);  // CTC, prescaler 8
  OCR1A = 999;
}

void loop() {
  Serial.println(millis());    // Timer0 is untouched
  delay(1000);
}

Здесь Timer1 перенастроен под меандр 1 кГц на выводе 9, а Serial, millis() и delay() работают как раньше, потому что Timer0 не тронут. Зато analogWrite() на выводах 9 и 10 больше не даст ШИМ 490 Гц.

Частые вопросы

Где в скетче Arduino функция main()?

Её добавляет ядро Arduino (файл main.cpp): main() вызывает init(), затем setup(), а потом в бесконечном цикле loop() и после каждого прохода — обработчики serialEvent(), если они есть. Свою main() в скетче писать не нужно; если написать, ядро не выполнит init() и таймеры, millis() и analogWrite() работать не будут.

Почему millis() иногда прибавляет сразу 2?

millis() считает прерывания по переполнению Timer0, а они приходят каждые 64 · 256 тактов — 1024 мкс при 16 МГц. На каждом переполнении к счёту добавляется 1 мс, а лишние 24 мкс копятся в дробной части; когда их набирается на целую миллисекунду, счёт растёт сразу на 2. Так бывает примерно раз в 42 мс. В среднем время верное, но соседние значения millis() иногда различаются на 2.

Какая частота ШИМ у analogWrite()?

У Uno и Nano 490,2 Гц на пинах 3, 9, 10 и 11 и 976,6 Гц на пинах 5 и 6. Ядро запускает все таймеры с делителем 64: Timer0 в режиме Fast PWM (период 256 тактов), остальные — в Phase Correct PWM (510 тактов). Частоту можно поменять, перенастроив таймер через регистры, но Timer0 трогать не стоит: от него идут millis() и delay().

Сколько длится analogRead()?

При 16 МГц ядро делит частоту на 128 и тактирует АЦП на 125 кГц — в пределах 50–200 кГц, которые даташит требует для полных 10 бит. Преобразование занимает 13 тактов АЦП, то есть 104 мкс, а самое первое после включения АЦП — 25 тактов, 200 мкс. Отсюда около 9600 измерений в секунду через analogRead().

Сколько памяти занимает загрузчик Arduino?

На Arduino Uno загрузчик Optiboot занимает 512 байт: из 32 768 байт флеш-памяти ATmega328P для скетча остаётся 32 256. На Nano в ядре фьюзами отведено 2 КБ, и скетч может занимать 30 720 байт. У Mega 2560 загрузчик stk500v2 и под скетч — 253 952 байта из 262 144. Фьюзы и lock-биты загрузчика — в файле boards.txt ядра.

Можно ли писать для Arduino на регистрах?

Да, скетч — обычная программа на C++ для avr-gcc, и регистры доступны по именам из avr-libc: PORTB, DDRB, TCCR1A, ADMUX. Можно смешивать: оставить millis() и Serial ядра, а нужный вывод переключать записью в PORT. Нельзя только забирать у ядра то, на чём оно держится, — Timer0 и прерывание по его переполнению.

Источники

  1. Arduino. ArduinoCore-avr, коммит 11b9130 (github.com/arduino/ArduinoCore-avr) — cores/arduino/main.cpp — main(); wiring.c — init(), millis(), micros(), delay(); wiring_digital.c — pinMode(), digitalWrite(); wiring_analog.c — analogRead(), analogWrite(); WInterrupts.c — attachInterrupt(); HardwareSerial.h — размер буферов; Arduino.h — опорные напряжения; boards.txt — микроконтроллеры, частоты, фьюзы, загрузчики, предельный размер скетча; variants/*/pins_arduino.h — таблицы выводов
  2. Microchip. ATmega48A/PA/88A/PA/168A/PA/328/P Data Sheet, DS40002061B — 15.7.3 и 15.7.4 — частота Fast PWM и Phase Correct PWM; 24.4 — тактовая частота АЦП 50–200 кГц для 10 бит, преобразование 13 тактов АЦП, первое — 25
  3. Atmel. ATmega640/1280/1281/2560/2561 datasheet, 2549Q–AVR–02/2014 — таймеры 3, 4, 5, входы INT0–INT7 и каналы АЦП 8–15 ATmega2560

Нашли ошибку?