Digital Signal Generator
2nd July 2026
This is a wide-range digital signal generator that generates a crystal accurate square-wave of from 2Hz to approximately 33.5MHz in precise steps of 2Hz. You can enter an exact frequency using a numeric keypad, or adjust the frequency continuously using a rotary encoder, and the frequency is displayed on an OLED display:

The Digital Signal Generator, based on an AVR16EB28, generates a square wave of from 2Hz
to approximately 33.5MHz in precise steps of 2Hz.
The project takes advantage of the new Numerical Controller Oscillator (NCO) mode available in the timer/counter TCF in the AVR EB-series microcontrollers. It uses an AVR16EB28, a crystal, an OLED display, a rotary encoder, a keypad, and a small number of other components.
Introduction
When Microchip released their AVR EB-series chips at the end of 2023 I was intrigued by one particular feature: the timer/counter TCF that works differently from all previous AVR timer/counters. Typically timer/counters divide the clock by a divisor in a compare register. This gives a range of frequencies with unequal steps. For example, with divisors of 1, 2, 3, 4 … a 20MHz clock gives you 20MHz, 10MHz, 6.67MHz, 5MHz … where the steps are getting progressively smaller.
The TCF timer/counter, by comparison, provides a mode called Numerical Controller Oscillator (NCO) which adds a value from a register to the count value on each clock cycle, and takes the output from the overflow [1]. For values of 1, 2, 3, 4 … and a 24-bit counter a 20MHz clock gives you frequencies of 1.192Hz, 2.384Hz, 3.576Hz, 4.768Hz … where the steps are equal.
Phase-Locked Loop (PLL)
Another feature of the AVR EB series is a PLL (Phase-Locked Loop) that can be used to multiply a clock of 2.2MHz to 5.5MHz by a factor of 8 or 16 to drive the TCF timer/counter with up to 80MHz, way beyond the processor's maximum 20MHz clock rate. These two features of the AVR EB series led to the idea of this simple wide-range digital signal generator.
Crystal
I wanted to generate frequencies with round numbers of Hz, and better accuracy than the processor's built-in oscillator, so I decided to use a crystal clock. I calculated that the value of crystal clock that would be needed to give round-number frequencies is 222Hz or 4.194304MHz, and I was pleased to find that this is a commonly available value.
The NCO oscillator can either produce an asymmetrical square-wave output with a pulse width of one clock cycle, or a symmetrical square wave of half the frequency; I chose the second option.
The frequency range
In this mode the output frequency f in Hz is given by:
where clk is the clock input to the NCO and inc is the increment value in the compare register. With a crystal of 222Hz and a x16 PLL this becomes:
Because inc can take the values 1 to 224–1 the result is a range of 2Hz to 33554430Hz in steps of exactly 2Hz. For alternative options see Further suggestions below.
Operation
The Digital Signal Generator works as follows: you enter the frequency you want, in Hz, on the numeric keypad as a sequence of up to eight digits, and press *. The display then shows the actual frequency that will be generated, and generates the square waves on the outputs. You can then adjust the frequency up or down in steps of 2Hz using the rotary encoder. Pressing # clears the current input.
The waveform outputs are taken from pins 1 and 28 on the AVR16EB14. For example, here are the two waveforms generated for a frequency of 100000Hz:

Jitter
I should mention one characteristic of the NCO which is that the output will have jitter when the value being added to the counter isn't an exact divisor of the maximum count. For example, with a 24-bit counter and an increment value of 3, which is not an exact divisor of 224, a 20MHz clock will give a square wave with a wavelength alternating between 0.419 secs and 0.210 secs. However, this is the worst case, and at higher frequencies the jitter rapidly becomes a negligible proportion of the wavelength. Note that, despite the jitter, the average frequency of the output is precise, and so even at low frequencies the jitter is not a problem for applications such as timing or clock generation.
The circuit
Here's the circuit of the Digital Signal Generator:

