If this project helped you, consider supporting future free uploads 🙏
Low Cost DIY Geiger Counter with SI3BG Tube ATmega168PA and TM1638 Display

In this project, I designed and built a compact Geiger-Müller radiation detector using the SI3BG GM tube and a custom PCB. The device is powered by three AA batteries and features a 4-digit 7-segment LED display driven by a TM1638 controller.
The high voltage for the GM tube is generated using a RadiationD-V1.1 style boost and multiplier circuit based on the NE555 timer and MMBTA42 transistor. The output voltage is adjustable and calibrated to operate the SI3BG tube in its optimal plateau region (~400–450V).
The microcontroller is an ATmega168PA (TQFP-32), fully compatible with ATmega328P. Pulse counting is handled using the INT0 external interrupt to ensure accurate event detection even during sleep mode.
The device supports:
- CPM (Counts Per Minute) display
- µSv/h display with automatic decimal formatting
- 10-minute background calibration mode (via jumper)
- EEPROM storage of calibration values
- Automatic sleep after 5 minutes of inactivity
- Wake-up via push button
- Support for both Common Cathode and Common Anode 7-segment displays
All PCB files (Gerbers, KiCad project, schematics) and source code are provided.
This design aims to be:
- Compact
- Low power
- Electrically safe (proper creepage in HV area)
- Flexible for different display types
#define DISPLAY_COMMON_ANODE 1 // 1 = Common Anode (SEG/GRID swapped)
// 0 = Common Cathode (standard)SOURCE CODE
/* =========================================================== DIY Geiger Counter – SI3BG + ATmega168PA + TM1638 =========================================================== Features: - Counts Geiger pulses via INT0 (PD2) - Displays CPM or µSv/h - 10 minute calibration mode (JP2 closed at boot) - Stores background CPM in EEPROM - Auto sleep after 5 minutes - Wake on button press - Supports Common Cathode AND Common Anode 7-segment displays Display type selection: DISPLAY_COMMON_ANODE = 0 → Common Cathode DISPLAY_COMMON_ANODE = 1 → Common Anode (SEG/GRID swapped) Author: [Your Name] =========================================================== */ #define DISPLAY_COMMON_ANODE 1 #include#include #include #include // ---------------- PIN DEFINITIONS ---------------- #define PIN_PULSE 2 // PD2 / INT0 #define PIN_BUTTON 3 // PD3 / INT1 #define PIN_STB 4 // TM1638 STB #define PIN_CLK 5 // TM1638 CLK #define PIN_DIO 6 // TM1638 DIO #define PIN_CAL A0 // Calibration jumper // ---------------- CONSTANTS ---------------- #define CAL_TIME_SECONDS 600 #define SLEEP_TIMEOUT 300 #define CPM_WINDOW 10 // µSv conversion factor (0.005 µSv/h per CPM default) #define K_X1000 5 // ---------------- GLOBAL VARIABLES ---------------- volatile uint16_t pulses_1s = 0; volatile bool wdt_flag = false; volatile bool button_flag = false; uint32_t seconds = 0; uint32_t cpm = 0; uint16_t bg_cpm = 0; enum Mode { MODE_CPM, MODE_USV }; Mode displayMode = MODE_CPM; // ---------------- INTERRUPTS ---------------- void pulseISR() { pulses_1s++; } void buttonISR() { button_flag = true; } ISR(WDT_vect) { wdt_flag = true; } // ---------------- WDT SETUP ---------------- void setupWDT() { cli(); MCUSR &= ~(1< >=1; } } void tmCommand(uint8_t cmd){ digitalWrite(PIN_STB,LOW); tmWrite(cmd); digitalWrite(PIN_STB,HIGH); } // ---------------- DISPLAY DRIVER ---------------- const uint8_t digits[10]={ 0x3F,0x06,0x5B,0x4F,0x66, 0x6D,0x7D,0x07,0x7F,0x6F}; void display4(uint8_t seg[4]){ #if DISPLAY_COMMON_ANODE uint8_t grid[8]={0}; for(int s=0;s<8;s++){ for(int d=0;d<4;d++){ if(seg[d]&(1< 9999){ for(int i=0;i<4;i++) out[i]=0x40; } else{ for(int i=3;i>=0;i--){ out[i]=digits[value%10]; value/=10; } } display4(out); } void showUSV(uint32_t cpm){ uint32_t usv_x100 = (cpm>K_X1000)? (cpm*K_X1000)/10 : 0; uint8_t out[4]={0}; uint16_t v=usv_x100; out[3]=digits[v%10]; v/=10; out[2]=digits[v%10]|0x80; v/=10; out[1]=digits[v%10]; v/=10; out[0]=digits[v%10]; display4(out); } // ---------------- CALIBRATION ---------------- void calibration(){ uint32_t total=0; for(int i=0;i=CPM_WINDOW){ cpm=sum*6; sum=0; window=0; } } if(button_flag){ button_flag=false; displayMode = (displayMode==MODE_CPM)?MODE_USV:MODE_CPM; } if(displayMode==MODE_CPM) showCPM(cpm); else showUSV(cpm); }
1. Interrupts
-
pulseISR()counts every Geiger pulse. -
buttonISR()handles toggle and wake. -
WDT_vectcreates a precise 1-second time base.
2. Watchdog Timer
Configured in interrupt mode (~1 second).
Used for:
-
CPM calculation window
-
Calibration timing
-
Sleep timing
3. Display Driver
Two modes:
-
Common Cathode: direct segment write.
-
Common Anode: converts digit segment data into segment-wise digit masks (SEG/GRID swap logic).
This allows a single firmware for both display types.
4. CPM Calculation
-
Counts pulses in 10-second window.
-
Multiplies by 6 to compute CPM.
5. µSv/h Calculation
Uses fixed-point math:
µSv/h ×100 = CPM × K / 10No floating-point used → lower flash usage.
6. Calibration Mode
If jumper is closed at boot:
-
Measures for 10 minutes
-
Calculates background CPM
-
Stores in EEPROM
No firmware re-upload required.
7. Sleep Mode
After 5 minutes inactivity:
-
Display off
-
MCU power-down
-
Wake on button interrupt