cancel
Showing results for 
Search instead for 
Did you mean: 

WS2812 library does not work on STM32F40

AD_716
Associate III

Hello,

I've been trying to find a solution on the forums for several hours now, to no avail!
I have just put my programs on STM32F401, I compile with MBED Studio. I used to light my WS2812 LEDs with a KL25Z (NXP) always using Mbed Studio and my library worked perfectly!

But with the STM32, I couldn't get the LEDs to work, so I did some tests on different pins, with no success.
that the signal wasn't square...
I concluded that the code was wrong. After several searches I see that you have to activate certain functions to use the WS2812 with STM32 but I don't understand what's missing from my code.

Can you please help me?

Thank you in advance,
Sincerely
Antoine

Here is the program:

WS2812.h

#ifndef MBED_WS2812_H
#define MBED_WS2812_H

#include "mbed.h"

class WS2812
    {
        private:
            
            void int_to_binary(int value, int* buffer, int bufferSize);
            
            DigitalOut Data;
            
            int nLed;
            
        public:                
            
            void WriteDataOut(int dataLed[]); 
        
            WS2812(PinName _data, int _nLed);

    };

#endif

WS2812.cpp

#include <mbed.h>

#include "WS2812.h"

WS2812::WS2812(PinName _data, int _nLed):
        Data(_data),
        nLed(_nLed)
    {}

void WS2812::int_to_binary(int value, int* buffer, int bufferSize) 
    {
        int *nextChar = buffer + bufferSize-1;//................location to write the least significant bit
     
        for (int i = 0; i<(bufferSize); i++) 
            {                    // for each bit
                if(value & (1<<i)) {*nextChar  = 1;} else{*nextChar  = 0;} // if set set to '1' else '0'
                nextChar --;
            }
    }
    
void WS2812::WriteDataOut(int dataLed[])
    {
        const int nBit8 = 8,
                  nBytePerLed = 3,
                  nBit = nLed * (nBytePerLed * nBit8);
        
        int data[nBit+1];
        int buff[nBit8];
        
        for(int x = 0; x < nLed; x ++)
            {
                int_to_binary(dataLed[x * nBytePerLed], buff, nBit8);
                for(int bit = 0; bit < 8; bit ++){data[bit + (x * (nBit8 * nBytePerLed))] = buff[bit];}        
                        
                int_to_binary(dataLed[(x * nBytePerLed)+1], buff, nBit8);
                for(int bit = 0; bit < 8; bit ++){data[bit + (x * (nBit8 * nBytePerLed)) + nBit8] = buff[bit];}
                
                int_to_binary(dataLed[(x * nBytePerLed)+2], buff, nBit8);
                for(int bit = 0; bit < 8; bit ++){data[bit + (x * (nBit8 * nBytePerLed)) + (2 * nBit8)] = buff[bit];}
            }
            
    __disable_irq();
    
    for (int i = 0; i < (nLed * (nBytePerLed * nBit8)); i++) 
        {
            int j = 0;
            if (data[i])
                {
                    Data = 1;
                    for (; j < 5; j++) {__nop();}
                    Data = 0;
                    for (; j < 0; j++) {__nop();}
                } 
            else 
                {
                    Data = 1;
                    for (; j < 0; j++) {__nop();}
                    Data = 0;
                    for (; j < 5; j++) {__nop();}
            }
        }
    
    __enable_irq();

    }
    

Main.cpp

#include "mbed.h"

#include "WS2812.h"


DigitalIn bpUser(PA_10);

WS2812 WSLed(PA_14, 2);



int buff[6] = {50, 100, 150, 200, 127, 255};


