Page 1 of 4

PWM and code in the main loop

Posted: Fri Mar 14, 2025 2:50 am
by julian_lpp
While working on a bluepill project that involves (amongst other things) :

Timer1 in capture more (reads pulses that come in pa8)
Timer2 interrupt on overlow
Timer3 PWM outputs the singal to PB0

I'd been having strange issues trying to send a signal to a NeoPixel ring (leds in different colors, leds turned off, etc), so I guessed it could be related to the timers, and decided to get a read of the library I'm using for the neopixels, which is Adafruit_NeoPixel.h, and I was surprised that it does not use in any form any timer not any other interrupt either, it just uses the SysTick functionality of the STMF1 (the method .SHOW is the important part of the code)

After then (even when there was not any kind of timer code involved) I tried a couple of things such as

Code: Select all

        NVIC_DisableIRQ(TIM1_IRQn);  // Disable TIM1  interrupt
        NVIC_DisableIRQ(TIM2_IRQn);  
        NVIC_DisableIRQ(TIM3_IRQn);
        NEOPIXEL CODE HERE  //<<<NOTE THIS CODE doesnt use any timer
        NVIC_EnableIRQ(TIM1_IRQn);  // Disable TIM1  interrupt
        NVIC_EnableIRQ(TIM2_IRQn);  
        NVIC_EnableIIRQ(TIM3_IRQn);

Code: Select all

   __disable_irq();
   NEOPIXEL CODE HERE  // //<<<NOTE THIS CODE doesnt use any timer
   __enable_irq();
And the problem was still there...

But, with

Code: Select all

TIM3->CR1 &= ~TIM_CR1_CEN;
TIM3->CCER &= ~(TIM_CCER_CC1E | TIM_CCER_CC2E | TIM_CCER_CC3E | TIM_CCER_CC4E);

   NEOPIXEL CODE HERE  // //<<<NOTE THIS CODE doesnt use any timer

TIM3->CCER |= (TIM_CCER_CC1E | TIM_CCER_CC2E | TIM_CCER_CC3E | TIM_CCER_CC4E);
TIM3->CR1 |= TIM_CR1_CEN;   //    
all ended up working as expected...

I'd like to know -for the sake of curiosity- in wich way a PWM signal can "interfere" with code in the main loop that uses the sysTick and needs to be very timing precise (neopixels require that)

regards
julian

Re: PWM and code in the main loop

Posted: Fri Mar 14, 2025 4:26 am
by ozcar
Julian,

I have looked at the Adafruit NeoPixel library a few times. First time, I could not understand how it could manage to work without disabling interrupts, but then I saw that it does do that - I just missed seeing that in the code, which is a bit hard to follow with all the #if stuff.

I don't feel entirely happy about the way it "borrows" the SysTick timer, but given that no SysTick interrupts can happen while it is in there, maybe this does not matter. I did see that for some other Arm processors, they use DWT_CYCCNT instead of SysTick. I don't think there is any real good reason for the difference, it is probably just due to different authors contributing different parts of the library.

How many LEDs do you have connected?

Do you have any way to check the timing of the generated LED pulses? I found there was noticeable variation in the timing, and thought that it could be out of spec for some LEDs. You could try tweaking the target pulse times, but that could be a bit tricky if you have to work blind.

I found that using PWM for the LED output (but still with interrupts disabled) could produce rock-steady pulse times (provided you have a timer available to do that, of course). There is an old thread here where somebody managed to use PWM for the LEDs without disabling interrupts, but that was on a much faster processor that could manage to process one interrupt per bit of LED data.

Re: PWM and code in the main loop

Posted: Fri Mar 14, 2025 4:47 am
by fpiSTM
DWT_CYCCNT is always incremented even if irq are disabled.

Re: PWM and code in the main loop

Posted: Fri Mar 14, 2025 5:08 am
by ozcar
SysTick->VAL also increments with interrupts disabled. What I did not like was that they saved the SysTick registers, changed them to suit their own purposes and then restored them. Probably does not matter, but they could have used DWT_CYCCNT instead and avoided all that.

PS Julian

If the LEDs are running on 5V, are you doing any level conversion 3.3V=>5V? If not it can be hard to sure that is not contributing to your troubles. There was another thread here recently where this came up, but I don't think the issue was resolved.

Re: PWM and code in the main loop

