4、传感器接口基础:I2C、SPI、ADC等总线协议在nRF平台上的配置与数据采集方法

在低功耗物联网终端中,传感器是数据世界的入口。nRF52系列(如nRF52832、nRF52840)和nRF53系列(如nRF5340)作为Nordic主推的BLE SoC,集成了丰富的外设接口,包括TWI(兼容I2C)、SPI、SAADC(逐次逼近型ADC)、PDM、QDEC等。本章聚焦于最常用的I2C、SPI和ADC三类接口,从协议原理、nRF平台驱动架构、Zephyr RTOS设备树配置到实际数据采集与校验方法,帮助学员建立从传感器物理信号到应用层数据的完整链路认知。

I2C (TWI) SPI SAADC SCL + SDA 双线 7位地址 + 读写位 速率:100k / 400k / 1M 上拉电阻 2.2k~10kΩ 典型传感器 BME280 / SHT30 / MPU6050 BH1750 / TSL2561 nRF外设:TWI0/TWI1 支持DMA + 挂起/恢复 Zephyr设备树 &twi0 { status="okay"; } compatible = "nxp,pca10056" SCK + MOSI + MISO + CS 全双工 / 主从模式 速率:最高 8MHz(nRF52) 模式0~3(CPOL/CPHA) 典型传感器 MAX31865 / LIS3DH / ADXL345 ICM-20948 / 闪存芯片 nRF外设:SPI0/SPI1/SPI2 支持EasyDMA自动传输 Zephyr设备树 &spi2 { status="okay"; } cs-gpios = &gpio0 24 模拟输入(8通道) 10/12位可配置分辨率 采样率:最高 200ksps 增益:1/4 ~ 4倍可编程 典型传感器 光敏电阻 / 电位器 / 电池电压 NTC热敏电阻 / 压电薄膜 nRF外设:SAADC 支持单端/差分输入 Zephyr设备树 &adc { status="okay"; } channels = &ch0 数据采集 数据采集 数据采集

4.1 核心原理:三种总线的物理层与协议特征

I2C(Inter-Integrated Circuit)由Philips(现NXP)于1982年推出,是一种两线制半双工串行总线。在nRF平台上,Nordic将其实现为TWI(Two-Wire Interface),完全兼容标准I2C协议。I2C使用SCL(时钟线)和SDA(数据线)两根线,所有设备通过7位或10位地址寻址。总线上的设备分为主设备(Master)和从设备(Slave),nRF SoC通常作为主设备发起通信。I2C的物理层采用开漏输出结构,因此必须外接上拉电阻(典型值2.2kΩ~10kΩ,取决于总线电容和通信速率)。标准模式100kbps、快速模式400kbps、高速模式1Mbps是常见的三种速率等级。I2C的帧格式包含起始条件(SDA在SCL高电平时下降)、地址字节(7位地址+读写位)、应答位(ACK/NACK)和数据字节。由于有应答机制,I2C能够检测从设备是否正常接收,但这也意味着每传输一个字节都需要额外的时钟周期。

SPI(Serial Peripheral Interface)由Motorola提出,是一种四线制全双工同步串行总线。nRF52系列提供最多3个SPI实例(SPI0/SPI1/SPI2),每个实例包含SCK(时钟)、MOSI(主出从入)、MISO(主入从出)和CS(片选)四根线。SPI没有地址概念,通过片选信号选择从设备,因此每个从设备需要占用一个独立的GPIO作为CS。SPI的时钟极性(CPOL)和相位(CPHA)可配置,组合成4种工作模式(Mode 0~3)。nRF52的SPI最高支持8MHz时钟频率,配合EasyDMA功能,可以在不占用CPU的情况下完成数据收发。SPI的传输效率高于I2C,因为它没有应答位和地址位,且支持全双工同时收发。但SPI的缺点是需要更多引脚,且没有标准的错误检测机制。

