通过 UART 启动 RP2350
通过 UART 启动 RP2350
2025年03月05日
RP2350 是 Raspberry Pi 出品的不错的微控制器。最近我在考虑一个新的项目,需要大量的 PWM 通道。RP2350B 有 24 个 PWM 通道,这已经很多了,但还不够。我至少需要 33 个 PWM 通道,以及更多的 GPIO,所以单单 RP2350 是不够的。所以我考虑使用端口扩展器…… 但为什么不使用第二个 RP2350 作为端口扩展器呢?
这个芯片相对便宜(大约 0.90 美元),周围只需要几个组件,而且... 固件呢?嗯,这不是一个理想的解决方案。在同一块板上的多个控制器上处理不同的固件可能会很有挑战性。而且我真的很喜欢拥有一个简单的端口扩展器,而不需要处理版本检查以及不同固件版本之间的不兼容问题。
所以我阅读了 RP2350 的 datasheet(第 5.8 章)。RP2350 不仅带有 USB 引导加载程序(这很可能是大多数人编程的方式),还带有一个 UART 引导加载程序(RP2350 相对于 RP2040 的一个新特性!)。与 USB 相比,UART 在微控制器上更容易实现,而且也是芯片之间通信的方式。
我做了一个关于这个主题的 YouTube-Video,如果你感兴趣的话,可以看看!
理论
通过 UART 将固件加载到芯片上的过程相对简单:只需使用一个魔法模式进行一些“解锁”,然后将固件映像以 32 字节的块发送到芯片。完成之后,运行它。
顺便说一句,这里是 RP2350 的引导加载程序代码。
当然也有缺点:
- 固件需要从 SRAM 运行,占用 SRAM 中的空间。但 RP2350 带有 520 kiB,对于端口扩展器来说应该足够了吧?(确实足够!)
- 每次启动都需要一些时间。我的第一个测试表明,通过 UART 发送映像需要不到一秒钟的时间(取决于大小)。在我的应用程序中不是问题,但可能会是问题。
简单地 Google 搜索了一下,发现网上还没有很多人尝试过,所以让我们开始吧!
启用 UART 引导加载程序的硬件更改很简单:在与 flash 的接口上,需要将 CSn 拉低(这也启用了 USB 引导加载程序),此外还要将 SD1 拉高。
当芯片以这种方式启动时,它将在引脚 SD2 (TX) 和 SD3 (RX) 上启用 1MBaud 的 UART。
不幸的是,我没有 Pico 2,但我有一个自定义板(将来会详细解释),我可以很容易地进行这些修改,因为我使用了非常大的 flash 封装:
(两个跳线用于 CSn 和 SD1,接头用于 TX 和 RX)
现在我们有以下目标:
- 让 RP2350 二进制文件从 SRAM 运行
- 通过 UART 将二进制文件传输到板上
- 将其嵌入到另一个微控制器的固件中
- 从那里启动
- 考虑可靠的连接,即使使用较长的电缆
从 SRAM 运行的二进制文件
这实际上并不难。只需转到你的 makefile 并添加行 set(PICO_NO_FLASH 1)
(实际上,根据 SDK documentation,第 6.4 章,set(PICO_DEFAULT_BINARY_TYPE no_flash)
应该是等效的。但是 目前 不起作用。)
就是这样。一旦你编译了它,你将在子文件夹 "build" 中得到一个 bin 文件:

