🫠

【Arduino 真性乱数】How to get a true random number without using analogRead

2025/02/08に公開

Arduinoで真性乱数を得る

真性乱数とは真に無作為な数にして、真ならずして無作為な数はこれを擬似乱数と言う。Arduinoでは乱数を得る関数random()を標準に備えると雖も、これは擬似乱数に外ならず、同じ条件であれば同じ結果しか得られない。randomSeed()seed値を与えてその乱数生成条件を変じようにも、今度はseed値が真性乱数でなければ意味がない。従って、実行するたびに異なる要素を見つけ出し、活用することが求められる。

analogRead()

調べると、アナログ入力が不安定であることに基づき、analogRead()の値を活用する方法が見られる。この方法は簡単だが、ピンの状態を正しく理解した上で管理する必要がある。言わずもがなArduinoの類として世に在する製品は実に多様であり、ピンの数や役割も異なる。幾つか遊覧してみよう。

Arduino系機器のピン配置

ピンの数や役割は機器によって異なる上、情報の質も異なる。これらを誰もが間違うことなく運用できる確証はない。斯く言う私こそが、M5Stackのピンを理解できていない一人である。
ピンの扱いを誤ることは、予期せぬ動作や故障を引き起こしかねない危険な行為である。

millis()micros()

第二の手段として、millis()micros()を用いることができる。これらは起動時点からの経過時間を得るものにして、Arduino標準の関数であるため、基本的にはどのような環境でも使える(とはいうものの、私はそう幾つも所持していないので確かめようがなく)。

/*
ミリ秒とマイクロ秒の値を掛け合わせて値を大きくする
且つ絶対値(abs)を取ることで負数になることを回避する
*/
uint32_t randomNumber = abs(millis()*micros());
Serial.printf("random: %d\n", randomNumber);

https://github.com/Seeed-Studio/ArduinoCore-samd/blob/master/cores/arduino/Print.cpp#L186-L196

seeeduino XIAOによる実行結果
random: 1322828900
random: 1369241640
random: 1232397480
random: 608497500
random: 1210286000
random: 1254704640
random: 722631750
random: 1081848560

全て異なる値を得たことが分かる。

真相

実を言うと、millis()micros()を用いて真性乱数を得ることはできない。単なる起動時間であれば、寸分違わず同じ値を取る。ではなぜ上の実行結果が真性乱数になっているのかと言うと、真性乱数を齎している要因が別にあるためである。

while(!Serial)
void setup() {
    Serial.begin(115200);
    while(!Serial);
}

Serialとは、USB経由のシリアル通信に関するものである(USBでないシリアル通信ではSerial1などが使われる)。従ってwhile(!Serial)とは、「シリアルモニターが開かれるまで処理を防ぐ」ことを意味する。シリアルモニターを開く前に処理が進み、初めに出力された内容が見えなくなってしまう⋯といったことを防ぐことができる。

結果として、縦令常にシリアルモニターを開いていようとも、処理が再開されるまでに誤差を生じていたらしい。その上、使用する環境次第では、while(!Serial)は必ずしも機能しない。故に、起動時間で真性乱数を得る方法は「極めて限られた場面」でしか使うことができず、汎用性はないと言える。

本題:記憶領域

ここまで用いてきたアナログ入力やシリアル通信には本来の使い方というものがあり、そちらを優先する場合には使えなくなる。では、記憶領域ならば、本来の使い方をしたまま真性乱数を得られないだろうか。

Arduinoでの記憶領域の扱いはこの記事を参考にした。

https://docs.arduino.cc/learn/programming/memory-guide/#measuring-memory-usage-in-arduino-boards

SRAM

Random Access Memoryは、temporary data⋯つまり一時的なデータを格納する領域である。Static Random Access Memoryとは、通電中に情報が失われないものである。裏を返せば、通電中にも関わらず情報が失われてゆくものもあり、そちらはDynamic Random Access Memoryという。

https://docs.arduino.cc/learn/programming/memory-guide/#sram-memory-measurement

ArduinoSRAMを測る方法は、大きく二種に分かれる。

  1. AVR

Arduino UNOに代表されるものは、「AVRマイコン」などと通称される演算装置を持つ。この場合、次のようなプログラムが有効である。

AVRの場合
int freeRam() {
  extern int __heap_start,*__brkval;
  int v;
  return (int)&v - (__brkval == 0  
    ? (int)&__heap_start : (int) __brkval);  
}
  1. ARM

