UF2 Batch Flasher

When you want to scale a project, one simple way to make it happen is to make it modular. However, making things modular in software is easy, as you can program some software to do something as many times as you want. However, when dealing with hardware, this is yet another problem to be faced.
The UF2 Batch Flasher is meant to provide a way to loop over up to 64 devices, in order to flash all the 64 devices with a single UF2 image. Using a Raspberry Pi Pico, and 64 USB A ports, the RP2040 will select one port at a time, and copy the transmitted image to each device.
The alternative is to find the next cable, find the board at the end of it, press a button, plug the cable while pressing the button, type a command / drag a file over, unplug the device and repeat this 63 times. Not only this error prone but this is time intensive and would probably take half an hour to iterate over that many devices manually.
Motivation
This project came up as a need for another project, for which 60 Raspberry Pi Picos are already reserved for prototyping it. At the end, the UF2 Batch Flasher should be integrated within this other project. In order to reduce chances of failures, while prototyping, splitting the functions in different sub-projects made sense.
Thus the UF2 Batch Flasher is a way to reduce the cost of unrelated failures while providing a versatile platform for experimenting with a large set of devices.
Overview
This project is designed around 3 main components. The Raspberry Pi Pico W, the FSUSB74 which is a 4 way-switch for USB signals and 74HC238 which is a selector.
The RP2040, the microcontroller of the Raspberry Pi Pico, is programmed to set the pins of both the 74HC238 and the FSUSB74 to select a single USB port among the 64. In addition to that, A data-enable pin and a power-enable pin are used to cut off the switches and turn off the power, even if the port is selected.
TinyUSB is used to implement a USB host port with the pins 0 and 1 connected to the selected USB device. The UF2 process is a device feature, as the host only sees it as a Mass Storage (MSC) device. Thus testing can be done with a USB flash drive.
In the case where the flashed device is also a RP2040, then there is a neat feature which implies that one does not have to press the Boot-select button. This is achieved in software by selecting a Baud Rate of 1200 Hz. Once set, the RP2040 will automatically reboot in Boot-select mode.
The image to be flashed is transmitted over Wifi, as well as the stdout debug information and the last recorded status of each USB device. A Web server is implemented using LwIP, which serves a web page which contains the high level logic and sends commands over, such as selecting each USB and transmitting the image to be flashed.
The Making Of
No surprises that such electronic design is based of 2 things, one which is the 4 layer circuit board and the other which is the software which is flashed on the Raspberry Pi Pico.
The Board
Selecting Components
One of the challenges about making a PCB is knowing what has to be achieved, looking in the part collection which one is the best for the task. Many components could be used to implement a USB switch, but ideally one should look for a component which toggles both data lines at the same time.
Surprisingly, it was difficult to find a single component which handled the data line and the power line. At the end, settling on the AP22804AW5-7 for regulating the 5V power and on the FSUSB74 for switching the data lines.
Having both separated is potentially necessary as some might have noticed on male USB A connectors that the data connectors are shorter than the power connectors.
The FSUSB63 documentation recommends the usage of 1pF TVS/ESD with a 2.2 Ohm. Thus, each USB port is protected by a PESD2USB3UVT which is used to let go if there are any spikes on the data lines. However, there is no protection in the case of surges from the GND or from the 5V power line.
To enable the AP22804AW5-7, we have to pull-up the enable pin. Given that the Pico does not have 64 GPIO, this is achieved using the 74HC238 which switches on the select line, in a similar way as the FSUSB74 selects a given pair of data lines.
The 74HC238 has 8 outputs whereas the FSUSB74 has 4, which implies that we have effectively 2 trees of components used to toggle a unique USB port at a time. Fortunately both can be used to decompose 64, which implies that no output pins are wasted. However, caution should be used to correctly map the select pins across both, such that we correctly toggle the power and data line of the same USB port. Thus using the high select bit near the microcontroller, and the lowest select bits when closer to the USB port, as well as using the same ordering on the decoded/selected inputs pins.
Capacitors
The USB specification requires that a 120 µF electrolytic capacitor is used per USB port. However, placing these components on the board was taking most of the space. Obviously these capacitors are not used either in small and cheap USB hubs that are found everywhere.
Apparently electrolytics capacitors can be replaced by multi-layer ceramic capacitors (MLCC). The reasoning is that Electrolytic capacitors have a higher internal resistance (ESR), thus one should have a higher capacitance to filter the same frequencies. As MLCC have a much lower ESR, one can pick a capacitor with a smaller capacitance.
Still there is a trap, MLCC capacitors have a reduced capacitance when used with direct current (DC), which is the case of the 5V power line for powering USB devices.
Bill Of Material
Except for the USB cables, all the components are sourced from Mouser, and the PCB is ordered from Aisler for 118.20€ (x3) with a stencil at 34.09€ as well as a 2.02€ donnation for KiCad.
Qnty | Value | Description | Vendor | Price |
---|---|---|---|---|
73 | 0.1uF | Unpolarized capacitor | eu.mouser.com | 1.60€ |
21 | 1nF | Unpolarized capacitor | eu.mouser.com | 0.27€ |
64 | 1uF | Unpolarized capacitor | eu.mouser.com | 0.90€ |
64 | 22uF | Unpolarized capacitor | eu.mouser.com | 3.60€ |
64 | Green | Light emitting diode | eu.mouser.com | 8.45€ |
64 | PESD2USB3UVT-QR | 3.3V dual ESD | eu.mouser.com | 12.60€ |
64 | USB_A | USB Type A connector | eu.mouser.com | 29.50€ |
1 | 10 kOhm | Resistor | eu.mouser.com | ~5.00€ |
64 | 120 Ohm | Resistor | eu.mouser.com | 5.00€ |
128 | 2.2 Ohm | Resistor | eu.mouser.com | 5.00€ |
1 | CUI_TS20 | Normally-open tactile switch | eu.mouser.com | 0.29€ |
21 | FSUSB74 | High-Speed USB Multiplexer / Switch | eu.mouser.com | 18.88€ |
9 | 74HC238 | 3-to-8 line decoder/multiplexer, DIP-16/SOIC-16/SSOP-16 | eu.mouser.com | 2.94€ |
1 | RaspberryPi_Pico_W | RP2040 dual-core Arm Cortex-M0+, 264kB SRAM, Infineon CYW43439 2.4GHz 802.11n wireless LAN | eu.mouser.com | 5.58€ |
64 | AP22804AW5-7 | Power switch | eu.mouser.com | 21.06€ |
Only considering all the parts, this makes a total of 270€ without taxes, the delivery taxes, the solder paste, the USB cables, nor the devices to be connected at the other end.
Most people do not have 64 USB cables resting in a closet, and finding the cheapest USB cable on Amazon would not help with the budget of this project. However Amazon can hint where one might look for. In this case TME and Assmann have a large collection of USB cables of various kinds and qualities, such as the AK-300127-018-S.
Beware that the price is aligned with the quality of the cable. At this price tag do not expect a long shielded cable capable of USB 3.0 speed. On the other hand, the PIO emulated USB port would not go at USB 3.0 speed either, but the 1.8m might be a problem for some applications. Be aware that all the 64 connected devices would be at a cable-length of the UF2 Batch Flasher.

