今天介绍一下我第一次 Flash bootloader 的波折经历,我最近才知道原来 Programmer 还有另一个意思,是指可以用来将 bootloader 或者其他程序写入一个目标设备的机器。我这次使用的是 Segger 的 J-Link Programmer,用来给一个装载 E73 nRF52840 芯片模块的 nrfMicro 板子写入 Bootloader。整个过程其实断断续续跨越了挺长一个时间段,因为我碰到了许多的问题,不过最后学到了很多东西。

nrfMicro 上的元件和一个耳机的尺寸对比。

首先 Bootloader 是什么,为什么要自己 Flash Bootloader 呢?简单来说 Bootloader 是 CPU 通电之后首先运行的代码。在 x86 世界里我们熟知的 BIOS 就是一个 Bootloader,它会做一些基本检查和初始化,并跳转到操作系统的代码段继续执行。BIOS 一个很常用的功能是我们可以设置它从不同的地方(比如光驱或者 U 盘)引导操作系统,这在以前需要重装系统的时候经常会用到。玩过 Linux/Windows 双系统的同学肯定都知道 GRUB,它在这里担当第二级 bootloader 的角色,在 BIOS 之后运行,并且可以根据用户的选择跳转并启动不同的操作系统。不过我们今天打交道的是嵌入式芯片里的 Bootloader,虽然概念大同小异,但是也有一些各自独特的地方,比如很多嵌入式项目都是没有一个操作系统的。AdaFruit 有一篇很不错的文章介绍了嵌入式的 Bootloader。

简而言之,嵌入式的 Bootloader 也是运行在正式的固件代码之前的一段代码,同电脑里 Bootloader 可以用来升级或者重装操作系统类似,嵌入式芯片里 Bootloader 的一个重要作用就是用来更新固件。通常在启动是 Bootloader 会检查一个设置,选择直接跳到固件代码正常执行,或是进入一个特殊模式,可以接收用户的输入写入新的固件。例如 UF2 Bootloader 在快速按两下 reset 键之后会重启并把自己伪装成一个 U 盘,操作系统会识别并在文件管理器里显示这个 U 盘,这个时候我们只要把 .uf2 格式的新固件拷贝到这个 U 盘里,它就会用它覆盖旧的固件并自动重启。

这是一个非常方便的机制,特别是你在自己做固件开发的时候。但是除了开发用板,大部分嵌入式芯片都是不自带 Bootloader 的,主要的原因是大部分芯片会有非常特定的用途,比如微波炉、电热水壶里的控制芯片之类的,它只需要在出场的时候写入一个固定的程序代码就可以了,不需要有更新固件这样的操作——很多情况下甚至出于安全考虑会不希望用户能很随便地覆盖固件代码。另外,Bootloader 虽然通常比较小,也会占用额外的空间,在嵌入式世界里这有可能会是非常宝贵的资源,而且有 Bootloader 会在系统启动的时候做一些额外的操作和检查,取决于不同的应用场景,这也有可能是希望节省的一点点时间。

所以市场上买到的通用的非开发板很有可能是没有 Bootloader 的,我们需要额外的设备——一个Programmer 来 Flash 一个 Bootloader 进去,这样我们在后续更新固件的时候就不需要额外设备和连接了。大部分嵌入式芯片会支持一些接口能在运行时拦截 CPU 指令并做一些特定的操作,两个主要的用途就是(Bootloader)代码写入和运行时断点调试。例如 JTAG 是一个比较老牌的标准接口,而我们今天用的是针对 ARM 芯片的 Serial Wire Debug (SWD) 接口,并用 Segger 的 J-Link 来进行编程,J-Link 本身是比较贵的,不过它有一个非商用的非常便宜的版本 J-Link Edu mini,功能基本一样,我使用的就是这个设备。

nrfMicro

