How to Make a Pulse Train of Exactly N pulses?

Status
Not open for further replies.

ProfHuster

Active member
I am programming an ultrasonic distance sensor and I need a pulse train of exactly eight 40 kHz pulses at 50% duty cycle. I have tried several approaches and can't get what I want. First I used an IntervalTimer that counted down then ended the timer. I got some double pulses.
BUT, here is my latest. The problem is that the timer is not synced with the PWM clock so it shuts off after the right time but most of the time there is a partial pulse at the beginning and as the end, so seven full pulses, and two partial.

What is a good way of just generating eight pulses?

Code:
#define PIN 10

IntervalTimer startTrain; // Triggers new pulse train
IntervalTimer stopTrain; // Turns train off

void setup() {
  Serial.begin(1000000L);
  delay(1000);
  Serial.println("TestDAC");
  Serial.print("Pin "); Serial.println(PIN);
  pinMode(PIN, OUTPUT);
  analogWrite(PIN, 0);
  analogWriteFrequency(PIN, 40000);
  analogWriteResolution(8); // 0 - 255

  startTrain.begin(startTr, 100000);
}

void loop() {
  delay(1000);
}

#define STOP_US ((int)(1e6 * 8 / 40e3 + 0.5))
void startTr(){
  analogWrite(PIN, 127);
  stopTrain.begin(stopTr, STOP_US);
}

void stopTr(){
  analogWrite(PIN, 0);
  stopTrain.end();
}

// EOF
 
Did you think of using a UART at 400kbaud, 8N1, send 8 0xF0 bytes. That's active low, but you can invert if you need.
 
I need a pulse train of exactly eight 40 kHz pulses at 50% duty cycle.

The program shown below will do what you want for pin 2 of Teensy 4.x. Many of the PWM pins on T4.x use the FlexPWM modules of iMRXT1062. This program uses pin 2, which is FLEXPWM module 4, sub-module 2, channel A. On each PWM period, there is a RELOAD FLAG (RF) event. That event can be configured to generate an interrupt, but as shown here you can also poll the appropriate status register (FLEXPWM4_SM2STS) for that flag. In loop(), when the FLEXPWM_SMSTS_RF flag is set, clear the flag and increment a counter, and when the counter reaches 8+1, set the duty cycle to 0, as shown here.

I haven't done this with T3.x, which has the FlexTimer Module (FTM) rather than FlexPWM, but I'm sure something similar is possible.

pwm8Cycles.png

Code:
const int pwmPin = 2; // pin 2 = flexpwm 4, submodule 2, channel A

void setup() {
  Serial.begin( 115200 );
  while (!Serial);
  Serial.println( "Starting application" );

  // set PWM frequency 
  analogWriteFrequency( pwmPin, 40000 );

  // set PWM duty cycle
  analogWrite( pwmPin, 128 );
}

uint32_t ncycle = 0;
uint32_t maxcycle = 8;

void loop() {
  if (FLEXPWM4_SM2STS & FLEXPWM_SMSTS_RF) { // if RELOAD FLAG is set
    FLEXPWM4_SM2STS = FLEXPWM_SMSTS_RF;     //   write 1 to clear
    if (++ncycle == maxcycle+1) {
      analogWrite( pwmPin, 0 );
    }
  }
}
 
As an exercise for the reader, use serial as 3 bit PWM with noise-shaping to generate high quality class D output!

I fear the noise-shaping part is above my pay grade - I know what it is, but the details... it's more than just oversampling & filtering, right?
And I have absolutely no idea how i.e. "moving the noise near to nyquist" would work. Something with "subtracting the errors from following samples"... eh.. big question marks.

@German readers: Is there a good explanation in German somewhere? I'd be really interested to know details, and to see a concrete algorithm would be great,

@Prof: Big sorry for hijacking your thread. Will not happen again ;)
 
Last edited:
Teensy 3.2

