17 or 18-bit PWM on Teensy 4.0 & 4.1?

NewLinuxFan

Well-known member
Is it possible to get a few more bits by modifying the file pwm.c or writing a new function? With my basic understanding, it seems that the quad timers are limited to 16 bits and flex timers limited to 15 bits with the standard AnalogWrite function.
 
Correct, the FlexPWM and QTimer hardware is only 16 bits.

Best to look at the GPT timers. They are 32 bits, but there's only 2 of them and only limited access to the pins they can control. Maybe there's some GPT test code on various forum threads, probably from Manitou.

Also consider that a timer which runs from the 150 MHz peripheral clock would end up having a PWM carrier frequency of 572 Hz at 18 bit resolution.
 
Thanks, it looks like I might be able to use the TeensyTimerTool library for GPT. Low-level coding is beyond my knowledge level.

With some testing today it appears that both quad and flex are set to 15 bits. The LED's attached to a quad timer only change brightness on odd numbers at AnalogWriteResolution(16); An LED attached to a flex timer changes on even numbers. An easy way for anybody to verify this is to look at the built-in LED attached to pin 13 which is quad timer, viewed in a dark room.

If there's a way to get the full 16 bits, that would smooth over the stepping that is subtly visible in a dark room.
 
Thanks, it looks like I might be able to use the TeensyTimerTool library for GPT. Low-level coding is beyond my knowledge level.

With some testing today it appears that both quad and flex are set to 15 bits. The LED's attached to a quad timer only change brightness on odd numbers at AnalogWriteResolution(16); An LED attached to a flex timer changes on even numbers. An easy way for anybody to verify this is to look at the built-in LED attached to pin 13 which is quad timer, viewed in a dark room.

If there's a way to get the full 16 bits, that would smooth over the stepping that is subtly visible in a dark room.

and from your cross-post

This all looks complicated and requiring knowledge of interrupts and low-level coding. Is there a simple way to do PWM like AnalogWrite? I'm just trying to do 16,17, or 18-bit dimming of LED's. The default max of AnalogWrite is actually 15 bits, even when resolution is set to 16.

The Teensy 4.x PWM code is in cores/Teensy4/pwm.c. The functions for Flex or Quad timer get called as a function of the chosen pin. The code intends to support 16 bits, and I can't see a specific reason why that wouldn't work, but there are some odd things in the code that look as if they could come into play with 16-bit resolution, and maybe they do effectively limit resolution to 15 bits. You're right that it's complicated and requires knowledge of low-level coding of the FlexTimer and QuadTimer peripherals. One thing I see that would limit resolution to 16 bits (or perhaps 15) is the duty cycle value is defined as uint16_t, which would not support a duty cycle of 100% at 16 bits. As Paul said, these are 16-bit timers, so you're not going to get resolution of more than 16 bits. May I ask why 15 bits is not sufficient?
 
It looks like pin 13 will do 16 bits on the lowest PWM numbers (where it matters) at 2289 Hz but not 2288.82 Hz. Here's code that changes the brightness by typing values into the serial monitor. I was using this on Teensy 4.0 and commented out pins that are on 4.1 but not 4.0
I don't know if the same is true on flex timers.

Code:
/*
PWM
PWM pin
PWM pin frequency
 */

//#define flex_frequency 2289 or 2288?
//#define quad_frequency 2289 or 2288?

#define flex_frequency 2288.82
#define quad_frequency 2288.82

#define start_PWM 300

