Серверы, собранные из специально не предназначенных для этого комплектующих, обычно имеют два недостатка. У них отсутствует аппаратный сторожевой таймер и часто не хватает энтропии для ряда сервисов. Нехватка энтропии особенно актуальна для не сильно нагруженных серверов. Это связанно с тем, что ядро Linux в качестве источника энтропии использует активность системы, а именно: сетевого оборудования, дисковой подсистемы и аппаратных прерываний.


Также в домашнем сервере часто возникает необходимость иметь более экономный, по сравнению с Wi-Fi, радиомодуль для коммуникации с автономными датчиками.


Существует большой выбор устройств, с помощью которых можно решить любую из этих проблем, но подключение каждого из них требует отдельный порт. Оценив ситуацию, в итоге решил разработать устройство 3-в-1 подключаемое в RS232 (COM) порт. Остальные требования получились следующими:


  • Аппаратный сторожевой таймер, пригодный для работы со стандартным демоном watchdog;
  • Генератор истинно случайных чисел на базе эффекта обратного лавинного пробоя p-n перехода;
  • Радиомодуль nRF24L01+ для сбора данных с автономных датчиков.

Таким образом устройство получило наименование WRN от названий составляющих его подсистем: WDT (WatchDog Timer), RNG (Random Number Generator), nRF24L01+.


WRN устройство


Забегая сильно вперед, хочу отметить, что нехватку энтропии легко можно устранить без дополнительных устройств, запуском демона rngd с ключом --rng-device=/dev/urandom. Алгоритмы работы /dev/urandom достаточно хороши для этого, но я захотел аппаратный RNG, поэтому деваться некуда, нужно делать.



Принципиальная схема


(кликнуть для увеличения)
Принципиальная схема


Устройство построено на базе микроконтроллера ATmega328P и работает на максимальной частоте 20MHz. Ради эксперимента, пробовал разогнать, но кристалла скромнее чем на 32MHz у меня не нашлось, с ним микроконтроллер не заработал. Тем не менее в устройстве можно использовать любой кварцевый резонатор в разумных, с точки зрения микроконтроллера, пределах. В статье ещё будет подробнее об этом, но фактически, достаточно вычислить константы для программных часов и собрать загрузчик с подходящими параметрами.


На схеме предусмотрены разъёмы для подключения SPI программатора и USB-UART преобразователя, который необходим для отладки, так как получить непосредственный доступ к RS232 сейчас не просто. Местами схема не оптимальна, поскольку собирал её из компонентов, которые были в наличии. При желании её можно оптимизировать, исходники для KiCAD доступны на GitHub alexcustos/wrn-project в директории pcb.


Описание стандартных компонентов схемы
  • C12 – защита от помех, необходимая при использовании аналого-цифрового преобразователя;
  • Y1, C3, C4 – стандартное подключение кварцевого резонатора;
  • D1, R11 – светодиод и резистор ограничивающий ток в его цепи, выбран эмпирически, так как опознать светодиод не удалось;
  • PB1, R2 – кнопка RESET и подтяжка (pull-up) вывода к высокому уровню с целью исключить срабатывание в результате наведённых помех;
  • U1, C1, C2 – регулятор напряжения для радиомодуля и сглаживающие фильтры, регулятор на 800mA здесь конечно не нужен, 100mA более чем достаточно;
  • U2, R1 – оптопара и резистор, ограничивающий ток до 20mA в цепи светодиода;
  • U3, C5, C6, C9, C10 – преобразователь уровней RS232-TTL и стандартная обвязка;
  • P3, C11 – питание +5 и +12V от разъема FDD и сглаживающий фильтр для 12V;
  • P4 – разъем для подключения радиомодуля;
  • P1 – ISP разъем для загрузки кода в микроконтроллер, необходим для прошивки загрузчика;
  • P2 – разъём для подключения к порту RS232 компьютера;
  • P5 – контактная площадка для подключения управляемой кнопки RESET и дублирующей кнопки с передней панели корпуса;
  • P7 – разъём для подключение преобразователя USB-UART.

Преобразователь уровней RS232-TTL


Преобразователь уровней RS232-TTL


Здесь использована микросхема ADM202EANZ от Analog Devices только потому, что у меня три экземпляра MAX232ACPE не заработали должным образом. В остальном эти микросхемы полностью взаимозаменяемы и можно использовать более распространённый вариант от Maxim.


Резисторы R3, R4, R12 необходимы для подключения к схеме USB-UART преобразователя. При разных уровнях на двух конкурирующих сигнальных линиях, сопротивление выступит в роли делителя напряжения. Поскольку сопротивление нагрузки много меньше 1k?, сигнал от подключенного напрямую преобразователя будет лишь немного ослаблен, при этом сигналом от U3 можно будет полностью пренебречь.


Использовать только SPI для прошивки устройства не удобно, поскольку требуется программатор и непосредственный доступ к устройству. Кроме этого на линиях SPI находится радиомодуль, который тоже пришлось бы подключить через резисторы.


Прошивка через загрузчик хороший вариант, но тоже имеет недостаток. Загрузчик воспринимает сигнал DTR (Data Terminal Ready) как команду на перезагрузку, что приводит к нежелательной перезагрузке устройства при подключении терминала.


Бороться с этим не сложно, достаточно добавить полевой транзистор Q4 в качестве выключателя и блокировать линию DTR сразу после штатной загрузки. При необходимости прошить устройство, достаточно отправить команду на разблокирование. Резистор R6 нужен, чтобы линия была подключена по умолчанию; конденсатор C7 – для надёжного срабатывания RESET.


Возможность прошить устройство может быть серьёзной уязвимостью для всей системы, поэтому необходима аппаратная возможность заблокировать прошивку, для этого достаточно разомкнуть джампер JP1. Кроме этого он полезен при отладке, когда подключён USB-UART преобразователь.


Генератор шума на эффекте лавинного пробоя p-n перехода


Генератор шума


