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

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 этот бит не трогает.

DDxnPORTxnЧто получаетсяКак это делает Arduino
00вход без подтяжки, третье состояниеpinMode(pin, INPUT)
01вход с подтяжкойpinMode(pin, INPUT_PULLUP)
10выход, низкий уровеньpinMode(pin, OUTPUT) + digitalWrite(pin, LOW)
11выход, высокий уровень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 В — это уверенный ноль.

Задание

  1. Шесть кнопок подключены к выводам 2–7 Uno и замыкают их на землю. Напишите настройку регистров с подтяжкой и выражение, которое даёт байт с единицами на месте нажатых кнопок. Выводы 0 и 1 заняты Serial — их не трогайте.
  2. В скетче выполнено analogWrite(6, 200);, а потом int s = digitalRead(6);. Что будет на выводе 6 после этого?
  3. Чему равно напряжение на входе с подтяжкой, если к нему подключён внешний резистор 10 кОм на землю? Возьмите крайние значения подтяжки, питание 5 В. Прочтётся ли это как ноль?
Ответ и решение
  1. Маска выводов 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().

  1. digitalRead(6) вызовет turnOffPWM(TIMER0A) и сбросит COM0A1: ШИМ на выводе 6 остановится. Вывод останется выходом с уровнем PORTD6 — analogWrite его не менял, так что, скорее всего, это постоянный низкий уровень. В s попадёт этот уровень, то есть LOW.

  2. Делитель из подтяжки и 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 читают выводы по очереди, с промежутком в десятки тактов, — для кнопок это неважно, а для энкодера или параллельной шины может дать несогласованные биты.

Источники

  1. 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
  2. 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 такт
  3. AVR-LibC 2.3.2: avr/sfr_defs.h (github.com/avrdudes/avr-libc) — макросы _BV, bit_is_set и bit_is_clear для проверки битов регистров

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