• Firmware update and future plans

    BleakyTex03/20/2023 at 16:54 0 comments

    I am finally able to post the firmware, it has been rewritten in full assembly and executes from RAM. As I expected, the device now consumes around 17-18 uA which means around 11 months of battery life, maybe even 12, since the previous firmware version lasted for 5 months instead of expected 4 months. However, I'm starting to have bigger plans for it. I realized that there is a lot of unused flash memory that can be used to log radioactivity data and display it via PC interface. Also check the drawing of a custom LCD that the final version may get:

    There also will be a major update to the HV booster to get as much efficiency as possible with this form factor but it requires a custom transformer, and it will take some time to design and make. Two years of battery life is the new goal!

  • A small update

    BleakyTex01/27/2023 at 19:18 0 comments

    It's been 4 months and this thing is still running fine. The battery voltage is 2.2V, so I'm expecting it to run for another month or two. Also the code has been rewritten in assembly and now executes from RAM, though it still needs some polishing, I'll post the details in the next update. Disabling flash reduced the consumption by 25 uA and now the entire device consumes 19 uA, which means 11 months of battery life. I've also managed to find some better parts which should reduce the PCB size and power consumption even further. My current goal is 1 year of non-stop operation.

  • First prototype

    BleakyTex10/11/2022 at 18:22 0 comments

    It's working!

    There are only two things left to implement — LCD display functions and basic control functionality: sound switch, backlight, buttons for switching display mode and resetting the dose. I don't see much point in explaining how it works, let's get to the results instead. The total current consumption when counter is idling is 44 µA, which is low enough to avoid the battery capacitance derating. Given that the speaker is disabled, and battery capacitance is 150 mAh (the device stops working at 1.8V, so we can have individual cells discharged to 0.9V, utilizing their full capacity), this gives us a total working time of 3409 hours or 142 days or 4.7 months. This is a best-case number, so I've rounded it up to 4 months. I've left the device turned-on and will post the actual number of days it managed to last. At the moment of writing this, it has been working for 12 days non-stop without any problem and the battery loses around 8mV per day. 

    I'm happy with the code performance too. The whole update routine (radiation rate, dose calculations, time update and LCD print function) takes only 16 ms or 524 clock cycles. We save some power because the CPU spends so little time in the active mode. A friend of mine decided to rewrite the firmware in full assembly, optimizing it even further. I will post an update if he will actually do it.

    Here's some photos and a video of a finished device. It was designed with an enclosure in mind but apparently, I won't be able to make it any time soon.

    Source code and schematics are available on my Github.

  • Data processing

    BleakyTex10/09/2022 at 00:24 0 comments

    Setting the update interval

    To save power, I've decided to update the display once in 5 seconds. Dose rate is a statistical value, so there's not much point to update it faster. Between updates we can stop the CPU, since the boost converter and the radiation event counter work independently of it. We have one last timer to spare, and we're going to use it to generate CPU wakeup interrupts every 5 seconds. After wakeup, we'll update the dose rate and total dose values and put the CPU back to sleep until the next interrupt. First, we should set up the timer:  

      /* TIM4 SETUP (5-SEC INTERRUPTS) */
      CLK_PCKENR1_bit.PCKEN12 = 1; // Enable TIM4 clock
      TIM4_PSCR_bit.PSC = 0xE;     // Div by 16384 -> 0.5 sec resolution
      TIM4_ARR = 9;                // 5-second period (sec =  + 1) / 2)
      TIM4_EGR_bit.UG = 1;         // Update timer registers to apply prescaler value
      TIM4_CR1_bit.CEN = 1;        // Start the timer
    
      TIM4_IER_bit.UIE = 1;        // Enable TIM4_UIF interrupt (timer overflow)
      WFE_CR3_bit.TIM4_EV  = 1;    // TIM4 interrupts can wake the CPU from Wait mode

    And then we just call the update function when the CPU is woken up by interrupt from TIM4 and then put it back into Wait mode after the function is executed: 

    int main(void)
    {
    ...
      FLASH_CR1_bit.WAITM = 1;   // Disable Flash memory during Wait mode
      
      while(1)
      {
        if (TIM4_SR1_bit.UIF == 1) {
          TIM4_SR1_bit.UIF = 0;  // Reset interrupt flag
          app_readGeiger();
        }
        
        asm("WFE");  // Enter Wait mode
      }
    }

    Notice that this happens in the main loop, there is no interrupt handler. Since there's nothing time-critical happening here, we don't need them, we'll just waste precious CPU cycles to enter and exit an interrupt. 


    Reading the event counter and applying dead-time correction

    The challenge with reading the value from the timer is that the event may occur during reading process, causing rollover or missed readings. Also, we should apply dead-time correction to the value we've read because at high counting rates, a Geiger tube may not have enough time to recover to register pulses occurring at quick succession, which leads to reduced measured activity. The equation for the dead-time correction is:

    where N ⁠— true counting rate (in events per second), n ⁠— measured counting rate, ⁠Td — dead time of the tube. This equation is meant for 1-sec measurement intervals, and we're measuring with 5-sec intervals. This will reduce the dead-time effect by 5. The dead time of SBM-20 tube is 190µS, so the effective dead time will be 190/5 = 38µS:

    This equation involves floating-point numbers and will be too challenging to calculate for an 8-bit microcontroller, running at 32.768 kHz. We can simplify it with a polynomial approximation:

    Now we can calculate N only using integer numbers. Before we get to coding, let's see how accurate our approximation is. For this, I'll plot the original equation and its approximation for n = 0 to 1400 CPS, which is the maximum counting rate for SBM-20.
    Red line — original equation, Blue line — approximation, Green line — deviation in %

    The deviation increases with n, and at 1400 CPS the approximation gives a value, that's 1.15% higher than the original equation gives. Not too much accuracy to lose, given a massive performance gain that we get from this approximation, isn't it? Let's write the code. Since we need to read and clear the timer as fast as possible, I'll use assembly to do this. And while I'm at it, I'll do the dead time correction in assembly as well. You can check the STM8 programming reference (PM0044) if something isn't clear.

    uint16_t cp5s;                 // Registered events during 5-sec interval
    
    void app_readGeiger(void) 
    {
      // Read and clear TIM3 counter
      // TIM3_CNTRH must be read first to prevent rollover
      asm("LD   A, TIM3_CNTRH");
      asm("CLR  TIM3_CNTRH");
      asm("PUSH TIM3_CNTRL");
      asm("CLR  TIM3_CNTRL");
      asm("PUSH A");
      asm("LDW  X, (0x01, SP)"); // now both stack and register X hold cp5s value
      
      // Apply dead time correction: cp5s...
    Read more »

  • Counting radiation events

    BleakyTex10/07/2022 at 01:49 0 comments

    Reading signals from a Geiger tube

    There are two ways to read the signal — from the anode and from the cathode of the tube:

    Two types of Geiger tube readout circuits (source: CN-0536 by Analog Devices)

    I'll be reading signals from the anode because it's more convenient to route the PCB this way and this approach provides more immunity to electrical noise. In case of anode readout, the main goal is to reduce the pulse amplitude, so it doesn't destroy the MCU. The schematic is as follows:

    C1 and C2 act as a capacitive divider, attenuating pulse amplitude. C1 must be rated for more than 400V, I'm using a 2kV rated one. R1 is just a pull-up resistor: because the pulse is negative going, the idle voltage level should be high. I am deliberately not using MCU's internal pull-up resistor here because its value is too low, and it will shorten the pulse too much because of that. The diodes are used to clip the pulse amplitude to a safe level. Let's see how the output pulse will look like when a Geiger tube registers a radiation event:

    The output pulse swings from supply to ground with some overshoot due to voltage drop in diodes. High amplitude of the pulse allows us to use a digital input to register it.


    Making loud clicks

    I would like to be able to hear when a radiation event is registered, and I want it loud. There are two types of sound transducers — magnetic and piezoelectric. Magnetic ones require high current, so they're out of the question, also they're not that loud compared to piezoelectric. The problem with piezoelectric transducers is that they require high voltage to produce a loud sound, so we need the means to generate high voltage pulses. Here's what I came up with:

    This circuit works similar to a boost converter. When the transistor is open, the inductor starts building up current and the remaining charge in the transducer is removed to increase voltage swing across it (electrically they behave like capacitors, so they can hold some charge). When the transistor closes, the energy that the inductor had at that time, is transferred into transducer's capacitance and it stays there until the next cycle. I've created a simulation if you want to take a closer look at the circuit's operation.

    I'm using a KPT-G1210 transducer, it's rated for 30V, so let's generate voltage somewhere in this vicinity. As I said before, the energy EL held by an inductor is transferred into transducer's capacitance:

    where:  

    • Et — energy, held by transducer's capacitance
    • L — inductance value
    • IL(pk) — peak inductor current
    • Ct — transducer's capacitance
    • Vt — voltage that the transducer will build up 

    From the formula above we get:

    where

    In this case, Ra is the sum of inductor's series resistance and MOSFET's drain-source resistance. We can control two parameters in this circuit — the inductor value and its charging time tON. I've selected a 33mH inductor to reduce its peak current; LR44 battery has high ESR, so if we pull too much current from it, we may get brownouts at low battery levels. This leaves us only with the tON to figure.

    We'll be using a TIM2 timer in the MCU to generate pulses. It has a one-shot mode and an external trigger, so we'll set it to produce a single pulse of fixed length when its trigger input TIM2_ETR detects a falling edge from the Geiger tube. I've added this code to the TIM_Init() function:

      /* TIM2 SETUP (ONE-PULSE MODE, EXT. TRIGGER) */
      PB_DDR_bit.DDR2 = 1;         // Set PB2 as output (TIM2_CH2)
      PB_CR1_bit.C12 = 1;          // Set PB2 as push-pull
      CLK_PCKENR1_bit.PCKEN10 = 1; // Enable TIM2 clock
      TIM2_ARRH = 0;
      TIM2_ARRL = 6;           // Pulse length = 30.52uS * TIM2_ARRL
      TIM2_CCR2H = 0;
      TIM2_CCR2L = 1;          
      TIM2_CCMR2_bit.OC2M = 7; // PWM mode 2
      TIM2_CR1_bit.OPM  = 1;   // One-pulse mode
      
      TIM2_SMCR_bit.TS  = 7;   // Trigger source - external
      TIM2_ETR_bit.ETP  = 1;   // Trigger on falling edge
      TIM2_SMCR_bit.SMS = 6;   //...
    Read more »

  • Boosting voltage

    BleakyTex10/03/2022 at 16:19 0 comments

    Towards efficient boost converter architecture

    Most DIY Geiger counters that I've seen on the Internet have very simple architecture: just an oscillator driving a transistor, sometimes with Zener diodes to limit the output voltage. This approach is inefficient, so I've decided to make a boost converter that's both efficient and compact. The first thing that comes to mind is that the converter has to disable the oscillator when it's not needed. This approach is described in an appnote by Maxim Integrated:

    Here they're using a comparator to control the oscillator output: when the feedback voltage is less than the reference voltage the oscillator is enabled, and vice versa. Since a Geiger tube only consumes energy when hit by a particle, the oscillator shall remain idle most of the time, and that saves a lot of power. Also there's a 6-stage voltage multiplier, so we can feed the lower output voltage from its first stage to a feedback divider, thus minimizing the current through it. I'll be reducing losses in the feedback divider as much as possible since it consumes the most power in the circuit above. 


    Setting up the microcontroller

    One huge benefit of microcontrollers is that they have peripherals that can replace discrete components, saving power and PCB space. In our case we can use a microcontroller to replace a voltage reference and an oscillator in the circuit above (and even a comparator if we wouldn't have to drive a 4-backplane LCD)

    We should set the MCU to output a reference voltage and a comparator-controlled PWM: when the comparator output is LOW (voltage divider voltage less than reference voltage), the timer generates PWM, otherwise it sets its output to LOW, closing the transistor. The only timer that we can use is TIM1 because pins, tied to other timers are needed for other functions. This timer has a break input which disables timer's output when it receives a signal, which is exactly what we need. The schematic is as follows:

    To use a lower value inductor for better energy and space efficiency, we'll set the PWM generation for shortest positive pulse possible, i.e. highest frequency and lowest duty cycle; the best we can get with a timer is half the system clock. The MCU is clocked from 32.768 kHz crystal, which means 50% duty cycle PWM at 16.384 kHz. 

    To write the code I'll be using the IAR Embedded Workbench for STM8.

    #include "iostm8l152k4.h"
    
    void Clock_Init(void)
    {
      CLK_SWCR_bit.SWEN = 1;         // Prepare to change the clock source
      CLK_SWR = 0x08;                // Change the clock source to LSE
      while(CLK_SWCR_bit.SWBSY==1);  // Wait until it starts up
      CLK_CKDIVR = 0x00;             // Change the clock prescaler from default /8 to /1
      CLK_ICKCR_bit.HSION = 0;       
      CLK_ICKCR_bit.LSION = 0;       // Disable internal oscillators
    }
    
    void VREF_Init(void)
    {
      while(!PWR_CSR2_bit.VREFINTF); // Wait for voltage reference to stabilize
      
      CLK_PCKENR2_bit.PCKEN25 = 1; // Enable clock for COMP peripheral
      COMP_CSR5_bit.VREFTRIG  = 2; // Disable Schmitt trigger on PD7 to reduce leakage
      COMP_CSR3_bit.VREFOUTEN = 1; // Enable VREF output
      RI_IOSR2_bit.CH8E = 1;       // Connect VREFOUT to PD7
      CLK_PCKENR2_bit.PCKEN25 = 0; // Disable clock for COMP, was only needed for init
    }
    
    void TIM_Init(void)
    {
      /* TIM1 SETUP (SYSCLK/2 GATED BY EXTERNAL SIGNAL) */
      PD_DDR_bit.DDR4 = 1;         // Set PD4 to output mode
      PD_CR1_bit.C14 = 1;          // Set PD4 output to push-pull mode
      CLK_PCKENR2_bit.PCKEN21 = 1; // Enable TIM1 clock
      
      TIM1_ARRH = 0; 
      TIM1_ARRL = 1;        // Highest frequency (16.384 kHz)
      TIM1_CCR2H = 0; 
      TIM1_CCR2L = 1;       // Lowest duty cycle (50%)
      
      TIM1_BKR_bit.BKE = 1;      // Enable output break
      TIM1_CCMR2_bit.OC2M = 0x6; // Set output mode: PWM mode 1
      TIM1_CR1_bit.CEN = 1;      // Enable timer counter
      TIM1_CCER1_bit.CC2E = 1;   // Enable TIM1_CH2 output
      TIM1_BKR_bit.AOE = 1;      // Automatically recover from output break
    }
    
    int main(void)
    {
      Clock_Init();
      VREF_Init();
      TIM_Init();
    }

    The beauty of this approach is that it's completely...

    Read more »