The circuit of the Digital Signal Generator, based on an AVR16EB28.
The processor
I initially thought about designing the circuit based on a 14-pin AVR16EB14, but this has the limitation that the timer/counter waveform output WO0 is on the same pin as the EXTCLK external clock pin needed for the input to the PLL. A 20-pin package would solve this problem, but I went to the 28-pin AVR16EB28 because it's available in a DIP package, and so could be fitted on the breadboard without needing a breakout board. The extra I/O pins might come in useful in a future expansion. You could of course also use the AVR32EB28.
The display
For the display I used an I2C 128x32 0.91" OLED display, available at low cost from a variety of vendors on AliExpress [2]. Note that the display is 3.3V only, so don't power the circuit from 5V.
Numeric keypad
For the numeric keypad I used the PCB keypad I designed in an earlier article; see One-Input Numeric Keypad. It fits neatly on the breadboard, and connects to a single analogue input, PD6. Alternatively you could build the keypad on a piece of Veroboard.
The crystal
The crystal I used is a 4.194304MHz through-hole type with a 0.2" lead spacing, an accuracy of 20ppm, and a load capacitance of 18pF [3]. To calculate the capacitor values I used the formula C = 2(CL - CS), where CL is the published load capacitance 18pF, and CS is the stray capacitance. On a breadboard there is probably close to 18pF of stray capacitance, so additional capacitors are probably not necessary, but if you're designing a PCB version of the circuit you should leave space for the capacitors.
Unlike the earlier ATmega processors the new AVR series don't support an external high-frequency crystal, so it's necessary to provide an external crystal oscillator with its output connected to the EXTCLK input. One possible crystal oscillator design uses an inverter across the crystal:

To avoid the need for an external inverter I used the Event System to construct an inverter as follows:
- Define PC2 as an event output, EVOUTC.
- Define PC3 as an event generator on channel 2
- Configure PC3 to invert the input.
The crystal is connected between PC3 and PC2, and the inverter output PC2 is then connected to the EXTCLK input on PA0.
Rotary encoder
For the rotary encoder I used a Bourns rotary encoder with 16 pulses per rotation [4] because the wire terminations fit neatly into a breadboard, but you could use any suitable rotary encoder, such as Adafruit's which includes a knob [5].
Construction
I built the whole circuit on a 360 hole breadboard [6], using pre-cut colour-coded jumper wires to make the interconnections easier [7].
I constructed a right-angle adapter from a 4-way pin header to mount the display horizontally, while plugging it into four consecutive breadboard columns; see the photograph at the start of the article.
The program
Here's a description of the main parts of the program:
Crystal oscillator
The routine SetupCrystal() uses the Event System to configure PC3 and PC2 as an inverter, with the crystal connected across it:
void SetupCrystal () {
PORTC.EVGENCTRLA = PORT_EVGEN0SEL_PIN3_gc; // Use PC3 as EVGEN0
EVSYS.CHANNEL2 = EVSYS_CHANNEL_PORTC_EV0_gc; // Link EVGEN0 to CHANNEL2
EVSYS.USEREVSYSEVOUTC = EVSYS_USER_CHANNEL2_gc; // Link CHANNEL2 to PC2
PORTC.PIN3CTRL = PORT_INVEN_bm; // Invert input PC3
}
The output on PC2 is connected to the input of the Phase-Locked Loop at EXTCLK on PA0.
Phase-Locked Loop (PLL)
The routine SetupPLL() sets up the PLL to multiply the frequency on EXTCLK by 16:
void SetupPLL () {
uint8_t pllctrla = CLKCTRL_SOURCE_EXTCLK_gc | CLKCTRL_MULFAC_16X_gc;
_PROTECTED_WRITE(CLKCTRL.PLLCTRLA, pllctrla); // EXTCLK and x16
}
The built-in macro _PROTECTED_WRITE() must be used because the Clock Controller registers are protected from being changed by a runaway program using Configuration Change Protection.
Numerically Controlled Oscillator (NCO)
The next step is to set up the NCO in the routine SetupNCO(). I found the Microchip application note TB3341 [8] helpful in writing this part of the program:
void SetupNCO () {
PORTMUX.TCFROUTEA = PORTMUX_TCF0_ALT1_gc; // WO0,WO1 on PA6,PA7
// Enable WO0 and WO1 and invert WO1 polarity
while((TCF0.STATUS & TCF_CTRLABUSY_bm) != 0);
TCF0.CTRLC = TCF_WO0EN_bm | TCF_WO1EN_bm | TCF_WO1POL_bm;
// Configure NCO Pulse-Frequency mode
TCF0.CTRLB = TCF_CLKSEL_PLL_gc | TCF_WGMODE_NCOFDC_gc; // Use PLL as clock
}
This first selects the alternative positions for the waveform outputs WO0 and WO1 because the default positions conflict with EXTCLK.
It then enables the waveform outputs and inverts WO1, to give two out of phase square waves.
Finally it selects NCO Pulse-Frequency mode, which gives a symmetrical square wave, and specifies the PLL as the clock source.
Setting the frequency
The function SetFrequency() is called each time the frequency changes:
void SetFrequency () {
while((TCF0.STATUS & TCF_CMP0BUSY_bm) != 0);
TCF0.CMP = Input>>1;
}
Starting and stopping the NCO
Finally, these two functions are called to start and stop the square-wave output:
void StartNCO () {
while((TCF0.STATUS & TCF_CTRLABUSY_bm) != 0);
TCF0.CTRLA |= TCF_ENABLE_bm;
}
void StopNCO () {
while((TCF0.STATUS & TCF_CTRLABUSY_bm) != 0);
TCF0.CTRLA &= ~TCF_ENABLE_bm;
}
Numeric keypad
The numeric keypad is read by the function ReadKeypad(), which returns the number of the key, or -1 if no key is being pressed. For more details see One-Input Numeric Keypad.
Display driver
The functions for driving the 128x32 OLED display are based on earlier projects. The frequency is plotted using double-sized characters, using smoothing as described in Smooth Big Text.
Rotary encoder
The rotary encoder uses a pin-change interrupt to update the frequency, based on my earlier project Bounce-Free Rotary Encoder.
Main loop
Finally the main loop polls the numeric keypad and updates the display until * is pressed, and then sets the output frequency to the entered value.
Installing the program
First install Spence Konde's DxCore from GitHub: see DxCore - Installation. I used version 1.6.2.
Then, in the Arduino IDE:
- Choose the AVR EB-series (no bootloader) option under the DxCore heading on the Board menu.
- Check that the subsequent options are set as follows (ignore any other options):
Board: "AVR EB-series (no bootloader)"
Chip: "AVR16EB28"
Clock Speed: "20 MHz internal"
You can leave the other options at their defaults, the first option on each submenu.
- Connect a UPDI programmer to the UPDI, GND, and VDD pins.
The recommended way to upload the program is to use a 3.3V Serial board, such as the SparkFun FTDI Basic board [9] connected with a Schottky diode as follows. You can substitute a 4.7kΩ resistor for the Schottky diode:

