From 67369403a92b4b02e16216aca69dc2eae7875536 Mon Sep 17 00:00:00 2001 From: Wolfgang Nagele Date: Sun, 3 Apr 2022 12:58:03 +0200 Subject: [PATCH] Improve WS2812 timings and add different hardware revision support --- CONTRIBUTORS | 1 + litex/soc/cores/led.py | 77 ++++++++++++++++++++++++++++----------- test/test_led.py | 83 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 21 deletions(-) create mode 100644 test/test_led.py diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 630cf82f0..1d2a65f71 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -124,6 +124,7 @@ Copyright (c) 2019 vytautasb Copyright (c) 2013 Werner Almesberger Copyright (c) 2015-2016 whitequark Copyright (c) 2015-2021 William D. Jones +Copyright (c) 2022 Wolfgang Nagele Copyright (c) 2013-2014 Yann Sionneau Copyright (c) 2015 Yves Delley Copyright (c) 2020 Xiretza diff --git a/litex/soc/cores/led.py b/litex/soc/cores/led.py index c7c328206..750f447bc 100644 --- a/litex/soc/cores/led.py +++ b/litex/soc/cores/led.py @@ -2,10 +2,9 @@ # This file is part of LiteX. # # Copyright (c) 2020-2021 Florent Kermarrec +# Copyright (c) 2022 Wolfgang Nagele # SPDX-License-Identifier: BSD-2-Clause -import math - from migen import * from migen.genlib.misc import WaitTimer @@ -17,6 +16,24 @@ from litex.soc.interconnect import wishbone _CHASER_MODE = 0 _CONTROL_MODE = 1 + +# Based on: migen.genlib.misc.WaitTimer +class ModifiableWaitTimer(Module): + def __init__(self, t): + self.wait = Signal() + self.done = Signal() + self.reset = Signal(bits_for(t), reset=t) + + # # # + + count = Signal(bits_for(t), reset=t) + self.comb += self.done.eq(count == 0) + self.sync += \ + If(self.wait, + If(~self.done, count.eq(count - 1)) + ).Else(count.eq(self.reset)) + + class LedChaser(Module, AutoCSR): def __init__(self, pads, sys_clk_freq, period=1e0): self.pads = pads @@ -57,6 +74,13 @@ class LedChaser(Module, AutoCSR): class WS2812(Module): """WS2812/NeoPixel Led Driver. + Hardware Revisions + ------------------ + WS2812 hardware has several different revisions that have been released over the years. + Unfortunately not all of them are compatible with the timings they require. Especially + the reset timing has substantial differences between older and newer models. + Adjust the hardware_revision to your needs depending on which model you are using. + Description ----------- @@ -117,12 +141,12 @@ class WS2812(Module): sys_clk_freq: int, in System Clk Frequency. """ - def __init__(self, pad, nleds, sys_clk_freq, bus_mastering=False, bus_base=None): + def __init__(self, pad, nleds, sys_clk_freq, bus_mastering=False, bus_base=None, hardware_revision="old", test_data=None): if bus_mastering: self.bus = bus = wishbone.Interface(data_width=32) else: # Memory. - mem = Memory(32, nleds) + mem = Memory(32, nleds, init=test_data) port = mem.get_port() self.specials += mem, port @@ -138,25 +162,25 @@ class WS2812(Module): # Internal Signals. led_data = Signal(24) bit_count = Signal(8) - led_count = Signal(int(math.log2(nleds))) + led_count = Signal(max = nleds + 1) - # Timings. - trst = 75e-6 - t0h = 0.40e-6 - t0l = 0.85e-6 - t1h = 0.80e-6 - t1l = 0.45e-6 + # Timings + self.trst = trst = 285e-6 if hardware_revision == "new" else 55e-6 + self.t0h = t0h = 0.40e-6 + self.t1h = t1h = 0.80e-6 + self.t0l = t0l = 0.85e-6 + self.t1l = t1l = 0.45e-6 # Timers. - t0h_timer = WaitTimer(int(t0h*sys_clk_freq)) - t0l_timer = WaitTimer(int(t0l*sys_clk_freq)) + t0h_timer = ModifiableWaitTimer(int(t0h*sys_clk_freq)) + t0l_timer = ModifiableWaitTimer(int(t0l*sys_clk_freq) - 1) # compensate for data clk in cycle self.submodules += t0h_timer, t0l_timer - t1h_timer = WaitTimer(int(t1h*sys_clk_freq)) - t1l_timer = WaitTimer(int(t1l*sys_clk_freq)) + t1h_timer = ModifiableWaitTimer(int(t1h*sys_clk_freq)) + t1l_timer = ModifiableWaitTimer(int(t1l*sys_clk_freq) - 1) # compensate for data clk in cycle self.submodules += t1h_timer, t1l_timer - trst_timer = WaitTimer(int(trst*sys_clk_freq)) + trst_timer = ModifiableWaitTimer(int(trst*sys_clk_freq)) self.submodules += trst_timer # FSM @@ -164,7 +188,6 @@ class WS2812(Module): fsm.act("RST", trst_timer.wait.eq(1), If(trst_timer.done, - NextValue(led_count, 0), NextState("LED-READ") ) ) @@ -185,11 +208,23 @@ class WS2812(Module): self.comb += port.adr.eq(led_count) fsm.act("LED-READ", NextValue(bit_count, 24-1), + NextValue(led_count, led_count + 1), NextValue(led_data, port.dat_r), NextState("BIT-TEST") ) fsm.act("BIT-TEST", + # by including the cycles spent on checking for conditions + # data shifting, etc. we make the timing more precise + If(bit_count == 0, + # BIT-SHIFT + LED-SHIFT + LED-READ + BIT-TEST + NextValue(t0l_timer.reset, t0l_timer.reset.reset - 4), + NextValue(t1l_timer.reset, t1l_timer.reset.reset - 4) + ).Else( + # BIT-SHIFT + BIT-TEST + NextValue(t0l_timer.reset, t0l_timer.reset.reset - 2), + NextValue(t1l_timer.reset, t1l_timer.reset.reset - 2) + ), If(led_data[-1] == 0, NextState("ZERO-SEND"), ), @@ -215,18 +250,18 @@ class WS2812(Module): ) fsm.act("BIT-SHIFT", NextValue(led_data, Cat(Signal(), led_data)), - NextValue(bit_count, bit_count - 1), If(bit_count == 0, NextState("LED-SHIFT") ).Else( + NextValue(bit_count, bit_count - 1), NextState("BIT-TEST") ) ) fsm.act("LED-SHIFT", - NextValue(led_count, led_count + 1), - If(led_count == (nleds-1), + If(led_count == nleds, + NextValue(led_count, 0), NextState("RST") ).Else( NextState("LED-READ") ) - ) + ) \ No newline at end of file diff --git a/test/test_led.py b/test/test_led.py new file mode 100644 index 000000000..b87da192f --- /dev/null +++ b/test/test_led.py @@ -0,0 +1,83 @@ +# +# This file is part of LiteX. +# +# Copyright (c) 2022 Wolfgang Nagele +# SPDX-License-Identifier: BSD-2-Clause + +import unittest + +from migen import * + +from litex.soc.cores.led import WS2812 + + +TEST_CLK_FREQS = (20e6, 16e6, 15e6, 10e6) + + +class TestWS2812(unittest.TestCase): + def generator(self, dut, led_signal, led_data, sys_clk_freq, iterations): + error_margin = 0.15e-6 # defined in datasheet + + # cap on how long a sequence will be evaluated + max_cycles_per_seq = int(dut.trst * sys_clk_freq * 2) + + # initial reset + rst_cycles = 0 + for _ in range(max_cycles_per_seq): + if (yield led_signal) != 0: + break + rst_cycles += 1 + yield + rst_time = rst_cycles / sys_clk_freq + assert rst_time >= dut.trst + + length = len(led_data) + for _ in range(iterations): + for i_num, num in enumerate(led_data, start = 1): + for idx_bit, bit in enumerate(TestWS2812.to_bits(num), start = 1): + exp_high, exp_low = (dut.t0h, dut.t0l) if bit == 0 else (dut.t1h, dut.t1l) + + # end of chain reset + if i_num == length and idx_bit == 24: + exp_low += dut.trst + + high_cycles = 0 + for _ in range(max_cycles_per_seq): + if (yield led_signal) != 1: + break + high_cycles += 1 + yield + high_time = high_cycles / sys_clk_freq + assert high_time >= exp_high - error_margin + assert high_time <= exp_high + error_margin + + low_cycles = 0 + for _ in range(max_cycles_per_seq): + if (yield led_signal) != 0: + break + low_cycles += 1 + yield + low_time = low_cycles / sys_clk_freq + assert low_time >= exp_low - error_margin + assert low_time <= exp_low + error_margin + + + def to_bits(num, length = 24): + return ( int(x) for x in bin(num)[2:].zfill(length) ) + + + def run_test(self, hardware_revision, sys_clk_freq): + led_signal = Signal() + led_data = [ 0x100000, 0x200000, 0x300000, 0x400000, 0x500000, 0x600000, 0x700000, 0x800000, 0x900000 ] + iterations = 3 + dut = WS2812(led_signal, len(led_data), sys_clk_freq, hardware_revision = hardware_revision, test_data = led_data) + run_simulation(dut, self.generator(dut, led_signal, led_data, sys_clk_freq, iterations)) + + + def test_WS2812_old(self): + for sys_clk_freq in TEST_CLK_FREQS: + self.run_test("old", sys_clk_freq) + + def test_WS2812_new(self): + for sys_clk_freq in TEST_CLK_FREQS: + self.run_test("new", sys_clk_freq) \ No newline at end of file