nrfMicronice!nano 是非常类似的板子:都是兼容 ProMicro 的针脚布局,同时支持 USB-C 了 Bluetoothe Low Energy,最大的区别在于 nice!nano 是作为成品出售的,而 nrfMicro 是一个开源项目,需要自己订购 PCB,然后自己购买对应的蓝牙模块(E73 nRF52840)和其他相关元件,并手工把它们焊到板子上。当时订购 nrfMicro 的 PCB 是因为 nice!nano 一直处于缺货难买的状态,自己组装 nrfMicro 可以作为替代品,另外也是一个很好的学习机会:一个是组装小 SMD 元件甚至是芯片模块的手艺,一个是蓝牙模块、电池管理等相关模块的连接和使用方法。当然我当时完全没有意识到这个焊接难度和我们之前焊的板子完全不在一个数量级上。本文最开始的图是焊好的板子和一个耳塞的大小对比,上面的那些凸起的芝麻大小的挤在一起的小元件全都是要手工焊上去的。

例如电阻是所谓的 0603 package,大约是 1.6 毫米长、0.85 毫米宽的尺寸,这大概是能手工焊接的最小的元件尺寸了。另外 USB-C 接头焊起来也比较费事,因为一大排针脚排在一起很密。不过最困难的还是 E73 这个蓝牙芯片模块了。大概是由于为了兼容 ProMicro 的尺寸,导致 PCB 上没有足够的空间为 E73 的每个针脚都做很长的金属 pad,所以只有一点点接触的地方,有时候即使涂了很多 flux 也很难让焊锡流进去连起来,此外这个芯片除了周围一圈针脚之外,在底部也有两排 pin。焊接的时候需要提前在这些地方加一点焊锡,芯片放到 PCB 上之后这个区域就完全被遮住了,nrfMicro 设计的时候在 PCB 上打了一些孔,可以从反面加焊锡进去,但是由于完全看不到,所以到底里面有没有连上,或者相邻的 pin 有没有短路完全是不知道也无法控制和修改的,唯一能做的就是根据经验调整实现放上去的焊锡的量。更麻烦的是由于 E73 卖的时候是 lock 住的,需要先用前面的方法写入固件才能测试有没有焊好,如果失败了基本上无法诊断到底是哪里出了问题。

好在 PCB 和元件都是批量购买的,所以有足够的余量可以练习,在第一个板子焊失败之后(由于使用了不是很好的焊接顺序,导致有些地方很难处理,最后 E73 的针脚是否有连上都搞不清楚)决定对设备进行一次升级:换了更厉害的 Hakko 焊枪和尖头弯钩的焊头,NC 559 的 flux 和 Kester 44 焊锡,隔热胶带和精细镊子,还买了一个便宜的电子显微镜。如下图所示,用这个显微镜看确实能看的更清楚,也更容易检查焊接是否完成或者有短路,不过看着正前方的屏幕操作下面的东西还是需要一定的练习才能习惯的(有点像在用 Wacom Intuos 的绘图板)。

N 正在对着显微镜焊 nrfMicro 上的一个 LED 灯。

另外这个显微镜还有自带的电池,所以其实可以从架子上取下来带到野外去观察一些昆虫或者植物啥的。不过我觉得可能应该叫它放大镜更合适,因为显然这个放大倍数虽然对于电路焊接来说很够用了,但是并没有能大到可以看清细胞的程度。

虽然最后成功焊好了一个板子,但很难说这个过程是轻松愉悦的,如果要再尝试一次肯定也还是会提心吊胆。但是确实是学到了许多东西,而且如果以后要自己设计类似的东西,在没有 PCB 尺寸限制的情况下就可以把元件铺的更开,芯片的 pad 也能做更长,整个焊接难度就会降低很多。

Flashing the Bootloader

nrfMicro 和 nice!nano 的区别除了在于需要自己焊元件之外,就是需要自己 flash Bootloader 了。使用 J-Link 对 nrfMicro 进行编程写入 Bootloader 也是今天的正题。电脑、J-Link 和 target 板的连接方式如下图。J-Link 需要通过 USB 连接到电脑,因为我们要在电脑上进行调试,nrfMicro 这里通过另一个 USB 连接到电脑,不过这个连接目前只是供电作用,同时还让它和 J-Link 具有相同的接地电压。然后再将 J-Link 和 nrfMicro 通过 SWD 协议连接起来就行了——不过这一步也是比较费事的。

