放到建構器 (constructor) 內就不亂的亂數種子
Tags: arduino, cpp, esp32, esp8266
Published: 2022-02-19 02:52:44

在 Arduino 中使用亂數大概都會是以下的寫法, 利用空接的類比腳位輸入值當亂數種子：

```cpp
void setup() {
  Serial.begin(9600);
  randomSeed(analogRead(A0));
  for(int i = 0; i < 5; i++) {
    Serial.print(random(300));
    Serial.print(" ");
  }
  Serial.println("");
}

void loop() {
}
```

以下是按 5 次重置鈕執行 5 次的結果, 的確符合透過亂數種子讓每次產生的亂數沒有固定順序的期望：

```
10 38 62 81 237 
19 104 42 108 265 
239 57 235 210 70 
274 55 112 206 279 
110 247 121 80 226 
```

由於利用類比輸入值設定亂數種子是產生亂數前的固定步驟, 我們可以將亂數物件化, 將設定亂數種子的工作擺到建構器 (constructor) 中, 就不需要自己設定亂數種子了, 像是這樣：

```cpp
class RndGen {
  RngGen() {
    randomSeed(analogRead(A0));
  }
  public:
    long rnd(long maxNum) {
      return random(maxNum);
    }
};

RndGen r;

void setup() {
  Serial.begin(9600);
  for(int i = 0; i < 5; i++) {
    Serial.print(r.rnd(300));
    Serial.print(" ");
  }
  Serial.println("");
}

void loop() {
}
```

不過實際上執行後卻發現每次執行產生的亂數順序都一樣：

```
7 49 173 158 130 
7 49 173 158 130 
7 49 173 158 130 
7 49 173 158 130 
7 49 173 158 130 
```

## 執行順序很重要

之所以會發生前述問題, 主要是因為 Arduino 是 C++ 程式, 執行時會先建立全域物件, 再執行 `main()` 主函式, 而 ADC 功能的初始設定是在 `main()` 中才完成, 因此上例中 `r` 的建構器使用類比輸入值設定亂數種子時根本還無法進行 ADC, 所以會得到相同的亂數種子, 我們可以透過以下的例子來驗證：

```cpp
class adc {
  public:
  int adcs[5];
  adc() {
    for(int i = 0; i < 5; i++)
      adcs[i] = analogRead(A0);
  }
  void print(void) {
    for(int i = 0; i < 5; i++) {
      Serial.print(adcs[i]);
      Serial.print(" ");
    }
    Serial.println("");
  }
};

adc a;

void setup() {
  Serial.begin(9600);
  a.print();
}

void loop() {
}
```

底下是按 5 次重置鈕執行 5 次的結果：

```
0 0 0 0 0 
0 0 0 0 0 
0 0 0 0 0 
0 0 0 0 0 
```

你可以看到不論怎麼讀, 類比輸入值都是 0。

## 等等, 你說的`main()` 在哪裡？

你可能會想說, 我寫 Arduino 這麼久, 只看過 `setup()`、`loop()`, 哪來的 `main()`？其實 Arduino 程式的基本架構已經固定好, 你可以在 hardware\arduino\avr\cores\arduino\main.cpp 找到 [`main()` 的原始碼](https://github.com/arduino/ArduinoCore-avr/blob/b3243815e2ed8c72f9ac3669e69632a590b1d048/cores/arduino/main.cpp#L33)：

```cpp
int main(void)
{
	init();

	initVariant();

#if defined(USBCON)
	USBDevice.attach();
#endif
	
	setup();
    
	for (;;) {
		loop();
		if (serialEventRun) serialEventRun();
	}
        
	return 0;
}
```

在後半段可以看到它會叫用我們自己寫的 `setup()` 和 `loop()`, 而在開始的地方會叫用 `init()`, 它就是許多初始工作進行的地方, [`init()` 的原始碼](https://github.com/arduino/ArduinoCore-avr/blob/b3243815e2ed8c72f9ac3669e69632a590b1d048/cores/arduino/wiring.c#L241)可以在相同路徑下的 wiring.c 中找到：

```cpp
void init()
{
	// this needs to be called before setup() or some functions won't
	// work there
  ...
#if defined(ADCSRA)
	// set a2d prescaler so we are inside the desired 50-200 KHz range.
  ...
}
```

這個函式大部分都是組合語言, 我們不會深入探悉, 只是標示出設定 ADC 的地方。

## 套用 Arduino 的設計模式解決問題

了解問題所在後, 就可以解決問題了。大部分的 Arduino 程式庫若是以物件的形式呈現, 都會為類別加上 `begin()` 讓使用者設定初始狀態, 像是我們常用的`Serial.begin()`、`Wire.begin()` 等等, 就可以把跟硬體相關的設定延到 `main()` 執行完 `init()` 後再進行, 依照此模式, 我們就可以將產生亂數的物件改成以下的設計方式：

```CPP
class RndGen {
  public:
    begin() {
      randomSeed(analogRead(A0));
    }
    long rnd(long maxNum) {
      return random(maxNum);
    }
};

RndGen r;

void setup() {
  Serial.begin(9600);
  r.begin();
  for(int i = 0; i < 5; i++) {
    Serial.print(r.rnd(300));
    Serial.print(" ");
  }
  Serial.println("");
}

void loop() {
}
```

執行後就會發現亂數順序又變回我們需要的亂了：

```
156 1 208 275 41 
142 203 56 206 28 
205 97 184 40 263 
233 46 135 178 289 
219 195 36 109 276 
```

## 小結

建議大家在設計自己的程式庫時, 都可以遵循 Arduino 的模式, 加入 `begin()`, 一方面可以避免本文提到的硬體設定問題, 一方面則可以和既有的 Arduino 程式庫一致, 方便使用者用習慣的方式寫程式。