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 Uno | ATmega328P | 16 МГц | 32 256 байт | 2048 байт | Optiboot | 115 200 бит/с |
| Arduino Nano | ATmega328P | 16 МГц | 30 720 байт | 2048 байт | Optiboot (новый) | 115 200 бит/с |
| Arduino Mega 2560 | ATmega2560 | 16 МГц | 253 952 байта | 8192 байта | stk500v2 | 115 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:
У 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:
| Плата | Low | High | Extended | Загрузочная область |
|---|---|---|---|---|
| Arduino Uno | 0xFF | 0xDE | 0xFD | 512 байт |
| Arduino Nano | 0xFF | 0xDA | 0xFD | 2 КБ |
| Arduino Mega 2560 | 0xFF | 0xD8 | 0xFD | 8 КБ |
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 и прерывание по его переполнению.
Источники
- 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 — таблицы выводов
- 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
- Atmel. ATmega640/1280/1281/2560/2561 datasheet, 2549Q–AVR–02/2014 — таймеры 3, 4, 5, входы INT0–INT7 и каналы АЦП 8–15 ATmega2560
Нашли ошибку?
Смотрите также
- Пины ArduinoПин Arduino → порт ATmega, ШИМ, прерывания и регистры
- ПлатыArduino Uno, Nano, Mega 2560: распиновка, питание, загрузчик
- ATmega328PРаспиновка, фьюзы, Optiboot, периферия, питание, схема и код на C
- Фьюзы AVRБиты фьюзов, заводские значения и avrdude
- Таймер AVR и STM32Предделитель, OCRnA, PSC и ARR под частоту
- AVRATmega, ATtiny, AVR Dx/Ex: семейства, прошивка, инструменты
- Система команд AVRВсе инструкции с флагами и тактами, адресация, примеры кода
- UARTКадр 8N1, скорость и допуск, TX и RX, уровни, USB–UART, AVR и STM32