因为就好像 deploy 的程序一般在编译的时候不会带调试信息一样,除非是专门用于开发和原型设计用的目标板子,否则一般都不会专门搞一个用于调试用的接口可以直接连上。J-Link 一端提供了 2x10 排布的针脚,我用的 J-Link mini 使用的是 2x5 的 9-针脚接口(如下图左上所示)。J-Link 的盒子里提供了适合 19 针脚和 9 针脚的 柔性扁平排线 (Ribbon Cable)——如下图中间所示。

虽然在 J-Link 一端直接把排线连接头插上即可,但是另一端的 target 板子很少有提供突出的针脚可以直接插上排线的。我在网上找了半天也没有找到标准的做法,也许这是玩电路的基础知识不需要讲,或者是根本没有“标准的做法”,我使用的办法是直接用跳线插入排线的另一端接口——如上图右边所示——然后将跳线的另一端连到 nrfMicro 对于的位置。

我的跳线比这个连接头的孔稍微大一点,不过扭一扭晃一晃还是能插进去。跳线的头明显比较大,要在并排相邻的位置插入两个跳线比较麻烦,需要把金属部分弯一弯。我不知道是不是应该找直径更细的线来自制一些跳线,或者有其他的方式可以连接,总之比较 hacky,我甚至不知道是不是因此弄坏了排线连接头——具体待会会讲。

下图是我手里有的几个板子上提供的 SWD 连接口,最左边是 nrfMicro,是我今天要烧固件的板子,可以看到竖着有四个孔,分别是 SWC、SWD、GND 和 3v3,这几个口都会用到。这是普通的焊接用的口,最稳固的方法当然是把跳线焊上去,但是那样之后又得费力地通过 desolder 把焊上的线取下来,临时的办法是直接把跳线“放在孔里”,用手扶住保证金属接触能导电即可——同时用手扶住四根跳线并不是一件容易的事情,特别是另一只手可能还要操作万用表、输入命令之类的,而且不是很很保证是否接触会很稳固,我一度以为连接失败是这里接触不良的问题,不过后来发现似乎其实连接还是蛮稳固的。

中间和右边分别是 nice!nanoAdafruit Feather nRF52832,这两个由于是买的现成的板子,所以里面已经有 bootloader 了,不需要再通过 J-Link 进行编程。不过它们也提供了 SWD 接口,可以用于调试(并且制造商最初也需要一个接口把默认的 bootloader 写进去)。nice!nano 是提供了 SWD 和 SWC 两个金属 pad,用手把跳线按在 pad 上应该就可以——或者用胶布粘上。Adafruit Feather 则是有一个 2x5 的条状金属 pad 的区域,应该也可以用胶布大法,不过这些 pad 距离比较近可能比较难搞,实际上这些 2x5 的 pad 是按照 J-Link 的连接规则来排布的,所以你可以把一个 SWD Pitch Connector 焊在这个区域,然后就可以不用使用跳线,自己插入排线连接头进行连接了。

由于我要对 nrfMicro 进行编程,所以需要手工连线,具体要连那几根线感觉不同的地方有不同的说法,SWD (Serial Wire Debug) 是 2-Pin Debug Port,至少是需要连接 SWDIO 和 SWCLK 这两个 pin 的(对应 nrfMicro 上的 SWD 和 SWC)。我在 nrfMicro 的 wiki 上看到说是要连接这两个 pin,但是实际上似乎至少还要连接 VTref 这个 pin——也许这被当做基本知识自然省略了。

回顾前面的 9 针脚和 19 针脚的连接头 pinout 示意图,这三个针脚在 J-Link 上分别是第一、第二和第四号针脚。这个示意图是从 J-Link 用户指南 中截取出来的。事实上其实大部分需要解答的问题都能在用户指南中找到,只是(对于新手来说)非常难找就是了。SWD 和 SWC 分别连到目标板子的哪个接口是很清楚的,那么 VTref 连到哪里呢?用户指南里对 VTref 的描述是:

This is the target reference voltage. It is used to check if the target has power, to create the logic-level reference for the input comparators and to control the output logic levels to the target. It is normally fed from Vdd of the target board and must not have a series resistor.

