Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
713 changes: 713 additions & 0 deletions .github/instructions/ato.instructions.md

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,10 @@ __pycache__/
*.pyc
*.pyo
*.pyd

# LLM & AI
.cursor/mcp.json
.mcp.json
CLAUDE.md
.windsurf/
.cursor/rules/
3 changes: 0 additions & 3 deletions .vscode/settings.json

This file was deleted.

138 changes: 135 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,154 @@
# Cell Simulator

This cell-sim is designed to mimic a LiPo battery pack for develpoment of the surrounding electronics, like a BMS.
This cell-sim is designed to mimic a LiPo battery pack for development of surrounding electronics (e.g. a BMS).

- 16 channels
- Open-Source hardware design, you can embed onto your own HIL setup
- ⚡️ 0-4.5V and 0-500mA per channel
- DMM muxed to each channel for arbitarily precise measurment
- Open-circuit simulation on each channel
- DMM muxed to each channel for arbitrarily precise measurement
- Open-circuit (open-wire) simulation on each channel
- 📏 16bit ADC feedback for voltage and current
- 🔌 USB w/ Python software interface (+ 100MBit Ethernet + WiFi waiting for firmware support)

![IMG_0374 3](https://github.com/user-attachments/assets/d8fa4661-c460-48e2-a26a-71079aa79707)

## Documentation

- **Electronics**: this README (see diagrams below)
- **Software (firmware + Python)**: `SOFTWARE.md`

## Design overview

![Cell Diagram](docs/cell.png)

![Board Render](docs/board.png)

## How it works (electronics)

### System-level overview

```mermaid
flowchart TB
%% ----------------------------
%% Power tree
%% ----------------------------
subgraph PWR["Power tree"]
XT30["XT30 input<br/>12-20V"] --> PI["Pi filter"] --> ISENSE["Input current sensor"] --> VIN["POWER_IN<br/>12-20V rail"]
VIN --> BUCK5["LV2842X buck<br/>12-20V to 5V"]
USBC["USB-C"] -->|VBUS 5V| OR["Diode OR-ing"]
BUCK5 --> OR --> P5V["5V rail"]
P5V --> LDO33["LDK220 LDO<br/>5V to 3.3V"] --> P3V3["3.3V rail"]
end

%% ----------------------------
%% Controller + comms
%% ----------------------------
subgraph MCU["Controller + comms"]
ESP["ESP32-S3"]:::mcu
P3V3 --> ESP
USBC -->|USB2 data| ESP

ESP -->|I2C bus #1| I2C1["I2C1"]
ESP -->|I2C bus #2| I2C2["I2C2"]

I2C1 --> MUX1["TCA9548A #1<br/>addr 0x70"]
I2C2 --> MUX2["TCA9548A #2<br/>addr 0x70"]
P3V3 --> MUX1
P3V3 --> MUX2

ESP -->|SPI| W5500["W5500 Ethernet<br/>hw present; fw WIP"]
P3V3 --> W5500
end

%% ----------------------------
%% 16 cell channels (stacked)
%% ----------------------------
subgraph STACK["16x isolated cell channels<br/>stacked like a battery pack"]
direction TB
C1["Cell #1<br/>pack GND reference"] --> C2["Cell #2"] --> C3["Cell #3"] --> C4["Cell #4"] --> C5["Cell #5"] --> C6["Cell #6"] --> C7["Cell #7"] --> C8["Cell #8"]
C8 --> C9["Cell #9"] --> C10["Cell #10"] --> C11["Cell #11"] --> C12["Cell #12"] --> C13["Cell #13"] --> C14["Cell #14"] --> C15["Cell #15"] --> C16["Cell #16<br/>pack +"]
end

%% Per-cell services fed from global rails
VIN -->|12-20V feed| STACK
P5V -->|logic / relay / LED feed| STACK
P3V3 -->|isolator side power| STACK

%% I2C fanout
MUX1 -->|ports 0-7 to cells 1-8| C1
MUX2 -->|ports 0-7 to cells 9-16| C9

%% ----------------------------
%% DMM path (precision external measurement)
%% ----------------------------
subgraph DMM["External DMM measurement path"]
BANANA["Banana jacks<br/>DMM connector"] --> DMMBUS["DMM_OUT bus"]

ESP -->|S0..S3 + /EN| DMMSW["CD74HC4067<br/>16:1 mux"]
P3V3 -->|3.3V select level| DMMSW
DMMSW -->|CH0..CH15 -> per-cell dmm_relay_enable| STACK

STACK -->|selected cell closes DMM relay, routes output to DMM_OUT| DMMBUS
end

%% ----------------------------
%% Thermistor simulator
%% ----------------------------
subgraph THERM["4x digital thermistor simulator"]
TSIM["Isolated thermistor simulator<br/>MCP4728 DAC + ADS1115 ADC"] -->|4 analog thermistor nodes| THOUT["thermistor outputs"]
I2C2 -->|I2C control| TSIM
VIN -->|12-20V| TSIM
P3V3 -->|logic-side isolator power| TSIM
end

classDef mcu fill:#eef,stroke:#446;
```

### What a single cell channel does internally

Each channel is effectively an *isolated, digitally-controlled power supply* whose output is treated like a single LiPo cell. The channels are then wired in series to create a 16S “battery pack” at the output connector.

```mermaid
flowchart LR
%% One "Cell" module from elec/src/cell.ato
subgraph CELL["One cell channel<br/>isolated"]
direction LR

%% Power path (high-level)
VIN["POWER_IN<br/>12-20V"] --> ISO_DCDC["Isolated DC/DC<br/>B1205S"] --> ISO5V["Isolated 5V"]
ISO5V --> INTLDO["Local LDO<br/>5V to 3.3V"] --> ISO3V3["Isolated 3.3V<br/>logic"]

ISO5V --> DBUCK["Digitally-controlled buck"] --> DLDO["Digitally-controlled LDO"] --> PF["Pi filter"] --> CS["Current sense"] --> OUTRELAY["Output relay"] --> VOUT["Cell output"]

%% Control + monitoring (isolated I2C domain)
I2C_IN["I2C from mux port"] --> ISOI2C["ISO1640 I2C isolator"] --> I2C_ISO["I2C isolated"]
ISO3V3 --> ISOI2C

I2C_ISO --> DACB["DAC @0x61<br/>buck setpoint"]
I2C_ISO --> DACL["DAC @0x60<br/>LDO setpoint"]
I2C_ISO --> ADC["ADS1115 @0x48<br/>telemetry"]
I2C_ISO --> GPIO["TCA6408 @0x20<br/>GPIO expander"]
I2C_ISO --> ISENSE_IC["Current sense IC @0x21"]

GPIO -->|enable| DBUCK
GPIO -->|enable| DLDO
GPIO -->|control| LOADSW["Load switch"]
GPIO -->|control| OUTRELAY

%% Telemetry points
ADC -->|buck V| DBUCK
ADC -->|LDO V| DLDO
ADC -->|output V| VOUT
ADC -->|shunt V| CS

%% Fast discharge path
PF --> LOADSW --> RLOAD["Discharge resistor"] --> PF

%% Precision measurement path (selected externally)
CS --> DMMRELAY["DMM relay"] --> RLIM["Current-limiting resistors"] --> DMM_OUT["DMM_OUT bus"]
end
```

## Getting Started

1. Install python if you don't have it already
Expand All @@ -44,6 +175,7 @@ We use PlatformIO to build and upload the firmware via USB.
2. Connect the board via USB (might need to accept connection popup on Mac)
3. Run `pio run -t upload` or install the PlatformIO VSCode extension and use the upload button.

See `SOFTWARE.md` for firmware architecture, protocol, and troubleshooting.

## API Docs
### Connecting to cellsim
Expand Down
145 changes: 145 additions & 0 deletions SOFTWARE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
## Cell-sim software

This project has two software layers:

- **Firmware** on the on-board **ESP32-S3** (`firmware/src/`), built with PlatformIO (Arduino framework).
- **Host tools** in **Python** (`lib/` + `example_set_voltages.py`) that talk to the board over **USB serial**.

### Architecture overview

#### Firmware (`firmware/src/main.cpp`)

- **Transport**: USB CDC serial (`USBSerial`) using a simple line-based ASCII protocol.
- **Cell abstraction**: `Cell` class (`firmware/src/cell.h`, `firmware/src/cell.cpp`) represents one of the 16 channels.
- **I²C topology**:
- Two MCU I²C controllers: `Wire` and `Wire1`.
- Each bus drives one **TCA9548A** I²C mux (address `0x70`) so each bus provides **8 mux ports** → **16 cell channels total**.
- Every `Cell` selects its port by writing `1 << mux_channel` to the mux (see `Cell::setMuxChannel()`).
- **Main loop responsibilities**:
- Parse and execute serial commands (enable relays, set targets, read telemetry).
- Periodically update **addressable LEDs** based on measured voltage/current.
- Apply the current `voltageTargets[]` array to all 16 cells at a fixed cadence.

#### Per-cell firmware behavior (`firmware/src/cell.*`)

Each `Cell` controls and measures a channel via I²C devices behind the mux:

- **Two DACs** (`MCP4725` @ `0x60` and `0x61`) drive the “digital LDO” and “digital buck” control points.
- **ADC** (`ADS1115` @ `0x48`) reads:
- buck output voltage
- LDO output voltage
- shunt/current sense voltage
- output voltage
- **GPIO expander** (TCA6408 @ `0x20`) controls:
- buck enable
- LDO enable
- output relay
- load switch
- **Calibration mapping**:
- `calculateSetpoint(voltage, ...)` does piecewise-linear interpolation between calibration points to convert a desired voltage into a DAC code.
- `calibrate()` sweeps DAC values and records \((measured\_voltage, dac\_setpoint)\) pairs.

#### Host Python (`lib/` and `example_set_voltages.py`)

- `lib/client.py`: low-level serial transport (`CellSimClient.send_command()`).
- `lib/cellsim.py`: a friendlier wrapper (`CellSim`) with methods like `setVoltage()`, `getAllVoltages()`, etc.
- `example_set_voltages.py`: finds the serial port (via `PING`) and runs a basic functional test.

### Serial protocol (USB)

Commands are ASCII lines terminated by `\n`.

- Responses are typically a single line starting with `OK:` or `Error:`.
- Some “read-all” commands return one `OK:` line containing comma-separated values.

Supported commands (implemented in `firmware/src/main.cpp`):

- **Connectivity**
- `PING` → `OK:PONG`
- **Voltage control**
- `SETV <cell 1-16> <voltage>` → `OK:voltage_set:<v>`
- `SETALLV <voltage>` → `OK:all_voltages_set:<v>`
- **Voltage telemetry**
- `GETV <cell 1-16>` → `OK:voltage:<v>`
- `GETALLV` → `OK:voltages:v1,v2,...,v16`
- **Current telemetry**
- `GETALLI` → `OK:currents:i1,i2,...,i16`
- **Output relay (open-wire simulation)**
- `ENABLE_OUTPUT <cell>` / `DISABLE_OUTPUT <cell>`
- `ENABLE_OUTPUT_ALL` / `DISABLE_OUTPUT_ALL`
- **Load switch (speed up discharge / settling)**
- `ENABLE_LOAD_SWITCH <cell>` / `DISABLE_LOAD_SWITCH <cell>`
- `ENABLE_LOAD_SWITCH_ALL` / `DISABLE_LOAD_SWITCH_ALL`
- **DMM routing**
- `ENABLE_DMM <cell>` (selects the DMM mux channel on the MCU)
- `DISABLE_DMM`
- **Calibration**
- `CALIBRATE <cell>` / `CALIBRATE_ALL`

Notes:

- The firmware clamps internal target rails (buck/LDO) to safe min/max ranges.
- `Cell::setVoltage()` typically sets the buck slightly above the target (currently ~5% headroom) and the LDO to the requested voltage.

### How to use (firmware)

Prereqs:

- PlatformIO CLI or the PlatformIO VSCode extension.

Build + flash:

```bash
pio run -t upload
```

Serial monitor (optional):

```bash
pio device monitor
```

### How to use (Python)

Prereqs:

- Python 3
- Install dependencies:

```bash
pip install -r requirements.txt
```

Run the example script (auto-discovers the board by sending `PING`):

```bash
python example_set_voltages.py
```

Minimal API example:

```python
from lib.cellsim import CellSim

cellsim = CellSim("/dev/ttyACM0") # replace with your port
cellsim.setAllVoltages(3.5)
cellsim.enableOutputAll()
print(cellsim.getAllVoltages())
print(cellsim.getAllCurrents())
cellsim.close()
```

Interactive CLI (optional):

```bash
python -m lib.cli -p "/dev/ttyACM0"
```

### Functionality summary

- **Set per-channel “cell” voltages** (0–~4.5V typical operating envelope).
- **Measure per-channel voltage and current** via the onboard ADS1115.
- **Simulate open-wire faults** by opening the per-channel output relay.
- **Speed up voltage transitions** with a per-channel load switch.
- **Route one channel at a time to an external DMM** for high-accuracy measurement.

Loading
Loading