Fun with Lasers and PWM

When I rebuilt my homebrew laser cutter last year, basically doubling it in size, I took the opportunity to improve a number of key systems, including automating laser power output. Previously, I used a simple 0-5V potentiometer, connected directly to the laser power supply to set the mA output of the tube before starting a job. Worked well enough but more than a few jobs got ruined when I forgot to reset the power level, usually when switching between cutting and engraving operations.

My CNC control software, UCCNC, has support for controlling spindle speed via pulse width modulation (PWM). While a laser cutter has no spindle, of course, the laser power supply can also accept a PWM signal instead of the potentiometer analog voltage to set the output level. Perfect, straightforward solution – I’d be able to set a job’s power requirement programmatically via standard S and M3 g-code commands.

Initially, I thought everything worked great, though it did seem a higher mA laser output was needed on PWM driven jobs. I also noticed the quality of engraving jobs was down a bit from my old setup. Initially, I figured this was due to X-axis backlash from the doubling of the belt length, but after too many hours fiddling with backlash settings and other mechanical aspects of the now oversized X gantry, I happened to read another Makers experience with PWM manipulation of laser power supplies. His supply specifically required a 20kHz frequency. My supply listed no such requirement, but I knew I was driving it at 5kHz, the max frequency UCCNC can do. Could this be a problem?

I fired off a note to my laser parts supplier in China and they got back to me the next day – sure enough, though it wasn’t listed in the specs, the supply expects a 20-50kHz PWM frequency. In hindsight, this makes sense. In reality, all the laser supply is doing with that PWM signal is converting it back into the same 0-5V signal the potentiometer delivers via a simple RC filter. By driving the supply at 5kHz, I was effectively sending it a signal with a couple volts of ripple – effectively the same as if I’d rapidly wiggled the potentiometer during a running job. The analog mA meter didn’t react quickly enough to display the continual changes so I never noticed the problem there, but this certainly explained all the issues.

OK, so how do I convert a 5kHz PWM signal to a 20kHz one? My first thought is this is probably a common problem and there’s ready-made solutions out there. It turns out Analog Devices makes some PWM DAC and ADC PWM-out combos that would work fine. Spendy though – like $16 in single quantities from Digikey. Seems overkill for my needs. What could I use that’s already in the shop (free)? What about an Arduino?

These days, a Maker admitting to using an Arduino is like the dog coming home from the vet wearing a cone of shame. There’s so many microcontrollers on the market that are faster, better specs, talk Python as their native tongue, blah, blah, blah… You know what? I’ll say it. I still like Arduinos. I know the Atmel micro family very, very well. And while I like Python, I’m most comfortable writing code in C. The Arduino IDE is intuitive and easy to use. Plus, when you work with an Arduino, you’re standing on the shoulders of giants. No matter what you’re doing, someone’s done it before (or something very similar) so there’s always some code to start with.

Oh, I’ve also got a crapton of Arduino programmable ATTINY85 Digispark clones – I use them for various neopixel projects, among other things, and they cost well under $2.

In the back of my mind, a mental note triggers about that little ATTINY85 – it has the ability to tie a timer to the internal PLL with a multiplier, yielding upwards of 64MHz. That’s crazy for a $1 part, and perfect for PWM generation needs! Off to the part bins I go!

So there’s two parts to the task at hand – accurately reading the PWM signal coming from UCCNC, and generating the matching 20kHz signal to send off to the laser supply. My first instinct is to do just what the supply does – RC filter down to an analog voltage, sample via the built-in ADC on the ATTINY85 and use that value to drive the new PWM out. Turns out, this is easier on paper than in reality. RC filters are a constant tradeoff between jitter and response time. A filter that rapidly achieves the average voltage of the PWM signal has high jitter. A low-jitter value takes a long time to reach. Even once I found a reasonable compromise, I discovered the output ADC values were a bit nonlinear across the full range, and even more troubling, they were quite sensitive to input voltage, which the ATTINY uses for its ADC comparison. In short, this meant a given spindle S value may or may not yield the same mA output to the laser. Not good.