Posted: Fri Mar 14, 2025 6:36 am
by fpiSTM
SysTick->VAL isn't incremented when irq disabled IIWR

Re: PWM and code in the main loop

Posted: Fri Mar 14, 2025 9:48 am
by ozcar
fpiSTM wrote: Fri Mar 14, 2025 6:36 am SysTick->VAL isn't incremented when irq disabled IIWR
Maybe you are thinking of something different. They use SysTick->VAL, which is a hardware register at address 0xE000E018, aka SYST_CVR. I looked it up, and I was wrong, it doesn't increment, it actually decrements, but if it did not count, their code, which runs with interrupts disabled, would not work and could spin forever on instructions like this:

Code: Select all

while (SysTick->VAL > cyc);
(cyc is just a local variable set according to "0" or "1" pulse length).

I see I'm not the only one not so happy with what they do there. There is this comment in the code for SAMD51, which uses SysTick in the same way as is done for STM32:

Code: Select all

  // ...                       So a kind of horrible thing is done here...
  // since interrupts are turned off anyway and it's generally accepted
  // by now that we're gonna lose track of time in the NeoPixel lib,
  // the SysTick timer is reconfigured for a period matching the NeoPixel
  // bit timing (either 800 or 400 KHz) and we watch SysTick->VAL very
  // closely (just a threshold, no subtract or MOD or anything) and that
  // seems to work just well enough.  When finished, the SysTick
  // peripheral is set back to its original state.

Re: PWM and code in the main loop

Posted: Fri Mar 14, 2025 10:23 am
by julian_lpp
ozcar wrote: Fri Mar 14, 2025 4:26 am I don't feel entirely happy about the way it "borrows" the SysTick timer,
Indeed that's what it does (I'm pasting the code below.. LINE 2881 of Adafruit_NeoPixel.cpp )

Code: Select all

	uint8_t *p = pixels, *end = p + numBytes, pix = *p++, mask = 0x80;
	uint32_t cyc;
	uint32_t saveLoad = SysTick->LOAD, saveVal = SysTick->VAL;

	if (is800KHz) {

		uint32_t top = (F_CPU / 800000);       // 1.25µs
		uint32_t t0 = top - (F_CPU / 2500000); // 0.4µs
		uint32_t t1 = top - (F_CPU / 1250000); // 0.8µs
		SysTick->LOAD = top - 1; // Config SysTick for NeoPixel bit freq
		SysTick->VAL = 0;        // Set to start value
		for (;;) {
		  LL_GPIO_SetOutputPin(gpioPort, gpioPin);
		  cyc = (pix & mask) ? t1 : t0;
		  while (SysTick->VAL > cyc)
			;
		  LL_GPIO_ResetOutputPin(gpioPort, gpioPin);
		  if (!(mask >>= 1)) {
			if (p >= end)
			  break;
			pix = *p++;
			mask = 0x80;
		  }
		  while (SysTick->VAL <= cyc)
			;
		}

	} else {                                 // 400 kHz bitstream
		uint32_t top = (F_CPU / 400000);       // 2.5µs
		uint32_t t0 = top - (F_CPU / 2000000); // 0.5µs
		uint32_t t1 = top - (F_CPU / 833333);  // 1.2µs
		SysTick->LOAD = top - 1; // Config SysTick for NeoPixel bit freq
		SysTick->VAL = 0;        // Set to start value
		for (;;) {
		  LL_GPIO_SetOutputPin(gpioPort, gpioPin);
		  cyc = (pix & mask) ? t1 : t0;
		  while (SysTick->VAL > cyc)
			;
		  LL_GPIO_ResetOutputPin(gpioPort, gpioPin);
		  if (!(mask >>= 1)) {
			if (p >= end)
			  break;
			pix = *p++;
			mask = 0x80;
		  }
		  while (SysTick->VAL <= cyc)
			;
		}
	}
ozcar wrote: Fri Mar 14, 2025 4:26 am How many LEDs do you have connected?
8

ozcar wrote: Fri Mar 14, 2025 4:26 am Do you have any way to check the timing of the generated LED pulses?
Not at all


I'll try to reproduce the issue with small code to let anyone with a scope to get a look
regards
julian

Re: PWM and code in the main loop

Posted: Fri Mar 14, 2025 10:35 am
by julian_lpp

