For most cases of random number generation, systems use a pseudo-random number generator (PRNG), that is an algorithm that generates numbers that "look" random. But for certain applications, where true randomness is required — as in cryptography or simulations — there’s a different approach: draw on a physical process that, by its nature, is unpredictable. Radioactive decay is one such process that can be exploited to produce random numbers. For this project we are going to show you how to create a simple random number generator with the help of a Geiger counter, Geiger tube, and an ATtiny13.
Random bytes |
Particle Detection:
The Geiger counter detects ionizing particles (like beta particles or gamma rays) passing through the Geiger tube. Each time a particle is detected, a pulse is generated.
Signal Capture:
This pulse is sent to the ATtiny13, which is set up to measure the time between two consecutive pulses. The ATtiny uses this timing data to assess the randomness of the intervals.
Assigning a Bit:
The ATtiny13 then compares the duration between consecutive pulses. If the time difference is shorter than a certain threshold, it assigns a bit value of "1". If the time difference is longer, it assigns a bit value of "0". This method relies on the natural randomness of radioactive decay to generate unpredictable bit sequences.
The result is then transmitted to a serial port/ can easily be analysed on a computer. I used background radiation and the random number generation was very slow. Alternatively you could use the output from this setup as a seed for a PRNG.
Wiring/connections
I hooked up the PB4 on the attiny13 to the particle count output ( speaker) on an inexpensive counter (BR-6). The PB1 is used as the serial output (TX) using software serial (library from Ćukasz Podkalicki).
Source Code for attiny13
1: #include <avr/io.h>
2: #include <avr/interrupt.h>
3: #include <stdint.h>
4: // Pin definitions
5: #define GEIGER_PIN PB4
6: // Global variables
7: volatile uint32_t timer_overflows = 0;
8: volatile uint32_t last_timer_value = 0;
9: volatile uint32_t last_interval = 0;
10: volatile uint8_t random_byte = 0;
11: volatile uint8_t bit_count = 0;
12: volatile uint8_t edge_pending = 0;
13: volatile uint32_t pending_timer_value = 0;
14: // Timer overflow interrupt
15: ISR(TIM0_OVF_vect) {
16: timer_overflows++;
17: }
18: // Pin Change Interrupt (PCINT) for detecting edges on PB4
19: ISR(PCINT0_vect) {
20: static uint8_t last_state = 1;
21: uint8_t current_state = (PINB & (1 << GEIGER_PIN)) ? 1 : 0;
22: if (last_state == 1 && current_state == 0) { // Negative edge detected
23: uint32_t current_timer_value = ((uint32_t)timer_overflows << 8) | TCNT0;
24: if (edge_pending == 0) {
25: pending_timer_value = current_timer_value;
26: edge_pending = 1;
27: }
28: }
29: last_state = current_state;
30: }
31: // Safely process pending edges
32: void process_pending_edge() {
33: if (edge_pending) {
34: edge_pending = 0;
35: uint32_t interval = pending_timer_value - last_timer_value;
36: last_timer_value = pending_timer_value;
37: // Process interval
38: if (interval > 10 ) { // Valid range
39: if (interval != last_interval) { // Avoid noise
40: uint8_t random_bit = (interval > last_interval) ? 1 : 0;
41: last_interval = interval;
42: // Add bit to random byte
43: random_byte = (random_byte << 1) | random_bit;
44: bit_count++;
45: // If we have 8 bits, send the random byte
46: if (bit_count == 8) {
47: cli(); // Disable interrupts during serial print
48: uart_putu(random_byte);
49: uart_puts(",");
50: sei(); // Re-enable interrupts
51: bit_count = 0;
52: random_byte = 0;
53: }
54: }
55: }
56: }
57: }
58: // Initialize timer
59: void timer_initx() {
60: TCCR0B = (1 << CS00); // No prescaler
61: TIMSK0 = (1 << TOIE0); // Enable overflow interrupt
62: }
63: // Initialize Pin Change Interrupt for PB4
64: void pcint_init() {
65: GIMSK = (1 << PCIE); // Enable pin change interrupt
66: PCMSK = (1 << PCINT4); // Enable interrupt on PB4
67: sei(); // Enable global interrupts
68: }
69: int main() {
70: // Initialize peripherals
71: DDRB &= ~(1 << GEIGER_PIN); // Set GEIGER_PIN as input
72: // Set PB1 (TX) as output for bit-banging UART
73: DDRB |= (1 << PB1);
74: PORTB |= (1 << PB1); // Start with high state (idle state for UART)
75: DDRB &= ~(1 << PB4);
76: PORTB |= (1 << PB4);
77: timer_initx();
78: pcint_init();
79: while (1) {
80: process_pending_edge(); // Handle pending edges in the main loop
81: }
82: return 0;
83: }
If you want to compile use avr-gcc and following uart library (https://github.com/lpodkalicki/attiny13-software-uart-library). Hook a usb to serial connector to the pc and the rx pin to the PB1 on the attiny13. Include uart.h & uart.c from library and use following makefile
MCU=attiny13
FUSE_L=0x6A
FUSE_H=0xFF
F_CPU=1200000
CC=avr-gcc
LD=avr-ld
OBJCOPY=avr-objcopy
SIZE=avr-size
AVRDUDE=avrdude
CFLAGS=-std=c99 -Wall -g -Os -mmcu=${MCU} -DF_CPU=${F_CPU} -I. -I.. -DUART_RX_ENABLED -DUART_TX_ENABLED -DUART_BAUDRATE=19200
TARGET=main
SRCS=main.c uart.c
all:
${CC} ${CFLAGS} -o ${TARGET}.o ${SRCS}
${LD} -o ${TARGET}.elf ${TARGET}.o
${OBJCOPY} -j .text -j .data -O ihex ${TARGET}.o ${TARGET}.hex
${SIZE} -C --mcu=${MCU} ${TARGET}.elf
flash:
${AVRDUDE} -p ${MCU} -c usbasp -U flash:w:${TARGET}.hex:i -F -P usb
fuse:
$(AVRDUDE) -p ${MCU} -c usbasp -U hfuse:w:${FUSE_H}:m -U lfuse:w:${FUSE_L}:m
clean:
rm -f *.c~ *.o *.elf *.hex