はじめに
スタンドアロンのマイコン環境を想定
タイトルにC言語と書いてますが想定はマイコンボード、特にArduinoです。
マイコンで乱数を使用する処理を含む場合、疑似乱数の生成方法の影響により同じプログラムを複数のマイコンに書き込んでも同じ値を生成してしまい、結果的に乱数として望ましい動作をしないという課題があります。
未接続のADコンバータをシード値にするなどの方法もありますが、今回はパソコンでコンパイルした時刻をプログラム内に取り入れる方法を紹介し、それをシード値として使用しすることでコンパイルごとに乱数値がばらつくようにします。
なお、コンパイル時刻が異なるプログラム同士では異なる乱数値を生成しますが、秒単位で同時刻にコンパイルしたり、書き込み済みのマイコンをリセットした場合は同じ値を生成します。
また、コンパイル後のファイルを複数のマイコンに書き込む場合もコンパイル時刻に依存するので同じ値を生成することになります。
注意事項
本記事ではCコンパイラのプリプロセッサの機能を使います。avr-gcc系の環境で検証しており、情報収集した範囲では他のコンパイラでも共通した処理になると思われますが未検証ですのでご了承ください。(他の環境等で気づいたことがあればコメントいただければ)
OS上で動作したりRTCが接続された環境など、プログラム実行後にコンピュータの時刻情報を取得することができる環境では、この記事の方法を使うメリットはあまりないと思います。
検証環境
- Windows 10
- Arduino IDE 1.8.13
- Arduino UNO(互換機)
- Arduino Leonardo(互換のPro Micro)
プリプロセッサの定義済みマクロ
C/C++言語でよく使用されるプリプロセッサ指令には#define
や#include
がありますが、コンパイルするファイルのファイル名やコンパイル時の日時、日付を置換する文字列が仕様として定義されています。
-
__FILE__
:ファイル名 -
__DATE__
:日時(文字列リテラル) -
__TIME__
:時刻(文字列リテラル)
GCC系の環境でMicrosoftのリファレンスを紹介するのもアレですが、記事の信頼性が高くわかりやすく書いてあるので、下記リンク先の標準の定義済みマクロの部分はC言語のコンパイラで共通にサポートされているものだと思います。
定義済みマクロの内容をシリアル出力する
単純にSerial.print()
に入力して値を確認します。
プログラム例
void setup() {
Serial.begin(115200);
while(!Serial)delay(1000);
Serial.print("Compile Date : ");
Serial.print(__DATE__);
Serial.print(" , Compile Time : ");
Serial.println(__TIME__);
randomSeed(0);
for(int i=0; i<3; i++){
delay(1000);
Serial.print("Random : ");
Serial.println(random(), DEC);
}
}
void loop() {
delay(1000);
}
21:18:38.033 -> Compile Date : Nov 28 2022 , Compile Time : 21:18:32
21:18:39.058 -> Random : 16807
21:18:40.035 -> Random : 282475249
21:18:41.060 -> Random : 1622650073
定義済みマクロの内容を変数に格納し、ランダムシードに使用する
定義済みマクロは置換後、文字列配列const char*
として扱われるので配列要素にアクセスする要領で格納していきます。
アラビア数字の文字を数値に変換する際に'0'
からの差分を計算して数値型配列に格納します。
こちらのソースコードでは日付情報の__DATE__
も処理に含んでいますが、記事作成日以外で検証しておらず他の日付で期待通りに動作しない可能性があるので注意してください。
ソースコード
uint8_t compile_date[8] = {__DATE__[7]-'0', __DATE__[8]-'0', __DATE__[9]-'0', __DATE__[10]-'0', 0, 0, 0, 0,};
uint8_t compile_time[6] = {__TIME__[0]-'0', __TIME__[1]-'0', __TIME__[3]-'0', __TIME__[4]-'0', __TIME__[6]-'0', __TIME__[7]-'0'};
unsigned long compile_timesec;
unsigned long myMillis;
unsigned long myClock;
void setup() {
// ↓DATE部分は未検証
if(__DATE__[0] == 'D') compile_date[4] = 1, compile_date[5] = 2;
else if(__DATE__[0] == 'N') compile_date[4] = 1, compile_date[5] = 1;
else if(__DATE__[0] == 'O') compile_date[4] = 1, compile_date[5] = 0;
else if(__DATE__[0] == 'S') compile_date[4] = 0, compile_date[5] = 9;
else if(__DATE__[2] == 'g') compile_date[4] = 0, compile_date[5] = 8;
else if(__DATE__[2] == 'l') compile_date[4] = 0, compile_date[5] = 7;
else if(__DATE__[1] == 'u') compile_date[4] = 0, compile_date[5] = 6;
else if(__DATE__[2] == 'y') compile_date[4] = 0, compile_date[5] = 5;
else if(__DATE__[1] == 'A') compile_date[4] = 0, compile_date[5] = 4;
else if(__DATE__[0] == 'M') compile_date[4] = 0, compile_date[5] = 3;
else if(__DATE__[0] == 'F') compile_date[4] = 0, compile_date[5] = 2;
else if(__DATE__[0] == 'J') compile_date[4] = 0, compile_date[5] = 1;
compile_date[6] = (__DATE__[4]==' ')?(0):(__DATE__[4]-'0'); //日付が10未満の場合にブランク文字で埋められると仮定
compile_date[7] = __DATE__[5]-'0';
// ↑DATE部分は未検証
Serial.begin(115200);
while(!Serial)delay(1000);
Serial.print("Compile Date : ");
Serial.print(__DATE__);
Serial.print(" , Compile Time : ");
Serial.println(__TIME__);
Serial.print("Compile Date(yyyyMMdd) : ");
Serial.print(compile_date[0], DEC);
Serial.print(compile_date[1], DEC);
Serial.print(compile_date[2], DEC);
Serial.print(compile_date[3], DEC);
Serial.print(compile_date[4], DEC);
Serial.print(compile_date[5], DEC);
Serial.print(compile_date[6], DEC);
Serial.print(compile_date[7], DEC);
Serial.print(" , Compile Time(hhmmss) : ");
Serial.print(compile_time[0], DEC);
Serial.print(compile_time[1], DEC);
Serial.print(compile_time[2], DEC);
Serial.print(compile_time[3], DEC);
Serial.print(compile_time[4], DEC);
Serial.println(compile_time[5], DEC);
compile_timesec = (((unsigned long)compile_time[0]*10+(unsigned long)compile_time[1])*60+(unsigned long)compile_time[2]*10+(unsigned long)compile_time[3])*60+(unsigned long)compile_time[4]*10+(unsigned long)compile_time[5];
Serial.print("Compile Time(sec) : ");
Serial.println(compile_timesec, DEC);
randomSeed(0);
for(int i=0; i<3; i++){
delay(1000);
Serial.print("Random : ");
Serial.println(random(), DEC);
}
randomSeed(compile_timesec);
}
void loop() {
delay(1000);
Serial.print("millis() : ");
Serial.print(myMillis = millis());
myClock = compile_timesec + myMillis/1000;
Serial.print(" , Clock(sec) : ");
Serial.print(myClock, DEC);
Serial.print(" , Clock(hh:mm:ss) : ");
Serial.print((myClock%(24UL*3600))/3600, DEC); //hh
Serial.print(":");
Serial.print((myClock%3600)/60, DEC); //mm
Serial.print(":");
Serial.print(myClock%60, DEC); //ss
Serial.print(" , Random : ");
Serial.println(random(), DEC);
}
21:21:40.353 -> Compile Date : Nov 28 2022 , Compile Time : 21:21:35
21:21:40.353 -> Compile Date(yyyyMMdd) : 20221128 , Compile Time(hhmmss) : 212135
21:21:40.353 -> Compile Time(sec) : 76895
21:21:41.385 -> Random : 16807
21:21:42.362 -> Random : 282475249
21:21:43.381 -> Random : 1622650073
21:21:44.362 -> millis() : 4007 , Clock(sec) : 76899 , Clock(hh:mm:ss) : 21:21:39 , Random : 1292374265
21:21:45.385 -> millis() : 5010 , Clock(sec) : 76900 , Clock(hh:mm:ss) : 21:21:40 , Random : 1284666097
リセット
(↓PC側の受信時刻は変化するが、同じ乱数値を出力している)
21:21:47.538 -> Compile Date : Nov 28 2022 , Compile Time : 21:21:35
21:21:47.538 -> Compile Date(yyyyMMdd) : 20221128 , Compile Time(hhmmss) : 212135
21:21:47.538 -> Compile Time(sec) : 76895
21:21:48.563 -> Random : 16807
21:21:49.548 -> Random : 282475249
21:21:50.583 -> Random : 1622650073
21:21:51.571 -> millis() : 4007 , Clock(sec) : 76899 , Clock(hh:mm:ss) : 21:21:39 , Random : 1292374265
21:21:52.554 -> millis() : 5010 , Clock(sec) : 76900 , Clock(hh:mm:ss) : 21:21:40 , Random : 1284666097
同じソースコードを再度コンパイルして書き込み
(時刻変数の内容が変化しており、異なる乱数値を出力している)
21:24:32.039 -> Compile Date : Nov 28 2022 , Compile Time : 21:24:26
21:24:32.039 -> Compile Date(yyyyMMdd) : 20221128 , Compile Time(hhmmss) : 212426
21:24:32.039 -> Compile Time(sec) : 77066
21:24:33.021 -> Random : 16807
21:24:34.048 -> Random : 282475249
21:24:35.059 -> Random : 1622650073
21:24:36.036 -> millis() : 4007 , Clock(sec) : 77070 , Clock(hh:mm:ss) : 21:24:30 , Random : 1295248262
21:24:37.062 -> millis() : 5010 , Clock(sec) : 77071 , Clock(hh:mm:ss) : 21:24:31 , Random : 195809795
この手法の応用先と余談
この手法の応用先としては、プログラミングの講義などでソースコードを配布した際に、受講者の手元で乱数がある程度バラつくような仕掛けの一つとして使えるかもしれませんし、同じソースコードで多数のマイコンを動かし、異なる挙動を期待する場合などにもいいかもしれません。
この手法を着想した本当のところは、マイコンで時計タイプのプロダクトを開発する際に時刻の初期化処理が面倒で、開発中に何度もコンパイルすることを利用し、かつ、だいたい現在時刻に近い毎回異なる値で初期化処理が可能な手法ということで実装しました。
とはいっても、正確にはパソコンのコンパイル開始時刻と、マイコンに書き込みに成功した後マイコンの内部クロックの初期化タイミングに数秒のズレがあるので、それを織り込む必要があるのと、millis()が50日程度で変数のオーバーフローを起こしたりと、RTC的な使い方には難があるのであくまでそれっぽい雰囲気の初期化ができる、という手法だと理解いただければと思います。