- 物联网
- 嵌入式
- 操作系统
- 实时系统
【免费下载链接】RIOT
RIOT - The friendly OS for IoT
导读
本文基于 RIOT 操作系统的外设自检测试应用tests/periph/selftest_shield,介绍如何借助一块与 Arduino UNO 兼容的 "Peripheral Selftest Shield" 扩展板,对板载的 ADC、GPIO、GPIO-IRQ、I2C、PWM、SPI、UART 七类外设驱动进行全自动回环自检。读完本文后,你将掌握从硬件准备、DIP 开关配置、编译烧录、结果解读到失败排障的完整闭环流程,并理解该测试应用的底层实现机制(如 R-2R 电阻梯形网络 DAC、PCF8574 GPIO 扩展器回环、基于定时器的波特率/时钟频率校验等),从而快速定位 BSP 或外设驱动中的问题。
⚠️重要安全警告:在给板卡上电之前,务必确认 Shield 上的 VCC 电压选择开关与板卡的逻辑电平匹配。例如,在逻辑电平为 3.3 V 的板卡上把 VCC 选择为 5 V,且引脚不兼容 5 V 时,可能永久损坏板卡。该警告来自 tests/periph/selftest_shield/README.md,是硬件操作的第一步。
一、快速开始:五步完成外设自检
1. 选择兼容的板卡
- 待测板卡必须机械与电气兼容 Arduino UNO Shield(即符合 Arduino UNO 排针间距与引脚定义);
- 板卡需要提供 Arduino GPIO 引脚映射,理想情况下还需要 SPI、I2C、ADC、PWM 的引脚映射。
在 RIOT 中,这类能力由arduino_pins、arduino_shield_uno等特性标识,测试应用通过FEATURES_REQUIRED强制要求它们存在(见 Makefile):
FEATURES_REQUIRED += arduino_pins FEATURES_REQUIRED += arduino_shield_uno同时,其余外设能力(arduino_analog、arduino_i2c、arduino_pwm、arduino_shield_isp、arduino_spi、arduino_uart以及periph_adc、periph_gpio、periph_gpio_irq、periph_i2c、periph_pwm、periph_spi、periph_timer、periph_timer_query_freqs、periph_uart)都被声明为可选特性(FEATURES_OPTIONAL),也就是说测试应用会"按板卡能力自动裁剪"测试范围(见 Makefile)。
以 nRF52840DK 为例,其 arduino_iomap.h 中提供了完整的映射:
#define ARDUINO_UART_D0D1 UART_DEV(1) #define ARDUINO_SPI_D11D12D13 SPI_DEV(0) #define ARDUINO_SPI_ISP SPI_DEV(0) #define ARDUINO_I2C_UNO I2C_DEV(0) #define ARDUINO_A0 ADC_LINE(1) #define ARDUINO_A1 ADC_LINE(2) #define ARDUINO_A2 ADC_LINE(4)2. 断电并连接 Shield
先将板卡完全断电(拔掉电源),再插上 Shield。
3. 设置 VCC 电平选择开关
- 将 VCC 选择器拨到与板卡逻辑电平一致的位置;
- 不确定时优先选择 3.3 V;
- 文档特别指出:nRF52840DK 的逻辑电平虽然是 3.0 V,但用 3.3 V 档位工作正常(见 tests/periph/selftest_shield/README.md)。
4. 通过 DIP 开关配置待测外设回环
用 Shield 上的 DIP 开关为要测试的外设闭合回环(拨到ON),未测试的外设保持OFF(开路):
- 建议一开始先使能除 UART 以外的所有回环;
- 如果 UART 测试实际运行了但失败,再打开 UART 开关;
- 原因:如果 D0/D1 上的 UART 被用作 stdio(标准输入输出),该 UART 无法回环,也无法测试(见 tests/periph/selftest_shield/README.md)。
这一点在源码中也有体现——UART 测试的启用条件非常严格(见 main.c):
#if defined(ARDUINO_UART_D0D1) && defined(MODULE_PERIPH_UART) # if MODULE_STDIO_UART # define ENABLE_UART_TEST (STDIO_UART_DEV != ARDUINO_UART_D0D1) # else # define ENABLE_UART_TEST 1 # endif # define UART_TEST_DEV ARDUINO_UART_D0D1 #else # define ENABLE_UART_TEST 0 #endif即:必须提供 D0/D1 的 UART 映射,且该 UART 设备不与 stdio 使用的 UART 冲突时,才真正执行 UART 回环测试。
5. 编译烧录并运行
在tests/periph/selftest_shield目录下执行:
make BOARD=<YOUR_BOARD> flash test-with-configflash负责把固件烧录到板卡;test-with-config会结合tests-with-config/目录下的自动化测试脚本运行。
自动化测试脚本 01-run.py 的校验逻辑非常简洁:期望串口输出依次出现self-testing peripheral drivers与ALL TESTS SUCCEEDED两段标志性文本,从而判定测试整体通过:
def testfunc(child): child.expect("self-testing peripheral drivers") child.expect("ALL TESTS SUCCEEDED")对应地,main.c 在main()末尾根据是否有失败项打印SOME TESTS FAILED(返回 1)或ALL TESTS SUCCEEDED(返回 0)。
二、测试覆盖范围与设计细节
该测试应用会在板卡支持且存在 Arduino I/O 映射的前提下,对以下外设驱动做完整自检:
| 外设 | 测试方式要点 |
|---|---|
periph_adc(1) | 通过 4 位 R-2R 电阻梯形网络产生 ADC 输入电压 |
periph_gpio | D3/D4 引脚回环,覆盖推挽、上拉、下拉、开漏等模式 |
periph_gpio_irq | D3 上升沿/下降沿/双边沿中断计数验证 |
periph_i2c(2) | 间接驱动 PCF8574 I2C GPIO 扩展器并观测结果 |
periph_pwm | 通过 ADC 采样回读 PWM 占空比 |
periph_spi(3) | MOSI/MISO 回环,多模式、多时钟速率、片选时序验证 |
periph_uart(4) | D0/D1 回环,9600 与 115200 两种速率收发 |
注 1:ADC 测试与 R-2R 梯形网络
ADC 输入由4 位 R-2R 电阻梯形网络生成,其电压由 I2C 连接的 GPIO 扩展器(PCF8574)和/或 PWM 控制。若两者都不可用,则不做 ADC 测试;若都可用,最多测试映射到 A0、A1、A2 的三个 ADC 通道。
从源码看,R-2R 网络的 4 个 bit 由 PCF8574 的 P0.4~P0.7 驱动(见 main.c):
static void r_2r_dac_init(void) { ASSERT_NO_ERROR(pcf857x_gpio_init(&egpios, PCF857X_GPIO_PIN(0, 4), GPIO_OUT)); ASSERT_NO_ERROR(pcf857x_gpio_init(&egpios, PCF857X_GPIO_PIN(0, 5), GPIO_OUT)); ASSERT_NO_ERROR(pcf857x_gpio_init(&egpios, PCF857X_GPIO_PIN(0, 6), GPIO_OUT)); ASSERT_NO_ERROR(pcf857x_gpio_init(&egpios, PCF857X_GPIO_PIN(0, 7), GPIO_OUT)); }测试循环 16 个 DAC 值(i从 0 到 15),期望采样值约为i << 6,并允许 10% 误差(delta = 1024 / 10),以容忍 v0.3 板卡电阻精度不足(见 main.c)。
注 2:I2C 的间接测试
I2C 不直接回环,而是通过操作 PCF8574 I2C GPIO 扩展器、再用内部 GPIO 与扩展器 GPIO 互联观测结果来间接验证。该测试仅在periph_i2c和periph_gpio同时可用时启用。
从源码看(main.c),测试双向进行:
- 内部 GPIO(D9 输出)→ 扩展器 GPIO(P0.1 输入):内部置高/置低,读取扩展器侧电平;
- 扩展器 GPIO(P0.0 输出)→ 内部 GPIO(D8 输入):扩展器置高/置低,读取内部 GPIO。
每一方向都循环flaky_test_repetitions(默认 100)次。
PCF8574 模块的引入是按需的:只有在板卡同时具备periph_i2c与arduino_i2c特性时才链接pcf8574模块(见 Makefile.board.dep):
ifneq (,$(filter periph_i2c,$(FEATURES_USED))) ifneq (,$(filter arduino_i2c,$(FEATURES_USED))) USEMODULE += pcf8574 endif endif注 3:SPI 回环测试的局限
SPI 采用 MOSI 与 MISO 短接的回环测试,因此:
- 位序(bit order)和时钟相位(clock phase)无法校验(回环方式天然测不出相位差异);
- 时钟极性(clock polarity)可以校验(在空闲电平下检测);
- 若实现了
periph_timer,时钟频率的正确性可做粗略校验。
SPI 测试覆盖两种总线(ARDUINO_SPI_D11D12D13与ARDUINO_SPI_ISP,见 main.c),每种总线遍历SPI_CLK_400KHZ / SPI_CLK_1MHZ / SPI_CLK_10MHZ三档时钟,以及 SPI_MODE_0~3 四种模式(见 main.c)。空闲电平通过扩展器引脚 P0.2 / P0.3 读取确认,片选(CS)则由 D10 驱动、D7 观测。
注 4:UART 回环测试的约束
- 当 D0/D1 的 UART 接口被 stdio 占用时,跳过 UART 自检;
- 符号率(symbol rate,常被误称为 "baudrate")仅在
periph_timer可用时做合理性校验; - 不同的每字符位数、停止位数、校验位等参数理论上可借助
periph_timer检测,但当前版本尚未实现。
UART 测试使用 8 字节测试数据"Selftest",依次在 9600 与 115200 两种速率下进行(见 main.c)。当启用定时器后,发送耗时会被测量并与理论值比对(允许实际耗时在期望值的 75%~200% 之间,见 main.c)。此外还会验证:
- 数据内容与字节数完全一致(回环正确性);
uart_poweroff后不再收到数据(断电有效);uart_poweron后重新收发正常(上电恢复)。
三、失败排障指南
3.1 增加输出详细程度
编译时加上DETAILED_OUTPUT=1可提高输出详细度,代价是 ROM 占用增加:
make BOARD=<YOUR_BOARD> DETAILED_OUTPUT=1当出现 ROM 链接失败时,可通过FEATURES_BLACKLIST屏蔽部分无关外设特性来缩小固件体积。但注意:部分外设是借助其他外设测试的,例如屏蔽 ADC 会连带导致 PWM 输出占空比的正确性无法验证(见 tests/periph/selftest_shield/README.md)。从 Makefile 可以看到该宏被直接注入编译选项:
CFLAGS += \ '-DSTOP_ON_FAILURE=$(STOP_ON_FAILURE)' \ '-DDETAILED_OUTPUT=$(DETAILED_OUTPUT)' \ #DETAILED_OUTPUT开启后,每个测试在启动时会打印更完整的信息(包括测试名与细节描述,见 main.c),SPI 测试还会额外打印当前时钟频率(见 main.c)。
3.2 早期关键失败(Critical Failure)
如果应用在执行任何测试之前就失败,例如输出类似:
START main(): This is RIOT! (Version: 2023.10-devel-348-gf1c68-peripheral-selftest) self-testing peripheral drivers =============================== CRITICAL FAILURE in tests/periph/selftest_shield/main.c:<line-number>这表明GPIO 扩展器(PCF8574)初始化失败。可能原因按可能性排序:
- I2C 配置错误
- I2C 总线(通常在
periph_conf.h或其包含的文件中配置)是否正确配置?是否连接到了 Arduino UNO 的 D14 / D15 引脚? ARDUINO_I2C_UNO宏是否正确定义为连接到 D14 / D15 的 I2C 总线索引?
- I2C 总线(通常在
- 缺少上拉电阻
- Shield 的 I2C 总线上没有上拉电阻;
- 默认情况下 RIOT 的 I2C 驱动会启用 MCU 内部上拉,但部分 MCU 的某些引脚没有内部上拉,或在 I2C 模式下无法启用;
- 若可能,应在
periph_conf.h中配置 I2C 驱动/板级配置,使无外部上拉时也能开箱即用地稳定运行; - 若板卡必须依赖外部上拉(无合适的内部上拉、板卡在 D14/D15 上也没有外部上拉),最省事的办法是在 J2 的 I2C 引脚插座上插入一块 I2C 传感器/EEPROM 扩展板——这类扩展板通常自带外部上拉电阻。
- I2C 驱动 bug
- I2C 驱动本身的缺陷也可能导致此问题;
- 此时连接逻辑分析仪到 I2C 排针(J3)、I2C 插座(J2)或 I2C Grove 连接器(J1)会很有帮助。
- 硬件问题
- 这是最不可能的原因。
从源码结构看,pcf857x_init()是main()中第一个不可失败的操作——扩展器同时服务于 I2C 测试与 ADC 测试,因此只初始化一次(见 main.c):
/* the GPIO extender is used by the I2C test and the ADC test, so only * initialize it once here */ if (IS_USED(MODULE_PCF857X)) { ASSERT_NO_ERROR(pcf857x_init(&egpios, ¶ms)); }一旦初始化返回错误,ASSERT_NO_ERROR会打印错误码(通过tiny_strerror转为可读字符串)并触发ARCHITECTURE_BREAKPOINT(1)挂起(见 main.c),这正是"CRITICAL FAILURE"输出的来源。扩展器的 I2C 地址与设备类型在 main.c 中通过pcf857x_params_t固定为PCF857X_EXP_PCF8574。
3.3 同一错误反复出现
许多测试在循环中执行,例如部分"碰运气也可能过"的测试会重复flaky_test_repetitions次(默认 100次,见 main.c),以提高对间歇性问题的检出置信度;部分测试还会遍历不同配置(如多种时钟速度)并对每种配置重复测试。
若想减少噪声、快速定位首个失败点,可在编译时传入STOP_ON_FAILURE=1(环境变量或make参数均可),让测试在第一个失败用例处立即中止:
make BOARD=<YOUR_BOARD> STOP_ON_FAILURE=1 flash test-with-config源码中对应逻辑位于 main.c:do_test()在测试失败时打印失败行号,若STOP_ON_FAILURE==1则打印提示并触发断点挂起,否则继续执行后续测试:
static bool do_test(bool failed, uint16_t line) { if (failed) { print_test_failed(line); if (STOP_ON_FAILURE) { printf("Stopping, as STOP_ON_FAILURE==1\n"); ARCHITECTURE_BREAKPOINT(1); while (1) { /* stop */ } } } return failed; }另外注意:为节省 RAM,失败信息刻意只打印__FILE__与行号,而不保存冗长错误文本——代码注释明确指出,行号加上代码中的注释足以提供等价的排查信息(见 main.c)。因此排查时应"按行号回查源码"。
四、配置说明:默认全测 + 按需裁剪
该测试默认无需任何配置,会自动测试所有存在 Arduino I/O 映射的外设驱动。
如需屏蔽某类测试(例如为低端板卡瘦身以节省 RAM/ROM),可通过FEATURES_BLACKLIST关闭对应特性:
FEATURES_BLACKLIST += periph_<foobar>例如:
make BOARD=<YOUR_BOARD> FEATURES_BLACKLIST="periph_i2c periph_pwm" flash test-with-config各测试的自动启用条件汇总如下(均可在 main.c 中查到):
- ADC 测试:
periph_adc+ARDUINO_A0映射 +pcf857x模块(即同时具备periph_i2c与arduino_i2c); - GPIO 测试:
periph_gpio; - GPIO-IRQ 测试:
periph_gpio_irq; - I2C 测试:
periph_i2c+periph_gpio(间接经 PCF8574); - PWM 测试:
periph_pwm+periph_adc,且满足以下任一组合:- D5 的 PWM 映射且A2 的 ADC 映射;
- D6 的 PWM 映射且A1 的 ADC 映射;
- SPI 测试:
periph_spi(需ARDUINO_SPI_D11D12D13或ARDUINO_SPI_ISP之一); - UART 测试:
periph_uart+ARDUINO_UART_D0D1映射,且 D0/D1 不被 stdio 占用。
内存占用的额外细节:为保证尽可能多的板卡可运行,非 ESP32 平台会强制
-DMAXTHREADS=2(仅需 1 个线程 + 部分平台的 Idle 线程),ESP32x 平台则使用-DMAXTHREADS=3(因esp_timer需要额外线程)——见 Makefile。若你使用的是极小内存的板卡,这一点值得留意。
关于定时器的使用
测试会使用一个独立定时器来校验 SPI 时钟频率与 UART 符号率的合理性,且刻意避开 ztimer 占用的定时器(见 main.c):
#ifndef TIMER # if IS_USED(MODULE_ZTIMER_PERIPH_TIMER) && CONFIG_ZTIMER_USEC_DEV == TIMER_DEV(0) # define TIMER TIMER_DEV(1) # else # define TIMER TIMER_DEV(0) # endif #endif #if IS_USED(MODULE_ZTIMER_PERIPH_TIMER) # if CONFIG_ZTIMER_USEC_DEV == TIMER # error "Same timer used for ztimer and test" # endif #endif不同 MCU 族的测试定时器频率也做了针对性选择(见 main.c):
- SPI 测试希望频率尽量高:SAM3/STM32 用
CLOCK_CORECLOCK / 4,NRF51/NRF52 用 16 MHz,其余默认直接用核心时钟CLOCK_CORECLOCK; - UART 测试希望频率较低:AVR 用
CLOCK_CORECLOCK / 64,其余用 1 MHz(避免 16 位定时器溢出); - 支持
periph_timer_query_freqs的定时器会自动选择最接近且不低于目标值的真实可用频率(见 main.c)。
五、结果解读
- 每个单项测试以
[OK]或[FAILED]结尾(见 main.c),不支持的能力则打印(skipped); - 全部结束后输出汇总结论:
ALL TESTS SUCCEEDED # 退出码 0或:
SOME TESTS FAILED # 退出码 1main()中的最终判断逻辑见 main.c。由于执行顺序为 GPIO → GPIO-IRQ → I2C → UART → SPI → PWM → ADC(见 main.c),当使用STOP_ON_FAILURE=1时,首个失败用例的位置即可直接对应到上述顺序中的某一阶段。
小结
tests/periph/selftest_shield是 RIOT 生态中一个"零配置、按能力裁剪"的外设驱动回归测试工具:只要板卡兼容 Arduino UNO Shield 并提供 Arduino I/O 映射,就能在数分钟内完成七大外设驱动的自动回环自检。它通过 R-2R 梯形网络测 ADC、PCF8574 扩展器间接测 I2C、定时器辅助校验 SPI 时钟与 UART 符号率等精巧设计,将"驱动是否工作"转化为可重复、可自动化的断言。配合DETAILED_OUTPUT与STOP_ON_FAILURE两个编译期开关,它既可以作为新板卡 BSP 合入前的验收测试,也可以作为驱动开发时的回归测试基线。
- 物联网
- 嵌入式
- 操作系统
- 实时系统
【免费下载链接】RIOT
RIOT - The friendly OS for IoT
相关推荐
RIOT-OS 中的 ESP32 SoC 外设资源与引脚配置详解(GPIO / ADC / DAC / I2C / PWM / SDMMC / SPI / Timer / UART / JTAG)
RIOT OS 中的 ESP32 SoC 外设资源与引脚配置详解(GPIO / ADC / DAC / I2C / PWM / SDMMC / SPI / Ti
物联网嵌入式操作系统实时系统STM32外设调试终极指南:stlink实战GPIO、UART、I2C、SPI全解析
STM32外设调试终极指南:stlink实战GPIO、UART、I2C、SPI全解析 stlink作为开源的STM32 MCU编程工具集,为开发者提供了强大的外
嵌入式硬件开发开发工具调试器告别外设控制痛点:RPPAL全攻略 — 用Rust掌控树莓派GPIO/I2C/PWM/SPI/UART
告别外设控制痛点:RPPAL全攻略 — 用Rust掌控树莓派GPIO/I2C/PWM/SPI/UART 你是否还在为树莓派外设编程烦恼?尝试过Python库却受
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考