better low modes with dynamic PWM

Hi, just a quick note to talk about a code update.

But first… I’d like to present to you: A straight line:

Do you see it? That picture shows a straight line. Or at least, some parts which, when added together, make a straight line.

Anyway, there were a couple things bugging me ever since the Noctigon KR4 came out:

  • The low modes weren’t low enough. They were bright, and had big gaps between, so the bottom end of the ramp kinda sucked.
  • The FET made an audible tone during thermal regulation.

The low-modes issue happens because the linear power channel’s steps were too big. It made about 2000 lm and divided those into 1024 steps, so each step was about 2 lm. I could fix it by increasing the number of steps, but then the FET noise issue would get worse.

The FET noise issue happens because the pulses aren’t fast enough. With 1024 PWM steps, that means instead of running at the usual 16 kHz, it ran instead at 4 kHz… and people can hear that pretty easily. I could fix it by speeding it up, but then the low modes would be worse — 8 lm apart instead of 2 lm.


But then a few days ago I was looking through the attiny1634 reference manual, and I noticed something which could solve both problems. And not just on KR4, but on several other models too — KR1, some D4v2, DT8, maybe K1, and possibly more.

Basically, instead of using PWM alone (pulse width modulation), it combines PWM and PFM (pulse frequency modulation).

Here’s how the KR4’s ramp looks, on a logarithmic scale, if I calculate the closest possible steps to what it should be. It starts at 1/1024, repeats that a few times, then does 2/1024, repeats that, then 3/1024, and so on. The bottom end has big wide steps:

The top line (blue) is the raw PWM value from 1 to 1023. The bottom line (orange) represents how bright it appears by eye.

To fix it, the idea is simple. Let’s say you want the smoothest ramp possible, but you can only use integers. For some nice round numbers, let’s say the hardware can have up to 100 levels of PWM… like, 1/100 for moon, then 2/100, 3/100, and so on, up to 99/100 and then 100/100 for full power.

That gets us 100 different levels. But the bottom few have very visible “stair steps” between them. Going from 1/100 to 2/100 doubles the brightness in a single step, and then going up to 3/100 adds another 50% in one step, and then beyond that each step appears smoother and smoother.

This can be solved by changing the number on the bottom. For example…

  • Ramping from 1/100 to 2/100 can be done in 50 steps: 1/100, 1/99, 1/98, 1/97, … all the way to 1/51. Then 2/100.
  • Ramping from 2/100 to 3/100 can be done in 33 steps: 2/100, 2/99, 2/98, 2/97, … all the way to 2/67. Then 3/100.
  • Ramping from 3/100 to 4/100 can be done in 25 steps: 3/100, 3/99, 3/98, … 3/76, then 4/100.

See the pattern?

This gets us a really smooth ramp at the low end. By changing the numerator slowly (pulse width), and changing the denominator quickly (pulse frequency), it can reach many steps in-between the steps.

So the bottom end is fixed!

But it still has the noise issue on high modes. So how about we also make it start at a slow speed for low levels, and increase the speed for high levels? That way, it has precision at the bottom of the ramp, and extra speed at the top.

After combining both of those things, the ramp tables look like this:

Top (orange) line: Denominator — the length of each cycle.
Middle (blue) line: Numerator — the width of each pulse.
Bottom (green) line: How bright the result looks by eye (pulse width divided by pulse frequency).

Much better now. Divide one zig-zaggy line by another zig-zaggy line, and the result is … a smooth line.

(To make some other stuff easier, I set the “fast” point about halfway up the ramp, and everything above that runs at the fastest speed. So the weird shapes stop halfway up.)

If I combine both images into one graph, the difference is pretty easy to see:

However, there’s still a problem. The lowest brightness in both cases is still too high — about 2 lumens. So maybe instead of using 1024 at the bottom end, it should use a bigger number. This should give it a lower starting point.

Here are a few options, all together on one graph:

  • 512 step cycle: minimum of 4 lumens
  • 1024 step cycle: minimum of 2 lumens
  • 2048 step cycle: minimum of 1 lumen
  • 4096 step cycle: minimum of 0.5 lumens

Even the lowest one is still a bit high though… and the real KR4 has 2 power channels, not just 1. So how about bumping it all the way up to 16384 levels at the bottom? Here’s how that looks:

(apologies for the visual brightness stopping at the end of the first channel… it should keep going up when the second channel turns on)

… and that’s the same as the graph I started with, at the top of this post. It just has things in a logarithmic scale instead of a linear scale.

During use, the ramp looks pretty close to visually linear. It starts at about 0.2 lm at the bottom end, then increases smoothly all the way to 4000 lm. So it makes a straight line… when graphed on a perceptual scale.

It sure doesn’t look like a straight line when the individual parts are graphed, though.