Генератором здесь является обратно смещённый p-n переход транзистора Q1. Согласно паспорту Vebo=6V, это максимальное напряжение обратного включения при открытом (не подключенном) коллекторе. Конечно транзистор не выйдет из рабочего режима сразу при превышении этого порога, поэтому для надежности, нужно повысить напряжение до 12V. В таком режиме в p-n переходе будет возникать ударная ионизация в тех местах, где напряженность электрического поля достаточна. Этот процесс крайне не стабилен, постоянный срыв и возникновение ионизации в различных местах p-n перехода, приводит к случайному изменению тока. Транзистор здесь использован только потому, что у диодов напряжение обратного смещения существенно выше, например у популярного 1N4148, это 100V.


В остальном схема работает следующим образом: Q2 усиливает ток от Q1; R5 ограничивает ток в цепи коллектор-эмиттер Q2; C8 срезает постоянную составляющую сигнала от Q2, несколько выравнивая спектр; (R7+R8) обеспечивает ток в цепь база-эмиттер Q3 в качестве постоянной добавки к току от C8; Q3 усиливает суммарный ток от C8 и (R7+R8); R9, R10 делитель, ограничивает напряжение до 5V на входе микроконтроллера.



Печатная плата


В предыдущей статье уже была инструкция, как нарисовать схему и развести плату в KiCAD. Принципиальных отличий здесь нет, только плату теперь нужно напечатать.


Размеры печатной платы обычно определяются после компоновки, но можно задать их изначально и корректировать по мере необходимости. Эта работа производится на слое Edge.Cuts инструментом Add graphic line or polygon либо другим подходящим, если нужна круглая плата или с закругленными краями.


Затем желательно привести контактные площадки (Pads) в порядок. Дело в том, что форма площадки и диаметр отверстий у каждого элемента свои. Соответственно могут возникнуть лишние сложности при сверлении, пайке или даже во время трассировки, когда дорожки не будет проходить там где должны.


Процесс этот не очень удобен, поскольку в редакторе pcbnew макросы ещё в разработке, а горячая клавиша назначена только на редактирование контактной площадки. Тем не менее сделать это не сложно. Сначала нужно определиться с диаметром сверла, в большинстве случаев оптимальным будет 1мм, и назначить соответствующее отверстие всем площадкам. Затем исправить при необходимости форму площадок, чтобы на них было достаточно меди, но при этом они находились достаточно далеко друг от друга и от дорожек.


Для редактирования отдельной площадки (Pad) нужно указать на неё курсором и нажать E, или щёлкнуть на ней правой кнопкой мыши Pad, затем Edit Pad. Чтобы скопировать настройки, только что отредактированной площадки на текущую, нужно воспользоваться Copy Current Settings to this Pad, а для применения настроек к группе выбрать Edit All Pads. Последняя опция несколько упрощает процесс, но применима только к одному посадочному месту (Footprint) или группе идентичных. Здесь ещё требуется обращать внимание на галочки фильтра, которые ограничивают область действия.


Для проверки печатной платы предусмотрен инструмент Perform design rules check Perform design rules check > Start DRC обе закладки Problems / Markers и Unconnected должны быть пустыми.


В завершение необходимо визуально убедиться, что всё в порядке. Для этого обязательно следует полюбоваться результатом в View > 3D Viewer. Затем экспортировать печатную плату в Gerber формат (что-то вроде PDF для документов) File > Plot > Gerber (Plot format), здесь выбрать необходимые слои, в данном случае: B.Cu, Edge.Cuts и нажать Plot, далее получить файл с отверстиями Generate Drill File > Drill File, и закрыть оба окна.


В KiCAD есть встроенный просмотрщик GerbView GerbView, можно воспользоваться им либо любым другом. Если всё в порядке, осталось выбрать пункт меню File > Print для вывода платы на печать. Здесь выбрать слой B.Cu, снять галочку с Print frame ref и выбрать размер меток для отверстий. С полноразмерными отверстиями Real drill нужно будет шилом отмечать центр, чтобы сверло не соскальзывало, а с маленькой меткой Small mark придётся сверлить припой (после лужения) и медь. Если сверло хорошее, вероятно, последний вариант предпочтительнее.


Некоторая статистика
  • первая ревизия — Real drill без кернения, хорошее сверло Dremel 0.8мм, затупилось где-то на 3/4, заканчивал сверлом 1.0, результат плохой: отверстия начинались в случайном месте круга, часть контактных площадок порвана, DIP панельки устанавливались с трудом;
  • вторая ревизия — Real drill с кернением, предыдущее свело 1.0, рассверлил им всю плату, результат хороший;
  • третья ревизия — Small mark, свёрла из не понятного комплекта, предыдущее 1.0 затупилось сразу, затем ещё два по 1.0 и одно 0.9, заканчивал сверлом 1.1, результат хороший;
  • четвертая ревизия — Real drill с кернением, свёрла из другого не понятного комплекта, затупились 3 сверла 1.0, результат хороший, фото можно посмотреть ниже под спойлером.

Теперь осталось воплотить идею в «железе». Процесс этот увлекательный, но про него уже очень много написано. Поэтому фото процесса со своими субъективными комментариями спрятал под спойлер. Речь, конечно же, по ЛУТ.


ЛУТ (Лазерно-Утюжная Технология)

Сначала ацетоном или спиртом нужно обезжирить плату и зачистить её мелкой шкуркой вдоль и поперёк. Рисунок желательно печатать на термотрансферной бумаге.


Стеклотекстолит


Переводить нужно разогретым до 180°C (три точки) утюгом, обычно это максимум. При касании утюгом бумага прилипает сразу и надёжно, поэтому, придерживая её достаточно погладить с одного края, затем смело по всей площади. При появлении запаха горелой бумаги процесс можно прекращать, достаточно трёх-пяти минут. Затем дать стеклотекстолиту остыть и отклеить бумагу.


Переведённый рисунок


Границы печатной платы, желательно стереть, поскольку медь там не нужна. Для этого хорошо подходит ацетон, тонер удаляется одним движением, главное не зацепить дорожки.


Подготовленный рисунок


Наиболее удобный рецепт раствора для травления меди:


  • 100 мл воды;
  • 6 таблеток гидроперита;
  • 50 г лимонной кислоты;
  • 20 г (две чайные ложки) пищевой соли.

Вода и гидроперит эквивалентны 100 мл 3% перекиси водорода, но у перекиси специфический запах, поэтому лучше гидроперит.


