Skip to content

Commit e5c55d8

Browse files
authored
CH32V203 in LQFP-32 (#516)
Adds CH32V203 microcontroller, changes the keyboard example to use it as a test. Fixes to CH32V003 model
1 parent 9ab1083 commit e5c55d8

7 files changed

Lines changed: 965 additions & 259 deletions

File tree

edg/parts/microcontroller/Ch32v003.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def __init__(self, **kwargs: Any) -> None:
116116
self.vss,
117117
self.vdd,
118118
voltage_limit_abs=(-0.3, 5.5) * Volt, # table 3.1
119-
current_limits=(-20, 20) * mAmp, # table 3.1
119+
current_limits=(-20, 20) * mAmp, # 3.3.9 output drive current characteristics
120120
input_threshold_abs=( # table 3-16
121121
0.19 * (self.vdd.link().voltage.lower() - 2.7) + 0.65 + self.vss.link().voltage.lower(),
122122
0.22 * (self.vdd.link().voltage.upper() - 2.7) + 1.55 + self.vss.link().voltage.upper(),
@@ -128,7 +128,7 @@ def __init__(self, **kwargs: Any) -> None:
128128
self.vss,
129129
self.vdd,
130130
voltage_limit_tolerance=(-0.3, 0.3) * Volt, # table 3.1
131-
current_limits=(-20, 20) * mAmp, # table 3.1
131+
current_limits=(-20, 20) * mAmp, # 3.3.9 output drive current characteristics
132132
input_threshold_abs=( # table 3-16
133133
0.19 * (self.vdd.link().voltage.lower() - 2.7) + 0.65 + self.vss.link().voltage.lower(),
134134
0.22 * (self.vdd.link().voltage.upper() - 2.7) + 1.55 + self.vss.link().voltage.upper(),
@@ -157,7 +157,8 @@ def _io_pinmap(self) -> PinMapUtil:
157157
self.vss,
158158
self.vdd,
159159
voltage_limit_tolerance=(-0.3, 0.3) * Volt, # table 3.1
160-
impedance=(100, float("inf")) * kOhm,
160+
signal_limit_tolerance=(0, 0) * Volt, # table 3-23, conversion voltage range
161+
# impedance depends on sampling rate
161162
)
162163

163164
uart_model = UartPort(DigitalBidir.empty())
@@ -245,5 +246,7 @@ def generate(self) -> None:
245246

246247
self.vdd_cap = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2)))
247248