所以这个是用来测量目标板子的逻辑高电压的。我一开始没有这个 pin,所以在电脑上通过 J-Link 连接 target 的时候一直出现 ERROR: Low voltage detected at the target. Please make sure the device is properly supplied 这样的错误信息。PCB 上的 Vcc、Vdd 之类的什么区别我一直比较晕,不过这个 nrfMicro 上有一个 VBUS 是 5V 的,但是板子本身是 3.3V 下运行的,所以应该不是接 VBUS 而是接 3v3 这个 pin。最后,J-Link 和目标板子必须要有 common ground 才行,最简单的做法就是把 J-Link 的 GND 连到目标板子的 GND pin 上。nrfMicro wiki 上说如果两个都用 USB 连到同一个电脑上,那么它们应该已经具有 common ground 了,我用万用表测了一下似乎确实是这样,不过实际操作的时候由于一直无法连接并搞不清楚是什么问题,所以最后成功运行的时候我是把 GND 连上的。

事实上,在 J-Link 的用户指南的第 18.1.2.1 小节 Target board design 里给了一个如下图所示的典型的连接方式(带颜色的四条线是我用到的)——不过我是在事后才偶然找到这个。

我碰到的问题就是连不上,出现刚才说的 Low voltage 这样的错误信息。比较麻烦的是可能出问题的地方太多了,因为板子也是我们自己组装的,有可能板子本身组装就有问题,我也不知道我的连接方式是不是对的,是否是接触不良,或者是某条线少连了,也不知道有什么能给出更具体的错误信息的命令。

通过在网上搜索和在 nrfMicro 的 discord 频道里问问题,大致找到了一些线索。首先 J-Link 的 wiki 上有一些 trouble shooting target connection 的信息。首先通过 J-Link Commander 可以进行简单的连接测试,这个程序是保护在 J-Link 配套软件中的,在 MacOS 下它叫做 JLinkExe,在命令行输入如下命令

JLinkExe -device nRF52840_xxAA -if SWD

这里的两个参数一个是指定使用 SWD 协议(而不是默认的 JTAG),另一个是指定目标芯片类型。nrfMicro 1.4 使用的是 E73 nRF52840 蓝牙芯片模块,在 J-Link 的制造商 Segger 的网站上有一个 J-Link 支持的设备列表,搜索 nrf52840 就会找到 nRF52840_xxAA 这一项。运行这个命令就会进入一个 CLI,可以进行测试连接,不过在那之前它会做一些基本检测,做出类似如下的输出:

SEGGER J-Link Commander V6.88 (Compiled Nov 12 2020 17:44:25)
DLL version V6.88, compiled Nov 12 2020 17:44:09

Connecting to J-Link via USB...O.K.
Firmware: J-Link EDU Mini V1 compiled Nov 12 2020 13:31:42
Hardware version: V1.00
S/N: xxxxxxxxx
License(s): FlashBP, GDB
VTref=3.278V

这里最后一行显示的 VTref 电压值就是 J-Link 通过 VTref 这个针脚读取到的目标板子的 Vdd 电压值,可以看到我这里显示的大约是 3.3V,是正确的电压。如果这里显示的是零或者很低的一个电压值的话,那么说明连接有问题。

我碰到的正是连接问题,不过导致未连接的原因有很多种,首先一个是针脚接错了。虽然针脚示意图很清楚,但是排线连接头正着反着是都能插的,所以有可能会搞反。一开始我看到 pinout 示意图和排线连接头都在一侧有一个凸起(见前面的示意图),就以为那个是对照起来的,不过事实证明完全是我搞错了。其实仔细想一下排线是一个非常通用的连接线,它只是把一头对应的接口连接到另一头,并没有自己的正反和方向。要找到正确的连线,首先要搞清楚 J-Link 上的每个针脚对应什么,在 J-Link Mini 上有一个脚附近写了一个“1”,见下图,这里就是 1 号针脚,对应前面的 pinout 图,可以知道 1 号针脚就是 VTref,知道了这个之后就能推导出其他的针脚分别在哪里。

然后要搞清楚排线两个连接头之间的孔的对应关系,再根据自己是以怎样的方向把排线连到 J-Link 上的,就很容易搞清楚另一头对应的插孔了。一个比较简单的 sanity check 是,J-Link 的 pinout 上有两个 GND,这两个 GND 应该是短路连通的,插好排线之后可以使用万用表的短路测量功能来测量你认为是对应两个 GND 的那两个插孔之间的连通性,如果不连通说明对应关系搞错了。