Раствор нужно готовить в специальной посуде, которую в последствии не допустимо использовать для хранения и приготовления пищи. Лучше использовать стеклянную, но правила тут простые: никаких металлов и чтобы легко отмывалась. Также нужно помнить о технике безопасности, чтобы раствор не попал на кожу, в глаза, а тем более, внутрь.


Плата в растворе


Процесс протекает без запаха и спецэффектов. На поверхности платы образуются пузыри от которых необходимо избавляться. В данном случае сдвигом платы из раствора, и затем назад, когда пузыри схлопнутся. Вероятно, при 30°C в помещении раствор нагревать не нужно, но первые эксперименты я проводил при 23°C, что было явно не достаточно. Поэтому в этот раз тоже ставил ёмкость в таз с горячей водой из под крана.


Процесс травления


Пузыри образуются интенсивно, поэтому плату можно ворочать вообще не отходя, но вполне достаточно один раз в 2-3 минуты. Процесс нужно прерывать, когда очевидно, что лишней меди уже нет. Обычно это занимает 40-50 минут. Если раствор ещё не полностью отработал, пузыри будут образовываться, поскольку медь с боку от тонера остаётся доступной.


Готовая плата


Отработанный раствор можно вылить в бытовую канализацию. Тонер легко смывается ацетоном.


Плата без тонера


Теперь плату нужно залудить, чтобы устранить возможные мелкие трещины и предотвратить окисление меди. Сделать это проще всего сплавом Розе. Для этого понадобится эмалированная посуда (станет не пригодна для приготовления и хранения пищи), немного воды, чтобы накрыло плату примерно на 5 мм, маленькая ложка лимонной кислоты в воду (для снятия окислов с меди) и две-три капли сплава Розе. Теперь нужно нагреть это всё до кипения, при этом сплав Розе расплавится и его легко можно будет закидывать на плату и наносить на дорожки резиновой лопаткой. Когда сплав окажется на всех дорожках, останется только удалить излишки, сдвинув их за пределы платы. От трёх капель обычно остаётся одна размером полторы-две исходных, её можно использовать повторно.


Кстати, посуда не должна быть алюминиевой, поскольку лимонная кислота растворяет оксид алюминия, что заставляет алюминий активно окисляться, в результате в ёмкости образуется много мусора и нулевая видимость.


Залуженная плата


Сверлить желательно под тонким слоем воды, чтобы сверло не перегревалось, да и убирать потом будет проще.


(кликнуть для увеличения)
Плата с отверстиями


В итоге получилось следующее изделие:


(кликнуть для увеличения)
Спаянное изделие


Установленное изделие



Программное обеспечение


Проект разработан на C++ в AtmelStudio, все исходные коды доступны на GitHub alexcustos/wrn-project в директории wrn. Часть кода заимствована из репозитория Arduino IDE и модифицирована с целью убрать всё лишнее и отвязать код от ядра Arduino. С той же целью изменены две библиотеки, необходимые для работы с радиомодулем. Их исходный код также доступен на GitHub в виде отдельных проектов:



В коде встречается обработка данных с сенсора, ознакомиться с которым можно по ссылке: ATtiny85: прототип беспроводного сенсора.


Загрузчик


Для программирования устройства через последовательный порт необходим загрузчик. Optiboot — отличный вариант, подходящий для Atmel AVR микроконтроллеров. Его легко собрать с требуемыми параметрами, а именно задать тактовую частоту и скорость передачи данных для последовательного порта, а самое главное, указать порт, на котором он будет мигать светодиодом.


Для прошивки загрузчика, устройство, с отсоединённым радиомодулем, требуется подключить через SPI порт к программатору. Затем скомпилировать и прошить свой вариант загрузчика, либо воспользоваться, представленным чуть ниже, готовым вариантом.


Для компиляции понадобится набор программ, которые в Ubuntu можно установить командой:


sudo apt-get install git gcc-avr binutils-avr gdb-avr avr-libc avrdude

Затем выполнить:


git clone https://github.com/Optiboot/optiboot.git
cd optiboot/optiboot/bootloaders/optiboot
make atmega328_isp AVR_FREQ=20000000L BAUD_RATE=115200 LED=C0 EFUSE=FD HFUSE=DE LFUSE=F7 ISPTOOL=stk500v1 ISPPORT=/dev/ttyACM0

Микроконтроллер с тактовой частотой 20MHz вполне может работать с BAUD_RATE до 250000. Скорость пришлось понизить в связи с RS232-TTL преобразователем, даже ADM202EANZ с заявленной скоростью 230000, без ошибок работает только на 57600. На скорости 115200 у него, примерно раз в час, появляется сбойный байт. Для прошивки это не критично, поскольку avrdude проверяет всё, что записывает, и всегда можно повторить попытку.


Далее следует обратить внимание на строку, которая будет ближе к началу вывода последней команды:


BAUD RATE CHECK: Desired: 115200, Real: 113636, UBRRL = 21, Error=-1.3%

Значение Real имеет смысл сохранить, дело в том, что процесс прошивки через USB-UART у меня заработал только на данной скорости, а через RS232-TTL только на стандартной. Сводить Error к 0.0% смысла не имеет, поскольку ошибка в 1-2 процента не должна вызывать проблем, но я пробовал — не помогло.


FUSE-ы:


  • EFUSE=FD – перезагрузка при попытке записи в EEPROM при напряжении питания ниже 2.7V, можно указать FC (4.3V), но если отлаживать устройство с питанием от USB то 2.7V надёжнее.
  • HFUSE=DE – 512 байтный загрузчик, программирование через SPI разрешено;
  • LFUSE=F7 – внешний full-swing кварцевый резонатор со стандартными задержками, отличается от остальных тем, что микроконтроллер не будет снижать напряжение на выводе XTAL2.

В настройках программатора (ISPTOOL, ISPPORT) указана arduino как программатор, вполне подходящий вариант, если что-то подобное пылится без дела.


Загрузчик приготовленный по предложенной выше схеме можно скачать по ссылке: optiboot_atmega328p_20MHz.hex. Для его прошивки достаточно установить только avrdude и выполнить команду:


