Close
0%
0%

BenchPod

An open hardware bench tool that plugs into your CI: sensor sim, CAN, analog I/O, power control, and a Python SDK with pytest integration

Similar projects worth following
Embedded teams often end up with a bench full of equipment that's hard to share, hard to automate, and difficult to use remotely. Setting up tests often means being physically present, and that doesn't scale well across a team.

BenchPod is an attempt to make that easier. One board with sensor simulation over I2C and SPI, CAN bus, analog input and output, programmable power control, a logic analyzer, and an FPGA for fast protocol mocking, connected over WiFi so it can be shared across a team or accessed remotely. A Python SDK and pytest integration make it straightforward to go from manual bench work to automated tests without changing your setup.

BenchPod v2

v2 is a complete redesign from v1. The goal is the same — a single board that consolidates the instrumentation you'd normally spread across a bench — but the architecture is substantially upgraded based on what v1 taught us.

Compute

The main MCU switched from RP2350B to STM32H563ZIT6 (LQFP-144). The primary reason is native Ethernet: the H563 includes a 10/100 MAC that talks directly to a LAN8742A PHY, with an HR911105A MagJack handling the physical port. The STM32 also gives us six hardware SPI buses, hardware crypto, and a more mature ecosystem for the kind of SCPI-over-TCP instrument control we're building toward.

The iCE40UP5K FPGA returns from v1, again with its own W25Q64 flash for autonomous bitstream load at power-up. In v2 it takes on more: logic analyzer capture, ADC/DAC orchestration, and a planned SWD finite state machine for DUT programming. An APS6404L 8MB QSPI PSRAM sits alongside it for capture buffering.

WiFi is handled by an ESP32-C3-MINI-1, now explicitly treated as a dumb WiFi NIC running esp-hosted or AT firmware. The STM32 manages it over SPI.

Analog input

The analog front-end was the weakest part of v1. v2 replaces the 8-bit, 60 MSPS ADC with an MCP33131D-10, a 16-bit SAR ADC running at up to 1 MSPS. The signal chain is: a compensated ÷12 resistive attenuator, an OPA810 input buffer, a THS4551 fully differential amplifier driving the ADC, and an ADR4540 4.096V precision reference. Input protection is LBAT54SLT1G Schottky clamp pairs. Connectors are SMA (RF3–RF6) instead of BNC.

A relay-based calibration injection path uses four G6K-2F-Y reed relays driven by a TPL7407LA, allowing the DAC output to be switched into the ADC input for in-situ calibration without external connections.

Analog output

The DAC is a DAC8551 (16-bit SPI), buffered through OPA2992 op-amps with TMUX1104 analog mux routing. A TPS65131 dual-rail boost converter and LM27761 negative charge pump generate the bipolar supply rails needed for rail-to-rail output swing. An LM7705 provides the negative supply bias for the op-amp output stages. The XTR116UA handles 4–20 mA current loop output with a BCP56 pass transistor.

Power

