Built-in project · Arduino Uno R3

Geiger counter on an Arduino Uno

A RadiationD-v1.1 Geiger counter board on an Uno: the clicks counted on an interrupt, a running minute of counts, and the dose rate in uSv/h on an I2C LCD. The whole build, an Arduino Uno and 2 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Beginner About 20 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $69. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • Counting pulses with an interrupt
  • Why radioactive counts are random
  • A rolling window built from buckets
  • Converting counts per minute to a dose rate
  • Showing readings on an I2C LCD
  • Difficulty: Beginner
  • Time: About 20 minutes
  • Board: Arduino Uno R3
  • Parts: 4

How it works

Each time a particle sets off the Geiger tube, the board pulls its output on pin 2 low for a moment. An interrupt on the falling edge adds one to a click counter, so no click is missed while the sketch does other things.

Decays are random, so one minute's count differs from the next. The sketch keeps a running minute made of twelve buckets of five seconds each. The counts per minute, CPM, are their sum. In the first minute it scales up what it has so far.

The dose rate is CPM times 0.00812037, shown in uSv/h on the LCD. Treat it as approximate.

Parts list

4 parts, plus the jumper wires. They retail for about $69; on Mokxi they're free.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
Geiger counter, RadiationD-v1.1 pin VINArduino Uno R3 pin 2
Geiger counter, RadiationD-v1.1 pin 5VArduino Uno R3 pin 5V
Character LCD with I2C backpack pin VCCArduino Uno R3 pin 5V
Geiger counter, RadiationD-v1.1 pin GNDArduino Uno R3 pin GND
Character LCD with I2C backpack pin GNDArduino Uno R3 pin GND
Character LCD with I2C backpack pin SDAArduino Uno R3 pin A4
Character LCD with I2C backpack pin SCLArduino Uno R3 pin A5

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// Geiger counter: the RadiationD-v1.1 (CAJOE) board with its J305 tube, and
// a 16x2 LCD on an I2C backpack.
//
//   pin 2    the board's VIN pin, which is really its pulse output
//   5 V, GND the board's 5V and GND pins
//   A4, A5   the LCD backpack's SDA and SCL (address 0x27)
//
// Each time a particle sets the tube off, the board pulls its output low for
// a moment (it sits high on a 47 k pull-up of its own the rest of the time),
// so an interrupt on the falling edge counts the clicks.
//
// Decays are random. Even with nothing changing, one minute's count differs
// from the next by about its own square root, so the sketch keeps a running
// minute: twelve five-second buckets, and the counts per minute (CPM) are
// their sum. For the first minute it scales up what it has so far.
//
// The dose rate is CPM times the tube's conversion factor. 0.00812037 uSv/h
// per CPM (123 CPM for 1 uSv/h) is the figure the guides for this board give
// for the J305. Treat it as approximate: it depends on the tube and on the
// kind of radiation. Background is about 0.1 to 0.2 uSv/h.

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

const int GEIGER = 2;
const float FACTOR = 0.00812037;  // uSv/h per CPM, J305
const int BUCKETS = 12;           // twelve buckets of five seconds
const unsigned long BUCKET_MS = 5000;

LiquidCrystal_I2C lcd(0x27, 16, 2);

volatile unsigned long clicks = 0;
unsigned long bucket[BUCKETS];
int filled = 0;
int at = 0;
unsigned long counted = 0;
unsigned long lastBucket = 0;

void onClick() {
  clicks++;
}

void setup() {
  Serial.begin(115200);
  Serial.println("Mokxi Arduino Uno: Geiger counter");
  pinMode(GEIGER, INPUT);
  attachInterrupt(digitalPinToInterrupt(GEIGER), onClick, FALLING);
  lcd.init();
  lcd.backlight();
  lcd.print("Geiger counter");
  lcd.setCursor(0, 1);
  lcd.print("counting...");
  lastBucket = millis();
}

