soc/cores/led: Review/Rework #1265.

- Split FSM in Main FSM/Xfer FSM to decouple Led data read from bit xfer and do read during xfer.
- Only keep optimization that are easily to understand.
- Default to new WS2812 revision (Since also works on old revision).
- Test 75/50/25MHz sys_clk_freq.
This commit is contained in:
Florent Kermarrec 2022-04-04 15:24:54 +02:00
parent bd6f5fbf9d
commit d39c3ed626
2 changed files with 114 additions and 117 deletions

View File

@ -1,10 +1,12 @@
#
# This file is part of LiteX.
#
# Copyright (c) 2020-2021 Florent Kermarrec <florent@enjoy-digital.fr>
# Copyright (c) 2020-2022 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
@ -16,24 +18,6 @@ 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
@ -74,13 +58,6 @@ 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
-----------
@ -95,7 +72,7 @@ class WS2812(Module):
- Each Led will "digest" a 24-bit control word: (MSB) G-R-B (LSB).
- Leds can be chained through DIN->DOUT connection.
Each control sequence is separated by a reset code: Line low for > 50us.
Each control sequence is separated by a reset code: Line low for > 50us (old) or > 280us (new).
Ones are transmitted as:
T0H T0H = 400ns +-150ns
@ -107,6 +84,14 @@ class WS2812(Module):
T1L T1L = 450ns +-150ns
Hardware Revisions
------------------
Different revision of WS2812 have been released over the years. Unfortunately not all of them
have compatible timings, especially on reset that has a minimal width of 50us on older models
and 280us on newer models. By default, the core will use the timings of the newer models since
also working on older models. Reset pulse and refresh rate can be improve for older models by
setting revision to "old".
Integration
-----------
@ -141,12 +126,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, hardware_revision="old", test_data=None):
def __init__(self, pad, nleds, sys_clk_freq, bus_mastering=False, bus_base=None, revision="new", init=None):
if bus_mastering:
self.bus = bus = wishbone.Interface(data_width=32)
else:
# Memory.
mem = Memory(32, nleds, init=test_data)
mem = Memory(32, nleds, init=init)
port = mem.get_port()
self.specials += mem, port
@ -160,33 +145,36 @@ class WS2812(Module):
# Internal Signals.
led_data = Signal(24)
bit_count = Signal(8)
led_count = Signal(max = nleds + 1)
led_count = Signal(max=nleds)
led_data = Signal(24)
xfer_start = Signal()
xfer_done = Signal()
xfer_data = Signal(24)
# 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
self.trst = trst = {"old": 50e-6*1.25, "new": 280e-6*1.25}[revision]
self.t0h = t0h = 0.40e-6
self.t0l = t0l = 0.85e-6
self.t1h = t1h = 0.80e-6
self.t1l = t1l = 0.45e-6
# Timers.
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 = 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 = ModifiableWaitTimer(int(trst*sys_clk_freq))
trst_timer = WaitTimer(int(trst*sys_clk_freq))
self.submodules += trst_timer
# FSM
t0h_timer = WaitTimer(int(t0h*sys_clk_freq))
t0l_timer = WaitTimer(int(t0l*sys_clk_freq) - 1) # Compensate Xfer FSM latency.
self.submodules += t0h_timer, t0l_timer
t1h_timer = WaitTimer(int(t1h*sys_clk_freq))
t1l_timer = WaitTimer(int(t1l*sys_clk_freq) - 1) # Compensate Xfer FSM latency.
self.submodules += t1h_timer, t1l_timer
# Main FSM.
self.submodules.fsm = fsm = FSM(reset_state="RST")
fsm.act("RST",
trst_timer.wait.eq(1),
NextValue(led_count, 0),
trst_timer.wait.eq(xfer_done),
If(trst_timer.done,
NextState("LED-READ")
)
@ -199,69 +187,74 @@ class WS2812(Module):
bus.sel.eq(2**(bus.data_width//8)-1),
bus.adr.eq(bus_base[2:] + led_count),
If(bus.ack,
NextValue(bit_count, 24-1),
NextValue(led_data, bus.dat_r),
NextState("BIT-TEST")
NextState("LED-SEND")
)
)
else:
self.comb += port.adr.eq(led_count)
fsm.act("LED-READ",
NextValue(bit_count, 24-1),
NextValue(led_count, led_count + 1),
NextState("LED-LATCH")
)
fsm.act("LED-LATCH",
NextValue(led_data, port.dat_r),
NextState("BIT-TEST")
NextState("LED-SEND")
)
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"),
),
If(led_data[-1] == 1,
NextState("ONE-SEND"),
),
)
fsm.act("ZERO-SEND",
t0h_timer.wait.eq(1),
t0l_timer.wait.eq(t0h_timer.done),
pad.eq(~t0h_timer.done),
If(t0l_timer.done,
NextState("BIT-SHIFT")
)
)
fsm.act("ONE-SEND",
t1h_timer.wait.eq(1),
t1l_timer.wait.eq(t1h_timer.done),
pad.eq(~t1h_timer.done),
If(t1l_timer.done,
NextState("BIT-SHIFT")
)
)
fsm.act("BIT-SHIFT",
NextValue(led_data, Cat(Signal(), led_data)),
If(bit_count == 0,
fsm.act("LED-SEND",
If(xfer_done,
xfer_start.eq(1),
NextState("LED-SHIFT")
).Else(
NextValue(bit_count, bit_count - 1),
NextState("BIT-TEST")
)
)
fsm.act("LED-SHIFT",
If(led_count == nleds,
NextValue(led_count, 0),
If(led_count == (nleds - 1),
NextState("RST")
).Else(
NextValue(led_count, led_count + 1),
NextState("LED-READ")
)
)
# XFER FSM.
xfer_bit = Signal(5)
xfer_fsm = FSM(reset_state="IDLE")
self.submodules += xfer_fsm
xfer_fsm.act("IDLE",
xfer_done.eq(1),
If(xfer_start,
NextValue(xfer_bit, 24-1),
NextValue(xfer_data, led_data),
NextState("RUN")
)
)
xfer_fsm.act("RUN",
# Send a one.
If(xfer_data[-1],
t1h_timer.wait.eq(1),
t1l_timer.wait.eq(t1h_timer.done),
pad.eq(~t1h_timer.done),
# Send a zero.
).Else(
t0h_timer.wait.eq(1),
t0l_timer.wait.eq(t0h_timer.done),
pad.eq(~t0h_timer.done),
),
# When bit has been sent:
If(t0l_timer.done | t1l_timer.done,
# Clear wait on timers.
t0h_timer.wait.eq(0),
t0l_timer.wait.eq(0),
t1h_timer.wait.eq(0),
t1l_timer.wait.eq(0),
# Shift xfer_data.
NextValue(xfer_data, Cat(Signal(), xfer_data)),
# Decrement xfer_bit.
NextValue(xfer_bit, xfer_bit - 1),
# When xfer_bit reaches 0.
If(xfer_bit == 0,
NextState("IDLE")
)
)
)

View File

@ -11,17 +11,17 @@ 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
test_clk_freqs = [75e6, 50e6, 25e6]
# cap on how long a sequence will be evaluated
def generator(self, dut, led_signal, led_data, sys_clk_freq, iterations):
# Error Margin from WS2812 datasheet.
error_margin = 150e-9
# Cap on how long a sequence will be evaluated.
max_cycles_per_seq = int(dut.trst * sys_clk_freq * 2)
# initial reset
# Verify initial reset.
rst_cycles = 0
for _ in range(max_cycles_per_seq):
if (yield led_signal) != 0:
@ -31,16 +31,21 @@ class TestWS2812(unittest.TestCase):
rst_time = rst_cycles / sys_clk_freq
assert rst_time >= dut.trst
# Verify generated data pulses.
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)
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 = {
0 : (dut.t0h, dut.t0l),
1 : (dut.t1h, dut.t1l)
}[bit]
# end of chain reset
# On end of chain, add reset time to exp_low
if i_num == length and idx_bit == 24:
exp_low += dut.trst
# Verify high cycle.
high_cycles = 0
for _ in range(max_cycles_per_seq):
if (yield led_signal) != 1:
@ -51,6 +56,7 @@ class TestWS2812(unittest.TestCase):
assert high_time >= exp_high - error_margin
assert high_time <= exp_high + error_margin
# Verify low cycle.
low_cycles = 0
for _ in range(max_cycles_per_seq):
if (yield led_signal) != 0:
@ -61,23 +67,21 @@ class TestWS2812(unittest.TestCase):
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):
def run_test(self, 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))
led_data = [0x100000, 0x200000, 0x300000, 0x400000, 0x500000, 0x600000, 0x700000, 0x800000, 0x900000]
iterations = 2
dut = WS2812(led_signal, len(led_data), sys_clk_freq, revision=revision, init=led_data)
run_simulation(dut, self.generator(dut, led_signal, led_data, sys_clk_freq, iterations), vcd_name="sim.vcd")
def test_WS2812_old(self):
for sys_clk_freq in TEST_CLK_FREQS:
for sys_clk_freq in self.test_clk_freqs:
self.run_test("old", sys_clk_freq)
def test_WS2812_new(self):
for sys_clk_freq in TEST_CLK_FREQS:
for sys_clk_freq in self.test_clk_freqs:
self.run_test("new", sys_clk_freq)