还有一个偷懒的办法是各种情况都试一试:例如 VTref 有可能是左上、左下、右上、右下——但是千万别这么做,因为如果不小心对板子加了反向电压,有可能瞬间板子就烧坏了。我确实做了一些不同情况的尝试(而且一开始我对排线的错误认识导致我的连线是镜面反向的),但是幸运的是我并没有把板子烧坏,一个原因可能是 J-Link Mini 并没有供电功能,所以我是另外通过 USB 对 nrfMicro 进行供电的,另一个原因可能是我的排线插头有问题,因为我在修正了针脚对应关系之后电脑里依然显示 VTref 是低电压。

这个时候最直接的办法就是用万用表测量:首先是要测量 nrfMicro 上的 3v3 孔确实有输出 3.3V 的电压,测量的时候把万用表调到电压档,然后把正极贴在 3v3 孔上,负极贴在(nrfMicro 的)GND 孔上。测量没有问题,于是再测通过排线连接之后的电压,这个一方面检查直接“放”在 nrfMicro 的孔里的跳线是否接触很不稳定,一方面检查排线是否坏了。由于万用表的针尖不够小无法伸进排线连接头的孔里,于是我在另一头接了额外的跳线来和万用表接触,结果这个测量下来也是正常。可是当我把排线接头直接插到 J-Link 上,然后(把万用表探针放在暴露出来的针脚的焊锡上)测量 pin-1 和它旁边的 pin-3 之间的电压时,就只能读到零了。这是什么问题?百思不得其解……难道有什么干扰?我最后几乎要断定是自己之前乱连线把 J-Link 也烧坏了,幸好手工之前还是决定最后再测试一下,绕开排线直接连接。我有一些 male-female 头的跳线,female 端可以直接 fit 到 J-Link 的针脚上,不过这个 female 头太大了,只有最边缘的针脚能 fit,见下图(随机接了两个 pin 作为示意图)。所以中间的针脚只能用普通的跳线,手工让他贴在一起,既要保证接触,又不同同时碰到其他针脚,与此同时还得要用万用表的探针去连接同样很小很挤的 1 号和 3 号针脚,总之是非常困难的操作,在 N 的帮助下 4 只手终于完成了测量,发现得到了正确的电压!

总之问题似乎出在排线连接头上,似乎 J-Link 的针脚插进去之后仍然是断开的连接,但是跳线插进去是可以的。也许是排线头本身有问题?也有可能是因为我的跳线直径比 J-Link 的针脚直径稍大一点,所以我在把跳线插进去的时候把里面的簧片之类的撑开了,导致更细的针脚无法接触连接了。还好 J-Link Mini 还自带了一个 2x10 的排线,我换了那个之后连上合适的线,在电脑上 J-Link 就能够发现并连接到目标板子了。测试连接成功之后就可以直接运行 nrfMicro Wiki 上的两个命令解锁并写入新的 bootloader 了(nrfjprog 并不是 J-Link 自带的软件包里的,需要额外安装 Nordic 的 nRF Command Line Tools):

$ nrfjprog --recover --log
Recovering device. This operation might take 30s.
Erasing user code and UICR flash areas.
$ nrfjprog -f nrf52 --program bootloader.hex --sectorerase
Parsing hex file.
Erasing page at address 0x0.
Erasing page at address 0x1000.
Erasing page at address 0x2000.
Erasing page at address 0x3000.
...

问题虽然解决了,但是背后的原因并没有找到,不知道是不是我用跳线把排线接头戳坏了,不过我在 AdaFruit 的网站上找到了这个 SWD (2x5 1.27mm) Cable Breakout Board,换一根 2x5 的排线,然后配合这个使用的话以后就再也不用担心这个问题了!

另外想要提一下我在搜索过程中找到的 Embedded Computing 这个 blog,里面有介绍了许多不同的 microcontroller 和使用 J-Link 调试以及其他嵌入式编程相关的东西,感觉很不错。我在里面看到一个很 Q 的叫做 Seeeduino Xiao 的板子,非常小,看这名字应该是中国制造的吧,哈哈,回头有机会想要玩一玩。