SAADC(Successive Approximation ADC)是nRF52内置的逐次逼近型模数转换器,支持8个外部输入通道和3个内部通道(VDD/4、VDDHDIV5、温度传感器)。SAADC的分辨率可配置为8/10/12位,采样率最高可达200ksps。其核心原理是通过二分查找算法,将模拟输入电压与内部DAC产生的参考电压逐次比较,最终逼近输入电压值。nRF52的SAADC支持单端输入和差分输入两种模式,差分模式可以有效抑制共模噪声。SAADC还支持可编程增益放大器(PGA),增益范围为1/4到4倍,允许用户根据传感器输出范围调整量程。在Zephyr RTOS中,SAADC通过设备树配置通道、分辨率、增益和参考电压等参数。

三种总线选型速查表:
特性I2C (TWI)SPISAADC
引脚数2(SCL+SDA)4(SCK+MOSI+MISO+CS)1~2(模拟输入)
通信方式半双工全双工模拟→数字
寻址方式7/10位地址片选信号通道选择
速率100k/400k/1Mbps最高8MHz最高200ksps
典型应用环境传感器、温湿度加速度计、陀螺仪、闪存电池电压、光敏、NTC
nRF外设名TWI0/TWI1SPI0/SPI1/SPI2SAADC
Zephyr驱动i2cspiadc

4.2 操作流程:nRF平台上的设备树配置与驱动调用

在nRF平台上使用Zephyr RTOS进行传感器开发,核心工作集中在设备树(Device Tree)的配置和驱动API的调用。设备树描述了硬件资源与软件驱动的绑定关系,是Zephyr驱动模型的基础。以下以nRF52840 DK开发板为例,分别说明I2C、SPI和ADC的配置流程。

I2C(TWI)配置步骤:首先在设备树中使能TWI实例并配置引脚。nRF52840的TWI0默认映射到P0.08(SCL)和P0.09(SDA),但可以通过设备树重新映射到任意GPIO。在nrf52840dk_nrf52840.overlay文件中添加如下配置:

&twi0 {
    status = "okay";
    clock-frequency = <I2C_BITRATE_FAST>;  /* 400kHz */
    pinctrl-0 = <&twi0_default>;
    pinctrl-names = "default";
    
    bme280@76 {
        compatible = "bosch,bme280";
        reg = <0x76>;  /* I2C地址 */
        label = "BME280";
    };
};

&pinctrl {
    twi0_default: twi0_default {
        group1 {
            psels = <NRF_PSEL(TWIM_TWI, 0, 8)>,  /* SCL = P0.08 */
                    <NRF_PSEL(TWIM_TWI, 0, 9)>;  /* SDA = P0.09 */
            bias-pull-up;
        };
    };
};

配置完成后,在应用代码中通过device_get_binding()获取设备实例,然后使用i2c_configure()设置速率,i2c_write()/i2c_read()进行数据收发。对于BME280这类复合传感器,通常需要先写入寄存器地址,再读取数据,可以使用i2c_burst_read()或i2c_write_read()组合操作。

SPI配置步骤:SPI的配置与I2C类似,但需要额外指定CS引脚。nRF52840的SPI2默认映射到P0.24(SCK)、P0.23(MOSI)、P0.22(MISO),CS可以自由选择GPIO。设备树配置如下:

&spi2 {
    status = "okay";
    clock-frequency = <4000000>;  /* 4MHz */
    pinctrl-0 = <&spi2_default>;
    pinctrl-names = "default";
    cs-gpios = <&gpio0 24 GPIO_ACTIVE_LOW>;  /* CS = P0.24 */
    
    lis3dh@0 {
        compatible = "st,lis3dh";
        reg = <0>;  /* CS索引 */
        spi-max-frequency = <4000000>;
        label = "LIS3DH";
    };
};

