Build log: a DDS sine synth, from PWM to a clean sine on a 4-channel scope
· Ohmeris team
This is one of three demo circuits we built to show the Ohmeris bench with a real microcontroller program in the loop. An ATmega328P makes a 500 Hz sine wave out of nothing but a PWM pin, two RC filters and one transistor. Everything here is simulated. No board was built.
The idea: direct digital synthesis, the cheap way
A microcontroller pin can only be high or low. If you switch it fast enough and change the duty cycle smoothly, the average voltage traces whatever shape you want. A low-pass filter removes the switching and leaves the average. That is PWM-based direct digital synthesis (DDS).
The firmware (docs/qa/marketing-demos-2026-10-24/sketches/pwm_synth/pwm_synth.ino) does three things:
- Fast PWM on Timer1. Timer1 runs in 8-bit fast PWM mode with no prescaler. 16 MHz divided by 256 gives a 62.5 kHz carrier on OC1A (PB1, Arduino pin 9).
- A phase accumulator. On every timer overflow, an interrupt adds a fixed step to a 32-bit phase value. The step is
500 × 2³² / 62,500, so the phase wraps 500 times a second. That gives 125 PWM periods per sine cycle. - A sine table. The top 8 bits of the phase select one of 256 table entries. Each entry is
128 + 80·sin(angle). The duty cycle swings between about 48/256 and 208/256 instead of the full range. That keeps the waveform away from the rails and leaves headroom for the next stage.
A 20×4 character LCD on I²C (address 0x27) shows the settings and puts a second bus on the chip.
The analog path: two RC stages and a follower
The PWM pin feeds two identical RC low-pass stages, 1 kΩ and 22 nF each. One stage on its own has a corner of about 7.2 kHz (1 / 2π·1k·22n). That is far above 500 Hz, so the sine passes almost untouched. It is also well below 62.5 kHz, so each stage cuts the carrier by roughly a factor of nine. Two stages in a row cut it much further. This is a nominal calculation for unloaded stages. In this circuit the second stage loads the first, so treat the numbers as a guide.
Why not one bigger RC? A single stage strong enough to remove the carrier would also start to eat the 500 Hz signal. Two gentler stages give a steeper roll-off between the two frequencies.
The second capacitor would sag if you drew current from it. So the last stage is a 2N3904 emitter follower with a 4.7 kΩ load. Its output follows the base minus one base-emitter drop and can drive a load without disturbing the filter.
On the bench
We put one oscilloscope channel on each of the four points in the chain:
| Channel | Point | Simulated range |
|---|---|---|
| 1 | PB1, raw PWM | 0.01 to 4.99 V |
| 2 | After RC stage 1 | 0.73 to 4.26 V |
| 3 | After RC stage 2 | 0.93 to 4.06 V |
| 4 | Follower output | 0.34 to 3.40 V |
These are readings from a headless run of the compiled firmware on the AVR bench, recorded in docs/qa/marketing-demos-2026-10-24/README.md.
Channel 1 is a wall of edges. Channel 2 is a sine with a visible sawtooth on top. Channel 3 is mostly sine with a small ripple. Channel 4 is the same sine shifted down by roughly 0.6 V, the base-emitter drop, which is what an emitter follower should do.
The numbers line up with the firmware. A duty cycle from 48/256 to 208/256 at 5 V gives about 0.94 to 4.06 V. Stage 2 shows 0.93 to 4.06 V. In the bench still (synth-bench.png), the scope reads about 3.06 Vpp at 500 µs per division, with two and a half cycles across the 5 ms window.
Try this: delete the second RC stage and watch the ripple on the output grow. Or change the table amplitude in the sketch from 80 to 127, recompile, load the new HEX, and see how close to the rails the follower can go.
Schematic and board
The schematic was placed by hand on the grid, not by Benchmate and not by the automatic Tidy layout. Do not read it as an example of auto-layout quality. The wires were routed by the editor's normal orthogonal router.
The PCB is 55 × 40 mm, auto-placed and auto-routed with 25 traces and 3 vias, and passes the design rule check. The LCD appears on the board as a header connector, since the real module plugs in. The board files export to KiCad, Gerber and ODB++. We have not ordered or assembled this board.
Honest limits
- Simulation only. The firmware is real compiled AVR code running on a simulated ATmega328P. The 2N3904 uses a standard simulation model, and the resistors and capacitors are ideal.
- Slower than real time. When we recorded these demo projects, the bench ran at roughly 1/60 of real time on our machine. That means about a minute of waiting for each simulated second. That is why the walkthrough clips needed long recording runs.
- The LCD is a modeled device. It shows the text the firmware sends. It is not a timing-accurate model of a particular display module.
- No spectrum or distortion measurement. "Clean sine" here means clean on the scope, not a measured THD figure.
What we did not test
- We did not build this circuit or measure a real ATmega328P's PWM output, real filter components or a real 2N3904.
- We did not check the sine's distortion, the noise floor, or behaviour across component tolerances.
- We did not test other carrier frequencies, output frequencies or load resistances beyond the values above.
- We did not ask Benchmate to design this circuit. It is a hand-built demo of the bench, not of the AI.