void setup()
{
  Serial.begin(115200);

  pinMode(2, OUTPUT);
  digitalWrite(2, LOW);

  pinMode(3, OUTPUT);
  pinMode(4, OUTPUT);
  pinMode(5, OUTPUT);

  pinMode(6, OUTPUT);
  pinMode(9, OUTPUT);
  pinMode(10, OUTPUT);

  pinMode(11, OUTPUT);
//  pinMode(28, OUTPUT);
//  pinMode(29, OUTPUT);

  pinMode(13, OUTPUT);

//  pinMode(33, OUTPUT);
//  pinMode(36, OUTPUT);
//  pinMode(37, OUTPUT);

  pinMode(14, OUTPUT);
  pinMode(15, OUTPUT);
  pinMode(18, OUTPUT);

  pinMode(19, OUTPUT);
  pinMode(22, OUTPUT);
  pinMode(23, OUTPUT);

  analogWriteFrequency(3, flex_frequency);
  analogWriteFrequency(4, flex_frequency);
  analogWriteFrequency(5, flex_frequency);

  analogWriteFrequency(6, flex_frequency);
  analogWriteFrequency(9, flex_frequency);
  analogWriteFrequency(10, quad_frequency); // Quad timer

  analogWriteFrequency(11, quad_frequency); // Quad timer
//  analogWriteFrequency(28, flex_frequency);
//  analogWriteFrequency(29, flex_frequency);

  analogWriteFrequency(13, quad_frequency); // Quad timer. Indicator

//  analogWriteFrequency(33, flex_frequency);
//  analogWriteFrequency(36, flex_frequency);
//  analogWriteFrequency(37, flex_frequency);

  analogWriteFrequency(14, quad_frequency); // Quad timer
  analogWriteFrequency(15, quad_frequency); // Quad timer
  analogWriteFrequency(18, quad_frequency); // Quad timer

  analogWriteFrequency(19, quad_frequency); // Quad timer
  analogWriteFrequency(22, flex_frequency);
  analogWriteFrequency(23, flex_frequency);

  analogWriteResolution(16);

  setall(start_PWM);
}

void loop() {
  // if there's any serial available, read it:
  while (Serial.available() > 0)
  {

    // look for the next valid integer in the incoming serial stream:

    long PWM = Serial.parseInt();

    if (Serial.read() == '\n')
    {
      setall(PWM);
    }
    else
    {

      byte pin = Serial.parseInt();

      if (Serial.read() == '\n')
      {
        analogWrite(pin, PWM);
      }
      else
      {
        long freq = Serial.parseInt();
        analogWriteFrequency(pin, freq);
        analogWrite(pin, PWM);
      }
    }
  }
}

void setall(long PWM_value)
{
 
  analogWrite(3, PWM_value);
  analogWrite(4, PWM_value);
  analogWrite(5, PWM_value);

  analogWrite(6, PWM_value);
  analogWrite(9, PWM_value);
  analogWrite(10, PWM_value);

  analogWrite(11, PWM_value);
//  analogWrite(28, PWM_value);
//  analogWrite(29, PWM_value);

  analogWrite(13, PWM_value);

//  analogWrite(33, PWM_value);
//  analogWrite(36, PWM_value);
//  analogWrite(37, PWM_value);

  analogWrite(14, PWM_value);
  analogWrite(15, PWM_value);
  analogWrite(18, PWM_value);

  analogWrite(19, PWM_value);
  analogWrite(22, PWM_value);
  analogWrite(23, PWM_value);
}
 
It looks like pin 13 will do 16 bits on the lowest PWM numbers (where it matters) at 2289 Hz but not 2288.82 Hz. Here's code that changes the brightness by typing values into the serial monitor. I was using this on Teensy 4.0 and commented out pins that are on 4.1 but not 4.0
I don't know if the same is true on flex timers.

Okay, that's helpful. The smaller value (2288.82) results in a "modulo" value of 65536, which is too large for the 16-bit counter, so the prescaler is incremented by 1, and the modulo value is divided by 2, to 32768, which implies 15-bit resolution. The larger value (2289) yields a "modulo" value of 65531, which fits into the 16-bit counter. You won't get 65536 unique values (0-65535), but you will get very close to 16-bit resolution. I don't know why, but the logic in quadtimerFrequency() will increment the prescaler if the modulo value is > 65534, as opposed to 65535, and on the low end, limits the value to 2 rather than 1. I would have to carefully read the manual chapter on QuadTimer to understand why that's the case. There is a comment that the "low" time cannot be less than 2. Maybe that implies that the "high" time cannot be greater than 65534.