&pinctrl {
    spi2_default: spi2_default {
        group1 {
            psels = <NRF_PSEL(SPIM_SPI, 0, 24)>,  /* SCK */
                    <NRF_PSEL(SPIM_SPI, 0, 23)>,  /* MOSI */
                    <NRF_PSEL(SPIM_SPI, 0, 22)>;  /* MISO */
        };
    };
};

SPI驱动调用时,需要构造spi_cs_control结构体管理片选信号,使用spi_transceive()进行全双工通信。对于只读寄存器,可以发送虚拟字节同时接收数据。

SAADC配置步骤:ADC的配置相对简单,主要设置通道、分辨率和参考电压。在设备树中定义ADC通道:

&adc {
    status = "okay";
    #address-cells = <1>;
    #size-cells = <0>;
    
    channel0: channel@0 {
        reg = <0>;
        zephyr,device = <&adc>;
        input-positive = <NRF_SAADC_INPUT_AIN0>;  /* P0.02 */
        reference = <ADC_REF_INTERNAL>;          /* 0.6V */
        gain = <ADC_GAIN_1_6>;                   /* 1/6 */
        resolution = <ADC_RESOLUTION_12>;
    };
};

应用代码中,使用adc_channel_setup()配置通道,adc_read()读取采样值,最后通过公式电压 = (采样值 / 4095) × 参考电压 × 增益倒数转换为实际电压值。

4.3 案例拆解:多传感器数据采集与校准实战

假设我们需要设计一个环境监测节点,同时采集温湿度(I2C接口的SHT30)、加速度(SPI接口的LIS3DH)和电池电压(SAADC通道)。该节点需要每5秒上报一次数据,并要求电池电压测量精度达到±50mV。以下是完整的实现过程。

硬件连接:SHT30连接到TWI0(P0.08/P0.09),LIS3DH连接到SPI2(P0.24/P0.23/P0.22,CS=P0.20),电池电压通过分压电阻(100kΩ+100kΩ)连接到AIN0(P0.02)。分压比2:1,因此实际电压 = ADC电压 × 2。

设备树配置:按照4.2节的方法配置三个外设,注意LIS3DH的CS引脚改为P0.20。电池电压通道的增益设置为1/6,参考电压0.6V,则满量程电压 = 0.6V × 6 = 3.6V,经过2:1分压后实际可测电压范围0~7.2V,满足锂电池3.7V的测量需求。

数据采集代码:

/* 读取SHT30温湿度 */
static int read_sht30(const struct device *dev, float *temp, float *hum) {
    uint8_t cmd[2] = {0x2C, 0x06};  /* 高重复性测量命令 */
    uint8_t data[6];
    
    i2c_write(dev, cmd, 2, 0x44);  /* SHT30地址0x44 */
    k_sleep(K_MSEC(20));  /* 等待测量完成 */
    i2c_read(dev, data, 6, 0x44);
    
    *temp = -45.0 + 175.0 * ((data[0] << 8) | data[1]) / 65535.0;
    *hum = 100.0 * ((data[3] << 8) | data[4]) / 65535.0;
    return 0;
}

/* 读取LIS3DH加速度 */
static int read_lis3dh(const struct device *dev, int16_t *acc) {
    uint8_t reg = 0x28;  /* OUT_X_L寄存器 */
    uint8_t data[6];
    struct spi_config cfg = {
        .frequency = 4000000,
        .operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(8) | SPI_TRANSFER_MSB,
        .slave = 0,
    };
    
    spi_write(dev, &cfg, &reg, 1);  /* 发送寄存器地址 */
    spi_read(dev, &cfg, data, 6);   /* 读取6字节数据 */
    
    acc[0] = (int16_t)((data[1] << 8) | data[0]);
    acc[1] = (int16_t)((data[3] << 8) | data[2]);
    acc[2] = (int16_t)((data[5] << 8) | data[4]);
    return 0;
}