Three TPS259470A eFuses protect the switchable power paths. INA238 monitors (replacing v1's INA219) track current and voltage on each rail. The internal power tree uses a TPS82130 3A MicroSiP buck for 3.3V digital, AP2112K LDOs for 1.2V, 1.8V, 2.5V, and 3.3V analog rails, and a TPS7A1901 LDO for the precision analog supply. A TPS2116 handles power mux for input supply selection.

Connectivity and DUT interface

Ethernet via RJ45 (HR911105A) is the primary host interface. USB-C remains for firmware updates and initial config. The SN65HVD230 CAN transceiver returns, now wired to the STM32 directly. DUT-facing I/O includes I2C, SPI, UART, and GPIO, with SN74LVC2G66 analog switches for switchable pull-ups and SRV05-4 TVS arrays for ESD protection. Four TCA9554 I2C I/O expanders (replacing v1's single PCA9555) add GPIO without burning STM32 pins.

BenchPod v2.0.0.pdf

Adobe Portable Document Format - 1.21 MB - 06/19/2026 at 14:45

Preview

  • ADC And DAC Capabilities

    Edward Viaenean hour ago 0 comments

    Update this week: mainly working on the firmware part: validating ADC & DAC, and running test cases with a Device Under Test, controlling and monitoring a Step Motor.

    First of all, ran a FFT analysis using the calibration path (DAC -> ADC) and also did some noise analysis with some near field probes (amplifier from gpio.com: https://gpio.com/collections/low-noise-amplifiers/products/ultra-low-noise-amplifier-pga-103-2-ghz-unconditionally-stable-gain-20db). Everything looks pretty clear. Here's the results of the calibration path:

    Then, a few tests to capture the step motor output in our web UI (embeddedci.com)

    Normal scenario:

    Power cut-off scenario (including timings of the step pulsing so we can verify the difference between actual cut-off and DUT noticing):

    Stall scenario:

    They are stored in our library as well, so we can replay them without the actual step motor (simulating the step motor for the DUT):

  • Correlating digital and analog on a single trigger

    Edward Viaene7 days ago 0 comments

    The goal: show digital and analog in a single view.

    The test: to show how to verify stepper motor timing on a Device Under Test (an STM32F446RE). Logic and analog are sampled off the same clock (using an iCE40 FPGA on the BenchPod). 

    We have a 48MHz oscillator on the board. Unfortunately, I didn't get all the code running at 48 MHz immediately, so we're split between 24 MHz and 48 MHz in the deployed ICE40 code. Mainly, the SPI interface for receiving commands would need a serious rewrite if we wanted to run that at 48Mhz, and it doesn't need to, so those parts are running at 24Mhz.

    There are 14 Logic Analyzer pins available to either output timing or to use for a specific protocol, such as UART. This is all at runtime, so you can say pin 3 and 4 are now RX and TX for UART, and I want to see the output on pins 9 & 10, because that's where I have my step direction and step input connected.

    To monitor the current of the coils (or 1 coil in this example), we use a shunt resistor and an INA282 to amplify the analog signal. That way, we can see the analog data from the coil and match it with our digital signal from the step motor.

    The captured data is written over quad SPI to the PSRAM, where the STM32 can read it. The iCE40 is driving a signal on a pin for the STM32 to read, to ensure they're not both talking to the PSRAM at the same time, so the STM32 has to wait until the writes are complete. The data is then sent from the stm32 to the dashboard via a WebSocket over Ethernet. As described earlier in the previous log, we're using lwIP for all this.

  • Networking with ESP32 and STM32H563 without ESP-AT

    Edward Viaene07/03/2026 at 16:49 0 comments

    The BenchPod v1.0.0 setup was pretty simple: RP2350B with AT commands over UART to the ESP32 module for WiFi. In v2.0.0, I needed a different approach because we added Ethernet and wanted a streamlined solution.

    The STM32H563 has an Ethernet MAC, so the obvious solution would be to do lwIP in the firmware. That's all pretty well-documented, so it's easy to implement. We communicate over RMII to the LAN8742 PHY, and from that point, it's all physical layer. Doing AT at this point with the ESP32 sounds like maintaining 2 different approaches to networking.

    This brings us to esp-hosted-mcu (https://github.com/espressif/esp-hosted-mcu). Using this, we can keep lwIP, use 2 interfaces, and maintain 1 codebase for both WiFi and wired Ethernet. The lwIP interface for WiFi flattens the packet, wraps it in the esp-hosted framing, and puts it on the SPI bus to the ESP32, which then sends it out over WiFi. This gives us 1 IP layer inside the STM32, with the ESP32 just forwarding the packets. The AT protocol is no longer needed (esp-hosted has its own binary control channel for scan/connect instead), and the WiFi link shows up as just another native netif to lwIP.

    Still had to chase down some non-obvious bugs. IP packets have checksums, and in lwIP, checksum generation was disabled because it'd be handled by the STM32's Ethernet MAC in hardware. The catch is that lwIP's checksum setting is global by default, so when I brought up WiFi, those packets went out with zero checksums too. The ESP32 side of the SPI link has no hardware to generate them. I had to sniff the DHCP packets to figure out why nothing was coming back (a missing checksum on the outgoing frame):

    e8:3d:c1:21:2e:70 ... length 336, bad cksum 0 (->ba75)! 

    That "bad cksum" is the IP header checksum, which is mandatory, so the frame is dropped, and no DHCP offer ever comes back. The fix was to enable per-netif checksum control (LWIP_CHECKSUM_CTRL_PER_NETIF): the Ethernet netif keeps offloading to the MAC, while the WiFi netif computes IP/UDP/TCP checksums in software. That solved it.

    Also worth mentioning: the ESP32-C3 ships blank, and we only wire UART, EN, and BOOT lines to reach the STM32, because we want to flash over USB (which is connected to the STM32). That means the STM32 has to flash the ESP-hosted slave firmware into the ESP32 itself over its ROM bootloader before any of this networking works. The MCU bootstraps its own WiFi chip by loading the ESP32 binary from its flash and flashing it over UART.

  • Analog Calibration With Agilent 34401A

    Edward Viaene07/02/2026 at 15:33 0 comments

    This has been quite a journey. Measurement instruments can be very expensive, and I didn't want to immediately go there for our prototype. The solution was to buy a calibrated Agilent 34401A on eBay with a certificate and figure out how to hook it up to my machine so I could use it to calibrate the BenchPod.

    I bought a GPIB-USB-HS to connect the Digital Multimeter to my MacBook, but quickly realized there's no macOS support, so I connected it to my Raspberry Pi 4 instead. I tried using Linux-GPIB to read the voltages, but couldn't get it working because the bytestream I was getting wasn't what the library expected. Almost ready to buy another adapter, I plugged it back into my MacBook and asked Claude Opus 4.8 if it could get it to work on macOS. To my surprise, after a few minutes, it had written a Python module using pyusb that used the correct sequence to get it to work. There we go! Cheap solutions that work are the best! I put the repo online for anyone who wants to use the Agilent 34401A or similar cross-platform using Python. It's available at https://github.com/embeddedci-com/ni-gpib-usb-hs

    Back to the calibration part of BenchPod: with the Agilent connected via USB and the BenchPod online, I could manually execute the commands and compare them to the data from the Multimeter, but couldn't give Claude a go at it. I explained in the prompt how to access both tools, and it did the calibration for me, quickly switching the relays and doing a DAC sweep to get me the outputs. Here's a screenshot of the end result in Claude Code:

  • TVS, Connector Spacing

    Edward Viaene07/02/2026 at 14:29 0 comments

    As promised in the previous log, the TVS diode and the cable connector "fix" because the button is too close to it:

  • v2.0.0 Arrived

    Edward Viaene07/01/2026 at 03:05 0 comments

    Yesterday, v2.0.0 arrived. I decided to boot it up right away, which made me realize there was something wrong. After plugging the USB cable from my PSU in, it went straight into constant current mode, and the voltage dropped to 1A (the limit).

    My mistakes are often concentrated in the last additions just before shipping it off to the manufacturer, so that's where I went to look. I decided at the last minute that I needed an extra eFuse on the incoming USB, along with a DVS diode. The eFuse was all wired OK, but the SMAJ5.0 TVS pins were flipped. After removing the TVS diode with a hot air gun, the board came up. I was a bit concerned about the 0402 passives around the TVS, but luckily, they decided to stay put (kept the airflow low). 

    Next, I actually managed to flash the STM32 with the new firmware. I used dfu-util, as I'm pretty much allergic to the standard tooling and IDEs. The open source tooling works very well, and I manage to do everything using the console.

    The USB serial console didn't come up, though, so I needed to use SWD to have a peek. The downside was that the pins were not yet in place, and since I had chosen a 1.27mm pin header, I had to work carefully to get them in. That revealed another issue I should've anticipated: the reset button is in the way of the 10-pin cable fitting over it. Well, I managed to resolve it. I'll post a picture later so you can see how to creatively connect a cable when there's a button in the way.

    After gaining access to SWD, I managed to get the firmware on it. USB serial came online, and the benchpod used DHCP via its ethernet port, so we're pretty much set up now!

    Below is a picture of benchpod v2.0.0. The decent-sized boxes are the relays for the analog path (to enable the calibration paths):

    Here's the pod online in the UI:

    And here's what BenchPod V2.0.0 looks like (and V2.0.1 will have the TVS diode fixed):

  • The Switch From RP2350B To STM32H563

    Edward Viaene06/25/2026 at 16:14 0 comments

    A few notes on the switch from RP2350B to STM32H563:

    v1 of the board had the RP2350B, which had dual-core, 2x SPI, 2x UART, 2x I2C, and PIO support, which was genuinely useful for the deterministic timings we needed. The issues we hit, though:


    • No Ethernet support, which would mean an external PHY/MAC chip like the W5500. That'd work, but you'd also need an extra SPI bus for that
    • That brings us to the lack of peripherals in RP2350B. You can use PIO, but PIO state machines are also not unlimited, and if you have to use it for SPI/I2C/UART, you can't use it for something else anymore
    • The RP2350B has no internal flash, so you need an external flash, which adds floorspace on the PCB, an extra BOM item, extra feeder fees on low volume, etc
    • The RP2350B doesn't have built-in wireless. If you want to build the pico2 (RP2350B + wireless) architecture yourself, you would need to recertify the Wi-Fi. If you want your MCU on your pcb to be RP2350B, you'd still need an ESP32 or Pico2 module on your PCB separately if you want WiFi without having to recertify the WiFi module

    That's why we moved to the STM32H563ZIT6. It's a big step up, but the gap is smaller than it looks once you count the full architecture:

    RP2350B + boot flash + inductor is around 3 USD (low volume, during prototyping). You also have to account for extra feeder fees for the components you need, specifically the RP2350B, including resistors for the USB pins that other chips have internally. This disappears, of course, at volume, but a unique part count in a BOM can bump you to higher fabrication tiers.

    STM32H563ZIT6 with its internal flash and no external parts: 7.70 USD (prototype quantities) and around 5 USD at volume. That's a big jump, but you get a lot in return for external parts that you don't need. You now have MAC support for Ethernet, and you don't need the W5500 (which is ~2.7 USD at low volume). You have the extra SPIs/I2C/UARTs, but you lose the PIO. 

    Conclusion: the RP2350B is great at the low price level, but you might outgrow it quickly if you need Ethernet, more SPIs/UARTs/I2Cs, and the external components make the price difference smaller than you think, especially if you take into account that there are a lot of other MCUs in between the price point of an RP2350B and an STM32H563ZIT6.

  • CMSIS-DAP over TCP

    Edward Viaene06/24/2026 at 15:01 0 comments

    BenchPod is a tool that'll plug into CI, for example, GitHub actions. I spent some time building CMSIS-DAP over TCP in the firmware so we can run pytest flash commands within GitHub Actions over TCP. BenchPod opens a WebSocket connection to our Cloud Platform, and we encapsulate DAP packets as JSON messages between pytest (GitHub Actions), the Cloud Platform, and BenchPod.

    Our example repo with a GitHub workflow and the pytest code can be found here: https://github.com/embeddedci-com/examples/

    Also, an interesting fact: the GitHub Actions workflow doesn't need any credentials. It can fetch an OIDC token that the server can verify. We only need to know the repository owner and name (the IDs actually, as those are unique). The server can then validate ownership without having to put credentials in CI. On the firmware side, we generate a private key that stays on the device, authenticate with the cloud server, and establish an outgoing WebSocket over TCP to the cloud platform.

  • v2.0.0 Is Being Produced

    Edward Viaene06/19/2026 at 15:00 0 comments

    Finalized v2.0.0 and ordered it on JLCPCB (the new schematic is under files). A few notes

    • PCBA standard is needed for ESP32, I knew that from the v1 version, but when you have more components, at some point the benefit of PCBA economy will disappear once you have a lot of extended components. This is because it's 3 USD per extended component in economy, but 1.5 in standard
    • The RGB LED I picked needs some special treatment, which is another ~10 USD on top of that, and also bumps you out of PCBA economy
    • The combination of lots of extended components and the ESP32 (for WiFi) makes, in my case PCBA standard roughly the same price (it's perhaps 10 USD difference)

    There are lots of components in v2.0.0, but the board size is the same. On multiple occasions, I wondered about expanding the board or going to more layers. More layers are much more expensive, so a bigger board would be a better solution. Still managed to route everything with the same board size. Almost all the analog path is routed on top. The power at the bottom. For the digital path, it's more mixed, but that should be OK. Here's a screenshot of the wired PCB. Left is analog, middle and right is digital. I removed the ground fill to make it more readable.

    Here's the 3D overview. I picked SMA for the analog path (ADC / DAC) and screw terminals for the 4-20mA measurement and output. And yes, this STM32 is huge.

    The difficulty with wiring the STM32H563 is that the pins are scattered across multiple protocols. RMII is on all sides, SPI is on all sides, so there's no perfect orientation for the chip. That quickly translates into trying to get all these traces from the ESP32 and RMII down to the STM32 and wiring it up from there.

  • Analog v2

    Edward Viaene06/16/2026 at 13:38 0 comments

    Still working on the v2 version. Currently doing component placement, which means that (hopefully) the schematics are not going to change much anymore. What analog looks like right now:

    • ADC: +-30V input, OPA810 buffer, THS4551 Fully Differential Amplifier, and MCP33131D-10 as ADC (1MSPS, 16 bit). That is ~400 kHz usable signal bandwidth
    • A 4-20mA measurement path can be switched with a G6K-2F-Y relay. It'll connect before the 12:1 divider and reuse the same ADC
    • For calibration, there's also another G6K-2F-Y relay that can disconnect the input path and connect the DAC so that we can calibrate the DAC to ADC path

    Here's the ADC schematic:

    The DAC is a 16 bit DAC8551 with an OPA2365 buffer. I have a few TMUX1104 that can switch the path to different voltages: 0-5V, 0-3V, and -+14V. The -+14V is the path that can output to the ADC for calibration.

    There's also a 4-20mA output from the DAC. There's another relay that can switch between outputting to the regular path or to the 4-20mA output path.

    The DAC schematic:



    Lots of parts, which means lots of loader fees at JLC, so let's hope it doesn't need many respins! There are 8 different voltages to work with in the analog section:

View all 16 project logs

Enjoy this project?

Share

Discussions

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates