Merge pull request #1265 from wnagele/ws2812_improvements
Improve WS2812 timings and add different hardware revision support
This commit is contained in:
commit
bd6f5fbf9d
|
@ -124,6 +124,7 @@ Copyright (c) 2019 vytautasb <v.buitvydas@limemicro.com>
|
||||||
Copyright (c) 2013 Werner Almesberger <werner@almesberger.net>
|
Copyright (c) 2013 Werner Almesberger <werner@almesberger.net>
|
||||||
Copyright (c) 2015-2016 whitequark <whitequark@whitequark.org>
|
Copyright (c) 2015-2016 whitequark <whitequark@whitequark.org>
|
||||||
Copyright (c) 2015-2021 William D. Jones <thor0505@comcast.net>
|
Copyright (c) 2015-2021 William D. Jones <thor0505@comcast.net>
|
||||||
|
Copyright (c) 2022 Wolfgang Nagele <mail@wnagele.com>
|
||||||
Copyright (c) 2013-2014 Yann Sionneau <yann.sionneau@gmail.com>
|
Copyright (c) 2013-2014 Yann Sionneau <yann.sionneau@gmail.com>
|
||||||
Copyright (c) 2015 Yves Delley <hack@delley.net>
|
Copyright (c) 2015 Yves Delley <hack@delley.net>
|
||||||
Copyright (c) 2020 Xiretza <xiretza@xiretza.xyz>
|
Copyright (c) 2020 Xiretza <xiretza@xiretza.xyz>
|
||||||
|
|
|
@ -2,10 +2,9 @@
|
||||||
# This file is part of LiteX.
|
# This file is part of LiteX.
|
||||||
#
|
#
|
||||||
# Copyright (c) 2020-2021 Florent Kermarrec <florent@enjoy-digital.fr>
|
# Copyright (c) 2020-2021 Florent Kermarrec <florent@enjoy-digital.fr>
|
||||||
|
# Copyright (c) 2022 Wolfgang Nagele <mail@wnagele.com>
|
||||||
# SPDX-License-Identifier: BSD-2-Clause
|
# SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
|
||||||
import math
|
|
||||||
|
|
||||||
from migen import *
|
from migen import *
|
||||||
from migen.genlib.misc import WaitTimer
|
from migen.genlib.misc import WaitTimer
|
||||||
|
|
||||||
|
@ -17,6 +16,24 @@ from litex.soc.interconnect import wishbone
|
||||||
_CHASER_MODE = 0
|
_CHASER_MODE = 0
|
||||||
_CONTROL_MODE = 1
|
_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):
|
class LedChaser(Module, AutoCSR):
|
||||||
def __init__(self, pads, sys_clk_freq, period=1e0):
|
def __init__(self, pads, sys_clk_freq, period=1e0):
|
||||||
self.pads = pads
|
self.pads = pads
|
||||||
|
@ -57,6 +74,13 @@ class LedChaser(Module, AutoCSR):
|
||||||
class WS2812(Module):
|
class WS2812(Module):
|
||||||
"""WS2812/NeoPixel Led Driver.
|
"""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
|
Description
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
@ -117,12 +141,12 @@ class WS2812(Module):
|
||||||
sys_clk_freq: int, in
|
sys_clk_freq: int, in
|
||||||
System Clk Frequency.
|
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:
|
if bus_mastering:
|
||||||
self.bus = bus = wishbone.Interface(data_width=32)
|
self.bus = bus = wishbone.Interface(data_width=32)
|
||||||
else:
|
else:
|
||||||
# Memory.
|
# Memory.
|
||||||
mem = Memory(32, nleds)
|
mem = Memory(32, nleds, init=test_data)
|
||||||
port = mem.get_port()
|
port = mem.get_port()
|
||||||
self.specials += mem, port
|
self.specials += mem, port
|
||||||
|
|
||||||
|
@ -138,25 +162,25 @@ class WS2812(Module):
|
||||||
# Internal Signals.
|
# Internal Signals.
|
||||||
led_data = Signal(24)
|
led_data = Signal(24)
|
||||||
bit_count = Signal(8)
|
bit_count = Signal(8)
|
||||||
led_count = Signal(int(math.log2(nleds)))
|
led_count = Signal(max = nleds + 1)
|
||||||
|
|
||||||
# Timings.
|
# Timings
|
||||||
trst = 75e-6
|
self.trst = trst = 285e-6 if hardware_revision == "new" else 55e-6
|
||||||
t0h = 0.40e-6
|
self.t0h = t0h = 0.40e-6
|
||||||
t0l = 0.85e-6
|
self.t1h = t1h = 0.80e-6
|
||||||
t1h = 0.80e-6
|
self.t0l = t0l = 0.85e-6
|
||||||
t1l = 0.45e-6
|
self.t1l = t1l = 0.45e-6
|
||||||
|
|
||||||
# Timers.
|
# Timers.
|
||||||
t0h_timer = WaitTimer(int(t0h*sys_clk_freq))
|
t0h_timer = ModifiableWaitTimer(int(t0h*sys_clk_freq))
|
||||||
t0l_timer = WaitTimer(int(t0l*sys_clk_freq))
|
t0l_timer = ModifiableWaitTimer(int(t0l*sys_clk_freq) - 1) # compensate for data clk in cycle
|
||||||
self.submodules += t0h_timer, t0l_timer
|
self.submodules += t0h_timer, t0l_timer
|
||||||
|
|
||||||
t1h_timer = WaitTimer(int(t1h*sys_clk_freq))
|
t1h_timer = ModifiableWaitTimer(int(t1h*sys_clk_freq))
|
||||||
t1l_timer = WaitTimer(int(t1l*sys_clk_freq))
|
t1l_timer = ModifiableWaitTimer(int(t1l*sys_clk_freq) - 1) # compensate for data clk in cycle
|
||||||
self.submodules += t1h_timer, t1l_timer
|
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
|
self.submodules += trst_timer
|
||||||
|
|
||||||
# FSM
|
# FSM
|
||||||
|
@ -164,7 +188,6 @@ class WS2812(Module):
|
||||||
fsm.act("RST",
|
fsm.act("RST",
|
||||||
trst_timer.wait.eq(1),
|
trst_timer.wait.eq(1),
|
||||||
If(trst_timer.done,
|
If(trst_timer.done,
|
||||||
NextValue(led_count, 0),
|
|
||||||
NextState("LED-READ")
|
NextState("LED-READ")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -185,11 +208,23 @@ class WS2812(Module):
|
||||||
self.comb += port.adr.eq(led_count)
|
self.comb += port.adr.eq(led_count)
|
||||||
fsm.act("LED-READ",
|
fsm.act("LED-READ",
|
||||||
NextValue(bit_count, 24-1),
|
NextValue(bit_count, 24-1),
|
||||||
|
NextValue(led_count, led_count + 1),
|
||||||
NextValue(led_data, port.dat_r),
|
NextValue(led_data, port.dat_r),
|
||||||
NextState("BIT-TEST")
|
NextState("BIT-TEST")
|
||||||
)
|
)
|
||||||
|
|
||||||
fsm.act("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,
|
If(led_data[-1] == 0,
|
||||||
NextState("ZERO-SEND"),
|
NextState("ZERO-SEND"),
|
||||||
),
|
),
|
||||||
|
@ -215,16 +250,16 @@ class WS2812(Module):
|
||||||
)
|
)
|
||||||
fsm.act("BIT-SHIFT",
|
fsm.act("BIT-SHIFT",
|
||||||
NextValue(led_data, Cat(Signal(), led_data)),
|
NextValue(led_data, Cat(Signal(), led_data)),
|
||||||
NextValue(bit_count, bit_count - 1),
|
|
||||||
If(bit_count == 0,
|
If(bit_count == 0,
|
||||||
NextState("LED-SHIFT")
|
NextState("LED-SHIFT")
|
||||||
).Else(
|
).Else(
|
||||||
|
NextValue(bit_count, bit_count - 1),
|
||||||
NextState("BIT-TEST")
|
NextState("BIT-TEST")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
fsm.act("LED-SHIFT",
|
fsm.act("LED-SHIFT",
|
||||||
NextValue(led_count, led_count + 1),
|
If(led_count == nleds,
|
||||||
If(led_count == (nleds-1),
|
NextValue(led_count, 0),
|
||||||
NextState("RST")
|
NextState("RST")
|
||||||
).Else(
|
).Else(
|
||||||
NextState("LED-READ")
|
NextState("LED-READ")
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
#
|
||||||
|
# This file is part of LiteX.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2022 Wolfgang Nagele <mail@wnagele.com>
|
||||||
|
# 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)
|
Loading…
Reference in New Issue