Skip to content

Commit 57a2bbc

Browse files
authored
Add AA battery stack to library and Vin to Xiao RP2040 (#394)
Add an example with a Xiao RP2040 powered from a 4x stack of AA batteries. Library changes: - Add IoControllerVin, for microcontroller dev boards with a >=5v Vusb in / Vin pin - Add link type to Ground port and fix up some places that were still using VoltageLink - Make the link type covariant, so Port[GroundLink] and Port[VoltageLink] are both Port[Any] - Add AaBatteryStack for a series stack of AA batteries that boost the voltage - Change battery model to support offset ground for series stacks. A one-of change for now, in the future most robust / consistent support for offset ground is needed. - Change AA battery limits to 1.0-1.6v to accommodate NiMH rechargeable cells. 1.0v lower limit is a compromise to get the Xiao to build, devices may want to support down to 0.9v to maximize runtime off rechargables.
1 parent 0e06aaa commit 57a2bbc

File tree

14 files changed

+95
-38
lines changed

14 files changed

+95
-38
lines changed

edg/abstract_parts/AbstractDevices.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ def __init__(self, voltage: RangeLike,
1717
self.capacity = self.ArgParameter(capacity)
1818
self.actual_capacity = self.Parameter(RangeExpr())
1919

20-
self.require(self.pwr.voltage_out.within(voltage))
20+
self.require(self.pwr.voltage_out.within(voltage + self.gnd.link().voltage))
2121
self.require(self.pwr.current_limits.contains(current))
2222
self.require(self.actual_capacity.upper() >= capacity)

edg/abstract_parts/AbstractResistor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def __init__(self, resistance: RangeLike) -> None:
157157
DigitalSource.pulldown_from_supply(self.gnd)
158158
), [InOut])
159159

160-
def connected(self, gnd: Optional[Port[VoltageLink]] = None, io: Optional[Port[DigitalLink]] = None) -> \
160+
def connected(self, gnd: Optional[Port[GroundLink]] = None, io: Optional[Port[DigitalLink]] = None) -> \
161161
'PulldownResistor':
162162
"""Convenience function to connect both ports, returning this object so it can still be given a name."""
163163
if gnd is not None:

edg/abstract_parts/IoControllerInterfaceMixins.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,20 @@ class IoControllerPowerOut(BlockInterfaceMixin[IoController]):
9191
def __init__(self, *args, **kwargs) -> None:
9292
super().__init__(*args, **kwargs)
9393
self.pwr_out = self.Port(VoltageSource.empty(), optional=True,
94-
doc="Power output port, typically of the device's Vdd or VddIO rail; must be used with gnd_out")
94+
doc="Power output port, typically of the device's Vdd or VddIO rail at 3.3v")
9595

9696

9797
class IoControllerUsbOut(BlockInterfaceMixin[IoController]):
9898
"""IO controller mixin that provides an output of the IO controller's USB Vbus."""
9999
def __init__(self, *args, **kwargs) -> None:
100100
super().__init__(*args, **kwargs)
101101
self.vusb_out = self.Port(VoltageSource.empty(), optional=True,
102-
doc="Power output port of the device's Vbus, typically 5v; must be used with gnd_out")
102+
doc="Power output port of the device's Vbus, typically 5v")
103+
104+
105+
class IoControllerVin(BlockInterfaceMixin[IoController]):
106+
"""IO controller mixin that provides a >=5v input to the device, typically upstream of the Vbus-to-3.3 regulator."""
107+
def __init__(self, *args, **kwargs) -> None:
108+
super().__init__(*args, **kwargs)
109+
self.pwr_vin = self.Port(VoltageSink.empty(), optional=True,
110+
doc="Power input pin, typically rated for 5v or a bit beyond.")

edg/abstract_parts/PassiveFilters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def __init__(self, impedance: RangeLike, time_constant: RangeLike):
5151
self.io = self.Export(self.rc.output.adapt_to(DigitalSource.pullup_from_supply(self.pwr)), [Output])
5252
self.gnd = self.Export(self.rc.gnd.adapt_to(Ground()), [Common])
5353

