Page 2 of 4

Re: PWM and code in the main loop

Posted: Sat Mar 15, 2025 5:31 am
by ag123
well, off topic, I'm thinking that if we'd re-implement the neopixel drivers, 2 possible ways are:

- with the SPI method, use DMA instead. That'd alleviate the need to use systick timers for this purpose.

- with Timers method, this is likely more difficult to implement. Normally, for timers, it is possible for timers to initiate DMA.

This is well known but that it is mainly used to transfer data across parallel gpios (send / receive).

The other way which I'm not sure if it is feasible is to update the timer registers in particular the CCR *and* overflow register using DMA.

Based on the specs for WS2812B
https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf
it'd seemed that the period is constant and the difference is mainly the duty cycle period to differentiate '1' and '0' symbols.
This would seemed to say that it may be possible to update just CCR using DMA to drive neopixels.
But that I've not tried it and I'm not sure if it's possible.

using hardware timers is a rather interesting proposition as if hardware timers can be used to drive neopixels, it may resolve the timing gitter issues as they are clock / crystal driven.

Re: PWM and code in the main loop

Posted: Sat Mar 15, 2025 5:33 am
by ozcar
Julian,

So I could know what situation I was looking at, I changed the loop() processing to only do one test at a time, and I started with this:

Code: Select all

Set_neopixels_Color_NO_PWM( 0x55, 0x55, 0x55, 0x55);
That gives a nice alternating 01010101... binary pattern.

First thing I see is that there is a burst of pulses that lasts for around 1212μs. For 800KHz LEDs the target period is 1.25μs, and 30 * 4 * 8 * 1.25 = 1200μs. So pretty good in that regard. Also this did not change much when I subsequently tried with the PWM running.

For 800KHz LEDs the target pulse widths for 0 and 1 bits are 400ns and 800ns respectively. So, you'd expect to see it start out with pulse widths like this:

Code: Select all

400 800 400 800 400 800 400 800 ...
Well, not those exact numbers but more like closest multiple of 13.888888ns (from the 72MHz cycle time).

For the NO_PWM case, I got times like this:

Code: Select all

470 568 278 666 180 666 178 666 ...
Of course, don't expect those times to be accurate to the ns.

Some of those times are way off target, and are not very consistent. Some of the pulses might even be outside the official specs. That is if you can find trustworthy specs for the actual LEDs you have. However, it could still work in practice. Tim, the author of another pixel LED library (which unfortunately does not support STM32 very well, as far as I know), has done all sorts of experiments to try to work out how these sort of LEDs work internally. Starting point https://cpldcpu.com/2014/01/14/light_ws ... he-ws2812/, but he has several more related pages there. His take on it is that you can "get away with" straying outside of the specification in some ways. Taking his observations into account, it doesn't look too bad. However I have no LEDs attached to see. I don't have any RGBW LEDs, only RGB ones, so I'd have to make some changes anyway, and besides the LED specs do vary a bit in regard to the timing.

For the PWM case and the same 0x55 data, I saw pulse times like this:

Code: Select all

402 596 206 694 208 598 304 598 ...
Really not very different. Noting that your PWM frequency was a leisurely 100Hz, I increased the frequency. At 100kHz it was still much the same. I even pushed it up to 500kHz (had to remove the T3_channel_3 attachInterrupt, because with that in place loop() processing was not getting a chance to run at all).

So, I don't see any evidence that the timer PWM causes trouble just because it is running.

The actual times it gets are almost always below target. I think I saw that long ago when I looked at this library before. You might get away with the "0" time being below target, but it might be worth it to try bumping the "1" target time up from 800ns to 900ns or 950ns.

For sure that code could be improved to get less variation in the times, but I'd be making sure that the problem is not somewhere else before going down that path. Did you eliminate voltage level as a possible contributing factor?

Re: PWM and code in the main loop

Posted: Sat Mar 15, 2025 5:49 am
by ozcar
There are many code fragments posted in old threads here, some of which produced rock-steady timing.
ag123 wrote: Sat Mar 15, 2025 5:31 am . . .
The other way which I'm not sure if it is feasible is to update the timer registers in particular the CCR *and* overflow register using DMA.

I found it was possible to use a timer (assuming available to use), like a normal PWM generator, but with no interrupt or DMA to set the pulse times, instead just doing it in the code with something like:

Code: Select all

while( !(TIMx->SR & TIM_SR_UIF) );  // wait for update event
  (set CCR for next pulse time)
You could change ARR as well, but I'm not convinced that is necessary.

Re: PWM and code in the main loop

Posted: Sat Mar 15, 2025 5:55 am
by ag123
an off-topic question, as ws2812b tends to be based on 5v
https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf
with a V(IH) of 0.7 VDD ~ 3.5v if VDD = 5v.

Do anyone think it may be feasible to drive that simply using a 1k pull up to 5v?
otherwise it may take a 74LVC1G17
https://www.ti.com/lit/ds/symlink/sn74lvc1g17.pdf
https://assets.nexperia.com/documents/d ... VC1G17.pdf
to interface it.

