Skip to content

Crystal Tester

Quartz crystals can be used to create very accurate, stable, and low-cost crystal oscillators. They are usually specified in terms of parts per million (ppm): a 1 ppm crystal of 1 MHz has a frequency deviation of ≈ 1 Hz. That’s like walking for 1 km and stopping to within exactly 1 mm.

I have many crystals, both salvaged and purchased. To verify their resonance frequency, I have to set up an oscillator circuit for them and then measure the output signal. How tedious …


Here is a stand-alone USB-powered crystal tester with built-in oscillator and OLED display:

crystal tester|300

There are header pins to plug the crystal in and it’ll measure its frequency. The accuracy is limited, due to the fact that 1) the µC’s own reference crystal is uncalibrated and 2) the crystal may not be operating in its intended load capacitance range. But the precision of this crystal tester is surprisingly high: the reported frequency will fluctuate only one or two hertz.

These specs are sufficient to quickly ascertain that a crystal is working and to see its operating frequency within 100 Hz or so. It’s also perfect for matching crystals to build a crystal filter.

This unit has been tested with crystals from 4 to 32 MHz. It won’t work with 32 kHz crystals, but does accept ceramic resonators. The total current draw is around 18 mA (from USB).

Materials and tools

• an STM32F103 µC board, such as the HyTiny shown here
• 128x32 OLED display with I2C interface, based on SSD1306
• 2N3904 NPN transistor or similar, and a few Rs and Cs
• a ferrite choke to filter out some power supply noise
• perf-board with copper pads or islands
• a way to upload firmware, e.g. an ST-Link or a BMP
• the software is written in C++, see stm32x/xtester
• PlatformIO is used as development environment

Oscillator circuit

The oscillator circuit used here is a Colpitts oscillator (image copied from this web page):


Component values were adjusted to place the output signal within the optimal 1.0 .. 2.0V range for triggering a GPIO pin, even at higher frequencies where the output swing is lower:

T1 = 2N3904
C1, C2 = 100 pF
R1 = 150 kΩ
R2 = 470 Ω
100 nF added: Vcc to GND

The circuit is powered from the 5V USB supply and current consumption is 2..3 mA.

Frequency measurement

The STM32F103 µC is an ARM Cortex M3 microcontroller, running at up to 72 MHz. It can capture digital frequencies up to 36 MHz using the TIM1 timer in “external clock mode 1”, which uses pin PA12 as input. There may be other modes, but this is the one I selected for now.

I use the JeeH library in many of my STM32 projects, which has its own way of talking directly (and very succinctly) to the hardware inside a µC. Here are the gory details:

constexpr uint32_t tim1  = 0x4001'2C00;
constexpr uint32_t cr1   = tim1 + 0x00;
constexpr uint32_t smcr  = tim1 + 0x08;
constexpr uint32_t cnt   = tim1 + 0x24;

PinA<12> fin;

void initCounter () {
    fin.mode(Pinmode::in_pullup);           // use PA12 as ETR input
    Periph::bit(Periph::rcc+0x18, 11) = 1;  // enable TIM1
    MMIO16(smcr) = (7<<4) | (7<<0);         // TS=ETRF SMS=ext-mode-1
    Periph::bit(cr1, 0) = 1;                // CEN

This does not generate any interrupts, it just enables the counter, which can be read out at any point in time using MMIO16(cnt) (this is again based on JeeH’s library support).

The problem with this counter is that it’s only 16-bit: it rolls over after 65,536 pulses have been counted. There are many ways to address this, such as generating an interrupt on overflow or chaining the overflow to a second hardware counter. But these all introduce complexity in the form of tricky edge cases (i.e. reading out hardware registers while an overflow is occurring).

Instead, I opted for a different solution: I read out the hardware counter once every millisecond, based on the periodic systick timer which every ARM µC has. And then use two key tricks:

  • the counter value is compared with the last readout - the difference is how many new counts have been detected, without having to reset any timer registers
  • for each millisecond in a second, i.e. 1000 values, this difference is stored in an array - and now the sum of these values will - by design - always correspond to the exact frequency

It would be inefficient to sum the array each time the display needs to be updated and it’s not necessary. The array-updating code can easily keep track of this total as it updates the slots:

for each sytick interrupt in millisecond slot N:
    currFreq -= msSlot[N]
    msSlot[N] = < new count >
    currFreq += msSlot[N]

Note that this code runs at systick interrupt time and since currFreq will change accordingly, it needs to be declared volatile. The benefits of this approach are that there is no locking involved at all, and that the currFreq variable is accurate down to 1 Hz at any point in time!

With this all happening in the background, the main loop becomes very simple: 1) get a value, 2) display it, and 3) sleep for ≈ 500 ms. That’s it - the complete app is under 100 lines long, uses a bit over 2 kB of RAM (for that msSlot array), and compiles to under 6 kB of code.


The initial tryout for the oscillator circuit was done on a solderless breadboard. It’s not ideal for high-frequency work because of stray capacitance of the contacts inside the breadboard, and the potential length of jumper wires. But nothing beats it in terms of experimentation and reconfiguration speed.


Sure enough, it works … but with this 25 MHz crystal, the output levels on the oscilloscope were not suitable to directly feed into a logic-level GPIO pin of the STM32F103 µC, even though it includes a Schmitt trigger to cleanly switch on what is essentially a non-digital signal.

Fiddling around with capacitors and resistor values for a while, I managed to get the emitter output swinging between 1.0V and 2.0V, which is - barely - enough to meet I/O pin input specifications at the µC’s 3.3V supply voltage. For most lower-frequency crystals, this output swing ends up being much larger, by the way. Ok, so the basic idea works.


With component choices made, the next step was to make the circuit more permanent. I quite like to use these single-sided perf-board PCBs with solder islands. It supports a particular build style where everything happens on top of the board, which - like Manhattan style construction - is very convenient, as you rarely have to flip the board over.

Also visible here are the “Swiss machined-pin” headers, used to create a socket for inserting crystals. They support lots of wire insertions, as long as the wires are not too thick (square pin headers will - at best - not fit, or worse: ruin the springs inside the tiny machined-pin headers.


Here is a close-up, showing all the components fitted very tightly together, with the layout dictated by the position of the copper strips. It worked out well this time, but such an overly cramped setup also makes it nearly impossible to replace any components.


And this is the near-final configuration, illustrating again how nice it is to have machined-pin headers in various key places: not only can jumpers be used to try things out, it also allows attaching oscilloscope probes.

Erratic readings

As initially constructed on a solderless breadboard with jumper wires, the circuit worked, but not with all crystals, and the frequency readout was very erratic. It improved quite a bit when things were soldered together with much shorter connections, but still … a bit jumpy.

The oscilloscope indicated that there was a lot of noise on the +5V supply line to the oscillator. It changed when using different USB cables and ports. There was clearly noise coming through the cable. While this often doesn’t affect digital circuits, such as the HyTiny µC in this case, it does tend to mess up the more analogue parts of a circuit. And in a way, an oscillator is as analogue as it gets: it needs very specific conditions to work, and it can be very sensitive to external variations of all kinds (for details on oscillation, see Wikipedia).


There’s an easy fix: a small inductor, in the form of a “ferrite bead”, as shown in the image. This one acts as 50 Ω at 100 MHz, yet is only 10 mΩ at DC.

It blocks HF noise but passes DC and is connected between the 5V supply and the oscillator power-in.

As a result, the frequency readout is now solid as a rock, for every 4..32 MHz crystal I’ve tried.

In HF-land, signals can act as a pretty wild combination of sine waves, it’s not a zero or a one!