这是要通过 UART 发送的文件。为了测试它,你可以使用也生成的 uf2 文件,并通过 USB 将其复制到 RP2350。但请注意,它将从 RAM 运行,并且在断电循环后会消失。
通过 UART 传输固件
首先,我只是编写了一个快速的 Python 脚本。它发送固件,并且我还实现了检查。对于 7.3 KiB 的固件,发送花费了约 160 毫秒,验证花费了约 150 毫秒。
这不算特别糟糕,但让我们快速计算一下。
Baud 到 byte
这里的 UART 模式是 8n1
:一个起始位,八个数据位,无奇偶校验和一个停止位。这使得每个符号 10 位,但只有八位是用户数据。所以我们的速度是 1000000 Baud/s / 10 (每个符号的位数) * 8 (每个符号的用户数据位数) / 8 (每个字节的位数) = 100 kB/s (或 0.1 MB/s)
我的测试二进制文件正好是 7256 字节长,但我们需要制作 32 字节的块,所以最后一个块将填充零,我们得到总长度 7264 字节。每个块之前的写入命令会产生一个字节的开销,所以我们有 227 字节的开销。这 7491 字节现在应该在 74.91 毫秒内下载完毕。
我不会详细说明为什么我的 Python 脚本花费了双倍的时间(也许我犯了一个可怕的错误?)。显然,由于 任何原因 ,并非所有可能的时间都用于传输数据。我认为这可能是由于 Linux 和 USB 的原因,但我认为这不是很重要,所以让我们跳到有趣的部分!
二进制文件中的二进制文件
为了能够从另一个微控制器启动我们的 RP2350,我们希望第一个的固件嵌入到后者的固件中。
我们可以使用各种在线工具之一来创建一个 C 头文件,其中包含一个包含二进制文件数据的大数组。
但我想要一种更自动化的方式,而且看起来至少在 C++23 中,这个特性被实现为 #embed 预处理器指令。
但是,我无法在 2.1.0 版本的 pico SDK 中运行它。也许我只是很笨,但是设置 set(CMAKE_CXX_STANDARD 23)
仍然导致错误。也许是 GCC,据我所知,尚未实现 C++23 #embed?
但我在 Stackoverflow 上找到了一种不错的半自动方式。它只需要一些调整,但它有效(参见我的代码 这里)。
从另一个微控制器启动 RP2350
现在我们都准备好了:我们有一个 Python 概念验证,我们有嵌入在另一个微控制器的二进制文件中的固件二进制文件。我只是将 Python 代码或多或少地移植到 C,并且在经过一些无限循环之后... 它工作了!参见我的代码 这里。
让我们最后检查一下速度。该脚本给出以下输出:
Hello, world!
Device is there!
Start addr is 0x20001055
Finished sending FW, 7264 bytes were written!
The end. This took 74750 us
这比预期的最大速度快 0.2 毫秒。我必须考虑这怎么可能,但我不会抱怨 ;-)
缺失的特性
我注意到,引导加载程序使用的引脚,由于它们位于另一个 GPIO bank 中,因此在您的程序中无法用作 UART。但由于互联网是一个伟大的地方,在我 GitHub 上提交该问题后不久,就有人向我发送了一个 链接 到一个解决方案。我测试了它,效果很好。我想它最终会出现在 SDK 中,所以请密切关注该问题!
可靠的连接
UART 并不以在长电缆上具有鲁棒性而闻名。我认为电缆越长,我们就会遇到信号完整性问题,超过一米就会成为问题。
这个问题的解决方案非常简单:我们可以将单端 UART 信号转换为差分信号。我使用 TI 的 THVD1450 将 UART 转换为 RS-485。我制作了一个简单的 PCB,带有两个收发器和一个 RJ-45 连接器。RS-485 通常使用 120 欧姆的电缆,但我只是使用了 100 欧姆的以太网电缆,因为它们很容易获得而且便宜。
两端都有该板的情况下,我可以通过大约 10 米的电缆启动远程 RP2350,这是我家里最长的电缆。我猜它应该可以工作到 100 米,但 10 米对我来说已经足够了。
由于该板的工作方式是透明的,就像板之间存在直接连接一样,因此它可以开箱即用且非常可靠地工作。所以对于我的用例,我将采用该解决方案。敬请关注!
如果您有任何问题或意见,请通过邮件与我联系,或在 YouTube-Video 上发表评论。
© 2025 by Thomas Pfister - name [at] pagename (.dev) | Impressum/Datenschutz | Blog SW V0.1.2 | Design originally 2011 by Jakob Zeitler