soc/cores/esc: Add simple/initial ESC Dshot core supporting D150/300/600.
This commit is contained in:
parent
4fa0ea2fdb
commit
9addd52990
|
@ -0,0 +1,199 @@
|
|||
#
|
||||
# This file is part of LiteX.
|
||||
#
|
||||
# Copyright (c) 2023 Florent Kermarrec <florent@enjoy-digital.fr>
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
import math
|
||||
|
||||
from migen import *
|
||||
from migen.genlib.misc import WaitTimer
|
||||
|
||||
from litex.gen import LiteXModule
|
||||
|
||||
from litex.soc.interconnect.csr import *
|
||||
from litex.soc.interconnect import wishbone
|
||||
|
||||
# ESCDShot Timings ---------------------------------------------------------------------------------
|
||||
|
||||
class DShotTimings:
|
||||
def compute(self):
|
||||
self.t1l = (self.period - self.t1h)
|
||||
self.t0l = (self.period - self.t0h)
|
||||
self.tgap = 16*self.period # FIXME: Refine.
|
||||
|
||||
class D150Timings(DShotTimings):
|
||||
t1h = 5.00e6
|
||||
t0h = 2.50e6
|
||||
period = 6.67e6
|
||||
|
||||
class D300Timings(DShotTimings):
|
||||
t1h = 2.50e6
|
||||
t0h = 1.25e6
|
||||
period = 3.33e6
|
||||
|
||||
class D600Timings(DShotTimings):
|
||||
t1h = 1.250e6
|
||||
t0h = 0.625e6
|
||||
period = 1.670e6
|
||||
|
||||
# ESCDShot Core ------------------------------------------------------------------------------------
|
||||
|
||||
class ESCDShot(LiteXModule):
|
||||
"""ESC DShot Driver.
|
||||
|
||||
Description
|
||||
-----------
|
||||
|
||||
DShot is a digital protocol for FC (Flight Controller) to ESC (Electronic Speed Controler)
|
||||
communication.
|
||||
|
||||
It consists of 16 bit words send continously:
|
||||
- 11 bit throttle:
|
||||
0 : Disarmed command.
|
||||
1-47 : Special commands.
|
||||
48-2047 : Trotthe value (0-2000).
|
||||
- 1 bit telemetry request:
|
||||
0 : No telemetry.
|
||||
1 : Telemetry sent back on separate channel.
|
||||
- 4 bit CRC to validate/check throttle + telemetry request values.
|
||||
|
||||
Zeroes are transmitted as:
|
||||
┌─────┐
|
||||
│ T0H │ │
|
||||
│ │ │
|
||||
└───────────┘
|
||||
Ones are transmitted as:
|
||||
┌──────────┐
|
||||
│ T1H │ │
|
||||
│ │ │
|
||||
└──────┘
|
||||
With:
|
||||
D150 (150Kbit/s) : T1H: 5us / T0H: 2.5us / Bit Duration: 6.67us.
|
||||
D300 (300Kbit/s) : T1H: 2.5us / T0H: 1.25us / Bit Duration: 3.33us.
|
||||
D600 (600Kbit/s) : T1h: 1.25us / T0H: 0.625us / Bit Duration: 1.67us.
|
||||
|
||||
The checksum is calculated over the throttle value and the telemetry bit with the following
|
||||
formula: crc = (value ^ (value >> 4) ^ (value >> 8)) & 0x0f
|
||||
|
||||
With current implementation, user or software directly write the raw transmitted value on the
|
||||
CSR register, so formating and CRC has to be pre-computed by the user or software.
|
||||
|
||||
Example over LiteX-Server/Bridge:
|
||||
|
||||
# Integration in SoC:
|
||||
# -------------------
|
||||
|
||||
esc_pad = Signal() # Or platform.request("X") with X = esc pin name.
|
||||
|
||||
from litex.soc.cores.esc import ESCDShot
|
||||
self.submodules.esc0 = ESCDShot(esc_pad, sys_clk_freq, protocol="DSHOT150")
|
||||
|
||||
# Test script:
|
||||
# ------------
|
||||
|
||||
from litex import RemoteClient
|
||||
|
||||
bus = RemoteClient()
|
||||
bus.open()
|
||||
|
||||
class ESC:
|
||||
def __init__(self, bus, n, sys_clk_freq):
|
||||
self.value = getattr(bus.regs, f"esc{n}_value")
|
||||
self.set(0)
|
||||
|
||||
def set(self, value):
|
||||
value = max(value, 0)
|
||||
value = min(value, 99)
|
||||
if value == 0:
|
||||
throttle = 0
|
||||
else:
|
||||
throttle = value*20 + 48
|
||||
telemetry = 0
|
||||
data = (throttle << 1) | telemetry
|
||||
crc = (data ^ (data >> 4) ^ (data >> 8)) & 0x0f
|
||||
print(f"0b{(data << 4 | crc):016b}")
|
||||
self.value.write(data << 4 | crc)
|
||||
|
||||
|
||||
|
||||
esc = ESC(bus, n=0, int(100e6))
|
||||
esc.set(50) # 0-99.
|
||||
|
||||
bus.close()
|
||||
"""
|
||||
def __init__(self, pad, sys_clk_freq, protocol="D150"):
|
||||
self.value = CSRStorage(16)
|
||||
|
||||
# # #
|
||||
|
||||
# Internal Signals.
|
||||
xfer_start = Signal()
|
||||
xfer_done = Signal()
|
||||
xfer_data = Signal(16)
|
||||
|
||||
# Timings.
|
||||
timings = {
|
||||
"D150": D150Timings(),
|
||||
"D300": D300Timings(),
|
||||
"D600": D600Timings(),
|
||||
}[protocol]
|
||||
timings.compute()
|
||||
|
||||
# Timers.
|
||||
t0h_timer = WaitTimer(int(timings.t0h*sys_clk_freq))
|
||||
t0l_timer = WaitTimer(int(timings.t0l*sys_clk_freq) - 1) # Compensate Xfer FSM latency.
|
||||
self.submodules += t0h_timer, t0l_timer
|
||||
|
||||
t1h_timer = WaitTimer(int(timings.t1h*sys_clk_freq))
|
||||
t1l_timer = WaitTimer(int(timings.t1l*sys_clk_freq) - 1) # Compensate Xfer FSM latency.
|
||||
self.submodules += t1h_timer, t1l_timer
|
||||
|
||||
tgap_timer = WaitTimer(int(timings.tgap*sys_clk_freq))
|
||||
self.submodules += tgap_timer
|
||||
|
||||
# XFER FSM.
|
||||
xfer_bit = Signal(4)
|
||||
xfer_fsm = FSM(reset_state="IDLE")
|
||||
self.submodules += xfer_fsm
|
||||
xfer_fsm.act("IDLE",
|
||||
NextValue(xfer_bit, 16 - 1),
|
||||
NextValue(xfer_data, self.value.storage),
|
||||
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("GAP")
|
||||
)
|
||||
)
|
||||
)
|
||||
xfer_fsm.act("GAP",
|
||||
tgap_timer.wait.eq(1),
|
||||
If(tgap_timer.done,
|
||||
NextState("IDLE")
|
||||
)
|
||||
)
|
Loading…
Reference in New Issue