Okay. Here's a screenshot for T3.2, and code that works for both T3.2 and T4.x. For T3.2, I'm using pin 3, which is FTM 1, channel 0. The CHANNEL FLAG (bit 7) of the FTM1_C0SC (status and control) register goes from 0 to 1 on each PWM period, so that's the bit that gits polled. It requires a read, then write 0 to clear. For T3.2, the pin goes high for about a second before the PWM begins. I haven't looked into why that is. Going back to @manitou's post, another way to do this would be to use the output compare (OC) capability of one of the timers. The idea there would be to set up the timer to generate an event every 12.5 us (half of a 40 kHz PWM period), and toggle the output of the timer on each event, then stop the timer after 16 events.

T32pwm8Cycles.png


Code:
#if defined (__MK20DX256__)
const int pwmPin = 3; // T3.2 pin 3 = FTM 1, channel 0
#elif defined (__IMXRT1062__)
const int pwmPin = 2; // T4.x pin 2 = flexpwm 4, submodule 2, channel A
#endif

void setup() {
  Serial.begin( 115200 );
  while (!Serial);
  Serial.println( "Starting application" );

  delay( 100 );

  // set PWM frequency 
  analogWriteFrequency( pwmPin, 40000 );

  // set PWM duty cycle
  analogWrite( pwmPin, 128 );
}

uint32_t ncycle = 0;
#if defined (__MK20DX256__)
uint32_t maxcycle = 8;   // to get 8 PWM cycles on T3.2
#elif defined (__IMXRT1062__)
uint32_t maxcycle = 8+1; // to get 8 PWM cycles on T4.x
#endif

void loop() {
#if defined (__MK20DX256__)
  if (FTM1_C0SC & FTM_CSC_CHF) { // if CHANNEL FLAG is set
    FTM1_C0SC &= ~FTM_CSC_CHF;   //   write 0 to clear
    ncycle++;
  }
#elif defined (__IMXRT1062__)
  if (FLEXPWM4_SM2STS & FLEXPWM_SMSTS_RF) { // if RELOAD FLAG is set
    FLEXPWM4_SM2STS = FLEXPWM_SMSTS_RF;     //   write 1 to clear
    ncycle++
  }
#endif
  if (ncycle == maxcycle) {
    analogWrite( pwmPin, 0 );
    Serial.println( "8 x PWM cycles complete" );   
  }
}
 
you could generate 8 pulses with one interval timer
Code:
#define PIN 10
#define PULSE_CNT 8*2

IntervalTimer tmr;

volatile uint32_t pulses, polarity, done;

void pulser() {
  if (pulses++ < PULSE_CNT) {
    polarity = 1 - polarity;
    digitalWrite(PIN, polarity);
  } else {
    done = 1;
    tmr.end();
  }
}

void setup() {
  Serial.begin(9600);
  while (!Serial);
  pinMode(PIN, OUTPUT);
  tmr.begin(pulser, 12.5); // 1/80khz,

}

void loop() {
  while (!done);
  Serial.println("done");
  tmr.end();
  done = 0;
}
scope shows 41.7 khz (period 24 us)
 
Thanks Joe,
There are other tasks going on, ADC, filtering, and serial output, so I can't use polling. I also have a Teensy 4.0 if I need to switch, but I'd like to stick with the 3.2.
If you have time, how would I set up an ISR on the RF event for that particular module, submodule, channel?
 
How would I set up an ISR on the RF event for that particular module, submodule, channel?

You can set the CHIE (channel interrupt enable) flag in the same register that contains the CHF flag, so that would be as shown below. I don't have time today to go any further than that, and I'll be away for the next couple of days. Hopefully someone else can chime in with how to attach to the interrupt for FTM 1 channel 0. If you get that far, you'll want to clear CHF in the ISR the same way that I do in the polling loop. I'll be around on Saturday or Sunday and can go further if nobody else has.

Code:
   // to enable the interrupt, write 1 to CHIE field (channel interrupt enable) 
   FTM1_C0SC |= FTM_CSC_CHIE;

   // to clear the interrupt when it occurs, write 0 to CHF field (channel flag)
   FTM1_C0SC &= ~FTM_CSC_CHF;
 
Using SPI, you could also use DOUT (pin 7 or pin 11) with a 16-bit write (0b0101010101010101 = 0x5555 = 21845, or 0b1010101010101010 = 0xAAAA = 43690), with the SPI clock double the desired frequency. I haven't checked how close to 80,000 the SPI clock can be set, though.