avrdude -C/etc/avrdude.conf -v -patmega328p -cstk500v1 -P/dev/ttyACM0 -b19200     -e -Ulock:w:0x3F:m -Uefuse:w:0xFD:m -Uhfuse:w:0xDE:m -Ulfuse:w:0xF7:m     -Uflash:w:"optiboot_atmega328p_20MHz.hex":i -Ulock:w:0x2F:m

Первая строка определяет микроконтроллер и программатор, далее идут команды, запускаемые последовательно: очистка чипа, разблокирование области загрузчика, прошивка FUSE-ов, прошивка загрузчика и блокировка записи в область загрузчика.


Если всё прошло успешно, светодиод будет мигать три раза в секунду сигнализируя, что с Optiboot всё в порядке, но микропрограмма ещё не загружена.


Микропрограмма


В целом кода получилось достаточно много, но надеюсь разобраться с ним будет не сложно. Далее приведу список особенностей, на которые стоит обратить внимание.


Код инициализации SPI и последовательного порта требует определения F_CPU. Добавить его лучше в опциях компилятора или напрямую указав ключ -DF_CPU=20000000L. Программные часы также используют это определение, но константы там заданы жёстко и требуют ручного пересчёта при изменении F_CPU.


Функция main() традиционно содержит инициализацию и бесконечный цикл, в котором производится работа с устройством:


  • сброс сторожевого таймера микроконтроллера;
  • получение данных от радиомодуля;
  • работа с генератором шума;
  • работа с системным сторожевым таймером;
  • считывание команд из последовательного порта;
  • обработка команд и отправка данных через последовательный порт.

Команды устройству передаются в текстовом виде в формате [C|W|R|N][0-99]:[аргумент1]:[аргумент2], где первая буква, это идентификатор подсистемы, одна или две цифры до двоеточия — номер команды, затем следует аргумент. Второй аргумент в настоящее время не используется.


Данные передаются структурами (struct) в бинарном виде. Компилятор может оптимизировать структуры данных, добавляя промежутки для выравнивания полей, обычно до границы слова. Размер слова зависит от платформы, что в данном случае не приемлемо. Поэтому важно убедиться, что прошивка собирается с ключом -fpack-struct, в AtmelStudio данная опция включена по умолчанию.


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


Синхронизация приёмника с устройством производится по порядковому номеру. В случае получения пакета с неожиданным номером, приёмник инициирует процесс синхронизации. Сразу после загрузки устройство отправляет пакет, чтобы приёмник запустил процесс синхронизации и инициализировал устройство.


Устройство имеет собственный кольцевой лог перезагрузок и срабатываний сторожевого таймера, который ведётся в EEPROM микроконтроллера. Для записи в лог правильной даты и времени используются программные часы с возможностью синхронизации.


Системный сторожевой таймер работает с точностью до секунды и опирается на время работы устройства с последней перезагрузки (uptime). Во избежании проблем uptime полностью отвязан от синхронизации. Фактическое его отставание от часов чуть более секунды в сутки.


Данные от генератора шума считываются с аналого-цифрового входа, генератор калибруется, затем запускается процесс генерации случайных чисел, один бит за цикл. Калибровка прозрачно повторяется при переполнении счётчика с типом uint16_t (~12.5 сек).


Программные часы


Часы основаны на аппаратном прерывании от переполнения 8 битного таймера. Наибольшая точность необходима в библиотеках RF24/RF24Network, где часть таймаутов рассчитываются в миллисекундах. Поэтому нужно подобрать делитель таймера так, чтобы прерывание срабатывало примерно 1000 раз в секунду, 64 вполне подходит: 20000000 (частота) / 256 (8 бит) / 64 (делитель) ~= 1220, соответственно получаем одно прерывания в 256 * 64 * 1000 / 20000000 = 8192/10000 = 512/625 мс. Следовательно в TIMER0_OVF нужно добавить код для работы с дробными частями миллисекунды:


m += MILLIS_INC;  // = 0, известно на этапе компиляции, строчка будет оптимизирована (удалена) компилятором
f += FRACT_INC;  // = 512, суммируется числитель дроби
if (f >= FRACT_MAX) {  // = 625, если дробь больше либо равна единице
    f -= FRACT_MAX;  // нормализация
    m += 1;
}

Очевидно, что с помощью данного кода нельзя измерять интервалы длиной в одну миллисекунду, слишком большая погрешность, но в данном случае это и не требуется.


Генератор истинно случайных чисел


Процессы, приводящие к возникновения ударной ионизации в p-n переходе, сильно зависят от температуры и степени деградации p-n перехода из-за суровых условий его работы. В связи с этим программная калибровка равномерного распределения битов от генератора шума является исключительно важной задачей. Другими словами, в среднем число нулей должно равняться числу единиц.


В следствие необходимости обеспечить безостановочную работу генератора и прозрачную калибровку, максимально эффективным способом, получился интересный код с опасной игрой с переполнением и использованием переменных по нескольким назначениям сразу.


Далее, в комментариях к коду, поясню логику, но здесь важно понимать, что при программировании микроконтроллеров, это вполне нормальный стиль, в отличии от многих других областей, где так делать не желательно.


measure = read_measure();  // выборка старших 8 бит из 10 (уменьшение точности)
num_measures++;  // от единицы и до переполнения (естественного или искусственного)

// measure_limit > 0 в процессе быстрой калибровки
if (num_measures == measure_limit) {
    balance = false;
    // вычисление абсолютного значения разницы
    if (pan_left > pan_right) fault = pan_left - pan_right;
    else fault = pan_right — pan_left;
    // можно менять вместе с measure_limit, но эффект от оптимизации близок к нулю
    acceptable_fault = ((measure_limit - 1) / 256 + 1) * 3;

    if (fault > acceptable_fault) {
        // подгонка методом последовательного приближения,
        // резкие движения, как в методе деления пополам, здесь не требуются
        if (pan_right > pan_left && threshold < uint8_t(-1)) threshold++;
        else if (pan_left >= pan_right && threshold > 0) threshold--;
    } else balance = true;

    ... // обнуление pan_left, pan_right, num_measures (искусственное переполнение)
    ... // отмена калибровки в случае проблем (если отключился генератор)

    if (balance && measure_limit) {  // баланс только что найден
        measure_limit = 0;  // признак, что данные можно отдавать
        return false;  // пропуск цикла, для начала формирования байта с первого бита
    }
} else {  // пропуск одного измерения, иначе переполнение если генератор отключён
    if (measure <= threshold) pan_left++;
    else pan_right++;
}

