Can I use a I/O as (input/supply) GND?
You can use an I/O pin as a supply for another (low power) device, but
you should power the Arduino itself from its Vcc and GND pins.
I could use the reset button, but would it still give me "random
enough" numbers?
You will likely see the very same sequence every time you reset it.
There are ways around this:
- You can seed the random number generator (RNG) at startup with an
analogRead()
from an unconnected pin, but it's not clear how much
entropy you can expect from this.
- You can save the RNG's entropy into the .noinit memory section, this
way it will not be wiped up during the program startup.
I would recommend, however, that you use a normal I/O pin to trigger the
dice, if only for one reason: the time when the user presses the button
(that you read with micros()
) is a very good entropy source for
seeding the RNG.
For example, whenever the user presses the button you could:
srandom(random() ^ ((uint32_t) analogRead(0) << 22) ^ micros());
This way you combine the entropy from the button press with the entropy
from the analog pin (if any), with whatever entropy may already by in
the RNG.
Edit: More on entropy gathering.
As you seemingly already know, random()
is a pseudo-random number
generator intended to produce a sequence of numbers that look random.
Unless you have some other source of randomness in your program, every
time you restart the Arduino you will get the very same sequence of
numbers. Getting true randomness (also called “entropy”) on an Arduino
is not so easy. Here are some ideas you may want to explore.
Read from an unconnected analog input
This trick seems to be pretty popular. It can even be found in the
Arduino documentation on
randomSeed():
use randomSeed() to initialize the random number generator with a
fairly random input, such as analogRead() on an unconnected pin.
In my experience, this seems to be a very poor entropy source. I wrote a
test program that prints out the output of analogRead(0)
and it seems
to cycle through the same few values in an almost predictable way.
I estimate this gives around one or two bits of entropy per call. On the
plus side, each call takes just over 100 µs.
Read from an analog noise source
There is a lot of literature on building analog noise sources. Such
sources are used inside crypto-grade random number generators, but the
circuits are likely to be more complex than you would like. There seems,
however, to be a very simple alternative. In a comment to this
post, CharlieHanson wrote:
Connecting the analogue input to the collector of a run-of-the-mill
transistor, with the emitter grounded and base open helps to make a
noisy input.
I have not tested this idea, but it is certainly worth exploring.
Time user input
If the user is required to press a button to throw the dice, the timing
of the button press is probably the best entropy source you can have.
With a 8 MHz clock, the micros()
function has a resolution of
8 µs (64 clock cycles). Assuming the user is unable to control his
timing better than 0.1 seconds, then you have more than
13 bits of entropy per button press.
If, instead of micros()
, you use timer 1 running at the full CPU
speed, then you would have 16 bits of entropy per button press. And
you would not need random()
at all, as you could just output
1+TCNT1%6
. That would be like throwing a dart at a carnival wheel
spinning at 80 million RPM (8 MHz / 6)!
Measure clock drift
The drift of a clock relative to another clock can be used as an
entropy
source.
The ATmega chip in your Arduino has two independent clocks: the internal
“calibrated” 8 MHz RC oscillator, and the 128 kHz oscillator
used by the watchdog timer. Both are low-accuracy clocks subject to
somewhat unpredictable drift, which is a good thing for this
application. You can gather entropy from this by configuring the
watchdog timer as a periodic interrupt source, and using timer 1 at full
speed to time the interrupt arrival times:
#include <avr/wdt.h>
#include <util/atomic.h>
// Configure Timer 1 for counting CPU cycles and the watchdog as an
// interrupt source firing (roughly) once a second. Call once in
// setup().
void configure_timers()
{
// Timer 1.
TCCR1A = 0; // normal counting mode
TCCR1B = _BV(CS10); // prescaler = 1
// Watchdog. This is a timed sequence.
ATOMIC_BLOCK(ATOMIC_FORCEON) {
WDTCSR = _BV(WDCE) // enable changing the prescaler
| _BV(WDE); // ditto
WDTCSR = _BV(WDIF) // clear interrupt flag
| _BV(WDIE) // enable interrupts
| WDTO_1S; // timeout ~ 1 second
}
}
volatile uint16_t wdt_time;
volatile bool wdt_fired;
// Watchdog interrupt service routine.
ISR(WDT_vect)
{
wdt_time = TCNT1;
wdt_fired = true;
}
// Add entropy to the RNG from the timing of the last WDT interrupt.
// Call periodically in loop().
void get_wdt_entropy()
{
if (!wdt_fired) return; // No WDT event since last time.
uint16_t wdt_time_copy;
ATOMIC_BLOCK(ATOMIC_FORCEON) {
wdt_time_copy = wdt_time;
wdt_fired = false;
}
srandom(random() ^ wdt_time_copy);
}
I have tested this idea by plotting the time differences between
successive interrupts. On an Arduino Uno clocked at 16 MHz, I see
short-time fluctuations with an amplitude of about 300. That should
provide around 8 bits of entropy per second.
Save the entropy pool across reboots
Once you have spent some effort to gather entropy from the environment,
it would be a pity to lose it all at power down. It is common practice
to save the entropy pool to disk when a computer shuts down, and restore
it at boot time. This avoids the problem of the RNG always starting in
the same predictable state. On the Arduino, you have no disk, but you
can use the EEPROM instead. The problem is you cannot know in advance
when the user is going to switch the device off. Then, ideally, you
would save the entropy every time the user requests a new dice value.
The new problem is that the EEPROM is only guaranteed to work reliably
for 100,000 write/erase cycles. This may not be a serious problem
(100,000 is plenty of throws), but it can nonetheless be mitigated by
using a wear-leveling scheme, where you distribute the writes across the
whole EEPROM instead of always overwriting the same bytes.
Wear-leveling can add significant complexity to your program, but it
turns out there is, in this particular case, a wear-leveling scheme that
is trivial to implement: just write at a random location! At startup,
you do not know what was the last location written to, but you do not
need to: you read and mix the whole contents of the EEPROM:
#include <avr/eeprom.h>
// Save the entropy to EEPROM. Call this every time the user requests a
// new value.
void save_entropy()
{
uint32_t *address = (uint32_t *)(random() & E2END & ~3);
eeprom_write_dword(address, random());
}
// Restore the entropy from the EEPROM. Call once in setup().
void restore_entropy()
{
uint32_t seed = 0;
for (uint32_t *address = 0; address < (uint32_t *) E2END; address++)
seed ^= eeprom_read_dword(address);
srandom(seed);
}
Combine the entropy sources with the XOR operator
As you probably already noticed, the bitwise xor operator (^) is used
every time one wants to combine entropy sources. The main reason is
that, if a
and b
are independent random variables, then a^b
is at
least as random as the most random of a
and b
. Which means that
XORing a “good” random variable with a “bad” one is harmless. This is
somewhat formalized as the piling-up
lemma.
As an example, doing randomSeed(analogRead(0))
at program startup is
fine, but doing it repeatedly during program execution is a terrible
idea: most of the time this will put the RNG in one of a very few
states. If instead you
srandom(random() ^ analogRead(0));
then you are combining the current randomness of the RNG with the output
of analogRead()
, and putting this back into the RNG. And this is
harmless even if analogRead()
is quite predictable.
A side note on randomSeed(): Arduino releases prior to 1.6.5 had a
bug that would truncate to 16 bits the entropy provided with
randomSeed()
. On those releases it is better to use the avr-libc
srandom()
function.
The bug has since been
fixed.