RS232 устройство 3-в-1 для домашнего Linux сервера: Часть 1 (Аппаратная)

Серверы, собранные из специально не предназначенных для этого комплектующих, обычно имеют два недостатка. У них отсутствует аппаратный сторожевой таймер и часто не хватает энтропии для ряда сервисов. Нехватка энтропии особенно актуальна для не сильно нагруженных серверов. Это связанно с тем, что ядро 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 checkStart DRC обе закладки Problems / Markers и Unconnected должны быть пустыми. Нужно периодически его использовать, чтобы быть уверенным, что ничего не сломалось.

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

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


Некоторая статистика
  • первая ревизия — Real drill без кернения, хорошее сверло Dremel 0.8 мм, затупилось где-то на ¾, заканчивал сверлом 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;
    // вызывается редко, оптимизация не требуется
    acceptable_fault = ((measure_limit - 1) / 256 + 1) * 2; 

    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 в целом результат достаточно хорош. Как бы то ни было, это генератор истинно случайных чисел, а с ними, как известно, вечная проблема, невозможно до конца быть уверенным в их случайности.

Прошить микропрограмму можно из AtmelStudio добавив в ToolsExternal 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), она

© Geektimes