Skip to content

Commit 9ab1083

Browse files
authored
CH32V003 microcontroller, BLE joystick example update (#514)
Adds the CH32V003 and uses it in the BLE joystick. Modifies the BLE joystick example with lessons learned / new features: - use nRF52 low power BLE MCU - add top button sub-board with d-pad buttons and LEDs - use soft power gate to turn on / off - use INA219 for battery voltage and current sensing - add IMU - support power gating of joystick This board has not been routed yet, that will happen later. Additional circuit modifications may be required during routing. Other fixes: - fixes IoControllerWrapper classes - improve documentation for the sot power switch - make DigitalBidir as_voltage_source capable - removes TC2050 SWD header with no deprecation path, it was a bad idea - add PROG control to the MCP73831 to support charging disable
1 parent 938283d commit 9ab1083

21 files changed

Lines changed: 2364 additions & 1095 deletions

edg/abstract_parts/IoControllerWrapper.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .IoController import BaseIoController
66

77

8+
@non_library
89
class BaseIoControllerModelable(BaseIoController):
910
"""Base class for a BaseIoController that can (optionally) be used as a (non-physical) model.
1011
This only adds parameters as a standard interface and is not functional.
@@ -17,9 +18,11 @@ def __init__(
1718

1819
self._model = self.ArgParameter(_model)
1920
self._allowed_pins = self.ArgParameter(_allowed_pins)
20-
self.generator_param(self._allowed_pins)
21+
if isinstance(self, GeneratorBlock):
22+
self.generator_param(self._allowed_pins)
2123

2224

25+
@non_library
2326
class BaseIoControllerWrapped(BaseIoController):
2427
"""Base class for IoController wrapped blocks, particularly footprints that are used
2528
with an outer WrapperSubboardBlock to implement e.g. a dev board or module around a modeling subcircuit.
@@ -156,6 +159,7 @@ def _make_pinning(
156159
return pinning
157160

158161

162+
@non_library
159163
class BaseIoControllerWrapper(BaseIoController):
160164
"""Base class for a block that contains a BaseIoControllerWrapped as the physical footprint
161165
as well as a non-physical device model.

edg/circuits/PowerConditioning.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,10 @@ def __init__(
347347
optional=True,
348348
doc="Allows the button state to be read independently of the control signal. Open-drain.",
349349
)
350-
self.btn_in = self.Port(DigitalBidir.empty(), doc="Should be connected to a button output. Do not connect IO")
350+
self.btn_in = self.Port(
351+
DigitalBidir.empty(),
352+
doc="Connect to button that pulls down to GND to turn on power. Do not connect to GPIO.",
353+
)
351354
self.control = self.Port(
352355
DigitalSink.empty(), doc="external control to latch the power on"
353356
) # digital level control - gnd-referenced NFET gate

edg/electronics_interfaces/DigitalPorts.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,9 @@ def __init__(
608608
self.pulldown_capable: BoolExpr = self.Parameter(BoolExpr(pulldown_capable))
609609
self._bridged_internal: BoolExpr = self.Parameter(BoolExpr(_bridged_internal))
610610

611+
def as_voltage_source(self) -> VoltageSource:
612+
return self._convert(DigitalSourceAdapterVoltageSource())
613+
611614

612615
class DigitalSingleSourceFake:
613616
@staticmethod

edg/parts/connector/SwdHeaders.py

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -51,35 +51,3 @@ def contents(self) -> None:
5151
self.conn = self.Block(TagConnect(6)).connected(
5252
{"1": self.pwr, "2": self.swd.swdio, "3": self.reset, "4": self.swd.swclk, "5": self.gnd, "6": self.swo}
5353
)
54-
55-
56-
class SwdCortexTargetTc2050(
57-
SwdCortexTargetConnector, SwdCortexTargetConnectorReset, SwdCortexTargetConnectorSwo, SwdCortexTargetConnectorTdi
58-
):
59-
"""UNOFFICIAL tag connect SWD header, maintaining physical pin compatibility with the 2x05 1.27mm header.
60-
NOT RECOMMENDED for use, this is a legacy artifact and will be removed.
61-
Use one of the official pinnings instead."""
62-
63-
@override
64-
def contents(self) -> None:
65-
super().contents()
66-
67-
self.gnd.init_from(Ground())
68-
self.pwr.init_from(VoltageSink())
69-
self.swd.init_from(SwdHostPort())
70-
self.swo.init_from(DigitalBidir())
71-
self.tdi.init_from(DigitalBidir())
72-
# reset modeled as pulldown to not conflict with other drivers, technically an open-drain
73-
self.reset.init_from(DigitalSource.pulldown_from_supply(self.gnd))
74-
75-
self.conn = self.Block(TagConnect(10)).connected(
76-
{
77-
"1": self.pwr,
78-
("2", "3", "5"): self.gnd,
79-
"10": self.swd.swdio,
80-
"9": self.swd.swclk,
81-
"8": self.swo,
82-
"7": self.tdi,
83-
"6": self.reset,
84-
}
85-
)

edg/parts/connector/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,4 @@
5353

5454
# Applications
5555
from .SwdHeaders import SwdCortexTargetHeader
56-
from .SwdHeaders import SwdCortexTargetTagConnect, SwdCortexTargetTc2050
56+
from .SwdHeaders import SwdCortexTargetTagConnect
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
from typing import *
2+
3+
from typing_extensions import override
4+
5+
from ...circuits import *
6+
from ...vendor_parts.jlc.JlcPart import JlcPart
7+
from ..connector.Headers import PinHeader254
8+
from ..connector.TagConnect import TagConnect
9+
10+
11+
@abstract_block_default(lambda: Ch32vSdiHeader254)
12+
class Ch32vSdiHeader(ProgrammingConnector):
13+
"""Abstract programming header for the CH32V using the one-pin SDI interface with SWIO pin."""
14+
15+
def __init__(self) -> None:
16+
super().__init__()
17+
18+
self.pwr = self.Port(VoltageSink.empty(), [Power])
19+
self.gnd = self.Port(Ground.empty(), [Common])
20+
self.swio = self.Port(DigitalBidir.empty())
21+
self.reset = self.Port(DigitalBidir.empty(), optional=True) # may not be connected internally
22+
23+
24+
class Ch32vSdiHeader254(Ch32vSdiHeader):
25+
"""3-pin minimal programming header for CH32V."""
26+
27+
@override
28+
def contents(self) -> None:
29+
super().contents()
30+
31+
self.gnd.init_from(Ground())
32+
self.pwr.init_from(VoltageSink())
33+
self.swio.init_from(DigitalBidir())
34+
self.reset.init_from(DigitalBidir()) # not connected
35+
36+
self.conn = self.Block(PinHeader254()).connected({"1": self.pwr, "2": self.gnd, "3": self.swio})
37+
38+
39+
class Ch32vSdiTc2030(Ch32vSdiHeader):
40+
"""UNOFFICIAL tag connect header, based on the SWD pinout and mapping SWDIO to SWIO."""
41+
42+
@override
43+
def contents(self) -> None:
44+
super().contents()
45+
46+
self.gnd.init_from(Ground())
47+
self.pwr.init_from(VoltageSink())
48+
self.swio.init_from(DigitalBidir())
49+
self.reset.init_from(DigitalBidir())
50+
51+
self.conn = self.Block(TagConnect(6)).connected({"1": self.pwr, "5": self.gnd, "2": self.swio, "3": self.reset})
52+
# unused: 4 = swclk, 6 = swo
53+
54+
55+
class Ch32v003_Device(
56+
IoControllerI2cTarget,
57+
InternalSubcircuit,
58+
BaseIoControllerPinmapGenerator,
59+
GeneratorBlock,
60+
JlcPart,
61+
FootprintBlock,
62+
):
63+
_PIN_MAPPING = { # F4P6 version, TSSOP-20
64+
"PD4": "1",
65+
"PD5": "2",
66+
"PD6": "3",
67+
# "PD7": "4", # NRST
68+
# "PA1": "5", # OSCI
69+
# "PA2": "6", # OSCO
70+
"PD0": "8",
71+
"PC0": "10",
72+
"PC1": "11",
73+
"PC2": "12",
74+
"PC3": "13",
75+
"PC4": "14",
76+
"PC5": "15",
77+
"PC6": "16",
78+
"PC7": "17",
79+
# "PD1": "18", # SWIO
80+
"PD2": "19",
81+
"PD3": "20",
82+
}
83+
84+
def __init__(self, **kwargs: Any) -> None:
85+
super().__init__(**kwargs)
86+
87+
# Additional ports (on top of BaseIoController)
88+
self.vdd = self.Port(
89+
VoltageSink(
90+
voltage_limits=(2.8, 5.5) * Volt, # stricter range when ADC used
91+
current_draw=(0.009, 8.0) * mAmp + self.io_current_draw.upper(), # standby min to 48MHz run max
92+
),
93+
[Power],
94+
)
95+
self.vss = self.Port(Ground(), [Common])
96+
97+
self.nrst = self.Port( # Table 3-19
98+
DigitalSink.from_supply(
99+
self.vss,
100+
self.vdd,
101+
voltage_limit_tolerance=(-0.3, 0.3) * Volt,
102+
input_threshold_abs=(
103+
0.28 * (self.vdd.link().voltage.lower() - 1.8) + 0.6 + self.vss.link().voltage.lower(),
104+
0.41 * (self.vdd.link().voltage.upper() - 1.8) + 1.3 + self.vss.link().voltage.upper(),
105+
),
106+
pullup_capable=True,
107+
),
108+
optional=True,
109+
) # note, switched internal pull-up resistor, 35-55 kOhm
110+
111+
self.osc = self.Port(
112+
CrystalDriver(frequency_limits=(4, 25) * MHertz, voltage_out=self.vdd.link().voltage), optional=True
113+
) # Table 3-10 crystal / resonator specs, typ 24 MHz
114+
115+
self._dio_ft_model = DigitalBidir.from_supply(
116+
self.vss,
117+
self.vdd,
118+
voltage_limit_abs=(-0.3, 5.5) * Volt, # table 3.1
119+
current_limits=(-20, 20) * mAmp, # table 3.1
120+
input_threshold_abs=( # table 3-16
121+
0.19 * (self.vdd.link().voltage.lower() - 2.7) + 0.65 + self.vss.link().voltage.lower(),
122+
0.22 * (self.vdd.link().voltage.upper() - 2.7) + 1.55 + self.vss.link().voltage.upper(),
123+
),
124+
pullup_capable=True, # 35-55 kOhm
125+
pulldown_capable=True, # 35-55 kOhm
126+
)
127+
self._dio_std_model = DigitalBidir.from_supply(
128+
self.vss,
129+
self.vdd,
130+
voltage_limit_tolerance=(-0.3, 0.3) * Volt, # table 3.1
131+
current_limits=(-20, 20) * mAmp, # table 3.1
132+
input_threshold_abs=( # table 3-16
133+
0.19 * (self.vdd.link().voltage.lower() - 2.7) + 0.65 + self.vss.link().voltage.lower(),
134+
0.22 * (self.vdd.link().voltage.upper() - 2.7) + 1.55 + self.vss.link().voltage.upper(),
135+
),
136+
pullup_capable=True, # 35-55 kOhm
137+
pulldown_capable=True, # 35-55 kOhm
138+
)
139+
140+
self.swio = self.Port(self._dio_std_model)
141+
142+
@override
143+
def _system_pinmap(self) -> Mapping[Union[Iterable[str], str], Union[Passive, HasPassivePort]]:
144+
return {
145+
"7": self.vss,
146+
"9": self.vdd,
147+
"5": self.osc.xtal_in, # TODO remappable to PA1
148+
"6": self.osc.xtal_out, # TODO remappable to PA2
149+
"4": self.nrst,
150+
"18": self.swio,
151+
}
152+
153+
@override
154+
def _io_pinmap(self) -> PinMapUtil:
155+
# Port models
156+
adc_model = AnalogSink.from_supply(
157+
self.vss,
158+
self.vdd,
159+
voltage_limit_tolerance=(-0.3, 0.3) * Volt, # table 3.1
160+
impedance=(100, float("inf")) * kOhm,
161+
)
162+
163+
uart_model = UartPort(DigitalBidir.empty())
164+
spi_model = SpiController(DigitalBidir.empty())
165+
# TODO SPI peripherals, which have fixed-pin CS lines
166+
i2c_model = I2cController(DigitalBidir.empty())
167+
i2c_target_model = I2cTarget(DigitalBidir.empty())
168+
169+
return PinMapUtil(
170+
[ # table 2-1
171+
PinResource("PD4", {"PD4": self._dio_std_model, "A7": adc_model}),
172+
PinResource("PD5", {"PD5": self._dio_std_model, "A5": adc_model}),
173+
PinResource("PD6", {"PD6": self._dio_std_model, "A6": adc_model}),
174+
PinResource("PD7", {"PD7": self._dio_std_model}), # NRST
175+
PinResource("PA1", {"PA1": self._dio_std_model, "A1": adc_model}),
176+
PinResource("PA2", {"PA2": self._dio_std_model, "A0": adc_model}),
177+
PinResource("PD0", {"PD0": self._dio_std_model}),
178+
PinResource("PC0", {"PC0": self._dio_std_model}),
179+
PinResource("PC1", {"PC1": self._dio_ft_model}),
180+
PinResource("PC2", {"PC2": self._dio_ft_model}),
181+
PinResource("PC3", {"PC3": self._dio_std_model}),
182+
PinResource("PC4", {"PC4": self._dio_ft_model, "A2": adc_model}),
183+
PinResource("PC5", {"PC5": self._dio_ft_model}),
184+
PinResource("PC6", {"PC6": self._dio_ft_model}),
185+
PinResource("PC7", {"PC7": self._dio_std_model}),
186+
PinResource("PD1", {"PD1": self._dio_std_model}), # SWIO
187+
PinResource("PD2", {"PD2": self._dio_std_model, "A3": adc_model}),
188+
PinResource("PD3", {"PD3": self._dio_std_model, "A4": adc_model}),
189+
PeripheralFixedResource(
190+
"U", uart_model, {"tx": ["PD5", "PD0", "PD6", "PC0"], "rx": ["PD6", "PD1", "PD5", "PC1"]}
191+
),
192+
PeripheralFixedResource("SPI", spi_model, {"sck": ["PC5"], "miso": ["PC7"], "mosi": ["PC6"]}),
193+
PeripheralFixedResource("I2C", i2c_model, {"scl": ["PC2", "PD1", "PC5"], "sda": ["PC1", "PD0", "PC6"]}),
194+
PeripheralFixedResource(
195+
"I2C_T", i2c_target_model, {"scl": ["PC2", "PD1", "PC5"], "sda": ["PC1", "PD0", "PC6"]}
196+
),
197+
]
198+
).remap_pins(self._PIN_MAPPING)
199+
200+
@override
201+
def generate(self) -> None:
202+
super().generate()
203+
204+
self.footprint(
205+
"U",
206+
"Package_SO:TSSOP-20_4.4x6.5mm_P0.65mm",
207+
self._make_pinning(),
208+
mfr="WCH",
209+
part="CH32V003F4P6",
210+
datasheet="https://www.wch-ic.com/downloads/CH32V003DS0_PDF.html",
211+
)
212+
self.assign(self.lcsc_part, "C5187096")
213+
self.assign(self.actual_basic_part, False)
214+
215+
216+
class Ch32v003(
217+
Resettable,
218+
IoControllerI2cTarget,
219+
Microcontroller,
220+
WithCrystalGenerator,
221+
IoControllerPowerRequired,
222+
GeneratorBlock,
223+
):
224+
"""Low-cost, bare bones RISC-V (RV32EC) microcontroller"""
225+
226+
DEFAULT_CRYSTAL_FREQUENCY = 24 * MHertz(tol=0.005) # 24 MHz as typical in datasheet
227+
228+
def __init__(self, **kwargs: Any) -> None:
229+
super().__init__(**kwargs)
230+
self.generator_param(self.reset.is_connected())
231+
232+
@override
233+
def generate(self) -> None:
234+
super().generate()
235+
236+
with self.implicit_connect(ImplicitConnect(self.pwr, [Power]), ImplicitConnect(self.gnd, [Common])) as imp:
237+
self.ic = imp.Block(Ch32v003_Device(pin_assigns=ArrayStringExpr()))
238+
self._wrap_inner(self.ic)
239+
240+
self.sdi = imp.Block(Ch32vSdiHeader())
241+
self.connect(self.ic.swio, self.sdi.swio)
242+
self.connect(self.ic.nrst, self.sdi.reset)
243+
244+
self.connect(self.xtal_node, self.ic.osc)
245+
246+
self.vdd_cap = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2)))
247+
248+
if self.get(self.reset.is_connected()):
249+
self.connect(self.reset, self.ic.nrst)

edg/parts/microcontroller/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from .Ch32v003 import Ch32v003, Ch32vSdiHeader, Ch32vSdiHeader254, Ch32vSdiTc2030
12
from .Lpc1549 import Lpc1549_48, Lpc1549_64
23
from .Stm32f103 import Stm32f103_48
34
from .Stm32f303 import Nucleo_F303k8

edg/parts/microcontroller/nRF52840.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,7 @@ def generate(self) -> None:
529529
self._make_model_pinning(Holyiot_18010_Footprint._PIN_REMAPPING, self.get(self.pin_assigns)),
530530
)
531531

532-
self.device = self.Block(Holyiot_18010_Footprint(pin_assigns=self.model.actual_pin_assigns, external=True))
532+
self.device = self.Block(Holyiot_18010_Footprint(pin_assigns=self.model.actual_pin_assigns), external=True)
533533
self.assign(self.actual_pin_assigns, self.device.actual_pin_assigns)
534534
self._export_tap_ios_inner(self.device)
535535
self.export_tap(self.gnd, self.device.gnd)

0 commit comments

Comments
 (0)