Math is weird.

Anyway, long story short… It was bugging me, so I fixed it.

1 Thank

I cannot see the images in the OP unless I open the image in a new tab.

Ah, right, I forgot about that. Ever since BLF switched to https, browsers aren’t loading embedded images from plain-old-http sites any more. I’ll have to fix that another day… because it’s not going to be a quick or easy process.

(name-based vhosts sharing a single external IP behind a reverse proxy… makes it tricky to not only route the traffic, but also to get certbot to work for LetsEncrypt certificate renewal)

Thanks for working on this project.

I have tested your PFM implementation on my KR4. Two observations:

1. Below 11/150 the output fluctuates slightly, but visible and it takes about 0.5-1 s to turn on. Haven’t checked with different temperatures. Even at 15/150 I can see small fluctuations.

2. While you hold the switch the output gets lower! First I thought this has something to do with the power consumption by the driver, but with the scope it was obvious that the frequency alternates between two values.

On the other hand I haven’t noticed any problems with the slow ripple :slight_smile:
I’d say this driver really needs a third channel for low levels. Maybe switching between two sense resistors would be enough.

Ah so I wasn’t completely wrong when I said it was for using 16kHz 8bit for the FET in order to eliminate the cap singing noise.

What actual dimming value those correspond to now ?

The KR4 driver can flicker at the lowest levels, same as Lume1/FF lume1, Vsense is 50mV on the KR4 5A driver so that’s 49uV at 1/1023, going to 1/16383 puts it at 3uV, well under the Op Amp max offset voltage (TLV333 : 15uV).

Several boost drivers that I’ve built are stable at 50uV but not under that, sometimes it doesn’t flicker but the current is higher than it should, which isn’t surprising as this is already a very low sense voltage, going higher resolution wont help achieve a lower low reliably. It does make the start of the ramp smoother though.

I have ended up with one of the D4V2’s with 219B’s and Hanks CC driver. I really hate the low end, if I go below 3/150 there is an odd shutoff and delay turning on and the LEDs start low and increase to the proper level. Hank did not find it a problem the the lowest usable setting is 3/150, I find it unacceptable and will be the last light I every buy with that driver. Moon levels are critical to my night vision in the house, to much and I can’t see for about 30 seconds and 3/150 is to much!

Does this or will this address this issue? This light will never see full brightness, I doubt. My D4V2 with SST20’s is actually much dimmer than the 219B light. I hate that, it will lead to me not using it often.

TK. you are a true driver/software goddess! :slight_smile:

Great firmware that got even better - Amazing! :beer:

Nope, there’s not much I can do about that. The regulation circuit is running way below its optimal level, and it takes a while to stabilize when things are that low… especially if the driver is hot from being on a high mode.

However, I did add something to make it turn on faster during hold-from-off for moon. It now pulses a higher level for a few milliseconds to “jump start” the regulation circuit and make it wake up sooner. Before adding this, when I needed moon in the middle of the night, the process was:

  • Aim light so I can see the aux LEDs.
  • Hold button until aux LEDs turn off.
  • Release button, and wait while moon warms up.

Now it’s more like other lights, where I can just hold the button and then let go as soon as I see light. It starts significantly faster.

Fixing it properly would require hardware changes, like adding a lower power channel which is more stable for these extra-low modes… but that hasn’t happened, so I tried to at least reduce the issue in firmware.

Due to hardware variability though, some lights will have steady output on moon, and some will have visible flickering. There’s nothing the firmware can do about that. And some lights will start a bit slowly on moon still, while others might have a bit of a “pre-flash”, because the jump-start thing isn’t one-size-fits-all. The jump start could potentially be configurable, but it’ll still start slower after the light was on turbo.

As for this dynamic PWM thing, it mostly just squishes the bottom end of the ramp downward to provide extra precision and smoother ramping for low modes. The results are something like this:

Before: (old KR4 firmware, 2020)

  • 1/150: 0.2 lm
  • 2/150: 0.25 lm
  • 3/150: 2.5 lm (default floor)
  • 4/150: 2.5 lm
  • 5/150: 4.2 lm
  • 6/150: 5.9 lm
  • 7/150: 7.6 lm
  • 120: ~1700 lm
  • 150: ~4000 lm


  • 1/150: 0.20 lm
  • 2/150: 0.30 lm
  • 3/150: 0.45 lm
  • 4/150: 0.65 lm
  • 5/150: 0.90 lm
  • 6/150: 1.20 lm
  • 7/150: 1.55 lm
  • 11/150: ~2.5 lm (default floor)
  • 130: ~1700 lm
  • 150: ~4000 lm

As a side effect, it’s also finally more feasible to include moon in the stepped ramping mode, because the levels are actually spaced pretty close to visually-linear near the bottom. I’m getting pretty good results with a ramp from 1 to 130 in 8 steps.