- Set Programmer to the "SerialUPDI - Normal: 230400 baud (recommended)" option.
- Select the USB port corresponding to the USB to Serial board in the Port menu.
- Select Burn Bootloader to reset the fuses (in case they were changed for an earlier project).
- Click Upload to upload the program to the AVR16EB28.
Resources
Here's the program for the Digital Signal Generator: Digital Signal Generator Program.
Get details of the One-Input Numeric Keypad here: One-Input Numeric Keypad.
Further suggestions
- If you prefer an output adjustable in steps of exactly 1Hz you can use the 4.194304MHz crystal divided by 2 to give a range of 1Hz to 16777215Hz in steps of 1Hz, with a symmetrical square-wave output. Note that the AVR EB-series Errata states that in NCO mode the prescaler in TCF0 CTRLA is non-functional, but you can divide the input to the PLL by 2 instead.
- If you want a higher maximum frequency you can change the NCO mode from NCOFDC (fixed duty-cycle) to NCOPF (pulse-frequency) to give a range of 4Hz to 67108860Hz in steps of 4Hz with an asymmetrical square-wave output one clock period wide.
- If your rotary encoder includes a switch you could add a pin-change interrupt on PD2 to toggle between these options, or to change the frequency step size of the rotary encoder.
- ^ This is like the phase accumulator used in DDS (Direct Digital Synthesis), except that the TCF timer/counter only has a one-bit output.
- ^ 0.91" 128x32 OLED Display Module White on AliExpress.
- ^ QCL4.194304F18B23B on Farnell.
- ^ Bourns Incremental Encoder on Halltronics.
- ^ Rotary Encoder + Extras on Adafruit.
- ^ AD-100 Advanced Solderless Breadboard on Rapid Electronics.
- ^ Jumper Wire Kit on The Pi Hut.
- ^ Getting Started with Timer/Counter Type F (TCF) on Microchip.
- ^ SparkFun FTDI Basic Breakout - 3.3V on Sparkfun.
blog comments powered by Disqus