/* 读取电池电压 */
static int read_battery(const struct device *dev, uint16_t *mv) {
    struct adc_sequence seq = {
        .channels = BIT(0),
        .buffer = &sample,
        .buffer_size = sizeof(sample),
        .resolution = 12,
    };
    
    adc_read(dev, &seq);
    *mv = (uint32_t)sample * 3600 / 4095 * 2;  /* 转换为mV */
    return 0;
}

校准与验证:使用高精度万用表测量电池实际电压,与ADC读数对比。假设万用表读数为3.812V,ADC读数为3.798V,偏差14mV,在允许范围内。如果偏差较大,可以通过软件校准:实际电压 = 读数 × 校准系数 + 偏移量。校准系数可以通过两点校准法获得:分别测量3.0V和4.2V两个点,计算斜率和截距。

经验提示:SAADC的参考电压(0.6V)存在±2%的制造偏差,如果对精度要求较高,建议使用外部精密参考电压或进行软件校准。另外,ADC采样时建议开启过采样(oversampling),例如4倍过采样可以将有效分辨率提升1位。

4.4 常见误区与总结复盘

误区一:I2C上拉电阻随意选择。上拉电阻过小会导致功耗增加和信号边沿变陡,过大则导致上升沿变缓,影响通信可靠性。对于400kHz速率,建议使用4.7kΩ;对于1MHz速率,建议使用2.2kΩ。同时要考虑总线上挂载的设备数量,设备越多,总线电容越大,需要更小的上拉电阻。

误区二:SPI的CS引脚管理不当。在Zephyr中,如果使用spi_transceive(),CS信号由驱动自动管理。但如果手动控制CS,必须确保在传输前拉低CS,传输完成后拉高CS,且CS拉低到SCK第一个边沿之间需要满足从设备的建立时间(通常≥100ns)。另外,多个SPI从设备共用一个CS是常见错误,这会导致总线冲突。

误区三:ADC输入阻抗不匹配。SAADC的输入阻抗约为1MΩ,如果传感器输出阻抗过高(如光敏电阻),会导致采样值偏低。解决方案是使用运放缓冲或增加采样保持时间。在Zephyr中可以通过adc_sequence的sampling_time参数调整采样时间,nRF52支持最短3μs的采样时间。

误区四:忽略传感器上电稳定时间。许多传感器在上电后需要几十毫秒才能完成内部初始化,如果立即读取会返回错误数据。建议在传感器驱动初始化后加入k_sleep()延时,或使用传感器提供的数据就绪引脚(DRDY)进行中断同步。

误区五:设备树地址与硬件不匹配。I2C设备的7位地址在设备树中使用reg属性表示,但要注意该地址是7位地址还是8位地址。Zephyr使用7位地址,而某些传感器数据手册可能标注8位地址(左移1位)。例如SHT30的7位地址是0x44,8位写地址是0x88,读地址是0x89,配置时务必使用7位地址。

风险提醒:在nRF平台上使用EasyDMA进行SPI/I2C传输时,缓冲区必须位于RAM中且地址对齐到4字节边界。如果使用栈上变量,需要确保编译器对齐;如果使用静态数组,建议使用__aligned(4)属性。否则可能导致HardFault或数据错乱。

总结复盘:本章从物理层协议出发,详细介绍了I2C、SPI和SAADC在nRF平台上的配置与数据采集方法。核心要点包括:I2C适合低速多设备场景,SPI适合高速大数据量场景,ADC适合模拟信号采集。在Zephyr开发中,设备树配置是硬件与软件解耦的关键,合理的引脚映射和驱动配置可以大幅提升开发效率。实际项目中,建议先使用i2c_scan()或逻辑分析仪验证总线通信,再编写应用层代码。对于电池供电的低功耗设备,还需要考虑传感器在休眠模式下的功耗,以及总线在空闲状态下的电平状态(I2C空闲时SDA/SCL应为高电平,SPI空闲时SCK应保持稳定电平)。