54-
def connected(self, *, gnd: Optional[Port[VoltageLink]] = None, pwr: Optional[Port[VoltageLink]] = None,
54+
def connected(self, *, gnd: Optional[Port[GroundLink]] = None, pwr: Optional[Port[VoltageLink]] = None,
5555
io: Optional[Port[DigitalLink]] = None) -> 'PullupDelayRc':
5656
"""Convenience function to connect both ports, returning this object so it can still be given a name."""
5757
if gnd is not None:

edg/abstract_parts/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
from .IoControllerExportable import BaseIoControllerExportable
9898
from .IoControllerInterfaceMixins import IoControllerSpiPeripheral, IoControllerI2cTarget, IoControllerTouchDriver,\
9999
IoControllerDac, IoControllerCan, IoControllerUsb, IoControllerI2s, IoControllerDvp8
100-
from .IoControllerInterfaceMixins import IoControllerPowerOut, IoControllerUsbOut
100+
from .IoControllerInterfaceMixins import IoControllerPowerOut, IoControllerUsbOut, IoControllerVin
101101
from .IoControllerInterfaceMixins import IoControllerWifi, IoControllerBluetooth, IoControllerBle
102102
from .IoControllerProgramming import IoControllerWithSwdTargetConnector
103103
from .IoControllerMixins import WithCrystalGenerator

edg/electronics_model/CircuitBlock.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from .KiCadImportableBlock import KiCadImportableBlock
1010

1111

12-
CircuitLinkType = TypeVar('CircuitLinkType', bound=Link)
12+
CircuitLinkType = TypeVar('CircuitLinkType', bound=Link, covariant=True)
1313
class CircuitPort(Port[CircuitLinkType], Generic[CircuitLinkType]):
1414
"""Electrical connection that represents a single port into a single copper net"""
1515
pass

edg/electronics_model/DigitalPorts.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ def __init__(self, voltage_out: RangeLike = RangeExpr.ZERO,
309309
self._bridged_internal: BoolExpr = self.Parameter(BoolExpr(_bridged_internal))
310310

311311
@staticmethod
312-
def low_from_supply(neg: Port[VoltageLink], *, current_limits: RangeLike = RangeExpr.ALL) -> DigitalSource:
312+
def low_from_supply(neg: Port[GroundLink], *, current_limits: RangeLike = RangeExpr.ALL) -> DigitalSource:
313313
return DigitalSource(
314314
voltage_out=neg.link().voltage,
315315
current_limits=current_limits,
@@ -329,7 +329,7 @@ def high_from_supply(pos: Port[VoltageLink], *, current_limits: RangeLike = Rang
329329
)
330330

331331
@staticmethod
332-
def pulldown_from_supply(neg: Port[VoltageLink]) -> DigitalSource:
332+
def pulldown_from_supply(neg: Port[GroundLink]) -> DigitalSource:
333333
return DigitalSource(
334334
voltage_out=neg.link().voltage,
335335
output_thresholds=(neg.link().voltage.upper(), float('inf')),
@@ -471,7 +471,7 @@ def __init__(self, *, voltage_limits: RangeLike = RangeExpr.ALL,
471471
class DigitalSingleSourceFake:
472472
@staticmethod
473473
@deprecated("use DigitalSource.sink_from_supply")
474-
def low_from_supply(neg: Port[VoltageLink], is_pulldown: bool = False) -> DigitalSource:
474+
def low_from_supply(neg: Port[GroundLink], is_pulldown: bool = False) -> DigitalSource:
475475
if not is_pulldown:
476476
return DigitalSource.low_from_supply(neg)
477477
else:

edg/electronics_model/GroundPort.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def __init__(self):
9595
))
9696

9797

98-
class Ground(CircuitPort):
98+
class Ground(CircuitPort[GroundLink]):
9999
link_type = GroundLink
100100
bridge_type = GroundBridge
101101

@@ -118,7 +118,7 @@ def __init__(self, voltage_limits: RangeLike = Range.all()) -> None:
118118
self.voltage_limits = self.Parameter(RangeExpr(voltage_limits))
119119

120120

121-
class GroundReference(CircuitPort):
121+
class GroundReference(CircuitPort[GroundLink]):
122122
link_type = GroundLink
123123

124124
def __init__(self, voltage_out: RangeLike = RangeExpr.ZERO) -> None:

edg/parts/Batteries.py

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Optional, Union
2+
13
from ..abstract_parts import *
24

35

@@ -6,7 +8,7 @@ def __init__(self, voltage: RangeLike = (2.0, 3.0)*Volt, *args,
68
actual_voltage: RangeLike = (2.0, 3.0)*Volt, **kwargs):
79
super().__init__(voltage, *args, **kwargs)
810
self.pwr.init_from(VoltageSource(
9-
voltage_out=actual_voltage, # arbitrary from https://www.mouser.com/catalog/additional/Adafruit_3262.pdf
11+
voltage_out=self.gnd.link().voltage + actual_voltage, # arbitrary from https://www.mouser.com/catalog/additional/Adafruit_3262.pdf
1012
current_limits=(0, 10)*mAmp,
1113
))
1214
self.gnd.init_from(Ground())
@@ -32,7 +34,7 @@ def __init__(self, voltage: RangeLike = (2.5, 4.2)*Volt, *args,
3234
actual_voltage: RangeLike = (2.5, 4.2)*Volt, **kwargs):
3335
super().__init__(voltage, *args, **kwargs)
3436
self.pwr.init_from(VoltageSource(
35-
voltage_out=actual_voltage, # arbitrary from https://www.mouser.com/catalog/additional/Adafruit_3262.pdf
37+
voltage_out=self.gnd.link().voltage + actual_voltage,
3638
current_limits=(0, 2)*Amp, # arbitrary assuming low capacity, 1 C discharge
3739
))
3840
self.gnd.init_from(Ground())
@@ -51,17 +53,18 @@ def contents(self):
5153
mfr='Keystone', part='1042'
5254
)
5355

54-
class AABattery(Battery, FootprintBlock):
55-
"""AA Alkaline battery"""
56+
57+
class AaBattery(Battery, FootprintBlock):
58+
"""AA battery holder supporting alkaline and rechargeable chemistries."""
5659
@init_in_parent
57-
def __init__(self, voltage: RangeLike = (1.3, 1.7)*Volt, *args,
58-
actual_voltage: RangeLike = (1.3, 1.7)*Volt, **kwargs):
60+
def __init__(self, voltage: RangeLike = (1.0, 1.6)*Volt, *args,
61+
actual_voltage: RangeLike = (1.0, 1.6)*Volt, **kwargs):
5962
super().__init__(voltage, *args, **kwargs)
63+
self.gnd.init_from(Ground())
6064
self.pwr.init_from(VoltageSource(
61-
voltage_out=actual_voltage, # arbitrary from https://www.mouser.com/catalog/additional/Adafruit_3262.pdf
65+
voltage_out=self.gnd.link().voltage + actual_voltage,
6266
current_limits=(0, 1)*Amp,
6367
))
64-
self.gnd.init_from(Ground())
6568

6669
def contents(self):
6770
super().contents()
@@ -76,3 +79,33 @@ def contents(self):
7679
},
7780
mfr='Keystone', part='2460'
7881
)
82+
83+
84+
class AaBatteryStack(Battery, GeneratorBlock):
85+
"""AA Alkaline battery stack that generates batteries in series"""
86+
@init_in_parent
87+
def __init__(self, count: IntLike = 1, *, cell_actual_voltage: RangeLike = (1.0, 1.6)*Volt):
88+
super().__init__(voltage=Range.all()) # no voltage spec passed in
89+
self.count = self.ArgParameter(count)
90+
self.cell_actual_voltage = self.ArgParameter(cell_actual_voltage)
91+
self.generator_param(self.count)
92+
93+
def generate(self):
94+
super().generate()
95+
prev_cell: Optional[AaBattery] = None
96+
prev_capacity_min: Union[FloatExpr, float] = float('inf')
97+
prev_capacity_max: Union[FloatExpr, float] = float('inf')
98+
self.cell = ElementDict[AaBattery]()
99+
for i in range(self.get(self.count)):
100+
self.cell[i] = cell = self.Block(AaBattery(actual_voltage=self.cell_actual_voltage))
101+
if prev_cell is None: # first cell, direct connect to gnd
102+
self.connect(self.gnd, cell.gnd)
103+
else:
104+
self.connect(prev_cell.pwr.as_ground(self.pwr.link().current_drawn), cell.gnd)
105+
prev_capacity_min = cell.actual_capacity.lower().min(prev_capacity_min)
106+
prev_capacity_max= cell.actual_capacity.upper().min(prev_capacity_max)
107+
prev_cell = cell
108+
109+
assert prev_cell is not None, "must generate >=1 cell"
110+
self.connect(self.pwr, prev_cell.pwr)
111+
self.assign(self.actual_capacity, (prev_capacity_min, prev_capacity_max))

