day73 嵌入式传感器技术全栈开发

本笔记系统性整理了嵌入式开发中常用传感器(GPIO、I2C、SPI、ADC、UART)的分类、工作原理、驱动开发框架及应用层实现,涵盖从硬件连接、设备树配置、内核驱动编写到用户空间程序调用的完整流程。内容基于Linux内核子系统(字符设备、杂项设备、I2C、SPI、IIO),并结合OV5640摄像头、HCSR04超声波、MAX30102心率血氧、MQ系列气体传感器等实例,提供可直接运行的代码及详细注释。


一、传感器分类与核心参数

(一)按连接方式分类

连接方式传感器型号功能
GPIO(单总线)dht11温湿度检测
ds18b20温度检测
hcsr-04超声波测距
I2CLM75温度检测
BH1750光照强度检测
MPU6050六轴姿态检测
MAX30100心率、血氧浓度检测
SPIADXL345三轴加速度检测
ADCMQ系列(MQ-2、MQ-135、MQ-7等)气体浓度检测(如MQ-2检测烟雾浓度)
UARTGPS传感器定位(精度、维度、海拔)检测
GY-35红外测距检测

(二)关键传感器指标参数

传感器功能连接方式量程(测量范围)精度分辨率工作电压
dht11温湿度GPIO(单总线)温度:0-50℃;湿度:20-90%RH温度:±2℃;湿度:±5%RH13.3V-5.5V
dht22温湿度GPIO(单总线)温度:-40-80℃;湿度:0-99.9%RH温度:±0.5℃;湿度:±2%RH0.1℃/0.1%RH3.3V-5.5V
ds18b20温度GPIO(单总线)温度:-55-125℃0-85℃:±0.5℃(典型);全范围:±2℃9位:0.5℃;10位:0.25℃;11位:0.125℃;12位:0.0625℃3V-5V
hcsr-04超声波测距GPIO2cm-450cm0.3cm15V
MAX30102心率、血氧I2C心率:30-240BPM(次/分钟);血氧:70-100%±2%-1.8V-5.5V

二、传感器工作原理与时序详解

(一)DHT11温湿度传感器

电路图

R1
VCC 4.7K
1 2 3
DATA
NC
GND P2
DHT11 GND

数据格式:5字节共40bit,高位先行。

  • 字节1:湿度整数部分
  • 字节2:湿度小数部分
  • 字节3:温度整数部分
  • 字节4:温度小数部分
  • 字节5:校验和(前4字节之和)

时序

  1. 主机起始信号:拉低DATA引脚 ≥18ms,再拉高 ≥20-40us。
  2. 从机应答信号:拉低 ≥80us,再拉高 ≥80us。
  3. 数据传输
    • bit “0”:拉低50us,再拉高26-28us。
    • bit “1”:拉低50us,再拉高70us。
  4. 结束信号:主机将DATA拉高。

(二)HC-SR04超声波传感器

实物图:标注“4.0000H00”和“HC-SR04”。

引脚

  • VCC:5V电源
  • GND:接地
  • Trig:触发端(控制)
  • Echo:回波端(接收)

工作原理/时序

  1. 向Trig发送≥10us的高电平脉冲。
  2. 传感器内部发射8个40kHz超声波脉冲。
  3. Echo引脚输出高电平,持续时间为超声波往返时间。
  4. 计算距离公式:距离 = (Echo高电平时间 × 340m/s) / 2

IMX6ULL原理图连接

  • VCC → P4 46号引脚
  • GND → P4 47号引脚
  • Trig → SNVS_TAMPER4 (P4 1号引脚)
  • Echo → SNVS_TAMPER5 (P4 6号引脚)

三、Linux驱动框架详解

(一)驱动类型概述

  1. 字符型设备驱动
    • 以字节为单位读写,无缓存。
    • 适用于鼠标、键盘、串口等。
    • 核心结构体:cdev, file_operations
  2. 块设备驱动
    • 以固定块大小(如512k)读写,带缓存。
    • 适用于硬盘、Flash等存储设备。
  3. 网络设备驱动
    • 基于TCP/IP协议栈,用于网络通信。

(二)平台驱动(Platform Driver)——以HCSR04为例

设备树配置(dts)
/* iomux节点增加引脚复用 */
trig:
#define MX6UL_PAD_SNVS_TAMPER4_GPIO5_IO04 0x002C 0x02B8 0x0000 0x5 0x0
echo:
#define MX6UL_PAD_SNVS_TAMPER5_GPIO5_IO05 0x0030 0x02BC 0x0000 0x5 0x0

