Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

drivers/ws281x: add SysTick + GPIO LL backend #20646

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cpu/cortexm_common/Makefile.features
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ FEATURES_PROVIDED += libstdcpp
FEATURES_PROVIDED += newlib
FEATURES_PROVIDED += periph_flashpage_aux
FEATURES_PROVIDED += periph_pm
FEATURES_PROVIDED += periph_systick
FEATURES_PROVIDED += puf_sram
FEATURES_PROVIDED += picolibc
FEATURES_PROVIDED += ssp
Expand Down
9 changes: 8 additions & 1 deletion drivers/ws281x/Makefile.dep
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Actually |(periph_timer_poll and periph_gpio_ll), but that's too complex for FEATURES_REQUIRED_ANY to express
FEATURES_REQUIRED_ANY += cpu_core_atmega|arch_esp32|arch_native|periph_timer_poll
FEATURES_REQUIRED_ANY += cpu_core_atmega|arch_esp32|arch_native|periph_systick|periph_timer_poll

ifeq (,$(filter ws281x_%,$(USEMODULE)))
ifneq (,$(filter cpu_core_atmega,$(FEATURES_USED)))
Expand All @@ -11,6 +11,9 @@ ifeq (,$(filter ws281x_%,$(USEMODULE)))
ifneq (,$(filter arch_esp32,$(FEATURES_USED)))
USEMODULE += ws281x_esp32
endif
ifneq (,$(filter periph_systick,$(FEATURES_USED)))
USEMODULE += ws281x_systick_gpio_ll
endif
# Not only looking for the used feature but also for the absence of any more specific driver
ifeq (-periph_timer_poll,$(filter ws281x_%,$(USEMODULE))-$(filter periph_timer_poll,$(FEATURES_USED)))
USEMODULE += ws281x_timer_gpio_ll
Expand Down Expand Up @@ -38,5 +41,9 @@ ifneq (,$(filter ws281x_timer_gpio_ll,$(USEMODULE)))
FEATURES_REQUIRED += periph_gpio_ll periph_timer periph_timer_poll
endif

ifneq (,$(filter ws281x_systick_gpio_ll,$(USEMODULE)))
FEATURES_REQUIRED += periph_gpio_ll periph_systick
endif

# It would seem xtimer is always required as it is used in the header...
USEMODULE += xtimer
9 changes: 9 additions & 0 deletions drivers/ws281x/include/ws281x_backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ extern "C" {
#endif
/** @} */

/**
* @name Properties of the systick_gpio_ll backend.
* @{
*/
#ifdef MODULE_WS281X_SYSTICK_GPIO_LL
#define WS281X_HAVE_INIT (1)
#endif
/** @} */

#ifdef __cplusplus
}
#endif
Expand Down
119 changes: 119 additions & 0 deletions drivers/ws281x/systick_gpio_ll.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright 2024 Marian Buschsieweke
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/

/**
* @ingroup drivers_ws281x
*
* @{
*
* @file
* @brief Implementation of the WS281x abstraction based on GPIO_LL and timers
*
* @author Marian Buschsieweke <marian.buschsieweke@posteo.net>
*
* @}
*/
#include <assert.h>
#include <errno.h>
#include <stdint.h>
#include <string.h>

#include "clk.h"
#include "cpu.h"
#include "irq.h"
#include "macros/math.h"
#include "periph/gpio_ll.h"
#include "time_units.h"

#include "ws281x.h"
#include "ws281x_params.h"
#include "ws281x_constants.h"

#define ENABLE_DEBUG 0
#include "debug.h"

static void _systick_start(uint32_t ticks)
{
/* disable SysTick, clear value */
SysTick->CTRL = 0;
SysTick->VAL = 0;
/* prepare value in re-load register */
SysTick->LOAD = ticks;
/* start and wait for the load value to be applied */
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk;
while (SysTick->VAL == 0) { /* Wait for SysTick to start and spin */ }
maribu marked this conversation as resolved.
Show resolved Hide resolved
}

static void _systick_wait(void)
{
while (!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk)) { /* busy wait */ }
}