Code: Select all

	uint8_t *p = pixels, *end = p + numBytes, pix = *p++, mask = 0x80;
	uint32_t cyc;
	uint32_t saveLoad = SysTick->LOAD, saveVal = SysTick->VAL;

	if (is800KHz) {

		uint32_t top = (F_CPU / 800000);       // 1.25µs
		uint32_t t0 = top - (F_CPU / 2500000); // 0.4µs
		uint32_t t1 = top - (F_CPU / 1250000); // 0.8µs
		SysTick->LOAD = top - 1; // Config SysTick for NeoPixel bit freq
		SysTick->VAL = 0;        // Set to start value
		for (;;) {
		  LL_GPIO_SetOutputPin(gpioPort, gpioPin);
		  cyc = (pix & mask) ? t1 : t0;
		  while (SysTick->VAL > cyc)
			;
		  LL_GPIO_ResetOutputPin(gpioPort, gpioPin);
		  if (!(mask >>= 1)) {
			if (p >= end)
			  break;
			pix = *p++;
			mask = 0x80;
		  }
		  while (SysTick->VAL <= cyc)
			;
		}

	} else {                                 // 400 kHz bitstream
		uint32_t top = (F_CPU / 400000);       // 2.5µs
		uint32_t t0 = top - (F_CPU / 2000000); // 0.5µs
		uint32_t t1 = top - (F_CPU / 833333);  // 1.2µs
		SysTick->LOAD = top - 1; // Config SysTick for NeoPixel bit freq
		SysTick->VAL = 0;        // Set to start value
		for (;;) {
		  LL_GPIO_SetOutputPin(gpioPort, gpioPin);
		  cyc = (pix & mask) ? t1 : t0;
		  while (SysTick->VAL > cyc)
			;
		  LL_GPIO_ResetOutputPin(gpioPort, gpioPin);
		  if (!(mask >>= 1)) {
			if (p >= end)
			  break;
			pix = *p++;
			mask = 0x80;
		  }
		  while (SysTick->VAL <= cyc)
			;
		}
	}
BTW, (not related with original question) given that I'm kindof supercharging the F103, couldnt this piece of code be set in setup time rather than do the math stuff on evey call ?
They dont seem to be very worring about performance 8-)

Code: Select all

		uint32_t top = (F_CPU / 800000);       // 1.25µs
		uint32_t t0 = top - (F_CPU / 2500000); // 0.4µs
		uint32_t t1 = top - (F_CPU / 1250000); // 0.8µs
		

Re: PWM and code in the main loop

Posted: Fri Mar 14, 2025 8:28 pm
by ozcar
Julian,

For sure I can try your code and see what the LED pulses look like.

It's been a long time since I looked at this, so I tried yesterday on a F103 Blue Pill and was a bit amazed that the timing was crazy, and that was without any timers running. I did not have any LEDs connected, but there is no way that they would have worked. Thinking that I might have messed up something in my copy of the library, I updated that to 1.12.5, but still the timing was way out. Only then I noticed that because of something else I had been doing, I had "No Optimization" set! So, changed that to "Fastest" and at last I got something reasonable - still some variation in the timing, but maybe good enough.

Today, after a slight diversion with updating the STM32duino core (as mentioned in another thread here), I got back to this, and it seemed that the timing was worse than I got yesterday. Then I saw that installing the new STM32duino core had reset the optimization to "Smallest" instead of "Fastest", so that code is very sensitive.

I would not be too concerned about those calculations that they do there. So, maybe they could do that once and keep the results, but it is only done once per show() call, and is before they start generating the pulses where the timing really matters.

Re: PWM and code in the main loop

Posted: Fri Mar 14, 2025 10:59 pm
by julian_lpp
ozcar wrote: Fri Mar 14, 2025 8:28 pm For sure I can try your code and see what the LED pulses look like.
Many thanks ozcar, I've just compacted the piece of code that is badly working in the original project, and surprisingly It fails now even stopping the pwm, really confusing...

I'd like to know what can you see with a scope, whenever you can of course. This topic may be useful for many neopixel users that encounter strange behaviors with them


Code: Select all

#include <Adafruit_NeoPixel.h>
#define PIN_NEOPIXELS PB15
#define PIN_T3_CH_3_PWM PB0_ALT1

