Merge pull request #1265 from wnagele/ws2812_improvements

Improve WS2812 timings and add different hardware revision support
This commit is contained in:
enjoy-digital 2022-04-04 15:20:15 +02:00 committed by GitHub
commit bd6f5fbf9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 140 additions and 21 deletions

View File

@ -124,6 +124,7 @@ Copyright (c) 2019 vytautasb <v.buitvydas@limemicro.com>
Copyright (c) 2013 Werner Almesberger <werner@almesberger.net>
Copyright (c) 2015-2016 whitequark <whitequark@whitequark.org>
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) 2015 Yves Delley <hack@delley.net>
Copyright (c) 2020 Xiretza <xiretza@xiretza.xyz>

View File

@ -2,10 +2,9 @@
# This file is part of LiteX.
#
# Copyright (c) 2020-2021 Florent Kermarrec <florent@enjoy-digital.fr>
# Copyright (c) 2022 Wolfgang Nagele <mail@wnagele.com>
# 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")
)
)
)

83
test/test_led.py Normal file
View File

@ -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)