You can get the highest possible modulo value of 65534 by specifying a frequency of 2288.88, i.e. between the two values you are trying now. It would be very hard to see the diffference between 2288.88 (modulo 65534) and 2289 (modulo 65531). Note that all of this is only an issue for 16-bit resolution because the modulo value will always be >= 32767, which is always enough for 15 bits, and it explains the behavior you saw where you were only getting an update on even or odd values when specifying 16 bits.

If you're wondering why it works this way, my answer would be that it's because frequency and resolution are independent, and there's no way you could have known, without inspecting the code or doing trial and error (as you did). It's all fine for resolution up to 15 bits, but for 16-bit resolution with a 16-bit timer, there's a conflict.
 
Last edited:
Thanks again! I tested 2288.88 Hz, and I can get the full 16 bits (observed low end) on pins 6, 9, and 10 which use Flex timers.
 
If you're wondering why it works this way, my answer would be that it's because frequency and resolution are independent, and there's no way you could have known, without inspecting the code or doing trial and error (as you did). It's all fine for resolution up to 15 bits, but for 16-bit resolution with a 16-bit timer, there's a conflict.

Yes, this was a minor issue that came up when I wrote pwm.c. Seems like a lifetime ago... before the pandemic and chip shortages!

Some timers were just 1 or 2 short of supporting full 16 bit range. The quick and easy solution was to just cap the scaled resolution to 15 bits. That was done in a time of writing all the basic Arduino API stuff, with USB barely working and serious problems like the Arduino IDE completely crashing under the strain of fast incoming data if Serial.print() was used without any delays. Exciting times those were, and oh how I wish to go back to those days of chips having only 4 month lead times and actually shipping when promised. Software development was so much easier back then!

Maybe at some point in the future the analogWrite code could be improved to allow scaling to the full not-quite-16-bits range of this timer hardware. If anyone has ideas about this, might as well discuss here. Any change to the analogWrite code absolutely must preserve identical behavior for all combinations of analogWrite() and analogWriteFrequency() with the default 8 bit setting, and probably all settings 12 bits or less. Even a minor difference like changing the round-off behavior in the default mode could really cause pain for people using these functions, so extreme caution for preserving their exact behavior is needed. If there's any doubt a change might break people's programs depending on analogWrite(), I would rather just not support more than 15 bits on those timers.
 
How about functions analogWrite16 and analogWriteFrequency16? Perhaps with the proviso that you must/cannot jump between analogWrite & analogWrite16 and the same for ..frequency.
 
Some timers were just 1 or 2 short of supporting full 16 bit range. The quick and easy solution was to just cap the scaled resolution to 15 bits.

Maybe at some point in the future the analogWrite code could be improved to allow scaling to the full not-quite-16-bits range of this timer hardware. If anyone has ideas about this, might as well discuss here.

The existing code does use the "full not-quite-16-bit" range if the specified frequency allows. What happens is that if the computed modulo is > 65534, the prescaler is incremented and the modulo is divided by 2, so the available range of values is always between 32767 and 65534. 15 bits of resolution is always possible, and if 16 is specified, the resolution varies from 15 to almost 16 as a function of frequency.
 
Last edited:
It looks like it rounds down before reaching a modulo. 150Mhz / 2288.88 hz = 65534.235 which is greater than 65534 but equal to when the decimal point is thrown away. I haven't tried 2288.85 yet to see what the LED does.
 
It looks like it rounds down before reaching a modulo. 150Mhz / 2288.88 hz = 65534.235 which is greater than 65534 but equal to when the decimal point is thrown away. I haven't tried 2288.85 yet to see what the LED does.

Yes, the decimal part is discarded when converting the float result to an integer, but 0.5 is added. The modulo value will be <= 64434 as long as 150M/Freq < 65534.5
 
Back
Top