Anyway, this change improves somethings, but it doesn’t eliminate the driver’s slow stabilization time at low modes. Much like how a human eye takes a while to adapt to darkness after seeing a bright light, the regulation circuit takes a while to adapt to very low power levels after it was burning bright. It’s slow when used at ~0.2% power or lower, especially after coming down from something bright.

Yes, that’s exactly what it’s doing now. It was 10-bit before, which was audible on some lights… but 8-bit is a high enough pitch to make it generally inaudible.

The exact numbers might vary over time as I refine the ramp calculator… but for now, it starts out like this on the DD FET + linear 5A model:

  • 0/16383, 0%
  • 1/16383, 0.006%
  • 1/11750, 0.009%
  • 2/14690, 0.017%
  • 2/9183, 0.021%
  • 3/12439, 0.024%
  • 4/13615, 0.029%
  • 5/13955, 0.035%
  • 6/13877, 0.043%
  • 7/13560, 0.051%
  • 8/13093, 0.061%
  • 9/12529, 0.072%
  • 11/13291, 0.083%
  • 12/12513, 0.096%
  • 14/12756, 0.11%

… but before it went like this:

  • 0/1023: 0%
  • 0/1023: 0%
  • 1/1023: 0.1%
  • 1/1023: 0.1%
  • 2/1023: 0.2%

The real-world numbers are a little more tricky than that, because zero isn’t actually zero… Sending the driver a “zero” signal actually produces about 0.011% of the maximum output. And there’s other error and approximation involved too. The ramp calculator tries to account for that stuff by factoring it into the calculation model, and the model isn’t perfect. But it seems to get close enough for a useful result.

The code running on the attiny chip is simple and easy. The hard part is making the ramp calculator’s model accurate enough to generate a good table of values.

There’s nothing I can do about the driver’s analog noise floor. It’s a property of the hardware. It can flicker randomly by +/- 0.2 lm, so increasing the digital PWM resolution beyond that is mostly pointless. What I did here is increase the digital resolution as far as the analog properties of the driver allow… and it turns out that’s about 12 or 13 bits.

… and then it decreases resolution smoothly to about 8 bits, as it gets brighter. Because higher modes just don’t need to be very precise. A difference of 0.3 lm is very visible when going from 0.3 to 0.6 lm, but it’s completely imperceptible when going from 1000.3 to 1000.6 lm. So it prioritizes precision for low modes, and speed for high modes.

Good to see improvements! Thanks for all your efforts Toykeeper.

Thank you ToyKeeper, that is a brilliant upgrade!

May I please also ask, there used to only be one 219 firmware for each light, to cover both 219c and 219b emitters. Now there seem to be both 219 and 219b. What is the difference and need for there to be 2?

Edit: I just flashed the nonFET to my E21A KR4. I noticed that the SOS mode is not there (not sure if it was before, just compared to some diagram I have).

AFAIK the SOS mode is only enabled for the LT1 build.

If the lights are really making noise I’d worry about the parts shaking themselves loose, particularly the inductor. Sometimes people glop elastomer over those to damp the vibrations, I think.

Anyway, this is great work, using these clever software methods to get the best from the limited hardware. I hope that the hardware can also keep improving.

It’s a capacitor (no inductor in the KR4 driver), and no it isn’t going to pop off.

The kr4-219 build uses 60% FET, while the kr4-219b version is 50% FET. Hank was experimenting with different levels when he started carrying 219B as an emitter option. I’m not sure which one he uses, or if it’s both. If it’s just one though, I’ll probably remove the other one.

This is a normal property of lights which use high-amplitude PWM to adjust brightness. It’s nothing to worry about; just an aesthetic inconvenience. On some lights, sound comes from a tail spring, on some it comes from other parts, and on some it’s not even coming from the torch itself. Rapid thermal expansion and contraction can be heard, even from nearby objects, thanks to the photoacoustic effect. Someone around here actually made a pillow play a song from Star Wars, using only high-intensity light pulses. The same could be done with an infrared laser, to turn arbitrary objects into makeshift speakers from a distance.

Thank you for the explanation. That is very useful to know.

I have modded a couple of Hank’s lights to Nichia 219b a long time ago and they have worked perfectly for a long time. I am sure I am not the only one. I would appreciate it if you kept the 60% FET version available regardless of whether Hank prefers the 50% or 60% FET.

I can't see the OP pics - not sure if you changed something already or not.

Thanks for these tweaks! This probably has usages in many of the newer CC drivers being developed.

I am thinking if this new configuration will work to provide a lower moonlight level for the Lume1 driver.

I can still see them. Strange that some can and others can’t. BTW, I’m using Safari on a MacBook.