一方で、ARM系の演算装置を持つものも存在する。この場合、上の例に見えるような__heap_startを使うことができず、全く別の手法を取る。

ARMの場合
extern "C" char* sbrk(int incr);
int freeRam() {
  char top;
  return &top - reinterpret_cast<char*>(sbrk(0));
}

このように記憶領域という物理的な環境に依存するため、販売ページやデータシートで、使用するArduinoの仕様を調べるとよい。

ここでは、Wio Terminalを例にする。販売ページを見ると、「技術仕様」とある表からARM側であることが分かる。

コアプロセッサ ARM Cortex-M4F

これまでの経験から、シリアル通信に依存しない方法で確認できるとよいことが分かっている。幸い、Wio Terminalには画面があるため、画面に値を表示する方法で確認できる。

プログラム例
#include "TFT_eSPI.h"

/* 画面制御 */
TFT_eSPI tft;

extern "C" char* sbrk(int incr);

/* なんとなく大域変数にしてみた */
static uint32_t u32Data = 0;

void getRandom() {
    char top;
    for (char* pcHeap = reinterpret_cast<char*>(sbrk(0)); pcHeap < &top; pcHeap++) {
        u32Data = u32Data ^ (uint32_t)*pcHeap; // XOR
    }
}

void setup() {
    getRandom();
    char acText[64];
    snprintf(acText, 64, "data: %d", u32Data);

    tft.begin();
    tft.setRotation(3); // 画面の向き
    tft.setTextSize(3); // 文字の大きさ
    tft.setTextColor(TFT_BLACK); // 文字の色を黒にする
    tft.fillScreen(TFT_WHITE); // 画面全体を白にする
    tft.drawString(acText, 60, 100); // acTextを画面に表示する
}

void loop() {}

実行結果として画面に表示された値を記録した。

1 182
2 184
3 189
4 131
5 141
6 99
7 59
8 182
9 208
10 213
他も試した記録

SRAMの他にEEPROM (Electrically Erasable Programmable Read Only Memory) という領域も存在する。

https://docs.arduino.cc/learn/programming/memory-guide/#eeprom-memory-measurement

RAM」に対して「ROM」というだけあり、EEPROMに書き込んだ内容は電力を絶たれても遺失しない。つまり、何らかの情報を保持したい際には有効であり、対して真性乱数を得られる可能性は絶望的である。SRAMは情報が保持されないため、真性乱数を得るには有効に働いたのである。

通常のArduinoであれば、EEPROM.hという標準ライブラリーが使えるとある。しかし、Wio Terminalを対象にしている場合、然様なものは御座らぬとはっきり断られる。

 #include <EEPROM.h>
          ^~~~~~~~~~
compilation terminated.
exit status 1

Compilation error: EEPROM.h: No such file or directory

Wio TerminalにはEEPROMがないのだろうか?との初歩的な疑念が浮かぶ。公式Wikiによると、演算装置はATSAMD51P19と称することが分かる。このデータシートを見ると、SmartEEPROMというものが確かに存在する。

Memory Mapping

SmartEEPROM
引用:https://onlinedocs.microchip.com/oxy/GUID-F5813793-E016-46F5-A9E2-718D8BCED496-en-US-14/GUID-34153851-04D9-468E-9F38-00C15768D936.html

つまり、EEPROMはあるがEEPROM.hはないらしい。そこで、次のものが使える。

https://docs.arduino.cc/libraries/flashstorage_samd/

samd」と名の付くものにも対応しているらしく、ATSAMD51P19も該当すると思われる。FlashStorage_SAMDArduino IDEからインストールできる。

flashstorage_samd

サンプルプログラムのdelay()の値だけ変えて、EEPROMの様子を覗った。

