Ночник «камин» был моим подарком другу: светодиод за матовым стеклом мигает со случайными паузами, как угли. Схему я собрал за час — тот же светодиод из первого скетча, только вместо ровной секунды задержки я написал random(100, 1000) и гордо вручил. Через неделю друг сказал слова, которые я запомнил на всю жизнь: «он каждый раз включается одинаково». Раз — вспышка, два — вспышка, длинная пауза. Угольки вели себя как миниатюрный метроном: я-то знал, что числа случайные, но любой сброс питания запускал скетч с одного и того же места, и серия повторялась точь-в-точь. Рандом без сида — это не случайность, а очень длинная заученная фраза, которая всегда начинается с одного и того же слова.
В этом уроке разберём обе формы random() и ту ловушку, о которой молчат половина туториалов: верхняя граница не выпадает никогда. Поговорим, что такое сид и почему одинаковый сид даёт одинаковые числа, откуда берут сид для настоящего шума — от плавающего аналогового входа до миллисекунды нажатия — и соберём собственный генератор из пяти строк, чтобы одинаковые серии поймать руками, а не на словах.
Симулятор внизу — с макеткой: светодиод мигает по встроенному random(), а серии печатает наш собственный генератор. Введите один и тот же сид дважды — и увидите совпадение до единицы; в симуляторе это безопаснее, чем жечь угольки.
Две формы random() и невыдаемая граница
У random() две формы. random(6) выдаёт целое от 0 до 5 — шесть вариантов, включая ноль и никогда не включая саму шестёрку. random(1, 7) выдаёт целое от 1 до 6 — вот так выглядит кубик. Правило держится в одной строке: левая граница входит в выборку, правая — нет, потому что внутри это random(max − min) + min. Из-за него все новички делятся на два лагеря: кто однажды написал random(1, 6) и ждал шестёрку, и кто ещё не играл в свой кубик. Скажу честно: в первом лагере я провёл вечер и ещё одну ставку с другом.
Второй факт, который бьёт по тем же нервам, — воспроизводимость. Внутри платы сидит детерминированный генератор: длинная цепочка чисел, которая всегда крутится по одному кольцу. random() лишь читает следующее звено. После подачи питания генератор всегда стартует с одного и того же звена — поэтому наш камин и мигал по нотам. randomSeed(число) перескакивает на другое место кольца: тот же сид — та же серия, другой сид — другая. Заметьте, я не сказал «случайнее»: сид не добавляет хаоса, он выбирает, с какой фразы начать.
Отсюда и практические выводы. Для отладки фиксированный сид — подарок: randomSeed(42) в setup, и каждый запуск теста идёт по одинаковым числам — граблю можно поймать дважды. Для игры, охраны или камина фиксированный сид — приговор: серия угадывается, а сигнализация «случайно» воет по чётным дням. Дальше разберём, откуда взять сид, который не повторяется, — и он лежит у вас на плате, в никуда не подключённом пине A0.
const int SVET = 12;int gorit = 0;unsigned long tDalshe = 0; void setup() { pinMode(SVET, OUTPUT); Serial.begin(9600); // A0 ни к чему не подключён: analogRead ловит наводки, // каждый запуск питания даёт другое значение — другой сид randomSeed(analogRead(A0)); Serial.println("Ugolki vklucheny");} void loop() { if (millis() >= tDalshe) { if (gorit == 1) { gorit = 0; digitalWrite(SVET, LOW); } else { gorit = 1; digitalWrite(SVET, HIGH); } unsigned long pauza = random(150, 950); tDalshe = millis() + pauza; Serial.print("Pauza_ms="); Serial.println(pauza); }}Про сид от A0 — пара слов и одна грабля. Пин, к которому ничего не подключено, ловит наводки от проводов, рук и самой платы: analogRead скачет по сотням единиц от запуска к запуску — это классический источник энтропии, подробно про сам вход мы говорили в уроке про analogRead. Грабля в том, что «плавающий» пин должен быть по-настоящему пустым: как только на A0 висит кнопка на землю или датчик с постоянным выходом, пин перестаёт плавать, читает стабильное число — и сид снова одинаков на каждом запуске. Другой рабочий источник — момент первого нажатия кнопки: millis() там стоит тысячи, и пока пользователь не ткнул кнопку, скетч сид не берёт.
Третий источник неожиданный, но рабочий: сам человек. Число, введённое в Serial командой из урока про parseInt, можно скормить randomSeed — да, это сид «из пальца», и в охранной сигнализации он бессовестно предсказуем. Но для двух бортов, которые должны крутить одинаковый «случайный» сценарий синхронно, общий сид, введённый обоим, — самое короткое решение: обе платы начнут с одного места кольца и пойдут по нему нога в ногу.
Теперь обещанный ход конём: в симуляторе ниже встроенный random() живёт своей жизнью — движок бросает кубик заново при каждом запуске, поэтому randomSeed на него не влияет никак, и это честная особенность среды, а не поломка. Зато свой генератор мы напишем руками — и в симуляторе, и на Uno он ведёт себя одинаково: одинаковый сид, одинаковая серия, без исключений. Так абстракция «сид решает всё» становится наблюдаемым фактом, а не постулатом из учебника.
| Источник сида | Что даёт | Когда провалится |
|---|---|---|
| Плавающий A0: analogRead(A0) | наводки 0-1023, почти всегда новое число | к пину что-то подключено и держит уровень — сид одинаков |
| millis() в момент первого нажатия | тысячи миллисекунд, у каждого запуск свой | автозапуск без человека — нажатия нет |
| Константа: randomSeed(42) | одна и та же серия — идеально для отладки | в игре и сигнализации серия угадывается |
| Счётчик запусков из EEPROM | каждый запуск плюс один — числа уходят вперёд | после долгой жизни счётчик вернётся к началу |
Ловим одинаковые серии за руку
Свой генератор — это одна формула, называемая линейным конгруэнтным методом: следующее число = прошлый множить на константу плюс вторая константа, взять остаток. В скетче ниже — классика Лемера: состояние умножается на 48271 и берётся остаток от 2147483647. И тут поджидает арифметическая грабля с конкретными числами: при состоянии около двух миллиардов произведение доходит до 1,03 × 10 в четырнадцатой — а long на Uno вмещает лишь до 2147483647. Спасает схема Шраге: состояние режем на частное и остаток от деления на 44488, умножаем по отдельности — наибольшее произведение 48271 × 44487 = 2 147 431 977, ровно на границе и не за ней. Пять строк деления и умножения — и генератор даёт на Uno ровно те же числа, что в симуляторе.
Что с этим делать — зависит от задачи, и это уже не математика, а дизайн. Одинаковый сид — ваш тестовый стенд: одинаковые условия прогона, повторяемый «случайный» сценарий, честные сравнения до и после правки. Разный сид — ваш камин, кубик и сигнализация. Про партитуры повторяющихся миганий, где массив шагов заменил бы и генератор, мы говорили в уроке про массивы; а если сид хочется переживать между включениями, его место в EEPROM — как хранить настройки, показано в уроке про первые настройки.
В симуляторе светодиод на пине 12 мигает по встроенному random(150, 900) — без блокировки, через millis, как учил урок про millis. Серии же печатает собственный генератор из скетча. Введите число 42 и Enter — увидите серию из шести чисел. Введите 42 ещё раз — серия совпадёт до единицы. Другое число — другая серия. На плате числа будут ровно теми же: арифметика Шраге умещается в long без потерь и в обеих средах считается одинаково.
Куда это ставит нас дальше. Случайные паузы мигания — уже трафик, который не отличить от живого; случайный выбор ветки в switch — это режимы, которые перестают быть скучными; случайные ноты на зуммере из урока про tone — генеративная музыка, которая не повторяется именно потому, что сид берётся от помех. А если в вашей задаче случайность нужна не для красоты, а для честности — например, чтобы система не поддавалась предсказанию, — вернитесь к таблице источников сида и выберите тот, который честно меняется каждый запуск, без исключений.
- 1. Что может выдать random(1, 6)?
- 2. Скетч вызывает random(), но randomSeed не вызывает вовсе. Что вы увидите после каждой перезагрузки?
- 3. Почему сид берут от никуда не подключённого A0?
- 4. Свой генератор со схемой Шраге запустили с одним и тем же сидом дважды. Что напечатает серия?
- 5. Вам нужен честный кубик от 1 до 6. Какой вызов правильный?
- Введите в симуляторе 42 дважды и убедитесь, что серия совпала. Потом введите 43 — серия уедет уже со второго числа: соседние сиды дают «близкие» старты, но сравнивать надо серию, а не одно число.
- Сломайте кубик нарочно: в строке Serial возьмите сид 7 и посчитайте, сколько раз из шести чисел выпадут числа выше 90. Потом поменяйте модуль с 100 на 6 — получите печать «кубика» и найдите, куда пропали все шестёрки.
- Перенесите мигание на сид-независимость: выкиньте из loop случайность и подставьте партитурный массив из урока про массивы — увидите, чем сценарий «по нотам» отличается от сценария «по кубику».
- На плате подключите A0 щупом-проводом в воздух и напечатайте пять значений analogRead подряд: разброс и есть ваш сид. Потом прикрутите кнопку на GND к тому же пину — разброс умрёт, и сид станет константой.
- random(max) выдаёт 0..max−1, random(min, max) — от min до max−1. Правая граница не выпадает никогда: кубик — random(1, 7).
- Встроенный генератор детерминированный: без randomSeed после каждой перезагрузки идёт та же серия — это и было «загадочным» одинаковым миганием.
- Сид — не добавка хаоса, а выбор стартовой точки: одинаковый сид даёт одинаковые числа. Для тестов это подарок, для игры — приговор.
- Классический сид — плавающий A0: analogRead ловит наводки 0-1023; пин с подключённой кнопкой или датчиком уже не плавает и сид не даёт.
- Свой LCG (умножить на 48271, остаток от 2147483647) через схему Шраге умещается в long: наибольшее произведение 2 147 431 977 — ровно на границе 32 бит.
Частые вопросы по теме урока
- Что может выдать random(1, 6)?
- Правая граница не входит в выборку. Кубик — это random(1, 7): от 1 до 6. Левая граница, наоборот, входит. От 1 до 5 — шестёрка не выпадет никогда
- Скетч вызывает random(), но randomSeed не вызывает вовсе. Что вы увидите после каждой перезагрузки?
- Генератор детерминированный и всегда стартует с одного звена цепочки. randomSeed перескакивает на другое место — без него серия повторяется. Одну и ту же серию чисел, точь-в-точь
- Почему сид берут от никуда не подключённого A0?
- Пустой аналоговый вход — антенна: analogRead гуляет по наводкам, и это как раз то, что нужно для сида. Если к пину что-то подключено, гуляние кончается. Плавающий вход ловит наводки: значения скачут до сотен, каждый запуск даёт новый сид
- Свой генератор со схемой Шраге запустили с одним и тем же сидом дважды. Что напечатает серия?
- Конгруэнтный генератор — чистая функция состояния: один сид даёт одну серию всегда. Именно поэтому фиксированный сид идеален для повторяемых тестов. Одни и те же числа — это свойство, а не баг
- Вам нужен честный кубик от 1 до 6. Какой вызов правильный?
- random(1, 7) даёт 1-6. random(6) даёт 0-5, random(0, 6) — то же самое, а random(1, 6) лишает кубик шестёрки. random(1, 7)