Формирование байта:


byte <<= 1;  // сплошной поток битов, байт считается готовым если num_measures % 8 == 0
if (measure > threshold) byte |= 0b00000001;

Тестирование потока утилитой rngtest из пакета rngd обнаружило большое число ошибок в monobit тесте. Проблема очевидно в гармонической природе сигнала выдаваемого транзисторами, иначе говоря, при достаточно большой частоте дискретизации ADC вероятны «полупериоды», на которые будет выпадать слишком много отсчётов. Самое простое и правильное решение, это разломать всю последовательность:


byte ^= bit_flip;  // XOR очередного бита с чередующимся битом
bit_flip = !bit_flip;

Данная операция не меняет суть последовательности, более или менее случайной она не становится, но уже легко проходит monobit тест. В остальном статистические тесты требуют очень большую выборку для получения значимого результата. На скорости 636 байт/сек собрать достаточное количество чисел для всесторонней проверки в dieharder требует слишком много времени. Поэтому все тесты запускались на одной и той-же выборке примерно в 500MB. Если тест пропущен, значит ему не хватило данных. Для сравнения приведены результаты тестирования аналогичной по объёму выборки из /dev/urandom. К выводу dieharder добавлен номер теста перед двоеточием.


rngtest
rngtest: bits received from input: 4195417088
rngtest: FIPS 140-2 successes: 209549
rngtest: FIPS 140-2 failures: 221
rngtest: FIPS 140-2(2001-10-10) Monobit: 21
rngtest: FIPS 140-2(2001-10-10) Poker: 70
rngtest: FIPS 140-2(2001-10-10) Runs: 59
rngtest: FIPS 140-2(2001-10-10) Long run: 72
rngtest: FIPS 140-2(2001-10-10) Continuous run: 0

rngtest (/dev/urandom)
rngtest: bits received from input: 4181721088
rngtest: FIPS 140-2 successes: 208937
rngtest: FIPS 140-2 failures: 149
rngtest: FIPS 140-2(2001-10-10) Monobit: 20
rngtest: FIPS 140-2(2001-10-10) Poker: 21
rngtest: FIPS 140-2(2001-10-10) Runs: 57
rngtest: FIPS 140-2(2001-10-10) Long run: 52
rngtest: FIPS 140-2(2001-10-10) Continuous run: 0

dieharder
#=============================================================================#
#            dieharder version 3.31.1 Copyright 2003 Robert G. Brown          #
#=============================================================================#
   rng_name    |rands/second|   Seed   |
stdin_input_raw|  1.99e+07  | 871678203|
#=============================================================================#
             test_name   |ntup| tsamples |psamples|  p-value |Assessment
#=============================================================================#
  0:    diehard_birthdays|   0|       100|     100|0.23013568|  PASSED
  1:       diehard_operm5|   0|   1000000|     100|0.41464749|  PASSED
  3:     diehard_rank_6x8|   0|    100000|     100|0.83194246|  PASSED
  4:    diehard_bitstream|   0|   2097152|     100|0.98469009|  PASSED
  7:          diehard_dna|   0|   2097152|     100|0.82184561|  PASSED
  8: diehard_count_1s_str|   0|    256000|     100|0.63516902|  PASSED
 10:  diehard_parking_lot|   0|     12000|     100|0.15579947|  PASSED
 11:     diehard_2dsphere|   2|      8000|     100|0.94799044|  PASSED
 12:     diehard_3dsphere|   3|      4000|     100|0.16755480|  PASSED
 14:         diehard_sums|   0|       100|     100|0.00420819|   WEAK
 15:         diehard_runs|   0|    100000|     100|0.58812798|  PASSED
 15:         diehard_runs|   0|    100000|     100|0.23381862|  PASSED
100:          sts_monobit|   1|    100000|     100|0.11747720|  PASSED
101:             sts_runs|   2|    100000|     100|0.12598371|  PASSED
102:           sts_serial|   1|    100000|     100|0.11747720|  PASSED
102:           sts_serial|   2|    100000|     100|0.98806196|  PASSED
102:           sts_serial|   3|    100000|     100|0.93420112|  PASSED
102:           sts_serial|   3|    100000|     100|0.88625906|  PASSED
102:           sts_serial|   4|    100000|     100|0.81837353|  PASSED
102:           sts_serial|   4|    100000|     100|0.44680983|  PASSED
102:           sts_serial|   5|    100000|     100|0.30069422|  PASSED
102:           sts_serial|   5|    100000|     100|0.59918415|  PASSED
102:           sts_serial|   6|    100000|     100|0.94111872|  PASSED
102:           sts_serial|   6|    100000|     100|0.97775411|  PASSED
102:           sts_serial|   7|    100000|     100|0.71034876|  PASSED
102:           sts_serial|   7|    100000|     100|0.37205549|  PASSED
102:           sts_serial|   8|    100000|     100|0.62281679|  PASSED
102:           sts_serial|   8|    100000|     100|0.61865217|  PASSED
102:           sts_serial|   9|    100000|     100|0.12357283|  PASSED
102:           sts_serial|   9|    100000|     100|0.62028539|  PASSED
102:           sts_serial|  10|    100000|     100|0.70302730|  PASSED
102:           sts_serial|  10|    100000|     100|0.36150774|  PASSED
102:           sts_serial|  11|    100000|     100|0.02416524|  PASSED
102:           sts_serial|  11|    100000|     100|0.00210157|   WEAK
102:           sts_serial|  12|    100000|     100|0.15545193|  PASSED
102:           sts_serial|  12|    100000|     100|0.25167693|  PASSED
102:           sts_serial|  13|    100000|     100|0.19659046|  PASSED
102:           sts_serial|  13|    100000|     100|0.56538654|  PASSED
102:           sts_serial|  14|    100000|     100|0.15529368|  PASSED
102:           sts_serial|  14|    100000|     100|0.99005364|  PASSED
102:           sts_serial|  15|    100000|     100|0.15517199|  PASSED
102:           sts_serial|  15|    100000|     100|0.91135159|  PASSED
102:           sts_serial|  16|    100000|     100|0.70484328|  PASSED
102:           sts_serial|  16|    100000|     100|0.71149544|  PASSED
201: rgb_minimum_distance|   0|     10000|    1000|0.00000000|  FAILED
202:     rgb_permutations|   5|    100000|     100|0.72724154|  PASSED
203:       rgb_lagged_sum|   0|   1000000|     100|0.79186771|  PASSED
204:      rgb_kstest_test|   0|     10000|    1000|0.46365770|  PASSED
206:              dab_dct| 256|     50000|       1|0.53224869|  PASSED
207:         dab_filltree|  32|  15000000|       1|0.87205525|  PASSED
207:         dab_filltree|  32|  15000000|       1|0.28341671|  PASSED
208:        dab_filltree2|   0|   5000000|       1|0.69766563|  PASSED
208:        dab_filltree2|   1|   5000000|       1|0.68877816|  PASSED
209:         dab_monobit2|  12|  65000000|       1|0.99154840|  PASSED

