通过UART启动RP2350
Booting the RP2350 from UART

原始链接: https://pfister.dev/blog/2025/rp2350-uart-bl.html

这篇博文探讨了如何使用第二个廉价的Raspberry Pi RP2350微控制器作为端口扩展器,以满足需要更多PWM通道和GPIO的项目需求。作者详细介绍了如何使用RP2350的UART引导加载程序(RP2040中没有此功能)作为一种比在多个控制器之间管理固件更简单的替代方案。 这个过程包括配置RP2350从SRAM(可用520kB)运行代码,将固件二进制文件嵌入到主微控制器的代码中,并在解锁引导加载程序后通过UART传输它。速度测试显示固件传输时间不到一秒。文中提供了Python概念验证和主微控制器的C端口代码。 为了解决UART范围限制的问题,作者成功地实现了RS-485收发器和标准以太网线缆,能够通过10米长的线缆可靠地远程引导RP2350。文章还讨论了如何解决使用特定GPIO组的UART引脚的问题。这提供了一种经济高效且可靠的方法来扩展微控制器的功能。

这个Hacker News帖子讨论了绕过闪存,通过UART引导RP2350微控制器的方案。原文探讨了这项技术,评论者建议了一些改进,例如使用小的二进制stub来加快速度,或利用两个QSPI插槽来使用PSRAM。这将为多轨循环器等应用提供更大的音频缓冲区。 讨论还延伸到使用SWD引导,用户提供了相关的ARM文档链接。虽然比UART更复杂,但SWD文档齐全,并提供了一种控制RP2350的替代方法。 一些评论者强调了UART引导的实际应用,例如在PicoVision中加载GPU固件、BLE控制器,甚至早期的WiFi芯片如ESP8089(ESP8266的前身)。该帖子展示了UART引导的多功能性及其在特定应用中的潜力,尤其是在闪存有限或不可用的时候。
相关文章
  • RP2350 几乎满足了我所有的愿望 2024-08-09
  • (评论) 2024-08-31
  • (评论) 2025-03-18
  • 我喜欢 RP2040 2024-06-12
  • (评论) 2024-06-12

  • 原文

    03.05.2025

    The RP2350 is a nice microcontroller from Raspberry Pi. Recently I thougt about a new project, where a lot of PWM channels would be needed. The RP2350B has 24 PWM channels, which is a lot - but not enough. I would need at least 33 PWM channels, and also some more GPIOs, so the RP2350 alone isn't enough. So I thought about a port expander... But what about using a second RP2350 as a port expander?

    The chip is relatively cheap (about $0.90), needs few components around it, and... firmware? Well, that's not an ideal solution. Dealing with different firmwares on multiple controllers on the same board can be challenging. Also I really like the idea of having a simple port expander and not needing to deal with version checks and incompatibility breaks between different firmware versions.

    So I read the datasheet of RP2350 (Chapter 5.8). And the RP2350 not only comes with an USB bootloader, which is most likely how most people program their chips, but also an UART bootloader (a new RP2350 feature over the RP2040!). UART is a lot easier to implement on a microcontroller than USB, and it's also the way to go for communication between the chips.

    I made a YouTube-Video about this topic, watch it if you're interested!

    The theory

    The process to load the firmware on the chip via UART is relatively simple: Just do some "unlocking" with a magic pattern, and then send the firmware image in 32-byte-chunks to the chip. After that's done, just run it.

    By the way, here is the bootloader code of the RP2350.

    Of course there are downsides:

    • The firmware needs to run from SRAM, consuming space in SRAM. But the RP2350 comes with 520 kiB, that should be enough for a port expander, right? (It is!)
    • It takes some time on every boot. My first tests show, that it takes something below a second to send the image via UART (depends on the size). Not a problem in my application, but could be one.

    A short Google search showed me, that not many people on the internet already tried that, so let's go!

    The hardware change to enable the UART-bootloader is simple: On the interface to the flash, CSn needs to be driven low (which also enables USB bootloader), and in addition drive SD1 high.

    When the chip is coming up strapped like this, it will enable a 1MBaud UART on pins SD2 (TX) & SD3 (RX).

    Unfortunately I don't have a Pico 2, but I have a custom board (which will be explained in detail in the future), where I could make those modifications very easily, since I used a very big flash package:

    (The two jumpers are for CSn and SD1, the header is for TX and RX)

    Now we have the following goals:

    • Get a RP2350 binary running from SRAM
    • Get the binary on the board via UART
    • Embed it in another microcontroller's firmware
    • Booting from there
    • Think about a reliable connection, even with longer cables

    A binary running from SRAM

    That's actually not very hard. Just go to your makefile and add the line set(PICO_NO_FLASH 1) (Actually, according to the SDK documentation, chapter 6.4, set(PICO_DEFAULT_BINARY_TYPE no_flash) should be equivalent. But that doesn't work for now.)

    And that's it. As soon as you compiled it, you will have a bin-file in the subfolder "build":

    Bildschirmfoto vom 2025-02-16 19-06-44.png

    That is the file to send via UART. To test it, you could use the also generated uf2-file and copy it via USB to the RP2350. But be aware that it will run from RAM and it will be gone after a power-cycle.

    Firmware-over-UART

    To start, I just wrote a quick Python-script. It sends the firmware, and I also implemented checking it. With a 7.3 KiB firmware, the sending took ~160ms, the verification ~150ms.

    That's not especially bad, but let's quickly do the math.

    Baud-to-byte

    The UART mode here is 8n1: One start bit, eight data bits, no parity and one stop bit. That makes 10 bit per symbol, but only eight of them are user data. So our speed is 1000000 Baud/s / 10 (Bits per symbol) * 8 (User data bits per symbol) / 8 (Bits per Byte) = 100 kB/s (or 0.1 MB/s)

    My test binary is exactly 7256 Bytes long, but we need to make 32-byte chunks, so the last chunk will be filled with zeros and we get a total length of 7264 Bytes. The write command before every chunk makes one more byte overhead, so we have 227 bytes overhead. Those 7491 Bytes should now we downloaded in 74.91ms.

    I won't elaborate why my Python script took double of that (maybe I did a horrible mistake?). Obviously due to anything, not the complete possible time is used to transfer the data. I think it might be due to the Linux and USB-stuff that is in between, but I think it's not very relevant, so lets jump to the interesting part!

    Binaries in binaries

    To be able to boot our RP2350 from another microcontroller, we want the firmware for the first embedded in the firmware of the latter.

    We could use one of the various online tools that creates a C header with a large array containing the binary's data.

    But I wanted a more automated way, and it looks like at least with C++23, this feature was implemented as #embed preprocessor directive.

    However, I couldn't get it running with the version 2.1.0 of the pico SDK. Maybe I'm just dumb, but setting set(CMAKE_CXX_STANDARD 23) did still result in an error. Maybe it's GCC, that as far as I can see, did not yet implement C++23 #embed?

    But I found a nice semi-automatic way at Stackoverflow. It just needed some tweaking, but it works (see my code here).

    Kickstarting the RP2350 from another microcontroller

    Now we're all set: We have a Python proof-of-concept, we have the firmware binary embedded in the binary of another microcontroller. I just ported the Python code more-or-less to C, and some infinite loops later... It works! See my code here.

    Let's finally check the speed. The script gives the following output:

    Hello, world!
    Device is there!
    Start addr is 0x20001055
    Finished sending FW, 7264 bytes were written!
    The end. This took 74750 us

    That's 0.2 ms faster then the expected maximum speed. I have to think about how that's possible, but I won't complain ;-)

    Missing features

    I noticed, that the pins the bootloader uses, since they reside in another GPIO bank, can with the current SDK not be used as UART in your program. But as the internet is a great place, soon after I submitted that issue on GitHub, someone already sent me a link to a solution. I tested it, and it works perfectly. I guess it will eventually land in the SDK, so keep an eye on that issue!

    A reliable connection

    UART isn't known for being robust over long cables. I think we'll have a problem with signal integrity the longer the cables get, a lot more than one meter will be a problem.

    The solution to that problem is very simple: We can convert the single-ended UART signal to a differential signal. I used TI's THVD1450 to translate the UART to RS-485. I made a simple PCB with two transceivers and a RJ-45 connector. RS-485 usually uses 120-Ohm cables, but I just used 100-Ohm ethernet cables, since they're just very easy to get and cheap.

    P1001175.jpg

    With that board on both ends, I could boot the remote RP2350 over a ~10m cable, which was the longest I have at home. I guess it should work up to 100m, but 10m is more than enough for me.

    P1001181.jpg

    Since the board works transparent, as if there would be a direct connection between the boards, it worked out of the box and very reliable. So for my use-case, I will go with that solution. Stay tuned!

    If you have questions or comments, just contact me via mail, or write a comment at the YouTube-Video.

    联系我们 contact @ memedata.com