edg/parts/Microcontroller_Rp2040.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,8 @@ def _crystal_required(self) -> bool: # crystal needed for USB b/c tighter freq
372372
return len(self.get(self.usb.requested())) > 0 or super()._crystal_required()
373373

374374

375-
class Xiao_Rp2040(IoControllerUsbOut, IoControllerPowerOut, Rp2040_Ios, IoController, GeneratorBlock, FootprintBlock):
375+
class Xiao_Rp2040(IoControllerUsbOut, IoControllerPowerOut, IoControllerVin, Rp2040_Ios, IoController, GeneratorBlock,
376+
FootprintBlock):
376377
"""RP2040 development board, a tiny development (21x17.5mm) daughterboard.
377378
Has an onboard USB connector, so this can also source power.
378379
@@ -382,6 +383,7 @@ class Xiao_Rp2040(IoControllerUsbOut, IoControllerPowerOut, Rp2040_Ios, IoContro
382383
The 'Seeed Studio XIAO Series Library' must have been added as a footprint library of the same name.
383384
384385
Pinning data: https://www.seeedstudio.com/blog/wp-content/uploads/2022/08/Seeed-Studio-XIAO-Series-Package-and-PCB-Design.pdf
386+
Internal data: https://files.seeedstudio.com/wiki/XIAO-RP2040/res/Seeed-Studio-XIAO-RP2040-v1.3.pdf
385387
"""
386388
SYSTEM_PIN_REMAP: Dict[str, Union[str, List[str]]] = {
387389
'VDD': '12',
@@ -411,8 +413,6 @@ def _vddio(self) -> Port[VoltageLink]:
411413

412414
def _system_pinmap(self) -> Dict[str, CircuitPort]:
413415
if self.get(self.pwr.is_connected()): # board sinks power
414-
self.require(~self.vusb_out.is_connected(), "can't source USB power if power input connected")
415-
self.require(~self.pwr_out.is_connected(), "can't source 3v3 power if power input connected")
416416
return VariantPinRemapper({
417417
'VDD': self.pwr,
418418
'GND': self.gnd,
@@ -430,14 +430,26 @@ def contents(self) -> None:
430430
self.gnd.init_from(Ground())
431431
self.pwr.init_from(self._iovdd_model())
432432

433+
self.pwr_vin.init_from(VoltageSink( # based on RS3236-3.3
434+
voltage_limits=(3.3*1.025 + 0.55, 7.5)*Volt, # output * tolerance + dropout @ 300mA
435+
current_draw=RangeExpr()
436+
))
433437
self.vusb_out.init_from(VoltageSource(
434438
voltage_out=UsbConnector.USB2_VOLTAGE_RANGE,
435439
current_limits=UsbConnector.USB2_CURRENT_LIMITS
436440
))
441+
self.require(~self.pwr_vin.is_connected() | ~self.vusb_out.is_connected(), "cannot use both VUsb out and VUsb in")
442+
self.require((self.pwr_vin.is_connected() | self.vusb_out.is_connected()).implies(~self.pwr.is_connected()),
443+
"cannot use 3.3v input if VUsb used")
444+
437445
self.pwr_out.init_from(VoltageSource(
438446
voltage_out=3.3*Volt(tol=0.05), # tolerance is a guess
439447
current_limits=UsbConnector.USB2_CURRENT_LIMITS
440448
))
449+
self.require(~self.pwr_out.is_connected() | ~self.pwr.is_connected(), "cannot use both 3.3v out and 3.3v in")
450+
self.assign(self.pwr_vin.current_draw, self.pwr_out.is_connected().then_else( # prop output current draw
451+
self.pwr_out.link().current_drawn, (0, 0)*Amp
452+
))
441453

442454
self.generator_param(self.pwr.is_connected())
443455

0 commit comments

Comments
 (0)