dieharder (/dev/urandom)
#=============================================================================#
#            dieharder version 3.31.1 Copyright 2003 Robert G. Brown          #
#=============================================================================#
   rng_name    |rands/second|   Seed   |
stdin_input_raw|  2.09e+07  |2043744116|
#=============================================================================#
             test_name   |ntup| tsamples |psamples|  p-value |Assessment
#=============================================================================#
  0:    diehard_birthdays|   0|       100|     100|0.04140546|  PASSED
  1:       diehard_operm5|   0|   1000000|     100|0.37860771|  PASSED
  3:     diehard_rank_6x8|   0|    100000|     100|0.51810908|  PASSED
  4:    diehard_bitstream|   0|   2097152|     100|0.87265669|  PASSED
  7:          diehard_dna|   0|   2097152|     100|0.28188785|  PASSED
  8: diehard_count_1s_str|   0|    256000|     100|0.01571303|  PASSED
 10:  diehard_parking_lot|   0|     12000|     100|0.27155245|  PASSED
 11:     diehard_2dsphere|   2|      8000|     100|0.56675436|  PASSED
 12:     diehard_3dsphere|   3|      4000|     100|0.95480977|  PASSED
 14:         diehard_sums|   0|       100|     100|0.00076186|   WEAK
 15:         diehard_runs|   0|    100000|     100|0.62119123|  PASSED
 15:         diehard_runs|   0|    100000|     100|0.79241488|  PASSED
100:          sts_monobit|   1|    100000|     100|0.76618520|  PASSED
101:             sts_runs|   2|    100000|     100|0.89128426|  PASSED
102:           sts_serial|   1|    100000|     100|0.76618520|  PASSED
102:           sts_serial|   2|    100000|     100|0.51804588|  PASSED
102:           sts_serial|   3|    100000|     100|0.54076681|  PASSED
102:           sts_serial|   3|    100000|     100|0.51414389|  PASSED
102:           sts_serial|   4|    100000|     100|0.18600760|  PASSED
102:           sts_serial|   4|    100000|     100|0.22984905|  PASSED
102:           sts_serial|   5|    100000|     100|0.25883020|  PASSED
102:           sts_serial|   5|    100000|     100|0.99315299|  PASSED
102:           sts_serial|   6|    100000|     100|0.40048642|  PASSED
102:           sts_serial|   6|    100000|     100|0.73022511|  PASSED
102:           sts_serial|   7|    100000|     100|0.79035813|  PASSED
102:           sts_serial|   7|    100000|     100|0.91930371|  PASSED
102:           sts_serial|   8|    100000|     100|0.51635740|  PASSED
102:           sts_serial|   8|    100000|     100|0.87010763|  PASSED
102:           sts_serial|   9|    100000|     100|0.95493347|  PASSED
102:           sts_serial|   9|    100000|     100|0.15935465|  PASSED
102:           sts_serial|  10|    100000|     100|0.32276697|  PASSED
102:           sts_serial|  10|    100000|     100|0.67645664|  PASSED
102:           sts_serial|  11|    100000|     100|0.64714937|  PASSED
102:           sts_serial|  11|    100000|     100|0.83931114|  PASSED
102:           sts_serial|  12|    100000|     100|0.98898429|  PASSED
102:           sts_serial|  12|    100000|     100|0.98306183|  PASSED
102:           sts_serial|  13|    100000|     100|0.73353342|  PASSED
102:           sts_serial|  13|    100000|     100|0.75717141|  PASSED
102:           sts_serial|  14|    100000|     100|0.18283051|  PASSED
102:           sts_serial|  14|    100000|     100|0.52874060|  PASSED
102:           sts_serial|  15|    100000|     100|0.35740156|  PASSED
102:           sts_serial|  15|    100000|     100|0.83391413|  PASSED
102:           sts_serial|  16|    100000|     100|0.61391208|  PASSED
102:           sts_serial|  16|    100000|     100|0.83537094|  PASSED
201: rgb_minimum_distance|   0|     10000|    1000|0.00000000|  FAILED
202:     rgb_permutations|   5|    100000|     100|0.85828591|  PASSED
203:       rgb_lagged_sum|   0|   1000000|     100|0.84986413|  PASSED
204:      rgb_kstest_test|   0|     10000|    1000|0.25942548|  PASSED
206:              dab_dct| 256|     50000|       1|0.62442278|  PASSED
207:         dab_filltree|  32|  15000000|       1|0.39920277|  PASSED
207:         dab_filltree|  32|  15000000|       1|0.57982406|  PASSED
208:        dab_filltree2|   0|   5000000|       1|0.90094772|  PASSED
208:        dab_filltree2|   1|   5000000|       1|0.58950861|  PASSED
209:         dab_monobit2|  12|  65000000|       1|0.94848945|  PASSED

Проблема с тестом 201 похоже из-за того, что он запускался отдельно. Десять попыток запустить команду:


cat /dev/urandom | dieharder -g 200 -d 201

закончились с тем же результатом, но в составе всех тестов:


cat /dev/urandom | dieharder -g 200 -a