EEPROM_read.ino
/******************************************************************************************************************************************
  EEPROM_read.ino
  For SAMD21/SAMD51 using Flash emulated-EEPROM

  The FlashStorage_SAMD library aims to provide a convenient way to store and retrieve user's data using the non-volatile flash memory
  of SAMD21/SAMD51. It now supports writing and reading the whole object, not just byte-and-byte.

  Based on and modified from Cristian Maglie's FlashStorage (https://github.com/cmaglie/FlashStorage)

  Built by Khoi Hoang https://github.com/khoih-prog/FlashStorage_SAMD
  Licensed under LGPLv3 license
  
  Orginally written by A. Christian
  
  Copyright (c) 2015-2016 Arduino LLC.  All right reserved.
  Copyright (c) 2020 Khoi Hoang.
  
  This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License 
  as published bythe Free Software Foundation, either version 3 of the License, or (at your option) any later version.
  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
  You should have received a copy of the GNU Lesser General Public License along with this library. 
  If not, see (https://www.gnu.org/licenses/)
 ******************************************************************************************************************************************/
/*
   EEPROM Read

   Reads the value of each byte of the EEPROM and prints it to the computer.
   This example code is in the public domain.
*/

//#define EEPROM_EMULATION_SIZE     (4 * 1024)

// Use 0-2. Larger for more debugging messages
#define FLASH_DEBUG       0

// To be included only in main(), .ino with setup() to avoid `Multiple Definitions` Linker Error
#include <FlashStorage_SAMD.h>

// start reading from the first byte (address 0) of the EEPROM
int address = 0;
byte value;

void setup()
{
  Serial.begin(115200);
  while (!Serial);

  delay(200);

  Serial.print(F("\nStart EEPROM_read on ")); Serial.println(BOARD_NAME);
  Serial.println(FLASH_STORAGE_SAMD_VERSION);

  Serial.print("EEPROM length: ");
  Serial.println(EEPROM.length());
}

void loop() 
{
  // read a byte from the current address of the EEPROM
  value = EEPROM.read(address);

  Serial.print(address);
  Serial.print("\t");
  Serial.print(value, DEC);
  Serial.println();
 
  if (++address == EEPROM.length()) 
  {
    address = 0;
  }

  /***
    As the EEPROM sizes are powers of two, wrapping (preventing overflow) of an
    EEPROM address is also doable by a bitwise and of the length - 1.

    ++address &= EEPROM.length() - 1;
  ***/

  delay(10); // 500では遅すぎたため10に変更した
}

その結果は、案の定といった様子であった。

EEPROMの読み取り結果
Start EEPROM_read on Unknown SAMD51 board
FlashStorage_SAMD v1.3.2
EEPROM length: 1024
0	255
1	255
2	255
3	255
4	255
5	255
6	255
7	255
8	255
9	255
10	255
11	255
12	255
13	255
14	255
15	255
16	255
17	255
18	255
19	255
20	255
21	255
22	255
23	255
24	255
25	255
26	255
27	255
28	255(中略)995	255
996	255
997	255
998	255
999	255
1000	255
1001	255
1002	255
1003	255
1004	255
1005	255
1006	255
1007	255
1008	255
1009	255
1010	255
1011	255
1012	255
1013	255
1014	255
1015	255
1016	255
1017	255
1018	255
1019	255
1020	255
1021	255
1022	255
1023	255
0	255
1	255
2	255(以下略)

01023の全てのアドレスに於いて、その値は255であった。これでは、真性乱数への活用は難しい。

跋:注意点

アナログ入力に特別の非があるわけではないが、高々乱数のため態々しくピンを使うことに対し、猜疑や違和の感を拭うことを得ず、終に別の方法を見つけるに至った。

しかしながら、不安定な値の入力にせよ、記憶領域の直接的な読み取りにせよ、予期せぬ動作を引き起こす危険な行為であることには違いない。特に、記憶領域を読み取る際に使った手法は代表的な危険行為である。

for (char* pcHeap = reinterpret_cast<char*>(sbrk(0)); pcHeap < &top; pcHeap++)

pointer」とは記憶領域内のアドレスを扱うものだが、アドレス自体には何の意味もなく、アドレスの示す位置に格納される内容にこそ意味がある。しかしながら、今回用いたプログラムでは、pcHeapというpointerが示す位置に意味のある内容が存在しないことを期待している。だからこそ真性乱数が得られたのである。

こうしたものを特に「row pointer」と称する。必ずしも意味のある位置を示さないため、実行時には何が起こるか分からない。
反対に、意味のある位置を示すことが保証されるものは「smart pointer」と称し、&topのような「参照」がこれに該当する。

危険 安全
row pointer smart pointer

本記事で扱った真性乱数自体が「決まった値が保証されない」ものであるから、安全が保障されない行為に手を染めざるを得ないと留意する必要がある。

Discussion