Day73 嵌入式传感器技术全栈开发
day73 嵌入式传感器技术全栈开发
本笔记系统性整理了嵌入式开发中常用传感器(GPIO、I2C、SPI、ADC、UART)的分类、工作原理、驱动开发框架及应用层实现,涵盖从硬件连接、设备树配置、内核驱动编写到用户空间程序调用的完整流程。内容基于Linux内核子系统(字符设备、杂项设备、I2C、SPI、IIO),并结合OV5640摄像头、HCSR04超声波、MAX30102心率血氧、MQ系列气体传感器等实例,提供可直接运行的代码及详细注释。
一、传感器分类与核心参数
(一)按连接方式分类
| 连接方式 | 传感器型号 | 功能 |
|---|---|---|
| GPIO(单总线) | dht11 | 温湿度检测 |
| ds18b20 | 温度检测 | |
| hcsr-04 | 超声波测距 | |
| I2C | LM75 | 温度检测 |
| BH1750 | 光照强度检测 | |
| MPU6050 | 六轴姿态检测 | |
| MAX30100 | 心率、血氧浓度检测 | |
| SPI | ADXL345 | 三轴加速度检测 |
| ADC | MQ系列(MQ-2、MQ-135、MQ-7等) | 气体浓度检测(如MQ-2检测烟雾浓度) |
| UART | GPS传感器 | 定位(精度、维度、海拔)检测 |
| GY-35 | 红外测距检测 |
(二)关键传感器指标参数
| 传感器 | 功能 | 连接方式 | 量程(测量范围) | 精度 | 分辨率 | 工作电压 |
|---|---|---|---|---|---|---|
| dht11 | 温湿度 | GPIO(单总线) | 温度:0-50℃;湿度:20-90%RH | 温度:±2℃;湿度:±5%RH | 1 | 3.3V-5.5V |
| dht22 | 温湿度 | GPIO(单总线) | 温度:-40-80℃;湿度:0-99.9%RH | 温度:±0.5℃;湿度:±2%RH | 0.1℃/0.1%RH | 3.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 | 超声波测距 | GPIO | 2cm-450cm | 0.3cm | 1 | 5V |
| 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字节之和)
时序:
- 主机起始信号:拉低DATA引脚 ≥18ms,再拉高 ≥20-40us。
- 从机应答信号:拉低 ≥80us,再拉高 ≥80us。
- 数据传输:
- bit “0”:拉低50us,再拉高26-28us。
- bit “1”:拉低50us,再拉高70us。
- 结束信号:主机将DATA拉高。
(二)HC-SR04超声波传感器
实物图:标注“4.0000H00”和“HC-SR04”。
引脚:
- VCC:5V电源
- GND:接地
- Trig:触发端(控制)
- Echo:回波端(接收)
工作原理/时序:
- 向Trig发送≥10us的高电平脉冲。
- 传感器内部发射8个40kHz超声波脉冲。
- Echo引脚输出高电平,持续时间为超声波往返时间。
- 计算距离公式:
距离 = (Echo高电平时间 × 340m/s) / 2
IMX6ULL原理图连接:
- VCC → P4 46号引脚
- GND → P4 47号引脚
- Trig → SNVS_TAMPER4 (P4 1号引脚)
- Echo → SNVS_TAMPER5 (P4 6号引脚)
三、Linux驱动框架详解
(一)驱动类型概述
- 字符型设备驱动:
- 以字节为单位读写,无缓存。
- 适用于鼠标、键盘、串口等。
- 核心结构体:
cdev,file_operations。
- 块设备驱动:
- 以固定块大小(如512k)读写,带缓存。
- 适用于硬盘、Flash等存储设备。
- 网络设备驱动:
- 基于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 0x0pinctrl_hcsr04: hcsr04_fsl,pins {MX6UL_PAD_SNVS_TAMPER4_GPIO5_IO04 0x10B0MX6UL_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;// 解析设备树获取GPIOgpio_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_operationsstatic 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); // 延时10usgpio_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 = <®_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; // 计算RsppmVal = 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 0x10B0MX6UL_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应用层采集流程
核心步骤:
- 打开设备:
open("/dev/video1", O_RDWR) - 查询能力:
ioctl(fd, VIDIOC_QUERYCAP, &cap) - 设置格式:
ioctl(fd, VIDIOC_S_FMT, &fmt)(设置分辨率、像素格式) - 申请缓冲区:
ioctl(fd, VIDIOC_REQBUFS, &req)(通常4个) - 映射内存:
mmap()将缓冲区映射到用户空间 - 缓冲区入队:
ioctl(fd, VIDIOC_QBUF, &buf) - 启动采集:
ioctl(fd, VIDIOC_STREAMON, &type) - 监听事件:
select()等待帧完成 - 缓冲区出队:
ioctl(fd, VIDIOC_DQBUF, &buf)读取数据 - 重新入队:
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²C | SPI |
|---|---|---|---|
| 线数 | 2(TX/RX) | 2(SCL/SDA) | 3–4(SCLK/MOSI/MISO/CS) |
| 速度 | 低(kbps 级) | 中(100kbps–400kbps) | 高(MHz 级) |
| 主从 | 点对点 | 多从机(地址区分) | 多从机(CS 区分) |
| 全双工 | 是 | 半双工 | 是 |
| 典型应用 | GPS、蓝牙、调试串口 | 光照、温湿度、RTC | 加速度计、Flash、显示屏 |
五、系统架构与开发流程
(一)通用开发流程
- 阅读手册:明确传感器通信协议、寄存器、时序。
- 配置设备树:添加设备节点,指定引脚、地址、频率。
- 编写驱动:基于Linux子系统(IIO/I²C/SPI/Platform)。
- 应用层测试:通过文件IO或自定义程序读取数据。
- 数据解析:转换为物理量(如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秒
}
