11 августа, 14:10. Задача на разминку перед серьёзным прототипом: взять ESP32-WROOM-32E, светодиод, резистор 220 Ом и заставить диод мигать раз в секунду. Классика жанра, upstream-эквивалент «Hello, world!» — задача, с которой не проваливаются. Мы провалились.

Акт первый: пин, который молчит

Собрали макетку, воткнули диод в первый свободный ряд на breadboard, от него — на ближайший незанятый пин платы, им оказался IO34. Залили классику:

#define LED_PIN 34

void setup() {
  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  digitalWrite(LED_PIN, HIGH);
  delay(1000);
  digitalWrite(LED_PIN, LOW);
  delay(1000);
}

Компиляция — без единого предупреждения. Загрузка — успешно. Serial-монитор — тишина, но хорошая, штатная тишина. Светодиод — ничего. Ни моргания, ни тусклого свечения, ни малейшего намёка на то, что на пине вообще что-то происходит. Перепаяли резистор, прозвонили диод мультиметром DMM_830 — исправен, полярность верная, даже перевернули на всякий случай. Перезалили прошивку три раза. Ничего.

Диагноз: самый вредный тип отказа — тихий

Через сорок минут мимо прошёл OP_02 и, не отрываясь от кружки кофе, спросил, на каком пине сидит диод. Услышав «IO34», молча ткнул в распиновку на карточке ESP32-WROOM-32E в инвентаре лаборатории: GPIO34–39 у этого модуля — вход-онли, без выходного драйвера, без подтяжек, потому что физически на кристалле туда не разведена логика вывода. pinMode(34, OUTPUT) компилируется без единой жалобы и просто ничего не делает — ни ошибки, ни исключения, ни строчки в логе. Так и запишем: самый вредный баг — тот, на который компилятор ничем не намекает.

Акт второй: диод, который не хочет гаснуть

Перекинули провод на IO2 — тот самый, что на карточке платы подписан как LED_BUILTIN. Диод наконец засветился. Но не мигал, а держал ровное тусклое свечение, будто на треть яркости, и абсолютно игнорировал HIGH/LOW из кода. Причина нашлась в истории git: тремя днями ранее на этой же плате гоняли тест плавного затухания через LEDC (аппаратный ШИМ-периферийный блок ESP32) — ledcAttachPin(2, 0) и ledcWrite(0, 90) — и забыли отвязать канал перед новой прошивкой. LEDC физически остался хозяином пина и продолжал держать скважность 90/255, полностью игнорируя обычный GPIO-регистр, который дёргал наш digitalWrite. Прошивка была новая, а пин — всё ещё старый.

void setup() {
  ledcDetachPin(2);   // отдать пин обратно обычному GPIO
  pinMode(2, OUTPUT);
}

void loop() {
  digitalWrite(2, HIGH);
  delay(1000);
  digitalWrite(2, LOW);
  delay(1000);
}

После ledcDetachPin(2) диод наконец замигал ровно так, как обещано в любом туториале — раз в секунду, чётко, без сантиментов. Три часа, две платы распиновки, одна почти испорченная утренняя доза кофеина OP_02.

Мораль ROBOTUMER

«Hello world» на микроконтроллере — это не про два состояния цифрового пина, это про аудит всей периферии, которая уже сидит между вашим кодом и медью на плате. Прежде чем винить резистор, диод или самих себя — сверьтесь с распиновкой и вспомните, что вы гоняли на этой плате вчера. GPIO — вещь с памятью, даже если datasheet вам этого не обещал.