он проходит нормально:


rgb_minimum_distance|   2|     10000|    1000|0.20617106|  PASSED
rgb_minimum_distance|   3|     10000|    1000|0.00275459|   WEAK
rgb_minimum_distance|   4|     10000|    1000|0.47683577|  PASSED
rgb_minimum_distance|   5|     10000|    1000|0.92418653|  PASSED

В тестах dieharder, фигурирует значение p-value (P-значение). Если не вдаваться в детали, то это значение изменяется в пределах [0, 1] и должно подчиняться нормальному распределению. Например результат ? 0.01 или ? 0.99 можно ожидать в 1% случаев, результат в пределах [0.3, 0.4) в 10% случаев и так далее. Поэтому если значение не равно строго 0 или 1, судить о качестве выборке можно только с определённой вероятностью.


Выборка от аппаратного генератора показала число ошибок в FIPS тестах большее, чем порог ложного срабатывания (примерно 1:1250), но выборка относительно не большая, и для rngd в целом результат достаточно хорош. Как бы то ни было, это генератор истинно случайных чисел, а с ними, как известно, вечная проблема, невозможно до конца быть уверенным в их случайности.


Updated: после оптимизации и настройки скорость генерации чисел увеличилась до 1324 байт/сек, а число ошибок пришло в норму:


rngtest (оптимизированная версия)
rngtest: bits received from input: 724458496
rngtest: FIPS 140-2 successes: 36199
rngtest: FIPS 140-2 failures: 23
rngtest: FIPS 140-2(2001-10-10) Monobit: 2
rngtest: FIPS 140-2(2001-10-10) Poker: 2
rngtest: FIPS 140-2(2001-10-10) Runs: 10
rngtest: FIPS 140-2(2001-10-10) Long run: 9
rngtest: FIPS 140-2(2001-10-10) Continuous run: 0

Выборка не такая большая как в прошлый раз, но для этого теста достаточно показательная. На качество существенно повлияла настройка допустимого интервала (acceptable_fault). Он должен быть достаточно широким, чтобы случайные перекосы не приводили к сдвигу отсечки (threshold), но достаточно узким, чтобы незамедлительно вызывать корректировку неправильного значения threshold. Интервал в три единицы оказался наиболее подходящим.


Прошить микропрограмму можно из AtmelStudio добавив в Tools > External Tools...:


Title: Deploy
Command: D:\UTILS\avrdude\avrdude.exe
Arguments: -CD:\UTILS\avrdude\avrdude.conf -v -patmega328p -carduino -PCOM5 -b113636 -D -Uflash:w:"$(BinDir)\$(TargetName).hex":i
Use Output window (галочка)

Здесь указаны параметры необходимые для прошивки через USB-UART преобразователь. Скорость порта установлена в 113636, подробнее об этом в разделе про загрузчик. Путь к бинарному файлу, определён через переменную окружения $(BinDir), она устанавливается в соответствующую папку текущего проекта, поэтому перед прошивкой необходимо убедиться, что в активной вкладке открыт файл wrn проекта.


Готовый бинарный файл, можно скачать по ссылке: wrn_atmega328p_20MHz.hex. Прошивка файла через USB-UART под Linux принципиально не отличается от приведённого выше варианта:


avrdude -C/etc/avrdude.conf -v -patmega328p -carduino -P/dev/ttyUSB0 -b113636 -D -Uflash:w:"wrn_atmega328p_20MHz.hex":i

Программировать устройство, подключенное через порт RS232, нужно с помощью специальной утилиты, поскольку, после первой загрузки микропрограммы, линия DTR будет заблокирована:


wrnctrl flash wrn_atmega328p_20MHz.hex

Подробнее о программном обеспечении Linux сервера в следующей статье, которая доступна на Habrahabr по ссылке: Часть 2 (Серверная).

Поделиться с друзьями
-->