OK, scrap the RC filter approach. What about reading the actual duration of the incoming PWM signal directly? Arduino has a pulseIn() function that returns the number of microseconds it takes for a signal to go from low to high (or high to low). Hmm, 5kHz is 0.0002 seconds or 200 microseconds. That’s not terribly great resolution to determine an accurate duty cycle off of. But wait, 5kHz is the max UCCNC can deliver – it can totally go slower. If I set the spindle PWM frequency to say, 400Hz, that’s 0.0025 seconds / 2500 microseconds. Now we’re cooking!

With reading the incoming signal sorted, now it was time to think about generating a 20kHz (or better) PWM signal out. Arduino’s default PWM generator, analogWrite(), runs by default at 490Hz. No bueno. Now each flavor of Atmel micro has various registers for directly fiddling with internal timers and counters for setting all flavors of frequencies. The ATTINY85 has the aforementioned PLL trick, which the Digispark uses to great advantage for creating the 16.5MHz clock it uses for USB emulation. This also means the Digistump devs had to throttle those timers back via code to make the Digispark Arduino time-based functions behave “normal”. Bless their little coding hearts, they anticipated folks wanting to take advantage of speedy PWM so they left hooks in the device definition files for resetting the PWM frequency scaler all the way up to 64.5kHz. On my Mac, the file to change is: ~/Library/Arduino15/packages/digistump/hardware/avr/1.6.7/cores/tiny/wiring.c and once MS_TIMER_TICK_EVERY_X_CYCLES is set to 1, analogWrite() will happily pump out a 32kHz signal on pin P0. Perfect!

All that’s left is some embarrassingly simple Arduino code. I even send a matching PWM signal to the on-board LED which is handy to confirm a signal is being output plus the brightness reflects the PWM duty cycle. One quirk worth mentioning, determining a 0% or 100% duty cycle may take some exception handling as there should be no pulse width to count. 0% should be a continuous 0V on the input pin and 100% should be 5V. With UCCNC at least, it never sends a true 0% so we’re able to get a reliable reading from pulseIn(). UCCNC does go continuous 5V at 100% so I had to write around that case – if pulseIn() times out and returns 0 yet the input pin reads HIGH, we’re at 100% so I set the output accordingly. Also keep in mind if you do a lot of work in your main loop() you’ll probably start a pulseIn() call mid signal, giving an incorrect low value. Consider doing back-to-back pulseIn() calls, discarding the 1st. In my case, I don’t really do any work in loop() and since I don’t need to continually update the spindle PWM output, I take the average of 100 pulseIn() readings prior to setting the output duty cycle.

Without further ado, here’s the code. I hope this helps you if you happen to stumble across this post in your PWM googlings. Cheers.

//Convert 400Hz PWM to 32kHz using an ATTINY85 based Digispark
//Set MS_TIMER_TICK_EVERY_X_CYCLES to 1 in Digispark wiring.c 
//Code is released to Public Domain

#define PWMIN 2
#define PWMOUT 0 
#define LED 1

const int numReadings = 100;
int readIndex = 0;
long total = 0;
long average = 0;

void setup() {
  pinMode(PWMIN, INPUT);
  pinMode(PWMOUT, OUTPUT);
  pinMode(LED, OUTPUT);

void loop() {
  total = total + (pulseIn(PWMIN, HIGH, 5000));
  readIndex = readIndex + 1;

  if (readIndex >= numReadings) {
    average = total / numReadings;
    readIndex = 0;
    total = 0;

    if (average == 0 & digitalRead(PWMIN) == 1) {
      analogWrite(LED, 255);
    } else {
      analogWrite(PWMOUT,map(average, 0, 2500, 0, 255)); 
      analogWrite(LED, map(average, 0, 2500, 0, 255));

Postscript: While the Digistump website and documentation are still up (yaay!), the company does seem effectively abandoned. They haven’t sold any Digisparks in many years. I’ve read their software-based USB stack doesn’t work with newer versions of Windows, but being a Mac guy, they still work just fine for me with High Sierra at least. Since the hardware was Open Source, there’s tons of Chinese clones on eBay, some for as little as $1.50. Pretty impressive when the ATTINY85 chip itself costs a $1 in 100 qty.