pinctrl_hcsr04: hcsr04_fsl,pins {
    MX6UL_PAD_SNVS_TAMPER4_GPIO5_IO04 0x10B0
    MX6UL_PAD_SNVS_TAMPER5_GPIO5_IO05 0x10B0
};

/* 添加hcsr04设备节点 */
hcsr04 {
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = "imx6ull,hcsr04";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_hcsr04>;
    gpio-trig = <&gpio5 4 GPIO_ACTIVE_LOW>;
    gpio-echo = <&gpio5 5 GPIO_ACTIVE_LOW>;
    status = "okay";
};
驱动入口函数
static int __init hcsr04_init(void) {
    int ret = 0;
    ret = platform_driver_register(&hcsr04_driver); // 注册平台驱动
    if (ret != 0) {
        pr_info("platform_driver_register failed!
");
        return -1;
    }
    return 0;
}
驱动出口函数
static void __exit hcsr04_exit(void) {
    platform_driver_unregister(&hcsr04_driver); // 注销平台驱动
    return;
}
平台驱动结构体
static struct platform_driver hcsr04_driver = {
    .probe = hcsr04_probe,          // 匹配成功后执行
    .remove = hcsr04_remove,        // 卸载时执行
    .driver = {
        .name = "hcsr04",           // 驱动名称,用于匹配compatible
    },
    .id_table = hcsr04_idtable,
    .of_match_table = hcsr04_of_match_table, // 设备树匹配表
    .owner = THIS_MODULE,
};
设备树匹配表
static struct of_device_id hcsr04_of_match_table[] = {
    {.compatible = "imx6ull,hcsr04"}, // 与设备树compatible字段匹配
    {},
};
probe函数实现
static int hcsr04_probe(struct platform_device *pdev) {
    int ret = 0;
    // 解析设备树获取GPIO
    gpio_trig = of_get_named_gpio(pdev->dev.of_node, "gpio-trig", 0);
    gpio_echo = of_get_named_gpio(pdev->dev.of_node, "gpio-echo", 0);
    // 申请GPIO资源
    ret = devm_gpio_request(&pdev->dev, gpio_trig, "hcsr04_trig");
    if (ret) return ret;
    ret = devm_gpio_request(&pdev->dev, gpio_echo, "hcsr04_echo");
    if (ret) return ret;
    // 注册混杂设备
    ret = misc_register(&misc_hcsr04);
    if (ret != 0) return ret;
    // 构建file_operations
    static struct file_operations fops = {
        .read = hcsr04_read,
    };
    return 0;
}
read函数实现
static ssize_t hcsr04_read(struct file *fp, char __user *puser, size_t n, loff_t *off) {
    int cnt = 0; // 存储Echo高电平持续时间(微秒)
    long nret = 0;
    struct timeval start_time, end_time;

    // 设置Trig为输出模式,发送触发脉冲
    gpio_direction_output(gpio_trig, 0);
    gpio_set_value(gpio_trig, 1);
    udelay(10); // 延时10us
    gpio_set_value(gpio_trig, 0);

    // 设置Echo为输入模式,等待高电平开始
    gpio_direction_input(gpio_echo);
    while (0 == gpio_get_value(gpio_echo)); // 等待Echo变为高电平

    // 记录开始时间
    do_gettimeofday(&start_time);

    // 等待Echo变为低电平
    while (gpio_get_value(gpio_echo));

    // 记录结束时间
    do_gettimeofday(&end_time);

    // 计算时间差(微秒)
    cnt = (end_time.tv_sec * 1000000 + end_time.tv_usec) - 
          (start_time.tv_sec * 1000000 + start_time.tv_usec);

    // 将结果拷贝给用户空间
    nret = copy_to_user(puser, &cnt, sizeof(cnt));
    if (nret) {
        pr_info("copy_to_user failed
");
        return -1;
    }

    return sizeof(cnt); // 返回拷贝的数据长度
}
应用层程序
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(void) {
    int fd = 0;
    int time_us = 0;
    double distance = 0;

    fd = open("/dev/misc_hcsr04", O_RDWR); // 打开设备节点
    if (-1 == fd) {
        perror("fail to open");
        return -1;
    }

    while (1) {
        read(fd, &time_us, sizeof(time_us)); // 读取超声波往返时间
        distance = (340.0 / 1000000.0 * time_us) / 2; // 计算距离(厘米)
        printf("time_us=%d, distance=%.2f cm
", time_us, distance);
        usleep(250000); // 延时250ms
    }

    close(fd);
    return 0;
}

理想运行结果

time_us=1000, distance=17.00 cm
time_us=1500, distance=25.50 cm
time_us=2000, distance=34.00 cm
...

(三)I2C子系统驱动 —— 以MAX30102为例

驱动注册
module_i2c_driver(max30102_driver); // 自动处理insmod/rmmod
I2C驱动结构体
static struct i2c_driver max30102_driver = {
    .driver = {
        .name = "putemax30102",
    },
    .remove = max30102_remove,
    .id_table = max30102_id_table,
    .probe = max30102_probe,
    .of_match_table = max30102_of_match_table,
};
设备树匹配表
static struct i2c_device_id max30102_id_table[] = {
    {.name = "putemax30102"},
    {},
};

static struct of_device_id max30102_of_match_table[] = {
    {.compatible = "imx6ull,max30102"},
    {},
};
探测函数(probe)
static int max30102_probe(struct i2c_client *client, const struct i2c_device_id *id) {
    int ret = 0;
    ret = misc_register(&misc); // 注册混杂设备
    if (ret != 0)
        return ret;
    // 初始化其他资源
    return 0;
}
文件操作结构体
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .read = max30102_read,
    .open = max30102_open,
    .release = max30102_close,
};

static struct miscdevice misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "misc_max30102",
    .fops = &fops,
};
I2C数据传输(write/read)
struct i2c_adapter *max30102_adp = NULL;
struct i2c_msg msg;
unsigned char i2cbuf[2] = {0};
int ret = 0;

max30102_adp = max30102_client->adapter;
i2cbuf[0] = regaddr;      // 寄存器地址
i2cbuf[1] = data;         // 数据值
msg.addr = max30102_client->addr; // 从机地址
msg.len = 2;
msg.flags = 0;            // 写操作
msg.buf = i2cbuf;
ret = max30102_adp->algo->master_xfer(max30102_adp, &msg, 1); // 发送数据
if (ret < 0)
    return ret;
open函数实现
static int max30102_open(struct inode *inode, struct file *fp) {
    int ret = 0;
    unsigned char id = 0;
    unsigned char i2cbuf[6] = {0};

    // 读ID寄存器验证芯片
    max30102_read_register(0xFF, &id, 1);
    printk("Part ID:%#x
", id);
    max30102_read_register(0xFE, &id, 1);
    printk("Revision ID:%#x
", id);

    // 重置芯片
    ret = max30102_write_register(0x09, 0x40);
    mdelay(10);
    ret = max30102_write_register(0x02, 0xCE);
    if (ret < 0)
        return ret;

    // 配置中断
    ret = max30102_write_register(0x03, 0x00);
    if (ret < 0)
        return ret;
    ret = max30102_write_register(0x04, 0x00); // FIFO_WR_PTR[4:0]

    // 初始化等待队列
    condition = 0;
    init_waitqueue_head(&waitqueue_head);

    // 申请中断(下降沿触发)
    irqno = of_irq_get(max30102_client->dev.of_node, 0);
    if (irqno < 0) {
        pr_info("of_irq_get failed
");
        return -1;
    }
    ret = request_irq(irqno, max30102_irq_handler, IRQF_TRIGGER_FALLING, "pute-max30102-irq", NULL);
    if (ret != 0) {
        pr_info("request irq failed
");
        return -1;
    }

    max30102_fifo_readbytes(0x07, i2cbuf); // 读取FIFO初始数据
    return 0;
}
中断服务函数
static irqreturn_t max30102_irq_handler(int irqno, void *arg) {
    condition = 1; // 标志中断发生
    wake_up_interruptible(&waitqueue_head); // 唤醒等待队列
    return IRQ_HANDLED;
}
read函数实现
static ssize_t max30102_read(struct file *fp, char __user *puser, size_t n, loff_t *off) {
    unsigned char i2cbuf[6] = {0};
    long nret = 0;

    wait_event_interruptible(waitqueue_head, condition); // 阻塞等待中断
    condition = 0; // 重置标志

    max30102_fifo_readbytes(0x07, i2cbuf); // 读取FIFO数据

    nret = copy_to_user(puser, i2cbuf, 6); // 拷贝给用户空间
    if (nret) {
        pr_info("copy_to_user failed
");
        return -1;
    }

    return 6; // 返回读取的数据长度
}

(四)ADC驱动 —— 以MQ-2气体传感器为例

设备树配置
&adc1 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_adc1>;
    num-channels = <2>;
    vref-supply = <&reg_vref_adc>;
    status = "okay";
};

&iomuxc {
    pinctrl_adc1: adc1grp {
        fsl,pins = <
            MX6UL_PAD_GPIO1_IO01__ADC1_CH1 0xb0b0
        >;
    };
};

// 在regulators节点添加基准电压
regulators {
    compatible = "simple-bus";
    #address-cells = <1>;
    #size-cells = <0>;
    reg_vref_adc: regulator@1 {
        compatible = "regulator-fixed";
        reg = <1>;
        regulator-name = "vref-adc";
        regulator-min-microvolt = <3300000>;
        regulator-max-microvolt = <3300000>;
    };
};
应用层程序(adc_app.c)
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <math.h>

typedef struct {
    int raw;
    float act;
} IMX6ULL_ADC;

int adc_read(IMX6ULL_ADC *imx6ulladc) {
    int fd_scale, fd_raw;
    char buf[32];
    int ret;

    fd_scale = open("/sys/bus/iio/devices/iio:device0/in_voltage_scale", O_RDONLY);
    if (fd_scale < 0) {
        perror("open scale failed");
        return -1;
    }

    fd_raw = open("/sys/bus/iio/devices/iio:device0/in_voltage1_raw", O_RDONLY);
    if (fd_raw < 0) {
        perror("open raw failed");
        close(fd_scale);
        return -1;
    }

    ret = read(fd_raw, buf, sizeof(buf));
    if (ret < 0) {
        perror("read raw failed");
        close(fd_scale);
        close(fd_raw);
        return -1;
    }
    sscanf(buf, "%d", &imx6ulladc->raw);

    ret = read(fd_scale, buf, sizeof(buf));
    if (ret < 0) {
        perror("read scale failed");
        close(fd_scale);
        close(fd_raw);
        return -1;
    }
    float scale;
    sscanf(buf, "%f", &scale);
    imx6ulladc->act = (scale * imx6ulladc->raw) / 1000.0f;

    close(fd_scale);
    close(fd_raw);
    return 0;
}

int main(void) {
    IMX6ULL_ADC imx6ulladc;
    int ret;
    float R0 = 6.64; // 校准值
    float Rs = 0;
    float ppmVal = 0;

    while (1) {
        ret = adc_read(&imx6ulladc);
        if (ret == 0) {
            Rs = (5 - imx6ulladc.act) / imx6ulladc.act * 0.5; // 计算Rs
            ppmVal = pow(11.5428 * R0 / Rs, 0.6549f) * 100; // 计算ppm浓度
            printf("ADC value:%d, v%.3fV, ppmVal = %.2f 
", imx6ulladc.raw, imx6ulladc.act, ppmVal);
        }
        sleep(1);
    }

    return 0;
}

理想运行结果

ADC value:1800, v1.449V, ppmVal = 25.00 
ADC value:1900, v1.529V, ppmVal = 20.00 
ADC value:2000, v1.609V, ppmVal = 15.00 
...

(五)摄像头驱动 —— 以OV5640为例

设备树配置
ov5640: ov5640@3c {
    compatible = "ovti,ov5640";
    reg = <0x3c>;
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_csi1 &csi_pwn_rst>;
    clocks = <&clks IMX6UL_CLK_CSI>;
    clock-names = "csi_mclk";
    pwn-gpios = <&gpio1 4 1>;
    rst-gpios = <&gpio1 2 0>;
    csi_id = <0>;
    mclk = <24000000>;
    mclk_source = <0>;
    status = "okay";

    port {
        ov5640_ep: endpoint {
            remote-endpoint = <&csi1_ep>;
        };
    };
};

&csi {
    status = "okay";
    port {
        csi1_ep: endpoint {
            remote-endpoint = <&ov5640_ep>;
        };
    };
};

&iomuxc {
    csi_pwn_rst: csi_pwn_rstgrp {
        fsl,pins = <
            MX6UL_PAD_GPIO1_IO02_GPIO1_IO02 0x10B0
            MX6UL_PAD_GPIO1_IO04_GPIO1_IO04 0x10B0
        >;
    };

    pwm3 {
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_pwm3>;
        clocks = <&clks IMX6UL_CLK_PWM3>;
        status = "disable"; // 禁用冲突的PWM3
    };
};
内核配置(menuconfig)
Device Drivers -> Multimedia support -> V4L platform devices
    <*> MXC Video For Linux Video output
    <*> MXC Video For Linux Video Capture
    <*> OmniVision ov5640 camera support
驱动模块加载
modprobe mx6s_capture
modprobe ov5640_camera
V4L2应用层采集流程

核心步骤

  1. 打开设备:open("/dev/video1", O_RDWR)
  2. 查询能力:ioctl(fd, VIDIOC_QUERYCAP, &cap)
  3. 设置格式:ioctl(fd, VIDIOC_S_FMT, &fmt) (设置分辨率、像素格式)
  4. 申请缓冲区:ioctl(fd, VIDIOC_REQBUFS, &req) (通常4个)
  5. 映射内存:mmap() 将缓冲区映射到用户空间
  6. 缓冲区入队:ioctl(fd, VIDIOC_QBUF, &buf)
  7. 启动采集:ioctl(fd, VIDIOC_STREAMON, &type)
  8. 监听事件:select() 等待帧完成
  9. 缓冲区出队:ioctl(fd, VIDIOC_DQBUF, &buf) 读取数据
  10. 重新入队:ioctl(fd, VIDIOC_QBUF, &buf) 循环采集

图像显示
由于OV5640默认输出YUYV,而LCD屏需RGB888,需在应用层进行转换:

// 示例:YUV422转RGB888
void yuv422_to_rgb888(unsigned char *yuv, unsigned char *rgb, int width, int height) {
    for (int i = 0; i < height; i++) {
        for (int j = 0; j < width; j++) {
            int y = yuv[i * width * 2 + j * 2];      // Y分量
            int u = yuv[i * width * 2 + j * 2 + 1];  // U分量
            int v = yuv[i * width * 2 + j * 2 + 2];  // V分量
            int r = y + 1.402 * (v - 128);           // RGB计算公式
            int g = y - 0.344 * (u - 128) - 0.714 * (v - 128);
            int b = y + 1.772 * (u - 128);
            rgb[(i * width + j) * 3 + 0] = CLAMP(r, 0, 255);
            rgb[(i * width + j) * 3 + 1] = CLAMP(g, 0, 255);
            rgb[(i * width + j) * 3 + 2] = CLAMP(b, 0, 255);
        }
    }
}

(六)串口(UART)驱动 —— 以GPS为例

硬件连接
  • GPS TX → i.MX UART_RX
  • GPS RX → i.MX UART_TX
  • 或使用USB-TTL模块,识别为/dev/ttyUSB0
数据协议
  • 波特率:9600
  • 格式:NMEA 0183,例如:
    $GPGGA,161229.00,3958.12345,N,11620.56789,E,1,08,1.0,50.0,M,0.0,M,,*62
    
应用层程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>

int main() {
    int fd = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY | O_NDELAY);
    if (fd < 0) {
        perror("Open serial port error");
        return -1;
    }

    struct termios options;
    tcgetattr(fd, &options);
    cfsetispeed(&options, B9600); // 设置波特率
    cfsetospeed(&options, B9600);
    options.c_cflag |= (CLOCAL | CREAD); // 启用本地模式和接收
    options.c_cflag &= ~CSIZE; // 清除数据位掩码
    options.c_cflag |= CS8; // 设置8位数据
    options.c_cflag &= ~PARENB; // 无奇偶校验
    options.c_cflag &= ~CSTOPB; // 1位停止位
    options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // 无行规程
    options.c_oflag &= ~OPOST; // 不处理输出
    options.c_cc[VMIN] = 1; // 最少读取1个字符
    options.c_cc[VTIME] = 0; // 无超时
    tcsetattr(fd, TCSANOW, &options);

    char buffer[1024];
    while (1) {
        int len = read(fd, buffer, sizeof(buffer) - 1);
        if (len > 0) {
            buffer[len] = '\0';
            printf("%s", buffer);
        }
    }

    close(fd);
    return 0;
}

四、接口对比总结

特性UART(串口)I²CSPI
线数2(TX/RX)2(SCL/SDA)3–4(SCLK/MOSI/MISO/CS)
速度低(kbps 级)中(100kbps–400kbps)高(MHz 级)
主从点对点多从机(地址区分)多从机(CS 区分)
全双工半双工
典型应用GPS、蓝牙、调试串口光照、温湿度、RTC加速度计、Flash、显示屏

五、系统架构与开发流程

(一)通用开发流程

  1. 阅读手册:明确传感器通信协议、寄存器、时序。
  2. 配置设备树:添加设备节点,指定引脚、地址、频率。
  3. 编写驱动:基于Linux子系统(IIO/I²C/SPI/Platform)。
  4. 应用层测试:通过文件IO或自定义程序读取数据。
  5. 数据解析:转换为物理量(如lux、g、经纬度)。

(二)系统初始化与循环检测

// 系统初始化
system_init();
// 循环采集
while (1) {
    read_hcsr04_distance(); // 读取超声波距离
    read_max30102_heart_rate(); // 读取心率
    read_mq2_gas_concentration(); // 读取气体浓度
    read_gps_position(); // 读取GPS位置
    process_data(); // 数据处理与算法计算
    output_control(); // 输出控制或显示结果
    delay_ms(1000); // 延时1秒
}

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