void ws281x_write_buffer(ws281x_t *dev, const void *buf, size_t size)
{
assert(dev);

/* the high time for one can be as high as 5 seconds in practise, so
* rather be on the high side by adding a few CPU cycles. */
const uint32_t ticks_one = DIV_ROUND_UP((uint64_t)WS281X_T_DATA_ONE_NS * (uint64_t)coreclk(), NS_PER_SEC) + 16;

Check warning on line 63 in drivers/ws281x/systick_gpio_ll.c

View workflow job for this annotation

GitHub Actions / static-tests

line is longer than 100 characters
/* the low time should rather be on the short side, so rounding down */
const uint32_t ticks_zero = (uint64_t)(WS281X_T_DATA_ZERO_NS - 50) * (uint64_t)coreclk() / NS_PER_SEC;

Check warning on line 65 in drivers/ws281x/systick_gpio_ll.c

View workflow job for this annotation

GitHub Actions / static-tests

line is longer than 100 characters
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For 48 MHz this is:

(325 - 50) * 48 * 10^6 / 10^9 = 13.2

Copy link
Contributor

@kfessel kfessel May 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so 13 cyles for one wait ( which has (a load, a compare and a jump back if bit 16 isn't set) i think this is plenty of time to do this multiple times.

the short wait (300ns) has exact the same number of wait as with your code but it does not do a setup in the low period but does just three short waits instead.

i thought you where talking about 13cycle for the 1 µsecond

/* the remaining time doesn't matter to much, should only be enough for the
* LEDs to detect the low phase. And not way to much to be detected as
* reset */
const uint32_t ticks_bit = DIV_ROUND((uint64_t)WS281X_T_DATA_NS * (uint64_t)coreclk(), NS_PER_SEC);

Check warning on line 69 in drivers/ws281x/systick_gpio_ll.c

View workflow job for this annotation

GitHub Actions / static-tests

line is longer than 100 characters

const uint8_t *pos = buf;
const uint8_t *end = pos + size;

gpio_port_t port = gpio_get_port(dev->params.pin);
uword_t mask = 1U << gpio_get_pin_num(dev->params.pin);

unsigned irq_state = irq_disable();
while (pos < end) {
uint8_t data = *pos;
for (uint8_t cnt = 8; cnt > 0; cnt--) {
uint32_t ticks_high = (data & 0x80) ? ticks_one : ticks_zero;
uint32_t ticks_low = ticks_bit - ticks_high;
_systick_start(ticks_high);
gpio_ll_set(port, mask);
_systick_wait();
gpio_ll_clear(port, mask);
_systick_start(ticks_low);
_systick_wait();
Comment on lines +87 to +88
Copy link
Contributor

@kfessel kfessel May 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh i see where something precise is need (high time) there are only: set; wait and clr. and for the low time the inprecise start; wait; and the next start is placed

instead of reconfiguring the systick each time; i would start the systick once configured to some interval that cover high and low and wait for the count flag to raise to raise n times.

eg.:

set the systick to 100ns
for  each bit b in byte do

set gpio 

if b == 1 
3 times  syst_wait()
else 
6 times syst_wait()
endif 

clear gpio 

if b == 1 
6 times  syst_wait()
else 
3 times syst_wait()
endif

done

this probably makes the whole thing less jittery

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That adds a branch in the timing sensitive phase and waiting for 300 ns on a STM32F072 is already pretty challenging with one time starting the systick timer. I doubt that you can start the systick timer three times within that time budget.

Copy link
Contributor

@kfessel kfessel May 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not starting you just have it running and wait for n ticks

this is much faster then what you are currently doing ( and also kindof dumber (a little more the stupid of KISS)

maybe the c version in the next comment clears this up

the 3 and the 6 where guess numbers looking at the manual it is

300ns

bit high low
1 2 waits 2 waits
0 1 wait 3 waits

or 400 ns

(ws2512b version 2 but it seems that that should also work with most valid v1 timings)

bit high low
1 2 waits 1 wait
0 1 wait 2 waits

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the manuals that i found that had tolerances with the highs an lows would also be fine with the 300 ns and the 1: 2/3 1/3 and 0: 2/3 1/3 ( going close to the tolerances)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the argument to split a single wait into three?

In any case: Each of the three waits would have to spin until a flag is set, clear the flag, and spin again. On the nucleo-f072rb the total wait is 13 cycles. Splitting that into three waits means 4 CPU cycles per wait. That is not possible to implement.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://godbolt.org/z/4nn3a3e7n
see function t2 the wait loop has 2 instructions

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but a conditional branch will take more than one CPU cycle.

And again: Why divide a single wait? It adds lines or code, bytes to .text. And why into three waits? Wouldn't 42 or 1337 be much better numbers?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because the has timing that are 1/4:3/4 or 1/3:2/3

you can put the branch outside of the time critical path

when i wrote the first suggestion i wasn't aware that we run the nucleo 72 @ 12MHz (I am still not sure but 13 cycles in about a microseconds would fit 12MHz)

but the reconfiguration of the systick is at least 7 instructions (if the register adresses are loaded) and a wait of unknown length

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it about not reconfiguring the systimer

this spreads the waits a little better so that they each have least amount of empty loops

    _systick_start( 4 );
    for (uint8_t cnt = 8; cnt > 0; cnt--) {
        int bit = !!(data & 0x80);
        if(!bit)
        {
            _systick_wait();
            gpio_ll_set();
            _systick_wait();
            gpio_ll_clear();
            _systick_wait();
        }else{
            _systick_wait();
            gpio_ll_set();
            _systick_wait();
            _systick_wait();
            gpio_ll_clear();
        }
        data <<= 1;
        _systick_wait();
    }

data <<= 1;
}
Comment on lines +80 to +90
Copy link
Contributor

@kfessel kfessel May 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets try to rewrite my pseudo code in pseudo c

Suggested change
for (uint8_t cnt = 8; cnt > 0; cnt--) {
uint32_t ticks_high = (data & 0x80) ? ticks_one : ticks_zero;
uint32_t ticks_low = ticks_bit - ticks_high;
_systick_start(ticks_high);
gpio_ll_set(port, mask);
_systick_wait();
gpio_ll_clear(port, mask);
_systick_start(ticks_low);
_systick_wait();
data <<= 1;
}
_systick_start( 300ns );
for (uint8_t cnt = 8; cnt > 0; cnt--) {
int bit = !!(data & 0x80);
gpio_ll_set(port, mask);
if(!bit){
_systick_wait();
}else{
_systick_wait();_systick_wait();_systick_wait();
}
gpio_ll_clear(port, mask);
if(!bit){
_systick_wait();_systick_wait();
}else{
_systick_wait();_systick_wait();
}
data <<= 1;
}

Copy link
Contributor

@kfessel kfessel May 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and then there is the potential use of the systick irq

global byte
global count

systick_irq(){
if(count % 4 = 0)  gpio_set();
else if (count % 4 =1 && !(byte & 0x80 >> (count / 4))) gpio_clr;
else if (count % 4 = 2 ) gpio_clr;
count++;
if count >= 4 * 8 load next byte and reset count
)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overhead of entering the IRQ, backing up the state etc. takes way longer than the total time budget

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok the irq thing might not be the right choice if you got 4 cycles

pos++;
}

irq_restore(irq_state);
}

int ws281x_init(ws281x_t *dev, const ws281x_params_t *params)
{
int err;

if (!dev || !params || !params->buf) {
return -EINVAL;
}

memset(dev, 0, sizeof(ws281x_t));
dev->params = *params;

gpio_port_t port = gpio_get_port(dev->params.pin);
uint8_t pin = gpio_get_pin_num(dev->params.pin);

err = gpio_ll_init(port, pin, gpio_ll_out);
DEBUG("Initializing port %x pin %d (originally %x): %d\n",
port, pin, (unsigned)params->pin, err);
if (err != 0) {
return -EIO;
}

return 0;
}
2 changes: 2 additions & 0 deletions features.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,8 @@ groups:
only provided by some STM32 MCUs.)
- name: periph_pio
help: A Programmable IO (PIO) is present. (Currently only RP2040)
- name: periph_systick
help: A SysTick timer is present. (All Cortex M MCUs)

groups:
- title: General-Purpose Input/Output (GPIO)
Expand Down
1 change: 1 addition & 0 deletions makefiles/features_existing.inc.mk
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ FEATURES_EXISTING := \
periph_spi_on_qspi \
periph_spi_reconfigure \
periph_spi_stmod \
periph_systick \
periph_temperature \
periph_timer \
periph_timer_periodic \
Expand Down
Loading