digitalRead и INPUT_PULLUP изнутри
digitalRead возвращает HIGH или LOW, но по дороге ищет вывод в таблицах, выключает ШИМ и читает один бит. Регистр PINx отдаёт сразу восемь выводов за один такт — посмотрим, когда это важно и как правильно включать подтяжку.
Обновлено Редакция GAW
От Arduino к регистрамурок 2 из 825 мин
Цель урока: Разобрать digitalRead и pinMode(INPUT_PULLUP) по исходнику ядра, читать кнопки через PINx с подтяжкой в PORTx и избегать короткого замыкания на входах с кнопками.
В уроке 1 мы разобрали запись в вывод. Чтение устроено похоже, но у него свои особенности: подтягивающий резистор, синхронизатор на входе и неожиданный побочный эффект для выводов с ШИМ. Исходник — wiring_digital.c из ArduinoCore-avr 1.8.8. Как подключать кнопку и подавлять дребезг контактов, подробно разобрано в уроке 3 трека «AVR с нуля», здесь — что делает ядро.
Что делает digitalRead
// ArduinoCore-avr 1.8.8, cores/arduino/wiring_digital.c, lines 165-179
int digitalRead(uint8_t pin)
{
uint8_t timer = digitalPinToTimer(pin);
uint8_t bit = digitalPinToBitMask(pin);
uint8_t port = digitalPinToPort(pin);
if (port == NOT_A_PIN) return LOW;
// If the pin that support PWM output, we need to turn it off
// before getting a digital reading.
if (timer != NOT_ON_TIMER) turnOffPWM(timer);
if (*portInputRegister(port) & bit) return HIGH;
return LOW;
}
Всё как у digitalWrite: три чтения таблиц во флеш-памяти и проверка таймера. Только вместо записи в PORTx — чтение регистра PINx по адресу из таблицы port_to_input_PGM. Запрещать прерывания здесь не нужно: чтение регистра — одна операция.
Обратите внимание на turnOffPWM(). Если вывод 5 выдаёт ШИМ после analogWrite(5, 100), то digitalRead(5) сбросит бит COM0B1, таймер отпустит вывод, и на нём останется уровень бита PORTD5 — после сброса это ноль. Читать вывод, который сейчас выдаёт ШИМ, через digitalRead нельзя без того, чтобы ШИМ не остановился.
Что делает INPUT_PULLUP
Ветка INPUT_PULLUP в pinMode (строки 47–52) сначала сбрасывает бит DDR, потом ставит бит PORT — при запрещённых прерываниях. По таблице 14-1 даташита комбинация DDxn = 0, PORTxn = 1 — это вход с подтягивающим резистором, если бит PUD в регистре MCUCR равен нулю. PUD выключает подтяжку сразу у всех выводов (раздел 14.1); ядро Arduino этот бит не трогает.
| DDxn | PORTxn | Что получается | Как это делает Arduino |
|---|---|---|---|
| 0 | 0 | вход без подтяжки, третье состояние | pinMode(pin, INPUT) |
| 0 | 1 | вход с подтяжкой | pinMode(pin, INPUT_PULLUP) |
| 1 | 0 | выход, низкий уровень | pinMode(pin, OUTPUT) + digitalWrite(pin, LOW) |
| 1 | 1 | выход, высокий уровень | pinMode(pin, OUTPUT) + digitalWrite(pin, HIGH) |
Порядок «сначала DDR, потом PORT» выбран не случайно. Между выходом с низким уровнем (10) и входом с подтяжкой (01) неизбежно промежуточное состояние, и даташит требует, чтобы им было третье состояние (00) или выход с высоким уровнем (11) (раздел 14.2.3). Ядро идёт через третье состояние: 10 → 00 → 01.
Сопротивление подтяжки — от 20 до 50 кОм (табл. 30-1). Нажатая кнопка на землю пропускает ток 5 / 50 000 = 0,1 мА — 5 / 20 000 = 0,25 мА (расчёт наш), а отпущенная даёт на входе единицу, нажатая — ноль.
Есть и старый способ включить подтяжку, который до сих пор встречается в библиотеках: pinMode(pin, INPUT) и затем digitalWrite(pin, HIGH). Функция digitalWrite пишет бит PORT, не проверяя направление вывода (строки 156–160), а у входа этот бит и есть подтяжка. Обратное тоже верно: digitalWrite(pin, LOW) у входа подтяжку выключает.
Пороги и висящий вход
Вход без подтяжки и без внешнего резистора не имеет определённого уровня: digitalRead вернёт то 0, то 1 — от наводок и от пальца рядом с проводом. Это плавающий вход. Даташит гарантирует ноль ниже 0,3 VCC и единицу выше 0,6 VCC (табл. 30-1), то есть при питании 5 В — ниже 1,5 В и выше 3 В (расчёт наш); между ними результат не определён. Неиспользуемым выводам даташит советует задать уровень, проще всего — включить подтяжку, а не соединять их напрямую с VCC или GND: если такой вывод по ошибке станет выходом, получится короткое замыкание (раздел 14.2.6).
То же самое регистрами
// Button on pin 2 (PD2) to GND, internal pull-up.
DDRD &= ~_BV(DDD2); // input
PORTD |= _BV(PORTD2); // pull-up on
if (!(PIND & _BV(PIND2))) {
// pressed: the button pulls PD2 low
}
Регистр PIND лежит по адресу 0x09, а биты регистров с адресами до 0x1F проверяют команды SBIS и SBIC (раздел 8.5) — от 1 до 3 тактов (раздел 37); такая проверка может обойтись одной командой. Те же проверки удобно писать макросами avr-libc bit_is_set(PIND, PIND2) и bit_is_clear(PIND, PIND2).
Главное преимущество регистров здесь не в скорости, а в одновременности. Команда IN читает регистр PIND за 1 такт — все восемь выводов порта D в один и тот же момент:
uint8_t snapshot = PIND; // pins 0-7 at the same instant
uint8_t a = snapshot & _BV(PIND2); // encoder phase A on pin 2
uint8_t b = snapshot & _BV(PIND3); // encoder phase B on pin 3
Два вызова digitalRead(2) и digitalRead(3) прочтут выводы с интервалом в десятки тактов. Если за это время фаза энкодера сменится, получится пара битов, которой на линиях никогда не было.
Синхронизатор
Внешний сигнал попадает в регистр PINx через синхронизатор, и его изменение видно с задержкой от ½ до 1½ такта (раздел 14.2.4). Если программа только что записала в PORTx и сразу читает PINx того же вывода, между записью и чтением нужна команда nop — даташит приводит такой пример. В скетче это _NOP(); — макрос объявлен в avr-libc, а если его нет, в Arduino.h (строки 117–120). У digitalRead эта задержка тонет в собственных тактах функции.
Кнопка и короткое замыкание
Кнопка, замыкающая вывод на землю, безопасна, пока вывод — вход. Если по ошибке в программе он станет выходом с высоким уровнем, нажатие кнопки замкнёт выход на землю, и ток ограничат только выходные транзисторы. Ток короткого замыкания даташит не нормирует, но он может превысить абсолютный максимум 40 мА на вывод (раздел 29.1). Защитить вход от такой ошибки просто: резистор 1 кОм последовательно с кнопкой ограничит ток до 5 В / 1 кОм = 5 мА (расчёт наш), а подтяжке 20–50 кОм он не мешает: нажатая кнопка даёт на входе не больше 5 · 1 / (1 + 20) ≈ 0,24 В — это уверенный ноль.
Задание
- Шесть кнопок подключены к выводам 2–7 Uno и замыкают их на землю. Напишите настройку регистров с подтяжкой и выражение, которое даёт байт с единицами на месте нажатых кнопок. Выводы 0 и 1 заняты
Serial— их не трогайте. - В скетче выполнено
analogWrite(6, 200);, а потомint s = digitalRead(6);. Что будет на выводе 6 после этого? - Чему равно напряжение на входе с подтяжкой, если к нему подключён внешний резистор 10 кОм на землю? Возьмите крайние значения подтяжки, питание 5 В. Прочтётся ли это как ноль?
Ответ и решение
- Маска выводов PD2–PD7 —
0xFC:
DDRD &= ~0xFC; // PD2-PD7 inputs, PD0/PD1 untouched
PORTD |= 0xFC; // pull-ups on
uint8_t pressed = ~PIND & 0xFC; // 1 where a button pulls the pin low
Запись сразу шести битов — это чтение-изменение-запись порта D. Если в программе есть прерывание, которое меняет PORTD или DDRD, оберните настройку в noInterrupts() и interrupts().
-
digitalRead(6)вызоветturnOffPWM(TIMER0A)и сбросит COM0A1: ШИМ на выводе 6 остановится. Вывод останется выходом с уровнем PORTD6 —analogWriteего не менял, так что, скорее всего, это постоянный низкий уровень. Вsпопадёт этот уровень, то естьLOW. -
Делитель из подтяжки и 10 кОм: при 20 кОм — 5 · 10 / 30 ≈ 1,67 В, при 50 кОм — 5 · 10 / 60 ≈ 0,83 В (расчёт наш). Ноль гарантирован ниже 0,3 · 5 = 1,5 В, поэтому при слабой подтяжке это ноль, а при сильной (20 кОм) напряжение попадает в неопределённую зону. Для надёжного нуля внешний резистор нужен меньше — например, 4,7 кОм: 5 · 4,7 / 24,7 ≈ 0,95 В даже при 20 кОм.
Что дальше
В уроке 3 разберём analogWrite(): какие таймеры ядро настраивает в init(), какой вывод от какого таймера и откуда берутся 490,2 и 976,6 Гц. Регистры PINx, DDRx, PORTx и MCUCR — на странице регистров GPIO ATmega328P. Что такое подтягивающий резистор и как выбрать внешний, объясняет термин «Подтягивающий резистор», а пороги входа — термин «VIH, VIL, VOH и VOL».
Частые вопросы
Почему digitalRead выключил ШИМ на выводе?
В ядре Arduino digitalRead для вывода с таймером вызывает ту же функцию turnOffPWM, что и digitalWrite: она сбрасывает бит COMnx1, и таймер отпускает вывод. Чтение analogWrite-вывода через digitalRead поэтому останавливает ШИМ, а на выходе остаётся уровень бита PORT — обычно низкий. Если нужно узнать состояние такого вывода, не трогая ШИМ, читайте регистр PINx напрямую.
Чем pinMode(INPUT) отличается от pinMode(INPUT_PULLUP)?
Оба делают вывод входом (бит DDR = 0), но INPUT ещё и сбрасывает бит PORT — подтяжка выключена, вход «висит», если к нему ничего не подключено. INPUT_PULLUP ставит бит PORT в единицу, и внутренний резистор 20–50 кОм тянет вход к питанию. Старые скетчи делают то же самое двумя строками: pinMode(pin, INPUT) и digitalWrite(pin, HIGH).
Можно ли прочитать несколько кнопок одновременно?
Да, если они на одном порту: одна команда IN читает регистр PIND целиком, и все восемь битов получены в один и тот же такт. Несколько вызовов digitalRead читают выводы по очереди, с промежутком в десятки тактов, — для кнопок это неважно, а для энкодера или параллельной шины может дать несогласованные биты.
Источники
- Arduino. ArduinoCore-avr 1.8.8: cores/arduino/wiring_digital.c и Arduino.h — тег 1.8.8 — коммит 86df345 от 21 мая 2026 (GitHub API, tags и commits); wiring_digital.c, стр. 165–179: digitalRead — три чтения таблиц, turnOffPWM для выводов с таймером, чтение регистра PINx через portInputRegister; стр. 41–52: INPUT сбрасывает бит PORT, INPUT_PULLUP — DDR = 0, затем PORT = 1 при запрещённых прерываниях; стр. 156–160: digitalWrite меняет бит PORT и у входа; Arduino.h, стр. 40–45: HIGH = 1, LOW = 0, INPUT = 0, INPUT_PULLUP = 2; pins_arduino.h: выводы 0 и 1 — PD0 и PD1
- Microchip. ATmega48A/PA/88A/PA/168A/PA/328/P Data Sheet (DS40002061B) — разд. 14.1: бит PUD в MCUCR отключает подтяжку у всех выводов; разд. 14.2.1 и табл. 14-1: PORTxn = 1 у входа включает подтяжку; разд. 8.5: биты регистров 0x00–0x1F проверяют команды SBIS и SBIC; разд. 14.2.3: между входом с подтяжкой и выходом с низким уровнем промежуточным состоянием должно быть третье состояние или выход с высоким уровнем; разд. 14.4.10: PIND по адресу 0x09; разд. 14.2.4: чтение PINxn через синхронизатор с задержкой от ½ до 1½ такта, после записи в порт перед чтением нужен nop; разд. 14.2.6: неиспользуемым выводам нужен определённый уровень, напрямую к VCC и GND их не подключают; табл. 30-1: подтягивающий резистор 20–50 кОм, VIL не больше 0,3 VCC и VIH не меньше 0,6 VCC при VCC 2,4–5,5 В; разд. 29.1: 40 мА на вывод; разд. 37: SBIS и SBIC — 1–3 такта, IN — 1 такт
- AVR-LibC 2.3.2: avr/sfr_defs.h (github.com/avrdudes/avr-libc) — макросы _BV, bit_is_set и bit_is_clear для проверки битов регистров
Нашли ошибку?
Смотрите также
- Кнопки и дребезгВход с подтяжкой, чтение PIND, антидребезг опросом
- Подтягивающий резисторPull-up и pull-down: определённый уровень на свободной линии
- Плавающий входЦифровой вход без определённого уровня: ловит наводки и увеличивает потребление
- VIH, VIL, VOH и VOLПороги входа и уровни выхода логики: что гарантирует таблица даташита
- Пины ArduinoПин Arduino → порт ATmega, ШИМ, прерывания и регистры