#define CONF_DELAY_MS_NEOPIXELS_PARA_SHOW 1
#define DATOS_NUM_NEOPIXELS_RELOJ 30

Adafruit_NeoPixel neopixels_(DATOS_NUM_NEOPIXELS_RELOJ, PIN_NEOPIXELS, NEO_GRBW + NEO_KHZ800);

uint32_t T3_channel_3;
HardwareTimer *gTimer3PWM = new HardwareTimer(TIM3); 


void setup() {
    pinMode(PIN_NEOPIXELS, OUTPUT); 
    neopixels_.begin();

    T3_channel_3 = STM_PIN_CHANNEL(pinmap_function(digitalPinToPinName(PIN_T3_CH_3_PWM), PinMap_PWM));
        
    gTimer3PWM->setMode(T3_channel_3, TIMER_OUTPUT_COMPARE_PWM1, PIN_T3_CH_3_PWM);   
    gTimer3PWM->setOverflow(10000, MICROSEC_FORMAT); 
    gTimer3PWM->setCaptureCompare(T3_channel_3, 50, PERCENT_COMPARE_FORMAT); 
    gTimer3PWM->attachInterrupt(IRQ_T3_Update_IT_callback); ///OVERFLOW IRQ
    gTimer3PWM->attachInterrupt(T3_channel_3, IRQ_T3_Compare_IT_callback);
    gTimer3PWM->resume();   //<<se activa el timer en PUESTA EN CONTACTO


}

void loop() {

    Set_neopixels_Color_NO_PWM( 210,230,0,100);
    delay(250);
    Set_neopixels_Color_NO_PWM(0,0,0,0);
    delay(1);    
    Set_neopixels_Color_PWM( 80,107,200,90);
    delay(250);
    Set_neopixels_Color_PWM(0,0,0,0);
    delay(1);

}



       
void Set_neopixels_Color_NO_PWM(byte a_r, byte a_g, byte a_b, byte a_w){
    /*aca se quiere poner un color de neopixels, que puede venir de un array, o con los bytes individuales*/
    byte byte_r = 0;
    byte byte_g = 0;
    byte byte_b = 0;
    byte byte_w = 0;

    byte_r = a_r;
    byte_g = a_g;
    byte_b = a_b;
    byte_w = a_w;

    for(int i_led=0; i_led<DATOS_NUM_NEOPIXELS_RELOJ; i_led++) {
        neopixels_.setPixelColor(i_led, neopixels_.Color(byte_r, byte_g, byte_b, byte_w));
        //DEF_DELAY_MICROSECONDS_ENTRE_NEOPIXELS
    }///for

    TIM3->CR1 &= ~TIM_CR1_CEN;
    TIM3->CCER &= ~(TIM_CCER_CC1E | TIM_CCER_CC2E | TIM_CCER_CC3E | TIM_CCER_CC4E);
    
    neopixels_.show();

    TIM3->CCER |= (TIM_CCER_CC1E | TIM_CCER_CC2E | TIM_CCER_CC3E | TIM_CCER_CC4E);
    TIM3->CR1 |= TIM_CR1_CEN;   //        

    delay(CONF_DELAY_MS_NEOPIXELS_PARA_SHOW); ///ver este delay

}


void Set_neopixels_Color_PWM(byte a_r, byte a_g, byte a_b, byte a_w){
    /*aca se quiere poner un color de neopixels, que puede venir de un array, o con los bytes individuales*/
    byte byte_r = 0;
    byte byte_g = 0;
    byte byte_b = 0;
    byte byte_w = 0;

    byte_r = a_r;
    byte_g = a_g;
    byte_b = a_b;
    byte_w = a_w;

    for(int i_led=0; i_led<DATOS_NUM_NEOPIXELS_RELOJ; i_led++) {
        neopixels_.setPixelColor(i_led, neopixels_.Color(byte_r, byte_g, byte_b, byte_w));
        //DEF_DELAY_MICROSECONDS_ENTRE_NEOPIXELS
    }///for

    
    neopixels_.show();
    delay(CONF_DELAY_MS_NEOPIXELS_PARA_SHOW); ///ver este delay

}

void IRQ_T3_Update_IT_callback(void){ // Update event correspond to Rising edge of PWM when configured in PWM1 mode
}

void IRQ_T3_Compare_IT_callback(void){ // Compare match event correspond to falling edge of PWM when configured in PWM1 mode
}