int main()
{
    WSLed.WriteDataOut(buff);
    

    while(1) {
        if (bpUser == 0) 
            {

            }

}

 

1 ACCEPTED SOLUTION

Accepted Solutions

You cannot use any UART above 9600 baud if you disable interrupts for 1.2 ms; data received by UART during the WS2812 transfer will be lost.

There are 3 SPI modules in F401. Do you really use all three in your application? For WS2812 you only need one SPI pin - MOSI. The others may be used as GPIO or any other function.

DMA is simple, just read the proper section in the Reference Manual and program it - 6 lines of code should be enough to setup the transfer using register-level programming.

View solution in original post

13 REPLIES 13

Where do you write to the pin?

What is the frequency it needs to hit?

Wouldn't most build a pattern table in RAM and feed it as a PWM via DMA+TIM ?

Tips, Buy me a coffee, or three.. PayPal Venmo
Up vote any posts that you find helpful, it shows what's working..

Hello,

Thank you for your reply

The pin used is PA_14, on an STM32F401RE.
There is no predefined frequency because it is clocked directly in the function.

I don't understand why this wasn't a problem under the old MCU, but here at STM it seems to be very complicated to drive a simple WS2812...

I don't know where to look, I've seen quite a few topics on DMA and PWM but I can't manage to include it in my code.

What do you recommend?

Sincerely
Antoine

AD_716
Associate III

Hello,

Merci de votre réponse

I read this post, it seeks like me to make a PWM for a WS2812 but on any pin, without going through SPI or UART.
But I don't see any solution at the end of the reading, am I missing something?

What can I do?

What do you recommend?

Best regards
Antoine

Pavel A.
Evangelist III

@AD_716 Your code in WS2812.cpp lines 52, 59 has Undefined Behavior (UB). It relies on overrun of a signed integer so that it changes sign.  The mbed IDE uses a different compiler than CubeIDE, it may implement this UB differently. IIRC, mbed uses one of Keil compilers - v5 or v6.

Recommendation is, obviously: enable advanced compiler warnings, use 3rd party code analysis tools (this one gets good reviews). Debug.

 

Hello and thank you for your reply,

I found the solution to my problem based on the datasheets of the WS2812 and my oscilloscope. As the STM32 was probably faster, the exposure times created by the __NOP() function were too short and so the frame sent was wrong.
So I corrected using the value readings on the oscilloscope to recreate the correct pause times.
But I still have a question: how long does __NOP() last?
Is it the time of a cycle? Or 1/F = cycle time?
My MCU is an STM32F401RE, and I understand that the maximum frequency is 84 Mhz.
So 1/84 Mhz = 11.90 ns, is that the time of a cycle?

In my case, if I loop 14 times on __NOP(), it takes 1.24us or 1240 ns.
But if I do 14 x 11.90 ns, I get 167 ns...
Does anyone have an explanation?

Thanks in advance

 

    __disable_irq();
    
    for (int i = 0; i < (nLed * (nBytePerLed * nBit8)); i++) 
        {
            int j = 0;
            Data = 1;
            if (data[i])
                {// ONE
                    for (; j < 8; j++) {__nop();} //......i = 8: 660ns
                    Data = 0;
                    for (; j < 14; j++) {__nop();}//......i = 14: 1.24us
                } 
            else 
                {// ZERO
                    for (; j < 3; j++) {__nop();}//.......i = 3: 380ns
                    Data = 0;
                    for (; j < 13; j++) {__nop();}//......i = 3: 1.20us
            }
        }
    
    __enable_irq();
gbm
Lead III

You are missing many important points here. With ANY modern processor you should never rely on the execution time of a particular instruction. It's not a question of how many clock cycles are needed for NOP, but rather what the compiler will do with the for () loop - and this depends on so many factors that it cannot me made portable. If you change the compiler, upgrade the compiler version or change the optimization level, you must recalculate the parameters of your fine-tuned loops. Also, the faster MCUs contain some forms of instruction buffers or caches, so the execution time of a sequence of instructions is practically unpredictable.

Even if the code above code works, the interrupts must be blocked during its execution, so any other part of code which needs a reliable time base will break. In particular, even the SysTick timer interrupts will be missed, and of course you will loose any data sent via UART (so it's not possible to use this approach with DMX512 or Modbus communicating via RS485).

There are 3 reliable ways of implementing WS2812 or any similar protocol on any contemporary MCU:

1. Using UART (the best) - requires negation on TX (not available in F1, F2, F4; available in all newer STM series starting with F0) - 3 WS2812 bits encoded in a byte

2. Using SPI (not bad) - requires the MCU frequency suitable for the protocol (SPI clock should be 2.4 MHz +-15%), 8 WS2812 bits encoded in 3 bytes.

3. Using timer PWM output (the worst) - single WS2812 bit encoded in 16 bits of data, which leads to huge memory usage, especially for long strings of WS2812.

For a string of 1000 WS2812 you need 8 kB of data with UART, 9 kB with SPI, 48 kB with a timer. In all cases you should use DMA; SPI can be implemented without DMA, for UART it's hard but possible with careful programming. With timer, only possible if interrupts are blocked (= not usable really).

AD_716
Associate III

Thank you for your feedback,

I understand, but it's impossible for me to use SPI and UART because they're reserved for other applications.
The maximum number of WS2812 is 40, which I think is still reasonable (960 Bit).
Disable irq disables all interruptions, I know!

I have to use a timer, but I don't know what to do with DMA. The only examples are with CubeProgrammer and I program with Mbed Studio.

Can you tell me more?

Thanks in advance

You cannot use any UART above 9600 baud if you disable interrupts for 1.2 ms; data received by UART during the WS2812 transfer will be lost.

There are 3 SPI modules in F401. Do you really use all three in your application? For WS2812 you only need one SPI pin - MOSI. The others may be used as GPIO or any other function.

DMA is simple, just read the proper section in the Reference Manual and program it - 6 lines of code should be enough to setup the transfer using register-level programming.