249+
self.reset_cap = self.Block(DigitalCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.ic.nrst)
250+
248251
if self.get(self.reset.is_connected()):
249252
self.connect(self.reset, self.ic.nrst)
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
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: Ch32vSdi2Header254)
12+
class Ch32vSdi2Header(ProgrammingConnector):
13+
"""Abstract programming header for the CH32V using the two-pin SDI interface."""
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.swclk = self.Port(DigitalBidir.empty()) # despite similar naming, this is not ARM SWD
21+
self.swdio = self.Port(DigitalBidir.empty())
22+
self.reset = self.Port(DigitalBidir.empty(), optional=True) # may not be connected internally
23+
24+
25+
class Ch32vSdi2Header254(Ch32vSdi2Header):
26+
"""4-pin minimal programming header for CH32V."""
27+
28+
@override
29+
def contents(self) -> None:
30+
super().contents()
31+
32+
self.gnd.init_from(Ground())
33+
self.pwr.init_from(VoltageSink())
34+
self.swclk.init_from(DigitalBidir())
35+
self.swdio.init_from(DigitalBidir())
36+
self.reset.init_from(DigitalBidir()) # not connected
37+
38+
self.conn = self.Block(PinHeader254(4)).connected(
39+
{"1": self.pwr, "2": self.gnd, "3": self.swdio, "4": self.swclk}
40+
)
41+
42+
43+
class Ch32vSdi2Tc2030(Ch32vSdi2Header):
44+
"""UNOFFICIAL tag connect header, based on the SWD pinout."""
45+
46+
@override
47+
def contents(self) -> None:
48+
super().contents()
49+
50+
self.gnd.init_from(Ground())
51+
self.pwr.init_from(VoltageSink())
52+
self.swclk.init_from(DigitalBidir())
53+
self.swdio.init_from(DigitalBidir())
54+
self.reset.init_from(DigitalBidir())
55+
56+
self.conn = self.Block(TagConnect(6)).connected(
57+
{"1": self.pwr, "5": self.gnd, "2": self.swdio, "4": self.swclk, "3": self.reset}
58+
)
59+
# unused: 6 = swo
60+
61+
62+
class Ch32v203_Device(
63+
IoControllerI2cTarget,
64+
IoControllerUsb,
65+
IoControllerCan,
66+
InternalSubcircuit,
67+
BaseIoControllerPinmapGenerator,
68+
GeneratorBlock,
69+
JlcPart,
70+
FootprintBlock,
71+
):
72+
_PIN_MAPPING = { # -K6/K8 version, LQFP-32 package
73+
# "PD0": "2", # OSCI
74+
# "PD1": "3", # OSCO
75+
"PA0-WKUP": "6",
76+
"PA1": "7",
77+
"PA2": "8",
78+
"PA3": "9",
79+
"PA4": "10",
80+
"PA5": "11",
81+
"PA6": "12",
82+
"PA7": "13",
83+
"PB0": "14",
84+
"PB1": "15",
85+
"PA8": "18",
86+
"PA9": "19",
87+
"PA10": "20",
88+
"PA11": "21",
89+
"PA12": "22",
90+
# "PA13": "23", # SWDIO
91+
# "PA14": "24", # SWCLK
92+
"PA15": "25",
93+
"PB3": "26",
94+
"PB4": "27",
95+
"PB5": "28",
96+
"PB6": "29",
97+
"PB7": "30",
98+
# "PB8": "31", # BOOT0
99+
}
100+
101+
def __init__(self, **kwargs: Any) -> None:
102+
super().__init__(**kwargs)
103+
104+
# Additional ports (on top of BaseIoController)
105+
self.vdd = self.Port(
106+
VoltageSink(
107+
voltage_limits=(3.0, 3.6) * Volt, # stricter range when USB used, otherwise down to 2.7
108+
current_draw=(0.0005, 23.37) * mAmp + self.io_current_draw.upper(), # standby min to 144MHz run max
109+
),
110+
[Power],
111+
)
112+
self.vss = self.Port(Ground(), [Common])
113+
114+
self.nrst = self.Port( # Table 3-19
115+
DigitalSink.from_supply(
116+
self.vss,
117+
self.vdd,
118+
voltage_limit_tolerance=(-0.3, 0.3) * Volt,
119+
input_threshold_abs=(
120+
0.28 * (self.vdd.link().voltage.lower() - 1.8) + 0.6 + self.vss.link().voltage.lower(),
121+
0.41 * (self.vdd.link().voltage.upper() - 1.8) + 1.3 + self.vss.link().voltage.upper(),
122+
),
123+
pullup_capable=True,
124+
),
125+
optional=True,
126+
) # note, switched internal pull-up resistor, 30-50 kOhm
127+
128+
self.osc = self.Port(
129+
CrystalDriver(frequency_limits=(3, 25) * MHertz, voltage_out=self.vdd.link().voltage), optional=True
130+
) # Table 4-11 crystal / resonator specs, typ 8 MHz
131+
132+
self._dio_ft_model = DigitalBidir.from_supply(
133+
self.vss,
134+
self.vdd,
135+
voltage_limit_abs=(-0.3, 5.5) * Volt, # table 4-1
136+
current_limits=(-20, 20) * mAmp, # 4.3.10 output drive current characteristics
137+
input_threshold_abs=( # table 3-16
138+
0.32 * (self.vdd.link().voltage.lower() - 1.8) + 0.55 + self.vss.link().voltage.lower(),
139+
0.42 * (self.vdd.link().voltage.upper() - 1.8) + 1.3 + self.vss.link().voltage.upper(),
140+
),
141+
pullup_capable=True, # 30-50 kOhm
142+
pulldown_capable=True, # 30-50 kOhm
143+
)
144+
self._dio_std_model = DigitalBidir.from_supply(
145+
self.vss,
146+
self.vdd,
147+
voltage_limit_tolerance=(-0.3, 0.3) * Volt, # table 4-1
148+
current_limits=(-20, 20) * mAmp, # 4.3.10 output drive current characteristics
149+
input_threshold_abs=( # table 3-16
150+
0.28 * (self.vdd.link().voltage.lower() - 1.8) + 0.6 + self.vss.link().voltage.lower(),
151+
0.42 * (self.vdd.link().voltage.upper() - 1.8) + 1 + self.vss.link().voltage.upper(),
152+
),
153+
pullup_capable=True, # 30-50 kOhm
154+
pulldown_capable=True, # 30-50 kOhm
155+
)
156+
157+
self.swdio = self.Port(self._dio_ft_model)
158+
self.swclk = self.Port(self._dio_ft_model)
159+
160+
@override
161+
def _system_pinmap(self) -> Mapping[Union[Iterable[str], str], Union[Passive, HasPassivePort]]:
162+
return {
163+
("16", "32"): self.vss,
164+
"5": self.vdd, # VddA
165+
("17", "1"): self.vdd,
166+
"2": self.osc.xtal_in, # TODO remappable to PD0
167+
"3": self.osc.xtal_out, # TODO remappable to PD1
168+
"4": self.nrst,
169+
"23": self.swdio, # TODO remappable to PA13
170+
"24": self.swclk, # TODO remappable to PA14
171+
"31": self.vss, # BOOT0, TODO theoretically usable as output-only PB8
172+
}
173+
174+
@override
175+
def _io_pinmap(self) -> PinMapUtil:
176+
# Port models
177+
adc_model = AnalogSink.from_supply(
178+
self.vss,
179+
self.vdd,
180+
voltage_limit_tolerance=(-0.3, 0.3) * Volt, # table 4-1
181+
signal_limit_tolerance=(0, 0) * Volt, # table 4-27 conversion range, up to Vref+ up to VddA
182+
# impedance depends on sampling rate
183+
)
184+
185+
uart_model = UartPort(DigitalBidir.empty())
186+
spi_model = SpiController(DigitalBidir.empty())
187+
# TODO SPI peripherals, which have fixed-pin CS lines
188+
i2c_model = I2cController(DigitalBidir.empty())
189+
i2c_target_model = I2cTarget(DigitalBidir.empty())
190+
191+
return PinMapUtil(
192+
[ # table 2-1
193+
PinResource("PC13-TAMPER-RTC", {"PC13": self._dio_std_model}),
194+
PinResource("PC14-OSC32_IN", {"PC14": self._dio_std_model}),
195+
PinResource("PC15-OSC32_OUT", {"PC15": self._dio_std_model}),
196+
PinResource("OSC_IN", {"PD0": self._dio_std_model}),
197+
PinResource("OSC_OUT", {"PD1": self._dio_std_model}),
198+
PinResource("PA0-WKUP", {"PA0": self._dio_std_model, "ADC_IN0": adc_model}),
199+
PinResource("PA1", {"PA1": self._dio_std_model, "ADC_IN1": adc_model}),
200+
PinResource("PA2", {"PA2": self._dio_std_model, "ADC_IN2": adc_model}),
201+
PinResource("PA3", {"PA3": self._dio_std_model, "ADC_IN3": adc_model}),
202+
PinResource("PA4", {"PA4": self._dio_std_model, "ADC_IN4": adc_model}),
203+
PinResource("PA5", {"PA5": self._dio_std_model, "ADC_IN5": adc_model}),
204+
PinResource("PA6", {"PA6": self._dio_std_model, "ADC_IN6": adc_model}),
205+
PinResource("PA7", {"PA7": self._dio_std_model, "ADC_IN7": adc_model}),
206+
PinResource("PB0", {"PB0": self._dio_std_model, "ADC_IN8": adc_model}),
207+
PinResource("PB1", {"PB1": self._dio_std_model, "ADC_IN9": adc_model}),
208+
PinResource("PB2", {"PB2": self._dio_ft_model}), # BOOT1
209+
PinResource("PB10", {"PB10": self._dio_ft_model}),
210+
PinResource("PB11", {"PB11": self._dio_ft_model}),
211+
PinResource("PB12", {"PB12": self._dio_ft_model}),
212+
PinResource("PB13", {"PB13": self._dio_ft_model}),
213+
PinResource("PB14", {"PB14": self._dio_ft_model}),
214+
PinResource("PB15", {"PB15": self._dio_ft_model}),
215+
PinResource("PA8", {"PA8": self._dio_ft_model}),
216+
PinResource("PA9", {"PA9": self._dio_ft_model}),
217+
PinResource("PA10", {"PA10": self._dio_ft_model}),
218+
PinResource("PA11", {"PA11": self._dio_ft_model}),
219+
PinResource("PA12", {"PA12": self._dio_ft_model}), # merged w/ SWDIO on some devices
220+
PinResource("PA13", {"PA13": self._dio_ft_model}), # SWDIO
221+
PinResource("PA14", {"PA14": self._dio_ft_model}), # SWCLK
222+
PinResource("PA15", {"PA15": self._dio_ft_model}),
223+
PinResource("PB3", {"PB3": self._dio_ft_model}),
224+
PinResource("PB4", {"PB4": self._dio_ft_model}),
225+
PinResource("PB5", {"PB5": self._dio_ft_model}),
226+
PinResource("PB6", {"PB6": self._dio_ft_model}),
227+
PinResource("PB7", {"PB7": self._dio_ft_model}),
228+
PinResource("PB8", {"PB8": self._dio_ft_model}), # merged w/ BOOT0 on some devices
229+
PinResource("PB9", {"PB9": self._dio_ft_model}),
230+
PeripheralFixedResource(
231+
"USART1", uart_model, {"tx": ["PA9", "PB6", "PB15", "PA6"], "rx": ["PA10", "PB7", "PA8", "PA7"]}
232+
),
233+
PeripheralFixedResource("USART2", uart_model, {"tx": ["PA2"], "rx": ["PA3"]}),
234+
# unavailable on K8
235+
# PeripheralFixedResource("USART3", uart_model, {"tx": ["PB10"], "rx": ["PB11"]}),
236+
# PeripheralFixedResource("USART4", uart_model, {"tx": ["PB0", "PA5"], "rx": ["PB1", "PB5"]}),
237+
PeripheralFixedResource("USB", UsbDevicePort(DigitalBidir.empty()), {"dm": ["PA11"], "dp": ["PA12"]}),
238+
# PeripheralFixedResource("USBFS", UsbDevicePort(DigitalBidir.empty()), {"dm": ["PB6"], "dp": ["PB7"]}),
239+
PeripheralFixedResource("I2C1", i2c_model, {"scl": ["PB6", "PB8"], "sda": ["PB7", "PB9"]}),
240+
PeripheralFixedResource("I2C1_T", i2c_target_model, {"scl": ["PB6", "PB8"], "sda": ["PB7", "PB9"]}),
241+
# PeripheralFixedResource("I2C2", i2c_model, {"scl": ["PB10"], "sda": ["PB11"]}),
242+
# PeripheralFixedResource("I2C2_T", i2c_target_model, {"scl": ["PB10"], "sda": ["PB11"]}),
243+
PeripheralFixedResource(
244+
"SPI1", spi_model, {"sck": ["PA5", "PB3"], "miso": ["PA6", "PB4"], "mosi": ["PA7", "PB5"]}
245+
),
246+
# PeripheralFixedResource("SPI2", spi_model, {"sck": ["PB13"], "miso": ["PB14"], "mosi": ["PB15"]}),
247+
PeripheralFixedResource(
248+
"CAN",
249+
CanControllerPort(DigitalBidir.empty()),
250+
{"rxd": ["PA11", "PB8"], "txd": ["PA12", "PB9"]},
251+
),
252+
]
253+
).remap_pins(self._PIN_MAPPING)
254+
255+
@override
256+
def generate(self) -> None:
257+
super().generate()
258+
259+
self.footprint(
260+
"U",
261+
"Package_QFP:LQFP-32_7x7mm_P0.8mm",
262+
self._make_pinning(),
263+
mfr="WCH",
264+
part="CH32V203K8T6",
265+
datasheet="https://www.wch-ic.com/downloads/CH32V203DS0_PDF.html",
266+
)
267+
self.assign(self.lcsc_part, "C5372188")
268+
self.assign(self.actual_basic_part, False)
269+
270+
271+
class Ch32v203(
272+
Resettable,
273+
IoControllerI2cTarget,
274+
IoControllerUsb,
275+
IoControllerCan,
276+
Microcontroller,
277+
WithCrystalGenerator,
278+
IoControllerPowerRequired,
279+
GeneratorBlock,
280+
):
281+
"""General-purpose RISC-V (RV32IMAC) microcontroller with USB"""
282+
283+
DEFAULT_CRYSTAL_FREQUENCY = 8 * MHertz(tol=0.005) # 8 MHz as typical in datasheet
284+
285+
def __init__(self, **kwargs: Any) -> None:
286+
super().__init__(**kwargs)
287+
self.generator_param(self.reset.is_connected(), self.usb.requested(), self.can.requested())
288+
289+
@override
290+
def generate(self) -> None:
291+
super().generate()
292+
293+
with self.implicit_connect(ImplicitConnect(self.pwr, [Power]), ImplicitConnect(self.gnd, [Common])) as imp:
294+
self.ic = imp.Block(Ch32v203_Device(pin_assigns=ArrayStringExpr()))
295+
self._wrap_inner(self.ic)
296+
297+
self.sdi = imp.Block(Ch32vSdi2Header())
298+
self.connect(self.ic.swclk, self.sdi.swclk)
299+
self.connect(self.ic.swdio, self.sdi.swdio)
300+
self.connect(self.ic.nrst, self.sdi.reset)
301+
302+
self.connect(self.xtal_node, self.ic.osc)
303+
304+
self.vdd_cap = ElementDict[DecouplingCapacitor]() # assume one 0.1uF cap per power pair
305+
for i in range(2):
306+
self.vdd_cap[i] = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2)))
307+
self.vdda_cap = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2)))
308+
309+
self.reset_cap = self.Block(DigitalCapacitor(0.1 * uFarad(tol=0.2))).connected(self.gnd, self.ic.nrst)
310+
311+
if self.get(self.reset.is_connected()):
312+
self.connect(self.reset, self.ic.nrst)
313+
314+
@override
315+
def _crystal_required(self) -> bool: # crystal needed for CAN or USB b/c tighter freq tolerance
316+
return (
317+
len(self.get(self.can.requested())) > 0
318+
or len(self.get(self.usb.requested())) > 0
319+
or super()._crystal_required()
320+
)

edg/parts/microcontroller/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .Ch32v003 import Ch32v003, Ch32vSdiHeader, Ch32vSdiHeader254, Ch32vSdiTc2030
2+
from .Ch32v203 import Ch32v203, Ch32vSdi2Header, Ch32vSdi2Header254, Ch32vSdi2Tc2030
23
from .Lpc1549 import Lpc1549_48, Lpc1549_64
34
from .Stm32f103 import Stm32f103_48
45
from .Stm32f303 import Nucleo_F303k8

0 commit comments

Comments
 (0)