DEV Community 👩‍💻👨‍💻

codemee
codemee

Posted on

放到建構器 (constructor) 內就不亂的亂數種子

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

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() {
}
Enter fullscreen mode Exit fullscreen mode

以下是按 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 
Enter fullscreen mode Exit fullscreen mode

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

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() {
}
Enter fullscreen mode Exit fullscreen mode

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

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 
Enter fullscreen mode Exit fullscreen mode

執行順序很重要

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

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() {
}
Enter fullscreen mode Exit fullscreen mode

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

0 0 0 0 0 
0 0 0 0 0 
0 0 0 0 0 
0 0 0 0 0 
Enter fullscreen mode Exit fullscreen mode

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

等等, 你說的main() 在哪裡?

你可能會想說, 我寫 Arduino 這麼久, 只看過 setup()loop(), 哪來的 main()?其實 Arduino 程式的基本架構已經固定好, 你可以在 hardware\arduino\avr\cores\arduino\main.cpp 找到 main() 的原始碼

int main(void)
{
    init();

    initVariant();

#if defined(USBCON)
    USBDevice.attach();
#endif

    setup();

    for (;;) {
        loop();
        if (serialEventRun) serialEventRun();
    }

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

在後半段可以看到它會叫用我們自己寫的 setup()loop(), 而在開始的地方會叫用 init(), 它就是許多初始工作進行的地方, init() 的原始碼可以在相同路徑下的 wiring.c 中找到:

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.
  ...
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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() {
}
Enter fullscreen mode Exit fullscreen mode

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

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 
Enter fullscreen mode Exit fullscreen mode

小結

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

Top comments (0)

🌚 Life is too short to browse without dark mode