Placing Components
The placement of components on the board is at the scale of being painful if done by hand, as there are more than 600 surface mounted components, for which solder paste and a reflow oven are recommended.
Having a LumenPnP from Opulo is definitely helpful, and using some feeders for the 2.2 Ohm resistors is a nice luxury. Except that LumenPnP feeders do not yet have a direct feedback of the tape position and there is a backlash which can cause important variations in the picking position. This is problematic enough that after picking a few of components, the picking procedure would miss the pocket.
After trying multiple mechanical solutions such as adding pressure or drag to the tape, a software solution came up as the best option. The software will look at the reference holes of the tape and compute the picking location. While this adds latency, this is less of a problem than dealing with every wrong pick. This extra vision is implemented in a fork of OpenPnP. Later, support of 0402 components should be added, as well as a way to skip using vision when the picking location's variance becomes small enough.
Also, verifying that you are using the same version of the fiducial placement helps avoid errors and reduce the time needed to watch for misplaced components.
Fixing QFN Solder Bridges
Before soldering the vertical USB ports, have a look at the FSUSB74 in case of short circuits. A short on the D+ / D- pins with the ground or 3.3V pins is possible and this might cause too much current, where the limit is at 20 mA.

For the first board, debugging was achieved using a perforated board on which a USB connector can be plugged with a male-male USB cable to the connector, and the other end can be connected to the power pins of the Raspberry Pi Pico, while the data pins are connected where the microcontroller is supposed to be. These debugging board buttons are made to emulate pull-up and pull-down of the device on the data pins, and check what the voltage would be on the microcontroller side.
If you were to make this process scale, you might want to have a procedure for testing the board before soldering any through hole components, in order to keep some space around the components to rework the solder bridges. This implies being able to plug the select pins, data and power enable pins, 3.3V and 5V pins, but not the D+/D- pins that you want to keep for testing.
Once identified visually or electrically, reworking the QFN solder bridges is easy, as long as you have the right tools:
- Solder flux: The solder flux is used to reduce the surface tension of the solder, thus forcing it to make smaller droplets.
- Flat surface iron: The flat surface iron is used to provide an area where the surface tension is higher, such that it pumps the solder out of the solder bridge.
- Solder: The solder is used to pre-populate the flat surface of the iron with a tiny layer of solder, such that it can pull excess solder on the flat surface of the iron.
The procedure is simple: Apply some solder flux on the sides where there are the solder bridges, apply some solder on the flat surface of the iron, and slide the flat surface on the sides of the components where you have a solder bridge. That's all!
The Code
Building
This project might be the first (released) project which relies on Nix to develop code for the Raspberry Pi Pico using the Pico SDK, and additional libraries such as LwIP, Cyg43-driver, TinyUSB and Pico-PIO-USB.
Nix is a package manager, which is used by NixOS Linux distribution. It also
offers an experimental feature named flakes
. While this feature is not as good
enough for managing all individual dependencies of a distribution, it is
excellent for language agnostic dependency management at the scale of a project,
similar to virtual-env in Python and npm in JavaScript.
While managing libraries this way introduces additional complexity, on top of CMake's strange composition rules, this makes managing dependencies reliable and potentially reproducible. The dependencies are all frozen to specific versions, which is good when you come back to a project after months, just to fix one annoying bug, and not a pile of integration issues.
In addition to providing the dependencies, this also helps building a working
environment. Using nix develop
one would have a shell with a set
of shell functions for building, flashing and debugging the firmware.
Dependencies
This project mostly interacts with the following libraries:
-
Pico SDK with the Cyg43-driver are useful libraries which simplify the manipulation of the internal registers of the RP2040 and the Wifi component of the Raspberry Pi Pico W.
-
TinyUSB and Pico-PIO-USB are used to emulate a USB host.
-
FatFS library, included with TinyUSB, is helpful for manipulating files on a Mass Storage Class device. As UF2 protocol goes by as a fake Mass Storage device, this is mandatory.
-
LwIP to implement the web server stack. While technically not needed, this is nice for implementing some client side logic, and for working remotely on the device. Having 64 USB cables on one desk is going to be messy otherwise.
The Pico SDK is really well documented and is a pleasure to work with, even
macros have documentations, such as PICO_STDIO_USB_RESET_MAGIC_BAUD_RATE
. This
feature implies that if you are using stdio_usb
, the Pico can be switched back
to Boot-select mode by changing the Baud rate.
On the other hand, TinyUSB, LwIP, FatFS are lacking proper documentation. Even if LwIP has a doxygen-like documentation, it is not actionable without reading examples, reading the code and even enabling debug information.
Remapped UART Pins
The RP2040 has multiple UART outputs, if you want to make use of the other corner of the Pico for UART, this can be achieved as follows:
target_compile_definitions(uf2-batch-flasher PRIVATE
PICO_DEFAULT_UART_TX_PIN=16
PICO_DEFAULT_UART_RX_PIN=17
)
This might be useful if you already designed a board without even thinking about UART, and later realize that having any output is better than none.
Web based Stdout
On the other hand, once you have a working Web Server, you might as well use it.
The Pico SDK provides the stdio_driver_t
structure to register handlers to
process inputs and outputs. Thus one could make use of it for additional use
cases, such as buffering everything which is generated by printf
calls and
dump this content each time a file such as stdout.ssi
is queried.
This feature is one of the most useful additions to this project, except when one freezes or locks the web server to a point where it does not answer anymore.
Testing with a single USB
Before designing the board, sometimes it is useful to validate the basics of the code. One example of it was to discover that setting the Baud rate to 1200 Hz was not enough.
While the USB device would properly switch to Boot-select mode, the TinyUSB library would not forget that the connected USB device was connected as CDC, and will not attempt to mount it as a Mass Storage device. Removing and restoring the power would reset the connected Pico and let it boot from the flash.
However, physically disconnecting the data lines and reconnecting them back proved to be effective as a way to force TinyUSB to unmount the CDC device and mount it as a MSC device.
Validating this part early also helps the design of the board, as we need both an enable power pin, but also an enable data pin.
Off-by-1 pin selections
Sometimes, what appears like a huge hardware mistake, is a simple software issue. When developing the initial version of the firmware, the select and enable pins got added, but one of the select pin got forgotten in the process. Thus instead of counting up to 64, it only counted up to 32.
The biggest problem with hardware and software development is that if you lack confidence in yourself or in your design, then you will always doubt that you made a mistake. Except that the mistake might be located where you are the most confident about your skills, and you lose an evening trying to look out for a bug at the location where it is not.
Naming RP2040 Pins
The picotool
utility from the Pico SDK can read the content of a binary and
displays the strings which are stored inside it. Some of these strings can be
used to describe the GPIO functions:
void usb_gpio_init() {
bi_decl_if_func_used(bi_program_feature("Select USB device"));
init_select_pin(PIN_SEL0);
bi_decl_if_func_used(bi_1pin_with_name(PIN_SEL0, "S0"));
init_select_pin(PIN_SEL1);
bi_decl_if_func_used(bi_1pin_with_name(PIN_SEL1, "S1"));
init_select_pin(PIN_SEL2);
bi_decl_if_func_used(bi_1pin_with_name(PIN_SEL2, "S2"));
init_select_pin(PIN_SEL3);
bi_decl_if_func_used(bi_1pin_with_name(PIN_SEL3, "S3"));
init_select_pin(PIN_SEL4);
bi_decl_if_func_used(bi_1pin_with_name(PIN_SEL4, "S4"));
init_select_pin(PIN_SEL5);
bi_decl_if_func_used(bi_1pin_with_name(PIN_SEL5, "S5"));
bi_decl_if_func_used(bi_program_feature("Toggle USB device"));
init_enable_pin(PIN_ENABLE_DATA);
bi_decl_if_func_used(bi_1pin_with_name(PIN_ENABLE_DATA, "EN_Data"));
init_enable_pin(PIN_ENABLE_POWER);
bi_decl_if_func_used(bi_1pin_with_name(PIN_ENABLE_POWER, "EN_Power"));
}
In this case, bi_decl_if_func_used
is simply a macro which declares a field in
a different section of the elf, named .binary_info
. The code has nothing to do
with being used or not, it relies on the compiler to remove dead code. Thus,
UART pins would be listed there if stdio_uart_init
is called. This is helpful
to verify that UART pins are correctly mapped and to verify that there is no typo
in the macro's name.
Then bi_1pin_with_name
would declare a variable, which would hold on the
string content which can be queried by the command line tool.
The previous code would then display the following when using picotool info -p
on the UF2 image:
Fixed Pin Information
0: USB Host D+
1: USB Host D-
2: S0
3: S1
4: S2
5: S3
6: S4
7: S5
8: EN_Data
9: EN_Power
16: UART0 TX, UART0 TX
17: UART0 RX
Flush It
Ok, all the assignment pins are fine, there are no more solder bridges, a file is
created on a testing USB flash drive. Why would this file be of 0 size? You read
all the FatFS examples and you cannot figure out what is wrong with your code.
You call f_open
, f_write
and f_close
, exactly like the example does.
This is where reading the code of the library and a bit of system knowledge
helps to remind you that f_sync
looks like a promising function. Indeed, the
content might be cached on the USB drive but not yet committed to its final
destination. Calling f_sync
ensures that the write persists.
LwIP: POST should not lock
However, writing content on the drive does not imply that this is the correct
content, and this is where the LwIP documentation is lacking. The throttling
mechanism for httpd_post_receive_data
is confusing at best. One has to call
httpd_post_data_recved
, but the code suggests that it should not be called with
more than what is received, while the calling it with what can be written to a
FIFO causes LwIP to lose most of the transmitted data.
When locking in httpd_post_receive_data
, then data is simply lost and some
chunks are not received. Strangely, TCP is supposed to prevent messages from
being munched while in transit, by numbering the packets and verifying that all
packets are well transmitted in the right order.
At the end, just transmitting a clone of the struct pbuf
and queuing this
allocation proved to work, despite getting closer to the RAM limits of the
RP2040. So far this works for UF2 images of 127 kB, but if a problem occurs, this
can be handled by writing files in multiple chunks and appending instead of
truncating existing files.
Strangely, once more the documentation of httpd_post_receive_data
is slightly
confusing as the data should be freed, but even if it is not the struct pbuf
given as argument remains the same from one call to the next, actually causing
issues when freeing it. Also, additional cares should also be taken with struct pbuf
as this managed by an allocation pool, and thus the allocation should be
returned to the web server core in order to free it properly.
Conclusion

This started as a tiny sub-part of a bigger project and at the end is much bigger than what was initially thought. Who would have thought that making one Raspberry Pi Pico flashing 64 others would face that many issues?
In the end, this remains a success. No more buttons to be pressed, 64 Raspberry Pi can be flashed remotely.
The Future
So far this project is a means to an end, but this might not be the end of the project. Much more could be achieved with this project, such as:
- Make the rest of the Pico pins available.
- Adding relays controlled by the Pico to turn all USB devices at once.
- Making it flashable from the command line, instead of the web interface.
- Improve the Web interface.
- Have a different images for specific ports.
- Patch a few bytes, to introduce small variations across all devices.
- Monitoring one of the devices using UART over USB.
If you are interested, you can find all the sources of this project on GitHub.
If you are interested in the final product, but not making one yourself, then beware that this is only the first version of it. And if despite this warning you still want one, send an email using the link below and we can discuss the details.