they are apparently pretty available on Aliexpress
https://www.aliexpress.com/w/wholesale-74LVC1G17.html

Re: PWM and code in the main loop

Posted: Sat Mar 15, 2025 1:23 pm
by julian_lpp
First of all thanks ozcar to taking the time to do the tests.

I think that the only reasonable conlcusion that we came into it that the adafruit implementation must be improved (besides it's not a new conclusion given all the topis related to neopixel problems). It's hard to accept that rock solid time pulse cant be acheived, so I'm starting to think that DMA PWM is the way to go. Another aproach would be assembler and block any IRQ, but i'm thinking in old PIC times when we can calculate how many cycles would an instruction spend (of course I forgot completely how to do that, just remember it was posible back in the days I wrote my own small compiler and implemented the "delay" function, just wasting exact number of cycles in assembler).


ozcar wrote: Sat Mar 15, 2025 5:33 am So, I don't see any evidence that the timer PWM causes trouble just because it is running.
very interesting. I probably came into a wrong conclusion, just beacause it is working better disabling pwn.. who knows
ozcar wrote: Sat Mar 15, 2025 5:33 am For sure that code could be improved to get less variation in the times, but I'd be making sure that the problem is not somewhere else before going down that path. Did you eliminate voltage level as a possible contributing factor?
Absolutely right. I'm back a mess of wires that comes and goes on a protoboard, the power source is the stlink v2 itself through usb, so doing a decent pcb with a decent psu is a must to keep investigating.

thanks again for your time
regards
julian

Re: PWM and code in the main loop

Posted: Sat Mar 15, 2025 1:27 pm
by julian_lpp
ozcar wrote: Sat Mar 15, 2025 5:49 am
I found it was possible to use a timer (assuming available to use), like a normal PWM generator, but with no interrupt or DMA to set the pulse times, instead just doing it in the code with something like:

Code: Select all

while( !(TIMx->SR & TIM_SR_UIF) );  // wait for update event
  (set CCR for next pulse time)
You could change ARR as well, but I'm not convinced that is necessary.
ozcar do you think this is much different to the sysTick approach? It looks to me pretty much the same "tactic"
regards.
julian

Re: PWM and code in the main loop

Posted: Sat Mar 15, 2025 6:18 pm
by ag123
julian_lpp wrote: Sat Mar 15, 2025 1:27 pm
ozcar wrote: Sat Mar 15, 2025 5:49 am
I found it was possible to use a timer (assuming available to use), like a normal PWM generator, but with no interrupt or DMA to set the pulse times, instead just doing it in the code with something like:

Code: Select all

while( !(TIMx->SR & TIM_SR_UIF) );  // wait for update event
  (set CCR for next pulse time)
You could change ARR as well, but I'm not convinced that is necessary.
ozcar do you think this is much different to the sysTick approach? It looks to me pretty much the same "tactic"
regards.
julian
well, it may not really be 'the same tactic'.
Assuming that you are using stm32f103, so the ref manual is rm0008
https://www.st.com/resource/en/referenc ... ronics.pdf
15.3.8 Output compare mode, page 386
15.3.8 Output compare mode
This function is used to control an output waveform or indicating when a period of time has
elapsed.
When a match is found between the capture/compare register and the counter, the output
compare function:
...
- Sends a DMA request if the corresponding enable bit is set (CCxDE bit in the
TIMx_DIER register, CCDS bit in the TIMx_CR2 register for the DMA request
selection).
The TIMx_CCRx registers can be programmed with or without preload registers using the
OCxPE bit in the TIMx_CCMRx register.
and
15.4.4 TIMx DMA/Interrupt enable register (TIMx_DIER) page 409
Bit 8 UDE: Update DMA request enable
0: Update DMA request disabled.
1: Update DMA request enabled.
15.4.13 TIMx capture/compare register 1 (TIMx_CCR1) page 419
Bits 15:0 CCR1[15:0]: Capture/Compare 1 value
If channel CC1 is configured as output:
CCR1 is the value to be loaded in the actual capture/compare 1 register (preload value).
It is loaded permanently if the preload feature is not selected in the TIMx_CCMR1 register
(bit OC1PE). Else the preload value is copied in the active capture/compare 1 register when
an update event occurs.
The active capture/compare register contains the value to be compared to the counter
TIMx_CNT and signaled on OC1 output.
now this is theory as I've not tried it:
  • configure the timer appropriately for output compare mode
  • if you configure the timer to trigger DMA on the update event (15.4.4 TIMx DMA/Interrupt enable register above),
    this implies it'd trigger DMA for every period.
  • you would need to configure DMA separately e.g. to connect to the timer and to read from a buffer and write to an address / register
    i.e. the timer register 15.4.13 TIMx capture/compare register 1 (TIMx_CCR1) page 419
  • with this you can keep the whole list of 'duty cycle' sequence in the DMA buffer, start DMA from the Timer, and the timer would start to trigger
    DMA for every update event (period), and that writes the next value to TIMx_CCR1
  • then that your code can simply configure and wait for the DMA half complete / complete interrupts, and fill in the next series of 'duty cycle' sequence in the DMA buffer
in short, it is 'completely hardware driven' i.e. DMA and Timer, but for now this is just 'theory' as I've not tried it.
But that if this works, it can possibly drive the neopixels at pretty high speeds and possibly with complex sequences.
As now your software only need to work on the complex sequences. make a neopixel worm game? well, it'd seem fairly possible.
well, the ready made boards are all there:
https://www.aliexpress.com/w/wholesale- ... atrix.html

it may well be worthwhile to try it out and if it works, perhaps make a 'library' perhaps as a 'contrib' add on to the stm32duino libraries.

come to think about this, if this 'trick' works, DMA + Timer can do pretty elaborate things, e.g. a sigma delta DAC, e.g. directly generating audio etc, I'd guess it may be possible to go to 'low RF' speeds e.g. 1 mhz? that is practically in the 'high ultrasound' and short wave spectrum.
and also to create elaborate complex wave trains, motors etc where there is no 'easy way' to do the same, and neopixels qualifies as one of such.

for what is worth, if we could figure out how to modulate RF signals say at 2.4 ghz, this similar way may make it posisble to 'bit bang BLE bluetooth'
https://en.wikipedia.org/wiki/Frequency-shift_keying
but I'd guess these days, using dedicated ready made BLE chips are far simplier.

Re: PWM and code in the main loop

Posted: Sat Mar 15, 2025 9:58 pm
by ozcar
Lots to cover here...

Doing what I suggested and just looping until you see the update event is the same tactic in that it still requires the code to run with interrupts disabled. That is not ideal, particularly if you have a lot of LEDs, but depending on what else needs to be done maybe this is of no concern (and of course is what the Adafruit library does for most processors, whether you like it or not).

What is different is that it offloads the exact timing to the timer hardware. That is often done using an interrupt generated by the update event, or by DMA triggered by the update event. The timer registers that determine the timing (ARR and CCR) have a "preload" or "buffered write" feature that can be turned on. With that on, updates to the ARR and CCR do not take effect immediately, but go to sort-of shadow registers, and get transferred to the actual registers automatically by timer hardware at the next update event. Because of this, there is a relatively long time to get in and set the new ARR and/or CCR values. For 800KHz LEDS, this "relatively long time" is only 1250ns, but that is about 3 times greater than the length of the "0" pulses that you otherwise have to deal with.

It does also avoid the need to borrow or steal the SysTick timer but of course there has to be an available timer associated with the pin you use.

That is the theory, but does it really help?

For the same alternating 01010101... pattern, with target times of 400ns and 800ns, and using a timer running at the maximum rate of 72MHz on F103, I get:

Code: Select all

400 804 402 804 402 804 402 804 ... 
Which is obviously a lot better than what I saw for the original "tactic". Also, the code is a lot less sensitive, I can compile that with "No Optimization" and it still works OK, while doing the same for the original Adafruit code screwed it up completely.

But why not use the update event interrupt to set the timer register(s)? Sure, and that has been done: viewtopic.php?t=357 . But that was on a much faster STM32F4 processor. For 400kHz LEDs you might manage it on a 72MHz F103 on a good day with the wind behind you, but 800kHz and needing to process one interrupt every 1250ns would be a challenge (feel free to try it).

DMA with a timer? Yes, can be done, and in fact has been done many times (even gets covered in the thread I linked to above). The most simple DMA approach uses quite a lot of RAM, - no problem for 8 LEDs as has been mentioned here, but some people have hundreds of LEDs. There are ways to reduce the RAM usage (also in the thread linked to), but it adds to the complications. Another tricky thing with DMA is having to handle differences between the various STM32 processor families. Hats off to the STM32DUINO team who deal with issues like that every day - it is not something I want to take on.

Re: PWM and code in the main loop

Posted: Sun Mar 16, 2025 3:10 pm
by julian_lpp
well, I'll start investigating about DMA and Timer combination, maybe with gpt help to speed it up, given that never looked into dma stuff, hardly imagine what it is, but need to figure out exacly how it works, how to configure a timer to start reading a buffer and increment its index, the way and order irqs are triggered, etc, etc

All your comments were higly valuable, and as soon as I can get some code to post i''ll do. Hopefully we could contibute with some usable code where neopixels management are part of much complex program, rather than adafruit approach that pretty much is "drive your leds and the rest of the code who knows :lol: "

kind regards
julian

Re: PWM and code in the main loop

Posted: Sun Mar 16, 2025 3:11 pm
by julian_lpp
ozcar wrote: Sat Mar 15, 2025 9:58 pm For the same alternating 01010101... pattern, with target times of 400ns and 800ns, and using a timer running at the maximum rate of 72MHz on F103, I get:

Code: Select all

400 804 402 804 402 804 402 804 ... 
:shock:
almost perfect