Комментарии (26)


  1. Rumlin
    19.05.2016 16:17

    На работу генератора шума не влияет, например, прикосновение рукой к пластиковому корпусу транзистора?


    1. custos
      19.05.2016 16:37

      По крайней мере откалиброванная граница от этого не плавает, её значение только от температуры зависит. Правда осциллографа нет, чтобы проверить подробно, но логично предположить, что влиять не должно, поскольку этот транзистор по сути диод, а лавинный пробой такая штука, что наводки на соседнем p-n переходе ничего не изменят.


      1. lolipop
        19.05.2016 17:45

        У интела на свежих процессорах уже есть аппаратный генератор https://ru.wikipedia.org/wiki/RdRand, почему бы не воспользоваться им? Помимо паранойи.


        1. custos
          19.05.2016 17:57
          +1

          Устройство собирал под платформу Bay Trail, процессор Celeron J1900, там ничего такого нет. А в целом, конечно вариант хороший.


          1. forbrak
            20.05.2016 06:39

            Хм, у меня планшет на Bay Trail — Z3735, в нем есть RdRand, сам проверял. Может не на всех моделях Bay Trail есть, но странно.


            1. custos
              20.05.2016 07:16

              Спасибо, посмотрел внимательнее, проверил настройки ядра и /proc/cpuinfo, оказалось он у меня тоже есть. Но в Linux с ним всё не просто, вроде его и не выпилили как во FreeBSD, но то, что он используется не видно. Ядро восстанавливает энтропию со скоростью меньше чем 2 байта в секунду, что в моём случае не приемлемо. А rngd принципиально против этого источника энтропии: RDRAND will be disabled и далее уточнение, что без вариантов. Поэтому эффекта я не заметил, и пришлось решать проблему сначала через /dev/urandom, а потом и доп.устройством.


              1. forbrak
                20.05.2016 13:29

                Понятно, я пробовал на visual studio express edition и с помощью GCC на Linux машине, вполне себе работает и там и там. Ну если вам понадобится в будущем.


        1. GreyCat
          20.05.2016 08:54

          В районе Pentium 3 (точнее, чипсета Intel 840) еще был RNG. Правда очень странный.


      1. Rumlin
        20.05.2016 15:07

        А насколько термостабильно?


        1. custos
          20.05.2016 16:06
          +1

          Калибровка примерно каждые 12 секунд обновляется, поэтому это не проблема в принципе. Если конкретно, то при изменении температуры в помещении от 23 до 32, отсечка плавала 119-126, сейчас стабильно 23°C и стоит фактически на месте — 126.


  1. proudmore
    19.05.2016 17:24

    Сильно прошу прощения за глупый вопрос, но не мог бы кто кинуть ссылкой на место, где рассказывают про энтропию, применительно к компьютерному железу?


    1. custos
      19.05.2016 17:35

      Ближе к железу, чем драйвер drivers/char/random.c вряд-ли что-то можно найти. Если что, он хорошо документирован.


  1. aitras
    19.05.2016 19:25

    Я вот не пойму одной вещи. Почему на GT практически всегда, когда в статье упоминается изготовление платы ЛУТом, автор считает своим долгом описать ЛУТ от начала и до конца? Неужели в интернете он описан недостаточно подробно?

    Хорошо хоть в данной статье автор запихнул описание ЛУТа под спойлер.


    1. custos
      19.05.2016 19:35
      +7

      Поэтому и спрятал под спойлер. А причина простая — метка tutorial обязывает не пропускать детали.


  1. KonstantinSoloviov
    19.05.2016 20:36

    Идеальный пост. Настолько, что боюсь даже холивар не зажечь.
    Но, я попробую :)

    Внутренний параноик сходу выдал: «если так просто сделать генератор 'истинно случайных чисел' почему это до сих пор не сделано».
    Вы гений? Или что-то не договариваете?


    1. arheops
      19.05.2016 20:55

      А не надо делать истинно случайный генератор. Надо только достаточно количество случайных эвентов, для разбавления последователности. Остальное доделывается псевдослучайным генератором. Допустим у вас псевдослучайный генератор который выдает последовательность в 128мбайт после чего повторяется. Если вы делаете по приходу например из это генератора эвента значения меньше 10 переход на количество шагов(n береться из следующего значения физического генератора) в псевдогенераторе, то в результате вы получите совершенно случайно повторяющуюся последовательность. Если вход генератора эвента, конечно, полностью случаен(а он тут случаен, ибо обусловлен квантовыми колебаниями поля).


      1. custos
        19.05.2016 21:17

        Да, /dev/urandom примерно так и работает, и нет ничего плохого в его использовании. Но ещё лучше, когда есть выбор. Так что, почему бы и не сделать.


      1. apple01
        20.05.2016 22:18

        В таком случае примененная схема генератора слишком избыточна, достаточно «подвешенного в воздухе» вывода ADC. Если же заботиться о качестве генерируемой случайной последовательности то тут могут быть вопросы.


    1. custos
      19.05.2016 20:57

      Спасибо, но схема этого генератора стандартная, ищется по ключевым "avalanche noise generator", только софт пришлось писать с нуля, поскольку примеры, сопровождающие эти схемы, по моему скромному мнению, не приемлемы.


  1. kukovik
    20.05.2016 17:27

    Наверное можно уменьшить частоту калибровок и качество случайной последовательности, если использовать не один бит, а два.

    При этом пары 00 и 11 отбрасывать, а пары 01 и 10 использовать как 0 и 1 соответственно. В этом варианте, при неравных вероятностях выпадения исходных нулей и единиц, вероятности выпадения 01 и 10 все равно будут равны и не будут зависеть от температурного дрейфа.

    А калибровку делать при заметном дисбалансе выпадения пар 00 и 11.


    1. custos
      20.05.2016 17:58

      Что-то смысл не уловил, какие два бита вы предлагается взять, чтобы равновероятно выпадали 01 и 10? Если что, то там по сути все 10 бит, в той или иной мере, случайные, но использовать их нельзя. Всегда будут зависимости, как рабочий диапазон, температура, износ. Само по себе это не плохо, если есть возможность откалибровать, но чем больше бит, тем больше требуется вычислений. А использованный метод, прост и надёжен, даже формул не требуется для обоснования.


      1. kukovik
        20.05.2016 18:16
        +1

        Правильно ли я понимаю, что результат каждого измерения АЦП сравнивается с неким порогом? А результат сравнения и если порог превышен, то это единичный бит. За восемь измерений наполняется байт. Порог требует регулярной калибровки.

        Т.е. одно измерение — один бит. Я предлагаю делать два измерения. Получать два бита. Их я и предлагаю взять. Не два бита из результата АЦП, нет.

        Есть такой метод получать гарантированно случайное измерение условной кривой монетой. Даже если орел будет падать с вероятностью 0.9, этот способ будет давать нам на выходе 0.5.


        1. custos
          20.05.2016 19:16

          Кстати да… даже когда подробно прочёл не сразу вспомнил о чём речь. Вариант очень интересный, но требует существенно больше вычислений, не говоря уже о том, что придётся выбросить половину измерений, в идеальном случае. Но ради эксперимента, вероятно даже проверю этот вариант.


  1. sim2q
    20.05.2016 19:43

    хорошо бы отдельный стабилизатор на генератор дабы отключить корреляцию с 12В шиной


  1. apple01
    20.05.2016 21:53

    У меня такой вопрос… Генератор шума тем более случайные сигналы генерирует, чем шире спектр. В идеале- идеальный генератор белого шума. Идеальным он быть не может потому что для бесконечно широкого спектра нужна бесконечная энергия. В реальности спектр ограничен но все равно ширина спектра наверняка достигает нескольких мегагерц — десятков мегагерц. В качестве устройства считывания шума у вас программно-аппаратный комплекс, который, если его рассматривать как черный ящик, имеет ограниченный частотный диапазон из-за частотной характеристики входной цепи, способа формирования случайной последовательности и быстродействия программы обработки. Исходя из сказанного, поскольку считывающее устройство воспринимает по сути низкочастотный шум, какой смысл в генераторе белого шума если им нельзя в полной мере воспользоваться? Вы оценивали качество выходной случайной последовательности?


    1. custos
      21.05.2016 06:32

      Оценки качества небольшой выборки, которую собирал кусками недели две, есть в статье. Речи о белом шуме здесь не идёт, диод генерирует, так называемый розовый (электронный) шум, и его наибольшая плотность именно в низкочастотном диапазоне. Так что тут всё неплохо сбалансировано.