void loop() {
  if (millis() - lastBucket < BUCKET_MS) return;
  lastBucket += BUCKET_MS;
  noInterrupts();
  unsigned long all = clicks;
  interrupts();
  bucket[at] = all - counted;
  counted = all;
  at = (at + 1) % BUCKETS;
  if (filled < BUCKETS) filled++;

  unsigned long sum = 0;
  for (int i = 0; i < filled; i++) sum += bucket[i];
  // Counts a minute: the sum over the buckets there are, scaled to a minute.
  unsigned long cpm = sum * BUCKETS / filled;
  float usvh = cpm * FACTOR;

  lcd.setCursor(0, 0);
  lcd.print("CPM ");
  lcd.print(cpm);
  lcd.print("          ");
  lcd.setCursor(0, 1);
  lcd.print(usvh, 2);
  lcd.print(" uSv/h       ");

  Serial.print("CPM: ");
  Serial.print(cpm);
  Serial.print("  uSv/h: ");
  Serial.print(usvh, 3);
  Serial.print("  total: ");
  Serial.println(all);
}

Code walkthrough

The key functions of the sketch, and what each one does.

onClick sketch.ino

The interrupt handler. It does one thing, add one to clicks. Keeping it this short is what makes interrupts safe. The variable is volatile because it changes outside the normal flow of the program.

void onClick() {
  clicks++;
}

setup sketch.ino

Sets pin 2 as an input and attaches onClick to its falling edge. It starts the LCD, shows a start-up message and records the time for the first five-second bucket.

void setup() {
  Serial.begin(115200);
  Serial.println("Mokxi Arduino Uno: Geiger counter");
  pinMode(GEIGER, INPUT);
  attachInterrupt(digitalPinToInterrupt(GEIGER), onClick, FALLING);
  lcd.init();
  lcd.backlight();
  lcd.print("Geiger counter");
  lcd.setCursor(0, 1);
  lcd.print("counting...");
  lastBucket = millis();
}

loop sketch.ino

Does nothing until five seconds have passed. Then it copies clicks with interrupts briefly off, stores the new clicks in the next bucket and sums the buckets. It scales that sum to a minute for CPM, multiplies by FACTOR for uSv/h and updates the LCD and serial monitor.

void loop() {
  if (millis() - lastBucket < BUCKET_MS) return;
  lastBucket += BUCKET_MS;
  noInterrupts();
  unsigned long all = clicks;
  interrupts();
  bucket[at] = all - counted;
  counted = all;
  at = (at + 1) % BUCKETS;
  if (filled < BUCKETS) filled++;

  unsigned long sum = 0;
  for (int i = 0; i < filled; i++) sum += bucket[i];
  // Counts a minute: the sum over the buckets there are, scaled to a minute.
  unsigned long cpm = sum * BUCKETS / filled;
  float usvh = cpm * FACTOR;

  lcd.setCursor(0, 0);
  lcd.print("CPM ");
  lcd.print(cpm);
  lcd.print("          ");
  lcd.setCursor(0, 1);
  lcd.print(usvh, 2);
  lcd.print(" uSv/h       ");

  Serial.print("CPM: ");
  Serial.print(cpm);
  Serial.print("  uSv/h: ");
  // ... 4 more lines in the editor

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • The reading jumps around in the first minute This project
    That is expected. The sketch is scaling up a few buckets. Wait a full minute for a steadier number.
  • The LCD stays blank This project
    Check SDA on A4 and SCL on A5, and that the backpack's address is 0x27, as in LiquidCrystal_I2C lcd(0x27, 16, 2).
  • The count is always zero, or counts at wrong times. Geiger counter
    VIN pulses LOW for each count. Use a pin interrupt on the falling edge, and count the number of pulses in a fixed time. Power the board with 5 V and join the grounds.
  • The counts per minute fluctuate a lot. Geiger counter
    Radiation counts are random, so short samples vary. Count for a minute or longer, or use a moving average. About 123 counts per minute is one uSv/h, so scale to match.
  • The screen stays blank or shows only a row of blocks. Character LCD with I2C backpack
    The I2C address is usually 0x27 or 0x3F. Run an I2C scanner to find yours. Then turn the contrast trimmer on the backpack slowly until text appears.
  • The backlight is on but there is no text, or text is garbled. Character LCD with I2C backpack
    Wire SDA and SCL to the right pins (A4 and A5 on an Uno), power it from 5 V, and call lcd.init() and the backlight call in setup.
  • Upload fails or nothing runs after wiring pins 0 and 1 to a module. Arduino Uno R3
    Pins 0 and 1 are the USB serial port. Move the module to other pins, or unplug it while uploading. Also check the right board is selected.
  • Pins misbehave or the board resets when you power motors or servos from it. Arduino Uno R3
    Each I/O pin gives about 20 mA, 40 mA max. Power motors and servos from their own supply and join its ground to the Uno's GND.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.