“Случайные” числа


Я думаю понятие случайных чисел/величин вам знакомо: они случайны. Иногда в самодельном устройстве бывает нужна случайная величина, например для какой-нибудь игры или световых эффектов. Программный источник настоящих случайных чисел организовать весьма непросто, в отличие от аппаратного. Банально игральный кубик или лото-машина являются вполне себе хорошими источниками случайных чисел. Микроконтроллер к слову не может генерировать случайные числа, потому что он – точное вычислительное устройство, случайностей быть не может. Что делать? Использовать такое понятие, как псевдослучайные числа. Чем они отличаются от случайных? Своей природой. Псевдослучайное число получается путём различных математических действий с начальным числом, то есть имея начальное число, мы можем сгенерировать на его основе целую кучу других чисел. Но тут есть две проблемы:

  • Через какое-то количество чисел (тысяч, а может и миллиардов, а может и больше) ряд сгенерированных псевдослучайных чисел начнёт повторяться. Для наших целей это не так страшно, можно об этом не думать
  • Генератору псевдослучайных чисел нужно начальное случайное число. Вот это реально беда… Но ничего, у нашего микроконтроллера есть и аппаратные средства

Ардуино и случайные числа


Ардуино имеет пару готовых функций для работы с псевдослучайными числами, давайте на них посмотрим:

  • random(max) – возвращает псевдослучайное число в диапазоне от 0 до (max – 1). max принимает unsigned long, то есть от 0 до 4 294 967 295
  • random(min, max) – возвращает псевдослучайное число в диапазоне от min до (max – 1). max принимает unsigned long, то есть от 0 до 4 294 967 295
  • randomSeed(value) – дать генератору псевдослучайных чисел новую опорную точку для отсчёта. value – любое число типа unsigned long, значит на Ардуино мы имеем 2^32 (4 294 967 295) наборов псевдослучайных чисел. На ваш век этого точно хватит!

Как правильно генерировать случайные числа, чтобы последовательность каждый раз была новая? Есть варианты:

  • При запуске программы задавать случайное число в randomSeed(). Как это сделать? Расскажу ниже
  • Если устройство как-то взаимодействует со внешним миром, или даже с пользователем, то можно при наступлении некоторых аппаратно случайных событий (нажатие кнопки, срабатывание датчика, принятие данных, и т.д.) скармливать randomSeed‘у текущее время с момента старта программы, т.е. функции millis() или micros(). Отличное решение кстати! Банально вызываем randomSeed(micros()) и всё.

Аппаратный рандом


Помните, в уроке про цифровые пины я говорил, что если пин никуда не подключен – то он ловит “из воздуха” всякие наводки. Шумы имеют природу, близкую к случайной, и было бы глупо этим не воспользоваться! Давайте посмотрим, какие значения можно получить с никуда не подключенного аналогового пина, опрашивая его обычным analogRead();

void loop() {
  Serial.println(analogRead(A0));
  delay(100);
}

Синусоида? Вполне логично, в стенах куча проводов с сетевым напряжением, вот они и наводят на микроконтроллер всякие помехи. Можно ли пользоваться сырым значением с аналогового пина в качестве “зерна” для randomSeed()? Нужно! То есть при запуске устройства делать вот так:

void setup() {
  // точка отсчёта для генератора случайных чисел
  // А0 никуда не подключен
  randomSeed(analogRead(A0));
}

Но есть ещё интересный вариант, который позволит получить гораздо более случайные числа для randomSeed().  Есть информация, что первые два бита из результата analogRead() имеют самый большой шум. Давайте на него посмотрим, выведя несколько (300) результатов в график. Получить первые два бита можно так: analogRead(A0) & 0b0000011, или более коротко – analogRead(A0) & 3

for (int i = 0; i < 300; i++) {
    Serial.println(analogRead(A0) & 3);
}

Выглядит весьма случайно, никакой закономерности не прослеживается! Но значения меняются всего от 0 до 3. Поэтому можно попробовать их перемножать и складывать, примерно так:

unsigned long seed = 0;
// 16 раз
for (int i = 0; i < 16; i++) {
  seed *= 4;
  seed += analogRead(A0) & 3;
}
// скормить генератору случ. чисел
randomSeed(seed);

Именно такой код я рекомендую использовать для получения случайного зерна для генератора случайных чисел при запуске программы. Напоследок давайте посмотрим, какой результат даёт такая конструкция:

unsigned long seed;
for (int i = 0; i < 400; i++) {
  seed = 1;
  for (byte j = 0; j < 16; j++) {
    seed *= 4;
    seed += analogRead(A0) & 3;
  }
  Serial.println(seed);
}

График я построил в excel, чтобы более чётко видеть рассеивание полученного случайного значения

Имеем практически равномерное рассеяние и огромное количество случайных значений, полученных путём перемножения и сложения “шума”. Пользуйтесь на здоровье.

Видео


Важные страницы


  • Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
  • Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
  • Полная документация по языку Ардуино, все встроенные функции и макро, все доступные типы данных
  • Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
  • Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
Последнее обновление Сентябрь 02, 2019