At the desired frequency, you could also use just the SCK (pin 13 or pin 14).
 
it's more than just oversampling & filtering, right?
It's simpler than it seems. The English Wikipedia Noise shaping article contains a very good Operation chapter, describing the basic idea behind noise shaping.

Essentially, whenever you produce an output sample, and it differs from the perfect modified sample value – for example, because you have fewer output bits than input bits, or because you scaled or otherwise manipulated the sample (in either the time or frequency domain) – the difference is the quantization error in this sample. These quantization errors are filtered (often using a finite impulse filter), the result dithered (rounding errors (pseudorandomly) distributed on top of the result), and added to the output signal.

The reason why this results in noise shaping, is closely related to FIR filtering. Quantization error forms a signal that is missing from the original. So, we add white noise at the correct amplitude – this is the contribution from dithering, and is essential! –, and filter the result. If we did it right, adding the filtered signal to the original gives us a signal which is closer to the original, with the error signal much better distributed. (As mentioned in the Wikipedia article, filtering the error-with-noise signal using the inverse of one of the Fletcher-Munson curves approximating the spectral sensitivity of human hearing, can push 44.1kHz/48kHz 16-bit samples from 96dB dynamic range to 120dB; almost as if you had 20-bit samples at the same sample rate.)

I'm not exactly up to date on this subject, since it's been a decade since I last did some real sonification work, though. I did first encounter this in 1998 or so, when LAME, the venerable MP3 encoder, gained the first psychoacoustic model (noise shaping using the inverse of the sensitivity of human hearing), much improving its encoding quality.

Similar effects do occur in human vision, too, and on multiple levels. You might have seen the famous picture that looks like Marilyn Monroe when viewed close up, and Einstein when viewed from afar. Our sight is different (colorless) in low light conditions (which is why pirates wore an eye-patch; that eye would be in low-light mode, and ready for use in dark!). We perceive color and shade differences than actual colors (consider the example image where an obstacle throws a shadow on a checkerboard; the light squares inside the shadow have exactly the same properties as the dark squares outside the shadow, but that's not how humans perceive them). Colors even contribute to depth perception. I myself use cell shading and other graphics art techniques, like varying the thickness of outlines to hint at depth differences, to control the information conveyed in images of large atomic systems. (The purpose is exactly the same: to shape the noise, or unnecessary or unintended information, out of human perception, so the viewer gets the correct impression: perhaps with less information in my case, but at least I can somewhat control what that information is. As you know, atoms really are nothing like marbles, and they don't actually have sticks between them, but correctly simplified (to avoid forming unwanted interpretations and connotations), that kind of ball-and-stick model can convey more than say a cross section of electron densities, especially so for large systems and molecules, like proteins and such.)
 
you could generate 8 pulses with one interval timer
Code:
#define PIN 10
#define PULSE_CNT 8*2

IntervalTimer tmr;

volatile uint32_t pulses, polarity, done;

void pulser() {
  if (pulses++ < PULSE_CNT) {
    polarity = 1 - polarity;
    digitalWrite(PIN, polarity);
  } else {
    done = 1;
    tmr.end();
  }
}

void setup() {
  Serial.begin(9600);
  while (!Serial);
  pinMode(PIN, OUTPUT);
  tmr.begin(pulser, 12.5); // 1/80khz,

}

void loop() {
  while (!done);
  Serial.println("done");
  tmr.end();
  done = 0;
}
scope shows 41.7 khz (period 24 us)

I modified the code to repeat at 10 Hz. I found that this gives very stable pulse trains with a total jitter on the length of about 50 ns using a Teensy 3.2. And the frequency is accurate. I got a period of 25.1 us.

In contrast when I used the TeensyTimerTool and a Teesny 3.2 the length of the pulse train jittered by 2.5 us and the period was stuck at an integer number of us, so the frequency was 41.6 kHz instead of 40 kHz.
It looks like IntervalTimer is the winner.
 
That's a problem of delay(), not the TeensyTimerTool.. apples vs oranges.
And please stop discussing this at different threads.
 
Status
Not open for further replies.
Back
Top