I've long had an interest in random number generation. In fact, a very early PCB I designed and built was exactly for this purpose: Arduino Random Number Generator.
That project used a property of transistors called "avalanche noise". Inconveniently, it required a supply of +-10V to work properly. Avalanche noise is sometimes explained as being a "quantum effect" and thus is supposed to be a source of true randomness.
When the TPS65131 went into the store, I knew it was time to revisit this project. This little PCB generates positive and negative voltages from a single +5V input.
You can choose almost any microcontroller for this project. I used the Metro ESP32-S3 because it was on my desk and because CircuitPython supports analogbufio, which allows efficiently reading many analog samples quickly.
Build the Random Number Generator
Follow the schematic or breadboard wiring plans. Note how the "Q1" transistor is not pointing the same way as the others, and one of its terminals isn't even connected. A transistor that is "reverse biased" like this can be used as a source of physical noise that is inherently unpredictable. Rob Seward's write-up has been on the internet for a very long time and remains my reference on this topic.
Transistors Q2 and Q3 amplify the very small amount of noise produced by Q1, ultimately producing a signal that goes to a microcontroller.
Note on component damage: Transistor Q1 is operated in a non-standard mode that can permanently affect its properties, as brought to my attention by graham on discord: 'Any transistor that has been operated in base-emitter avalanche mode should not be used as a regular transistor afterwards. Operation in base-emitter avalanche causes the gain of the transistor to drop permanently, and it may no longer meet specifications. The circuit works as you describe, but the transistor in avalanche changes permanently. You can search the internet for something like "Current gain degradation induced by emitter-base avalanche breakdown in silicon transistors."'
The figure below is from the cited paper. Note that in the circuit shown here the emitter-base current is up to about 5mA, based on the 4.7k resistance and +-12V supply rails.
Connecting to a microcontroller
Depending on your needs, you can access the randomness as digital or analog data.
On the Metro ESP32-S3 with CircuitPython, make these connections (it's similar for other microcontrollers):
- Connect 3v3 where the schematic calls out 5V (use the I/O voltage of your microcontroller)
- Connect GND to GND
- Connect the circuit output (Collector of Q3) to pin A5 (or choose any other input pin according to your needs; On the ESP32-S3, pin A5 is capable of both use as a digital pin and also as a high speed analog capture pin)
Grabbing random analog values in CircuitPython
The analogbufio module in CircuitPython allows grabbing analog data at a given sample rate. Here's how to grab 256 random analog values at once.
A plot of the data will show that while it is unpredictable, it is biased, with more values occurring in the center of the range. It is probably also correlated, meaning that adjacent samples are more likely to be close in value. This pattern, if it exists, is much harder to see here. However, it is obvious in the very top image of a high speed oscilloscope capture.
import board
import analogbufio
import array
buf = array.array('H', [0]) * 256
aio = analogbufio.BufferedIn(board.A5, 83333)
aio.readinto(buf)
print(buf)
array('H', [0, 25029, 33991, 12258, 17876, 35032, 41689, 26454, 40793, 33207,
40585, 18036, 11922, 17956, 15731, 37096, 26982, 43017, 37560, 31175, 27926,
…, 43001, 19300, 12994, 22133])
Grabbing random digital values with CircuitPython
It's possible to use the SPI peripheral to capture digital values at high speed. This works across a wide range of microcontrollers, with the caveat that an unused pin has to be available to be used as the clock or SCK signal.
import array
import board
import busio
spi = busio.SPI(SCK=board.A0, MISO=board.A5, baudrate=10_000_000)
spi.try_lock()
a.readinto(buf)
a.unlock()
print("".join(f"{bi:016b}" for bi in buf))
'11000111101100100111110111000000111111110111001100001001110111101001111 111110111111111111111111110011110111110111101011111011111001111111111000 011111111100111111111111111111111110000111100001111111001111111111111111 110000111111011111011111011111111101001101100000111100111010000001011000 011001111111011110000000011110110111111110011101110000111000001111111111 …1101011100011000111111100111111101001111000111111001110011101001000010'
Here, the effect of correlation is much more apparent: a "1" is very likely to be followed by a "1", and a "0" by another "0". For digital values it's easy to count the frequency of different 1-bit and 2-bit outcomes:
>>> s.count('0')
1502
>>> s.count('1')
2594
>>> s.count('00')
566
>>> s.count('01')
578
>>> s.count('10')
578
>>> s.count('11')
1122
On the whole, "1"s are much more plentiful than "0"s; "11" is twice as common as any of "00", "01", and "10". And, contrary to my intuition, "00" is about as common as either "01" or "10", meaning that runs of zeros are not actually excessively frequent despite what I think I see in the above data "by eye".
"Whitening" data
My previous RNG article describes how to use the sha1 hash to convert a biased & correlated stream into a fully unpredictable stream of bytes.
Closing remarks
You probably shouldn't use a homemade random number generator for anything of consequence. That doesn't mean it's not interesting to build one, to see that it works, & to try to understand the theoretical basis of why it works.
This page ( CircuitPython "Avalanche Noise" RNG with TPS65131 & NPN transistors) was last updated on March 31, 2025.