«Случайный» свет в прихожей выдал себя на третий день: я переткнул блок питания — и лампа мигнула в той же последовательности, что и вчера в ту же секунду. Кубик из базового урока про random и randomSeed грешил тем же: после каждого ресета — одна и та же «случайная» серия. Дело не в бракованной плате: без засева ядро Arduino ветки 1.0.x стартует с фиксированным зерном, и ГПСЧ честно прокручивает один и тот же ряд при каждом включении.
Проблема и решение симметричны: ГПСЧ — детерминированная машинка, случайность в неё надо принести снаружи. Источников в Uno три, и все бесплатные: шум аналогового входа, висящего в воздухе; значение millis() на момент первого опроса — оно зависит от того, когда ты воткнул питание; счётчик запусков в EEPROM, который растёт от ресета к ресету. По одному они слабы, в мешалке — крепкое зерно.
В уроке: таблица «какой сид даст какое повторение», XOR-мешалка из 7 замеров A0, микс с millis() и EEPROM-счётчиком, и прикладной выход — случайные интервалы для планировщика на millis из урока про планировщик задач: свет в прихожей ходит гулять каждые 2–8 секунд, и двух одинаковых пауз в ряд не бывает.
Шум A0 как источник энтропии: 7 замеров и XOR-мешалка
Классика жанра — randomSeed(analogRead(A0)) на неподключенном входе. Работает через раз: плавающий вход ловит наводки, но если вход что-то держит — подтяжка, длинный провод, плечо делителя — показания встают в узкий коридор, и зерно повторяется. Второй грабль: millis() в момент setup() после холодного старта всегда мал (10–30 мс), а после watchdog-ресета из урока про watchdog и сон он и вовсе ноль — одно millis() зерном не считается, слишком предсказуемо.
Мешалка, которую я использую: 7 замеров A0 с паузами по 3 мс, каждый замер сдвигается на своё место (i·3 бита) и складывается с остальными через XOR, в компанию подмешивается millis(). Сдвиг на i·3 раскидывает замеры по 32-битному слову — семь замеров занимают 21 бит, даже если вход скучный и стабильный; millis() в миксе добавляет ещё и время между замерами, а оно зависит от твоей руки. Результат — unsigned long, его отдаю в randomSeed() один раз в setup().
Третий источник — счётчик запусков в EEPROM: при каждом старте читаю ячейку, увеличиваю, пишу назад и XOR-ю с зерном. Два холодных старта с одинаковым положением ручки и «одной и той же» наводкой теперь дают разные серии — счётчик честно отщелкал. Схему с магическим числом в EEPROM беру целиком: магия отличает первый запуск на чистом чипе от нуля настоящих запусков. И правило гигиены: randomSeed() зовут один раз, не в loop() — пересеивание каждые миллисекунду ломает статистику ряда, числа начинают липнуть к кластерам.
// Настоящее зерно: шум A0 (7 замеров, XOR) + millis + EEPROM-счётчик.// randomSeed и EEPROM - библиотечные вызовы, движок их не исполняет.#include <EEPROM.h> const int PIN_LED = 8;const long MAGIC = 424242; // магия счётчика из EEPROM-урокаint zapuskov = 0; unsigned long sobratZerno() { unsigned long meshok = 0; for (int i = 0; i < 7; i++) { // 7 замеров с паузами meshok = meshok ^ ((unsigned long)analogRead(A0) << (i * 3)); meshok = meshok + millis(); // микс со временем между замерами delay(3); // наводке нужно время измениться } return meshok;} void setup() { Serial.begin(9600); pinMode(PIN_LED, OUTPUT); long magiya; EEPROM.get(0, magiya); if (magiya == MAGIC) { EEPROM.get(4, zapuskov); } zapuskov = zapuskov + 1; // счётчик: от ресета к ресету растёт EEPROM.put(4, zapuskov); EEPROM.put(0, MAGIC); unsigned long zerno = sobratZerno() ^ (unsigned long)zapuskov; randomSeed(zerno); // один раз в setup(), не в loop() Serial.print("zerno="); Serial.print(zerno); Serial.print(" zapuskov="); Serial.println(zapuskov);} void loop() { // случайные интервалы "умного дома": свет гуляет каждые 2-8 секунд static unsigned long tSled = 0; static unsigned long interval = 4000; // следующая пауза, случайная if (millis() - tSled >= interval) { tSled = millis(); interval = random(2000, 8000); // 2000..7999 мс, верхняя исключена digitalWrite(PIN_LED, !digitalRead(PIN_LED)); Serial.print("sleduyushiy interval: "); Serial.println(interval); }}Разбор по строкам. sobratZerno() — та самая мешалка: сдвиг на i·3 раскладывает семь замеров по слову, XOR складывает их без переносов, millis() примешивает время. EEPROM-блок в setup(): магия 424242 отделяет чистый чип от уже считавшего счётчик — приём из урока про магическое число, я его не придумывал заново. randomSeed() вызван ровно один раз: после него серия идёт своя, уникальная для каждого включения, и статистика ряда не испорчена.
Ловушка с random() в условии: напиши if (millis() - tSled >= random(2000, 8000)) — и кубик перекинется каждый оборот loop, пауза систематически выйдет короче задуманной, потому что сработает первое же значение меньше текущего окна. Кубик кидают один раз, когда предыдущий интервал отработал: interval = random(2000, 8000) внутри ветки. Границы — в миллисекундах, не в секундах: второй частый промах. И помни про исключённую верхнюю границу: random(2000, 8000) даёт 2000..7999.
Такие случайные окна отлично живут в кооперативном цикле как ещё одна задача без delay: единственные паузы в скетче — три миллисекунды между замерами в setup(), в loop() их нет. Мне этого хватает для сценариев присутствия: за месяц у прихожего светильника не нашлось двух одинаковых «случайных» серий подряд, хотя я искал их специально — счётчик запусков гарантирует, что даже две перезагрузки в одну секунду дадут разные зерна.
| Чем засеяли | Что после ресета | Когда этого хватает |
|---|---|---|
| ничем (дефолт ядра 1.0.x) | сид фиксирован: серия повторяется при каждом включении | нигде: это источник тикетов «random повторяется» |
| randomSeed(analogRead(A0)) | совпадает, если вход держит подтяжка или стабильная наводка | игрушки, где повторение не критично |
| A0 × 7 замеров XOR + millis() | совпадает почти никогда; watchdog-ресет даёт millis = 0 — но серия всё равно другая | большинство проектов |
| плюс счётчик запусков в EEPROM | два старта в одну секунду всё равно дают разные зерна | сценарии присутствия, игры на честность |
- 1. Почему random() без randomSeed() после каждого ресета выдаёт ту же серию?
- 2. Одна строка randomSeed(analogRead(A0)) — почему это ненадёжное зерно?
- 3. Как задать планировщику паузу «от 2 до 8 секунд»?
- без засева ядро 1.0.x стартует с фиксированным зерном: серия random() повторяется от ресета к ресету
- зерно = шум A0 (7 замеров, XOR со сдвигом i·3) + millis() + счётчик запусков из EEPROM
- randomSeed() зовут один раз в setup(): пересеивание в loop() ломает статистику ряда
- случайный интервал = random(2000, 8000), кубик кидают один раз, когда предыдущий отработал; random() в условии перекидывает кубик каждый оборот
Частые вопросы по теме урока
- Почему random() без randomSeed() после каждого ресета выдаёт ту же серию?
- ГПСЧ детерминирован: ряд целиком задаётся начальным сидом. Без засева ядро 1.0.x шьёт фиксированный сид, поэтому после каждого ресета серия одинаковая. ядро 1.0.x стартует с фиксированным зерном — ГПСЧ прокручивает один и тот же ряд
- Одна строка randomSeed(analogRead(A0)) — почему это ненадёжное зерно?
- Один замер — одна точка: если вход чем-то держится (подтяжка, провод, делитель), показания почти константа, и зерно от запуска к запуску совпадает. Нужна мешалка: 7 замеров, XOR со сдвигами, микс с millis(). один замер — узкий коридор значений: вход с подтяжкой даёт почти константу, зерно повторится
- Как задать планировщику паузу «от 2 до 8 секунд»?
- Границы random(min, max) — в миллисекундах, верхняя исключается: random(2000, 8000) даёт 2000..7999 мс. random(2, 8) дал бы 2..7 мс, random(8000) — 0..7999 мс с почти всегда малыми значениями. random(2000, 8000) — границы в мс, верхняя исключена: 2000..7999