RIOT 中 Adafruit Metro M4 Express 板级支持详解:外设为何与 Arduino 引脚如此映射、UF2 烧录与 STDIO 接入实战
【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT
本篇技术指南以 RIOT 仓库中 boards/adafruit-metro-m4-express/doc.md 板级文档为核心,系统讲解 Adafruit Metro M4 Express 在 RIOT 中的硬件规格、Arduino 引脚映射与外设配置、UF2 引导加载程序下的烧录流程(含make flash、MOST_RECENT_PORT等细节)以及通过 USB 模拟串口获得 STDIO 控制台的方式。读完之后,你能够独立在该板上编译、烧录 RIOT 应用,理解 D11/D12/D13 总线与 UART 共用 SERCOM 外设的冲突根源,并知道如何把 2 MiB QSPI Flash 挂到 VFS 上作为 littlefs2 文件系统使用。
一、板子概况与关键硬件规格
根据 板级文档,Adafruit Metro M4 Express 的主要特性如下:
- ATSAMD51 Cortex-M4 内核,主频 120 MHz;
- 硬件 DSP 与浮点单元支持;
- 片内 Flash 512 KiB(原文档写作 "512 MiB",结合
CPU_MODEL = samd51j19a与 ATSAMD51J19A 的实际规格可判定为原文笔误,真实容量为 512 KiB); - 片内 RAM 192 KiB;
- 板载 2 MiB QSPI Flash 外部存储(GD25Q16C);
- WS2812x RGB LED(NeoPixel)一颗;
- 32 位、3.3 V 逻辑与供电;
- 原生 USB(PA24/PA25 直连 USB 控制器)。
从源码结构看,这些规格在 RIOT 中都有对应落点:
- Makefile.features 声明
CPU = samd5x、CPU_MODEL = samd51j19a,并列出该板提供的外设 feature:highlevel_stdio、periph_adc、periph_dac、periph_i2c、periph_rtc、periph_rtt、periph_spi、periph_timer、periph_uart、periph_usbdev,以及一组 Arduino 兼容 feature(arduino_analog、arduino_pins、arduino_shield_isp、arduino_shield_uno、arduino_spi、arduino_i2c、arduino_uart); - Kconfig 中
BOARD默认值为adafruit-metro-m4-express,配置项BOARD_ADAFRUIT_METRO_M4_EXPRESS会select CPU_MODEL_SAMD51J19; - periph_conf.h 定义
CLOCK_CORECLOCK为MHZ(120),并启用内部 DC/DC 降压(USE_VREG_BUCK = 1,注释说明板上 VSW 引脚配有 10 uH 电感 L1)。
二、LED、NeoPixel 与 MTD 入口:board.h 中的板级符号
include/board.h 定义了该板三类关键符号:
1. 三颗 LED 的引脚与开/关/翻转宏
| 宏 | 引脚 | 说明 |
|---|---|---|
LED0_PIN | GPIO_PIN(PA, 16) | 用户 LED(红色),即 Arduino D13 |
LED1_PIN | GPIO_PIN(PA, 27) | TX 指示 LED(黄色),低电平点亮 |
LED2_PIN | GPIO_PIN(PB, 6) | RX 指示 LED(黄色),低电平点亮 |
注意 TX/RX LED 是低电平有效:LED1_ON/LED2_ON使用的是OUTCLR寄存器,而LED0_ON使用OUTSET。这一极性也被 gpio_params.h 中的 SAUL GPIO 参数继承:两颗黄色 LED 均带SAUL_GPIO_INVERTED标志。
2. WS2812x NeoPixel 默认参数
#ifndef WS281X_PARAM_PIN #define WS281X_PARAM_PIN GPIO_PIN(PB, 22) /**< GPIO pin connected to the data pin */ #endif #ifndef WS281X_PARAM_NUMOF #define WS281X_PARAM_NUMOF (1U) /**< Number of LEDs chained */ #endif该板板载 1 颗 WS2812x LED,数据引脚为 PB22;应用侧可通过WS281X_PARAM_PIN/WS281X_PARAM_NUMOF覆盖默认值(例如外接灯带)。
3. QSPI Flash 的 MTD 设备
#define MTD_0 mtd_dev_get(0) /**< MTD device for the 8 MiB QSPI Flash */ #define BOOTLOADER_UF2 1 /**< This board uses the UF2 bootloader */其中BOOTLOADER_UF2 = 1是 UF2 烧录流程的开关(详见第五节)。另外需要指出:board.c 中实际初始化的 SPI NOR 参数明确标注为 GD25Q16C(2 MiB 芯片,256 字节页、16 页/扇区),与文档"2 MiB QSPI Flash"一致;board.h 注释中的 "8 MiB" 与文档、驱动参数不一致,可视为注释笔误。
三、外设配置全景:periph_conf.h 逐项解读
periph_conf.h 是该板最核心的配置文件,它决定了 RIOT 各periph_*模块如何落到具体 SERCOM/TC 实例与引脚上。
3.1 ADC:A0~A5 映射到 ADC0/ADC1 的六个 MUXPOS
static const adc_conf_chan_t adc_channels[] = { /* port, pin, muxpos, dev */ { .inputctrl = ADC0_INPUTCTRL_MUXPOS_PA02, .dev = ADC0 }, /* A0 */ { .inputctrl = ADC0_INPUTCTRL_MUXPOS_PA05, .dev = ADC0 }, /* A1 */ { .inputctrl = ADC0_INPUTCTRL_MUXPOS_PA06, .dev = ADC0 }, /* A2 */ { .inputctrl = ADC0_INPUTCTRL_MUXPOS_PA04, .dev = ADC0 }, /* A3 */ { .inputctrl = ADC1_INPUTCTRL_MUXPOS_PB08, .dev = ADC1 }, /* A4 */ { .inputctrl = ADC1_INPUTCTRL_MUXPOS_PB09, .dev = ADC1 }, /* A5 */ };要点:时钟源为SAM0_GCLK_PERIPH、分频DIV8、负输入接 MUXNEG(0x18)、默认参考为内部 VCC1(ADC_REFCTRL_REFSEL_INTVCC1)。Arduino 的 A0~A5 分别落在 PA02/PA05/PA06/PA04/PB08/PB09 上,与 arduino_iomap.h 中ARDUINO_PIN_16~21一一对应,ARDUINO_A0~A5则直接映射到ADC_LINE(0)~ADC_LINE(5)。
3.2 DAC:参考电压取自 PA03
#define DAC_CLOCK SAM0_GCLK_TIMER /* Must not exceed 12 MHz */ #define DAC_VREF DAC_CTRLB_REFSEL_VREFPU注释特别指出:板背面的一个焊桥把 PA03 的 VREF 焊盘连接到 3V3,驱动默认该焊桥未被切断,即使用外部参考。
3.3 I2C:SERCOM5 位于 D14/D15
static const i2c_conf_t i2c_config[] = { { .dev = &(SERCOM5->I2CM), .speed = I2C_SPEED_NORMAL, .scl_pin = GPIO_PIN(PB, 3), /* D: SERCOM5.1 */ .sda_pin = GPIO_PIN(PB, 2), /* D: SERCOM5.0 */ .mux = GPIO_MUX_D, ... }, };这是 UNO/Mega 型 Shield 所期望的 I2C 位置(D14=SDA,D15=SCL),对应 arduino_iomap.h 中ARDUINO_I2C_UNO = I2C_DEV(0)。
3.4 SPI:三条总线与"最后一条"的特殊地位
spi_config[]最多包含三个条目:
- SPI_DEV(0):ISP 排针 SPI(
ARDUINO_SPI_ISP),SERCOM2,引脚 PA14(MISO)/PA12(MOSI)/PA13(SCK),走 GPIO MUX C; - D11/D12/D13 SPI(对应
ARDUINO_SPI_D11D12D13),SERCOM3,引脚 PA17(MISO)/PA19(MOSI)/PA16(SCK),走 GPIO MUX D。注意该条目被包在#if !MODULE_PERIPH_UART里——这正是下一节冲突问题的代码体现; - QSPI 复用为 SPI(
#if MODULE_PERIPH_SPI_ON_QSPI),设备为 QSPI 控制器本身,用于驱动外部 QSPI Flash,被 board.c 中的mtd_spi_nor以SPI_DEV(SPI_NUMOF - 1)即"最后一条 SPI 总线"引用。
3.5 UART:SERCOM3 与 SPI 的引脚重叠
/* D0 = RXD, D1 = TXD */ { .dev = &SERCOM3->USART, .rx_pin = GPIO_PIN(PA, 23), /* C: SERCOM3.1, D: SERCOM5.0 */ .tx_pin = GPIO_PIN(PA, 22), /* C: SERCOM3.0, D: SERCOM5.1 */ .mux = GPIO_MUX_C, .rx_pad = UART_PAD_RX_1, .tx_pad = UART_PAD_TX_0, ... },D0/D1(PA23/PA22)的 UART 复用 SERCOM3;而 D11/D12/D13 的 SPI 也挂在 SERCOM3 上。文件内注释解释了设计取舍:UART 也可以路由到 SERCOM5(与 I2C 共用),但 SERCOM5 的 pad 配置无法满足(TXD 无法路由到 pad 1),因此 RIOT 选择让periph_spi与periph_uart在构建期显式冲突——这一冲突规则维护在 Makefile.features 的 feature 依赖体系中,并在arduino_iomap.h中以#if !MODULE_PERIPH_UART控制ARDUINO_SPI_D11D12D13是否定义。
3.6 USB 与 RTC
USB 设备控制器使用 PA24(DM)/PA25(DP),GPIO_MUX_H,即板子的原生 USB 口:
static const sam0_common_usb_config_t sam0_common_usb_config[] = { { .dm = GPIO_PIN(PA, 24), .dp = GPIO_PIN(PA, 25), .d_mux = GPIO_MUX_H, .device = &USB->DEVICE, .gclk_src = SAM0_GCLK_PERIPH, } };实时时钟方面,periph_conf.h选择外部 32.768 kHz 晶振(EXTERNAL_OSC32_SOURCE = 1),RTT_FREQUENCY默认为 32768 Hz。
3.7 定时器
timer_config[]配置两路 32 位定时器:Timer 0 由 TC0 提供 2 个通道(ISRisr_tc0),Timer 1 由 TC2 提供 2 个通道(ISRisr_tc2),均挂SAM0_GCLK_TIMER时钟。
四、Arduino 兼容性与引脚映射表
文档明确写道:"The board is compatible with Shields for the Arduino UNO / Arduino Zero." 该板通过排针提供 UNO 型与 ISP 型两种 Shield 接口,RIOT 侧的对应实现集中在 arduino_iomap.h:
| Arduino 引脚 | MCU 引脚 | 备注 |
|---|---|---|
| D0 / D1 | PA23 / PA22 | UART0(RX/TX) |
| D2~D7 | PB17, PB16, PB13, PB14, PB15, PB12 | 数字 IO |
| D8~D10 | PA21, PA20, PA18 | 数字 IO |
| D11 / D12 / D13 | PA19 / PA17 / PA16 | 条件性 SPI(MOSI/MISO/SCK),且 D13 兼作用户红 LED |
| D14 / D15 | PB2 / PB3 | SDA / SCL(I2C0) |
| A0~A5(数字用法 16~21) | PA2, PA5, PA6, PA4, PB8, PB9 | 双作 ADC 通道 |
外设别名方面:ARDUINO_SPI_ISP = SPI_DEV(0)(ISP 排针)、ARDUINO_SPI_D11D12D13 = SPI_DEV(1)(仅在未启用periph_uart时定义)、ARDUINO_UART_D0D1 = UART_DEV(0)、ARDUINO_I2C_UNO = I2C_DEV(0)。
文档中的关键警告:D11/D12/D13 SPI 与 periph_uart 互斥
原文档以@warning指出:
The SPI bus on D11/D12/D13 is only available when
periph_uartis not used, as this pins are connected to the same SERCOM peripheral. This is board design flaw that we cannot work around unless resorting to desperate means such as bit-banging either the SPI bus or the UART interface.
结合源码可以完整还原这一限制的因果链:
- periph_conf.h 中 D11/D12/D13 的 SPI 条目(SERCOM3)被
#if !MODULE_PERIPH_UART条件编译包围; - arduino_iomap.h 中
ARDUINO_SPI_D11D12D13同样受该条件控制,注释说明它"是最后一条 SPI 总线"; - 冲突的根源在于 UART 的 D0/D1 与 D11/D12/D13 的 SPI 都依赖 SERCOM3,且 SERCOM5 无法承接 UART(pad 1 不可路由),因此 RIOT 选择让两者在构建期互斥,而不是运行期错乱。
实操含义:如果你的应用同时需要 D0/D1 串口和 D11/D12/D13 SPI,二者不可兼得——要么改用 ISP 排针上的ARDUINO_SPI_ISP(SPI_DEV(0),SERCOM2),要么软仿真(bit-bang)其中一条总线。
五、烧录流程:UF2 引导加载程序 +make flash
5.1 烧录原理
文档指出:该板通过板载 UF2 引导加载程序烧录(Adafruit 的 uf2-samdx1 工程),RIOT 将该流程封装进常规make flash目标。具体机制可从构建配置中印证:
- Makefile.include 将设备识别过滤器设为
--vendor 'Adafruit Industries' --model 'Metro M4 Express',并引入公共引导加载程序配置 boards/common/samdx1-arduino-bootloader/Makefile.include; - 公共配置中默认
PROGRAMMER ?= bossa,对samd5x架构设置ROM_OFFSET ?= 0x4000(跳过 16 KiB 引导区)、BOSSA_ARDUINO_PREFLASH = yes、PREFLASH_DELAY ?= 2(秒)。
5.2 何时需要手动进入引导加载程序
文档给出了两种情形:
- RIOT 正在板上运行:执行
make flash时会自动复位 CPU 并进入引导加载程序,无需人工干预。这一"USB 板复位"能力由 boards/common/samdx1-arduino-bootloader/Makefile.dep 控制——仅当应用使用了usbus_cdc_acm模块时,RIOT 才自动挂入boards_common_samdx1-arduino-bootloader与usb_board_reset模块(配套实现见 reset.c),因此"自动进入 bootloader"的前提是应用通过 CDC ACM 提供 USB 控制台(见第六节); - 板上跑着其他固件或 RIOT 已崩溃:需要双击复位按钮手动进入引导加载程序。引导程序就绪的指示是LED 红光闪烁(pulsing in red)。
5.3 手动进入时的端口提示
原文档附带的提示值得单独强调:
You may need to pass
MOST_RECENT_PORT=0and manually pass thePORTif you enter the bootloader by hand.
即手动双击复位进入 UF2 模式时,自动端口探测可能选错 USB 设备,此时应显式指定串口设备,例如:
make flash BOARD=adafruit-metro-m4-express MOST_RECENT_PORT=0 PORT=/dev/ttyACM05.4 常规烧录命令
在应用目录下执行:
make BOARD=adafruit-metro-m4-express -j8 flash构建系统会依次完成固件编译、生成 UF2 可写镜像并通过 bossa 程序写入;若需仅构建,将flash替换为无目标即可。PROGRAMMERS_SUPPORTED目前只包含bossa,即该板的默认(也是文档验证过的)烧录器。
六、STDIO 控制台:模拟 USB 串口
文档"Accessing STDIO"一节的结论很直接:
The usual way to obtain a console on this board is using an emulated USB serial port.
对应到 RIOT 的配置链路:
- 板级 feature 中包含
highlevel_stdio(见 Makefile.features),使stdio模块可用; - boards/common/samdx1-arduino-bootloader/Makefile.dep 首先引入公共的 CDC ACM STDIO 依赖(
include $(RIOTBOARD)/common/makefiles/stdio_cdc_acm.dep.mk),即当stdio生效时自动挂入usbus_cdc_acm等模块; - 应用通过原生 USB 口(PA24/PA25,periph_conf.h 中配置的
USB->DEVICE)枚举出一个 CDC ACM 虚拟串口,在 Linux 下通常表现为/dev/ttyACM*; - 由于该板使用
PROG_TTY_BOARD_FILTER(Makefile.include 中按--vendor 'Adafruit Industries' --model 'Metro M4 Express'过滤),make term/make flash可以准确找到对应串口。
这一链条同时解释了第五节中"自动复位进入 bootloader 依赖 CDC ACM"的原因:只有应用自身在 USB 上提供了 CDC ACM 端点,RIOT 才有能力在烧录前经 USB 下发复位命令。
七、进阶:把 2 MiB QSPI Flash 挂为 littlefs2 文件系统
虽然文档正文未展开存储细节,但 board.c 与 Makefile.dep 共同实现了该板的一个重要能力:将外部 QSPI Flash(GD25Q16C)作为 MTD 设备并可自动挂载 littlefs2。
board.c 中的关键参数(在MODULE_MTD_SPI_NOR下编译):
/* GD25Q16C */ static const mtd_spi_nor_params_t _samd51_nor_params = { .opcode = &mtd_spi_nor_opcode_default, .wait_chip_erase = 25 * US_PER_SEC, .wait_32k_erase = 150 * US_PER_MS, .wait_64k_erase = 200 * US_PER_MS, .wait_sector_erase = 50 * US_PER_MS, .wait_chip_wake_up = 10 * US_PER_MS, .clk = MHZ(54), .flag = SPI_NOR_F_SECT_4K | SPI_NOR_F_SECT_32K | SPI_NOR_F_SECT_64K, .spi = SPI_DEV(SPI_NUMOF - 1), .mode = SPI_MODE_0, .cs = SAM0_QSPI_PIN_CS, .wp = SAM0_QSPI_PIN_DATA_2, .hold = SAM0_QSPI_PIN_DATA_3, }; static mtd_spi_nor_t samd51_nor_dev = { .base = { .driver = &mtd_spi_nor_driver, .page_size = 256, .pages_per_sector = 16, }, .params = &_samd51_nor_params, }; MTD_XFA_ADD(samd51_nor_dev, 0); #ifdef MODULE_VFS_DEFAULT VFS_AUTO_MOUNT(littlefs2, VFS_MTD(samd51_nor_dev), VFS_DEFAULT_NVM(0), 0); #endif要点说明:
- SPI 时钟 54 MHz、模式 0;页大小 256 字节、每扇区 16 页(4 KiB 扇区),另声明支持 32 KiB/64 KiB 大扇区擦除;
- 设备注册为 MTD 0,与 board.h 中
MTD_0 mtd_dev_get(0)呼应; - 依赖链由 Makefile.dep 自动闭合:
vfs_default存在时引入littlefs2与mtd;mtd存在时强制periph_spi_on_qspi并挂入mtd_spi_nor模块——也就是说启用 QSPI SPI 模式后,外部 Flash 会占用"最后一条 SPI 总线",应用再申请 SPI 总线时需注意设备编号变化。
八、小结与验证要点
| 关注点 | 结论 | 依据 |
|---|---|---|
| 内核/主频 | ATSAMD51J19A(Cortex-M4,120 MHz) | Makefile.features、periph_conf.h |
| 烧录方式 | UF2 引导加载程序 + bossa,make flash自动化,手动进入需双击复位、红 LED 闪烁指示 | doc.md、Makefile.include |
| 串口控制台 | USB 模拟 CDC ACM 虚拟串口 | doc.md "Accessing STDIO"、boards/common/samdx1-arduino-bootloader/Makefile.dep |
| 引脚冲突 | D11/D12/D13 SPI 与 D0/D1 UART 互斥(共用 SERCOM3) | doc.md@warning、periph_conf.h |
| 外部存储 | 2 MiB QSPI Flash(GD25Q16C),可自动挂载 littlefs2 | board.c、Makefile.dep |
以上所有内容均以当前仓库boards/adafruit-metro-m4-express/目录下的文档与源码为准;若你的 RIOT 版本中该板目录结构或 feature 名称有差异,请以对应版本 Makefile.features 实际声明为准。
【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考