程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)

龙芯2k0300 - 智能车走马观碑组VL53L0X驱动移植

----------------------------------------------------------------------------------------------------------------------------

开发板 :久久派开发板
eMMC8GB
DDR4512MB
u-bootu-boot 2022.04
linux6.12
rootfsbuildroot-2024.08
----------------------------------------------------------------------------------------------------------------------------

在《龙芯2k0300 - 走马观碑组第21届智能汽车竞赛软硬件设计》中我们介绍到我们的开发板使用了1.8TFT显示模块,Vl53l0XST推出的第二代FlightSense技术的飞行时间传感器,与传统的测距传感器不同,它使用时飞行时间(Time-of-FlightTOF)测量原理,可以实现精确、快速的非接触式测量。无论目标颜色和反射率如何,都可以进行距离测量,抗干扰能力更强。

Vl53l0X激光测距模块的原理是利用激光脉冲的飞行时间来测量距离。该模块通过发射一束激光脉冲,并测量该激光脉冲从发射到被接收的时间来计算出物体与传感器之间的距离。

更多相关资料参考:《STM32传感器模块编程实践(九) VL53L0X激光红外测距传感器简介及驱动源码》。

img

Vl53l0X激光测距模块采用I2C通信方式:

img

Vl53l0X激光测距采用I2C通信方式,与龙芯2K0300开发板的连接关系需严格对应,确保I2C通信与GPIO控制正常,连接表如下:

屏幕引脚 功能 连接的龙芯GPIO 说明
VIN 为Vl53l0X提供工作电压 3.3V 接3.3V
GND 电源共地,确保电压稳定性 GND 必须可靠接地,否则可能出现显示异常
SCL I2C 时钟引脚 I2C1_SCL (GPIO50) 提供I2C同步通信时钟
SDA SDA数据引脚 I2C1_SDA(GPIO51) 传输I2C命令与显示数据
INT 传感器测量完成后,会输出一个中断信号 —— 不接也能用,只是不能用中断方式;
内核IIO驱动一般不用它,轮询即可
XS 模块复位引脚
1:复位、关机
0:正常工作
—— 不接也能正常用;
接 GPIO 可以实现软件复位、省电

一、VL53L0X设备驱动

VL53L0X由于采用了I2C通讯协议,所以涉及到了I2C设备驱动。如果对I2C驱动源码移植感兴趣,可参考:

龙邱科技提供了VL53L0X设备驱动实现,位于: i2c_vl53l0x_driver,只不过这个是基于linux 4.19的,如果移植这个改动的相对较多。

此外我们在《龙芯2k0300 - 久久派开发环境搭建及内核升级(下)》驱动移植章节下载的linux-6.9-WuwuSama-99pi.tar.gz中也包含了驱动代码,位于drivers/wuwu_drivers/wuwu_vl53l0x.c文件中。

1.1 内核配置

实际上我们下载的内核linux 6.12已经内置了VL53L0X的驱动,其使用了IIO框架,这里我们就直接使用内核驱动即可,无需从零开发驱动,需开启内核I2CIIO相关配置,并添加VL53L0X驱动代码,实现激光测距。

进入内核配置界面:

zhengyang@ubuntu:~$ cd /opt/2k0300/build-2k0300/workspace/linux-6.12
zhengyang@ubuntu:/opt/2k0300/build-2k0300/workspace/linux-6.12$ source ../set_env.sh && make menuconfig

依次进入以下菜单:

Device Drivers  → 
    I2C support   → 
        I2C Hardware Bus support → 
            <*> Loongson fast speed I2C adapter    # 龙芯 2K0300 I2C控制器驱动
        <*>  I2C device interface    
     [*]  Industrial I/O support  → 
          	Proximity and distance sensors  → 
        		<*> STMicroelectronics VL53L0X ToF ranger sensor (I2C) 

默认会生成配置:

CONFIG_I2C_LSFS=y  # 这个已经配置,不用追加
CONFIG_I2C_CHARDEV=y
CONFIG_IIO=y       # 这个已经配置,不用追加
CONFIG_VL53L0X_I2C=y

我们直接修改arch/loongarch/configs/loongson_2k300_defconfig文件,加入这几个配置。

1.2 VL53L0X驱动

驱动源码位于drivers/iio/proximity/vl53l0x-i2c.cvl53l0x-i2c.c驱动基于内核IIO框架开发,无需修改内核核心代码,只需编译进内核即可。

1.2.1 vl53l0x-i2c.c

drivers/iio/proximity/vl53l0x-i2c.c源码如下:

点击查看详情
// SPDX-License-Identifier: GPL-2.0
/*
 * Support for ST VL53L0X FlightSense ToF Ranging Sensor on a i2c bus.
 *
 * Copyright (C) 2016 STMicroelectronics Imaging Division.
 * Copyright (C) 2018 Song Qiang <songqiang1304521@gmail.com>
 * Copyright (C) 2020 Ivan Drobyshevskyi <drobyshevskyi@gmail.com>
 *
 * Datasheet available at
 * <https://www.st.com/resource/en/datasheet/vl53l0x.pdf>
 *
 * Default 7-bit i2c slave address 0x29.
 *
 * TODO: FIFO buffer, continuous mode, range selection, sensor ID check.
 */

#include <linux/delay.h>
#include <linux/gpio/consumer.h>
#include <linux/i2c.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/module.h>

#include <linux/iio/iio.h>

#define VL_REG_SYSRANGE_START				0x00

#define VL_REG_SYSRANGE_MODE_MASK			GENMASK(3, 0)
#define VL_REG_SYSRANGE_MODE_SINGLESHOT			0x00
#define VL_REG_SYSRANGE_MODE_START_STOP			BIT(0)
#define VL_REG_SYSRANGE_MODE_BACKTOBACK			BIT(1)
#define VL_REG_SYSRANGE_MODE_TIMED			BIT(2)
#define VL_REG_SYSRANGE_MODE_HISTOGRAM			BIT(3)

#define VL_REG_SYSTEM_INTERRUPT_CONFIG_GPIO		0x0A
#define VL_REG_SYSTEM_INTERRUPT_GPIO_NEW_SAMPLE_READY	BIT(2)

#define VL_REG_SYSTEM_INTERRUPT_CLEAR			0x0B

#define VL_REG_RESULT_INT_STATUS			0x13
#define VL_REG_RESULT_RANGE_STATUS			0x14
#define VL_REG_RESULT_RANGE_STATUS_COMPLETE		BIT(0)

struct vl53l0x_data {
	struct i2c_client *client;
	struct completion completion;
	struct regulator *vdd_supply;
	struct gpio_desc *reset_gpio;
};

static irqreturn_t vl53l0x_handle_irq(int irq, void *priv)
{
	struct iio_dev *indio_dev = priv;
	struct vl53l0x_data *data = iio_priv(indio_dev);

	complete(&data->completion);

	return IRQ_HANDLED;
}

static int vl53l0x_configure_irq(struct i2c_client *client,
				 struct iio_dev *indio_dev)
{
	int irq_flags = irq_get_trigger_type(client->irq);
	struct vl53l0x_data *data = iio_priv(indio_dev);
	int ret;

	if (!irq_flags)
		irq_flags = IRQF_TRIGGER_FALLING;

	ret = devm_request_irq(&client->dev, client->irq, vl53l0x_handle_irq,
			irq_flags, indio_dev->name, indio_dev);
	if (ret) {
		dev_err(&client->dev, "devm_request_irq error: %d\n", ret);
		return ret;
	}

	ret = i2c_smbus_write_byte_data(data->client,
			VL_REG_SYSTEM_INTERRUPT_CONFIG_GPIO,
			VL_REG_SYSTEM_INTERRUPT_GPIO_NEW_SAMPLE_READY);
	if (ret < 0)
		dev_err(&client->dev, "failed to configure IRQ: %d\n", ret);

	return ret;
}

static void vl53l0x_clear_irq(struct vl53l0x_data *data)
{
	struct device *dev = &data->client->dev;
	int ret;

	ret = i2c_smbus_write_byte_data(data->client,
					VL_REG_SYSTEM_INTERRUPT_CLEAR, 1);
	if (ret < 0)
		dev_err(dev, "failed to clear error irq: %d\n", ret);

	ret = i2c_smbus_write_byte_data(data->client,
					VL_REG_SYSTEM_INTERRUPT_CLEAR, 0);
	if (ret < 0)
		dev_err(dev, "failed to clear range irq: %d\n", ret);

	ret = i2c_smbus_read_byte_data(data->client, VL_REG_RESULT_INT_STATUS);
	if (ret < 0 || ret & 0x07)
		dev_err(dev, "failed to clear irq: %d\n", ret);
}

static int vl53l0x_read_proximity(struct vl53l0x_data *data,
				  const struct iio_chan_spec *chan,
				  int *val)
{
	struct i2c_client *client = data->client;
	u16 tries = 20;
	u8 buffer[12];
	int ret;
	unsigned long time_left;

	ret = i2c_smbus_write_byte_data(client, VL_REG_SYSRANGE_START, 1);
	if (ret < 0)
		return ret;

	if (data->client->irq) {
		reinit_completion(&data->completion);

		time_left = wait_for_completion_timeout(&data->completion, HZ/10);
		if (time_left == 0)
			return -ETIMEDOUT;

		vl53l0x_clear_irq(data);
	} else {
		do {
			ret = i2c_smbus_read_byte_data(client,
					       VL_REG_RESULT_RANGE_STATUS);
			if (ret < 0)
				return ret;

			if (ret & VL_REG_RESULT_RANGE_STATUS_COMPLETE)
				break;

			usleep_range(1000, 5000);
		} while (--tries);
		if (!tries)
			return -ETIMEDOUT;
	}

	ret = i2c_smbus_read_i2c_block_data(client, VL_REG_RESULT_RANGE_STATUS,
					    12, buffer);
	if (ret < 0)
		return ret;
	else if (ret != 12)
		return -EREMOTEIO;

	/* Values should be between 30~1200 in millimeters. */
	*val = (buffer[10] << 8) + buffer[11];

	return 0;
}

static const struct iio_chan_spec vl53l0x_channels[] = {
	{
		.type = IIO_DISTANCE,
		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
				      BIT(IIO_CHAN_INFO_SCALE),
	},
};

static int vl53l0x_read_raw(struct iio_dev *indio_dev,
			    const struct iio_chan_spec *chan,
			    int *val, int *val2, long mask)
{
	struct vl53l0x_data *data = iio_priv(indio_dev);
	int ret;

	if (chan->type != IIO_DISTANCE)
		return -EINVAL;

	switch (mask) {
	case IIO_CHAN_INFO_RAW:
		ret = vl53l0x_read_proximity(data, chan, val);
		if (ret < 0)
			return ret;

		return IIO_VAL_INT;
	case IIO_CHAN_INFO_SCALE:
		*val = 0;
		*val2 = 1000;

		return IIO_VAL_INT_PLUS_MICRO;
	default:
		return -EINVAL;
	}
}

static const struct iio_info vl53l0x_info = {
	.read_raw = vl53l0x_read_raw,
};

static void vl53l0x_power_off(void *_data)
{
	struct vl53l0x_data *data = _data;

	gpiod_set_value_cansleep(data->reset_gpio, 1);

	regulator_disable(data->vdd_supply);
}

static int vl53l0x_power_on(struct vl53l0x_data *data)
{
	int ret;

	ret = regulator_enable(data->vdd_supply);
	if (ret)
		return ret;

	gpiod_set_value_cansleep(data->reset_gpio, 0);

	usleep_range(3200, 5000);

	return 0;
}

static int vl53l0x_probe(struct i2c_client *client)
{
	struct vl53l0x_data *data;
	struct iio_dev *indio_dev;
	int error;

	indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data));
	if (!indio_dev)
		return -ENOMEM;

	data = iio_priv(indio_dev);
	data->client = client;
	i2c_set_clientdata(client, indio_dev);

	if (!i2c_check_functionality(client->adapter,
				     I2C_FUNC_SMBUS_READ_I2C_BLOCK |
				     I2C_FUNC_SMBUS_BYTE_DATA))
		return -EOPNOTSUPP;

	data->vdd_supply = devm_regulator_get(&client->dev, "vdd");
	if (IS_ERR(data->vdd_supply))
		return dev_err_probe(&client->dev, PTR_ERR(data->vdd_supply),
				     "Unable to get VDD regulator\n");

	data->reset_gpio = devm_gpiod_get_optional(&client->dev, "reset", GPIOD_OUT_HIGH);
	if (IS_ERR(data->reset_gpio))
		return dev_err_probe(&client->dev, PTR_ERR(data->reset_gpio),
				     "Cannot get reset GPIO\n");

	error = vl53l0x_power_on(data);
	if (error)
		return dev_err_probe(&client->dev, error,
				     "Failed to power on the chip\n");

	error = devm_add_action_or_reset(&client->dev, vl53l0x_power_off, data);
	if (error)
		return dev_err_probe(&client->dev, error,
				     "Failed to install poweroff action\n");

	indio_dev->name = "vl53l0x";
	indio_dev->info = &vl53l0x_info;
	indio_dev->channels = vl53l0x_channels;
	indio_dev->num_channels = ARRAY_SIZE(vl53l0x_channels);
	indio_dev->modes = INDIO_DIRECT_MODE;

	/* usage of interrupt is optional */
	if (client->irq) {
		int ret;

		init_completion(&data->completion);

		ret = vl53l0x_configure_irq(client, indio_dev);
		if (ret)
			return ret;
	}

	return devm_iio_device_register(&client->dev, indio_dev);
}

static const struct i2c_device_id vl53l0x_id[] = {
	{ "vl53l0x" },
	{ }
};
MODULE_DEVICE_TABLE(i2c, vl53l0x_id);

static const struct of_device_id st_vl53l0x_dt_match[] = {
	{ .compatible = "st,vl53l0x", },
	{ }
};
MODULE_DEVICE_TABLE(of, st_vl53l0x_dt_match);

static struct i2c_driver vl53l0x_driver = {
	.driver = {
		.name = "vl53l0x-i2c",
		.of_match_table = st_vl53l0x_dt_match,
	},
	.probe = vl53l0x_probe,
	.id_table = vl53l0x_id,
};
module_i2c_driver(vl53l0x_driver);

MODULE_AUTHOR("Song Qiang <songqiang1304521@gmail.com>");
MODULE_DESCRIPTION("ST vl53l0x ToF ranging sensor driver");
MODULE_LICENSE("GPL v2");
1.2.2 IIO框架介绍

工业I/OIIO)是专用于模数转换器(ADC)和数模转换器(DAC)的内核子系统。随着越来越多的具有不同代码实现的传感器分散在内核源上,收集它们变得必要,这就是IIO框架以通用的方式所做的事情。

加速度计,陀螺仪,电流/电压测量芯片,光传感器,压力传感器等都属于IIO系列器件。

IIO框架如下所示:

IIO子系统通过cdev向文件系统注册字符设备,提供应用层访问IIO子系统的接口,也使用sysfs框架向应用层提供在Linux文件系统中修改访问的接口,前者在用户驱动使用triggerbuffer模式时使用,后者则在direct模型下使用。

下面是用户空间与IIO驱动程序交互的两种方式:

  • /dev/iio:deviceX: 表示导出设备事件和数据缓冲区的字符设备;
  • /sys/bus/iio/devices/iio:deviceX/:表示传感器及其通道;

IIO子系统又coretriggerbufferevent几个模块组成,用户驱动根据自身需求,使用IIO子系统提供的注册接口,向子系统注册对应的channel信息以及访问的接口,这些信息与接口将有子系统在用户访问时分配访问和回调。

接下来我们简要分析一下vl53l0x-i2c.c驱动源码。

1.2.2.1 结构体及变量

定义结构体以及初始化变量:

  • vl53l0x_data:存放传感器私有数据;
  • vl53l0x_channels:通道定义;
  • vl53l0x_infoIIO设备操作集。

源码如下:

struct vl53l0x_data {
	struct i2c_client *client;         // I2C设备
	struct completion completion;      // completion内部有个计数器  0:未完成,需要等待  1:已完成,可以继续
	struct regulator *vdd_supply;      // 电源
	struct gpio_desc *reset_gpio;      // 复位引脚对应的GPIO描述符
};

static const struct iio_chan_spec vl53l0x_channels[] = {
    // 测距
	{
        // 这个通道的类型是距离(distance),在/sys/... 目录下生成in_distance_raw、in_distance_scale
		.type = IIO_DISTANCE,        
        // 这个通道支持哪些读取操,支持读原始数据(距离值)、读比例值(单位换算,毫米转米)
		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |  
				      BIT(IIO_CHAN_INFO_SCALE),
	},
};

//  IIO设备操作集
static const struct iio_info vl53l0x_info = {
	.read_raw = vl53l0x_read_raw,    // 读数据
};
1.2.2.2 读取测距数据

IIO接口函数read_raw,用于读取原始数据(寄存器值);

static int vl53l0x_read_proximity(struct vl53l0x_data *data,
				  const struct iio_chan_spec *chan,
				  int *val)
{
	struct i2c_client *client = data->client;
	u16 tries = 20;
	u8 buffer[12];
	int ret;
	unsigned long time_left;

    // 写入一个字节启动一次测距
	ret = i2c_smbus_write_byte_data(client, VL_REG_SYSRANGE_START, 1);
	if (ret < 0)
		return ret;

    // vl53l0x@29设备节点如果配置了中断会进入,即硬件接了INT中断脚
	if (data->client->irq) {
     	// 强制把它重置为0, 未完成,需要等待
		reinit_completion(&data->completion);
		
        // 当前进程/线程会在这里休眠,直到以下两种情况之一发生
        // 1. 有人调用 complete(&completion) → 唤醒
        // 2. 时间到(100ms) → 自动醒来
		time_left = wait_for_completion_timeout(&data->completion, HZ/10);
		if (time_left == 0)
			return -ETIMEDOUT;

        // 读完数据,清中断	
		vl53l0x_clear_irq(data);
	} else {
        // 不用中断(轮询),每隔一段时间查询一次
		do {
            // 从传感器的0x14寄存器,读1个字节回来,问传感器:“你测完距离了吗?”
			ret = i2c_smbus_read_byte_data(client,
					       VL_REG_RESULT_RANGE_STATUS);
			if (ret < 0)
				return ret;

            // 测距完成
			if (ret & VL_REG_RESULT_RANGE_STATUS_COMPLETE)
				break;

            // 休眠
			usleep_range(1000, 5000);
		} while (--tries);
		if (!tries)
			return -ETIMEDOUT;
	}

    // 从寄存器0x14开始,一口气读12个连续字节;把传感器的所有测距结果一次性读回来
	ret = i2c_smbus_read_i2c_block_data(client, VL_REG_RESULT_RANGE_STATUS,
					    12, buffer);
	if (ret < 0)
		return ret;
	else if (ret != 12)
		return -EREMOTEIO;

	/* Values should be between 30~1200 in millimeters. */
	*val = (buffer[10] << 8) + buffer[11];

	return 0;
}

static int vl53l0x_read_raw(struct iio_dev *indio_dev,
			    const struct iio_chan_spec *chan,
			    int *val, int *val2, long mask)
{
    // 获取私有数据
	struct vl53l0x_data *data = iio_priv(indio_dev);
	int ret;

	if (chan->type != IIO_DISTANCE)
		return -EINVAL;

	switch (mask) {
    // 原始数据
	case IIO_CHAN_INFO_RAW:
        // 调用真正的I2C测距函数 val返回距离值,单位:毫米(mm)   
		ret = vl53l0x_read_proximity(data, chan, val);
		if (ret < 0)
			return ret;

		return IIO_VAL_INT;
    // 告诉系统单位换算比例 毫米 → 米
	case IIO_CHAN_INFO_SCALE:
		*val = 0;
		*val2 = 1000;

		return IIO_VAL_INT_PLUS_MICRO;
	default:
		return -EINVAL;
	}
}

这里读取传感器数据有两种方式:

  • 中断方式:在vl53l0x@29设备节点配置中断引脚INT信息,开启中断方式,传感器主动通知测距完成;
  • 轮训方式:不断轮训查询测距结果。

在中断方式中,当测距完成了需要在中断处理函数中触发complete(&completion)

static irqreturn_t vl53l0x_handle_irq(int irq, void *priv)
{
    // 获取私有数据
	struct iio_dev *indio_dev = priv;
	struct vl53l0x_data *data = iio_priv(indio_dev);

    // 标记测距完成
	complete(&data->completion);

	return IRQ_HANDLED;
}

如果使用了中断,就需要向内核申请中断,并注册中断处理函数vl53l0x_handle_irq

static int vl53l0x_configure_irq(struct i2c_client *client,
				 struct iio_dev *indio_dev)
{
    // 从设备树里读取中断触发类型
	int irq_flags = irq_get_trigger_type(client->irq);
    // 获取私有数据
	struct vl53l0x_data *data = iio_priv(indio_dev);
	int ret;

	if (!irq_flags)
		irq_flags = IRQF_TRIGGER_FALLING;

    // 向内核申请中断号,注册中断处理函数 最后一个参数:传给中断函数的参数
	ret = devm_request_irq(&client->dev, client->irq, vl53l0x_handle_irq,
			irq_flags, indio_dev->name, indio_dev);
	if (ret) {
		dev_err(&client->dev, "devm_request_irq error: %d\n", ret);
		return ret;
	}

    // 往VL53L0X的寄存器写配置,当新的测距数据准备好时,触发 INT中断
	ret = i2c_smbus_write_byte_data(data->client,
			VL_REG_SYSTEM_INTERRUPT_CONFIG_GPIO,
			VL_REG_SYSTEM_INTERRUPT_GPIO_NEW_SAMPLE_READY);
	if (ret < 0)
		dev_err(&client->dev, "failed to configure IRQ: %d\n", ret);

	return ret;
}
1.2.2.3 probe函数

当设备树与驱动成功匹配后,内核自动调用vl53l0x_probe函数,完成传感器的初始化、上电、注册,让驱动可以工作。

static int vl53l0x_probe(struct i2c_client *client)
{
	struct vl53l0x_data *data;
	struct iio_dev *indio_dev;
	int error;

    // 1. 向内核申请一块内存,用于创建IIO设备
	indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data));
	if (!indio_dev)
		return -ENOMEM;

    // 2. 初始化私有数据
	data = iio_priv(indio_dev);
	data->client = client;
	i2c_set_clientdata(client, indio_dev);

    // 检查I2C控制器是否支持:单字节读写、块数据读写
	if (!i2c_check_functionality(client->adapter,
				     I2C_FUNC_SMBUS_READ_I2C_BLOCK |
				     I2C_FUNC_SMBUS_BYTE_DATA))
		return -EOPNOTSUPP;

    // 向内核获取传感器供电,对应设备树vdd-supply
    // 如果设备树没写vdd-supply,驱动会自动获取一个虚拟固定电源,不会报错。
	data->vdd_supply = devm_regulator_get(&client->dev, "vdd");
	if (IS_ERR(data->vdd_supply))
		return dev_err_probe(&client->dev, PTR_ERR(data->vdd_supply),
				     "Unable to get VDD regulator\n");

    // 获取复位引脚,没有也不报错
	data->reset_gpio = devm_gpiod_get_optional(&client->dev, "reset", GPIOD_OUT_HIGH);
	if (IS_ERR(data->reset_gpio))
		return dev_err_probe(&client->dev, PTR_ERR(data->reset_gpio),
				     "Cannot get reset GPIO\n");

    // 使能电源,释放复位,等待3_5ms让传感器启动
	error = vl53l0x_power_on(data);
	if (error)
		return dev_err_probe(&client->dev, error,
				     "Failed to power on the chip\n");

    // 驱动卸载时自动调用 vl53l0x_power_off,使传感器关机
	error = devm_add_action_or_reset(&client->dev, vl53l0x_power_off, data);
	if (error)
		return dev_err_probe(&client->dev, error,
				     "Failed to install poweroff action\n");

    // 3. 初始化IIO设备核心参数
	indio_dev->name = "vl53l0x";
	indio_dev->info = &vl53l0x_info;          // 操作函数
	indio_dev->channels = vl53l0x_channels;   // 传感器通道(距离)
	indio_dev->num_channels = ARRAY_SIZE(vl53l0x_channels);
    // 直接读取模式(轮询)
	indio_dev->modes = INDIO_DIRECT_MODE;

	/* usage of interrupt is optional,设备树配置了INT引脚  */
	if (client->irq) {
		int ret;

        // 初始化等待结构体
		init_completion(&data->completion);

        // 注册中断	
		ret = vl53l0x_configure_irq(client, indio_dev);
		if (ret)
			return ret;
	}
    
	// 4. 注册IIO设备
	return devm_iio_device_register(&client->dev, indio_dev);
}

整个probe函数的 真实工作流程:

① 分配IIO设备;

② 检查I2C功能

③ 使能电源,释放复位,等待3~5ms让传感器启动;

④ 配置IIO设备信息;

⑤ 如果有中断,注册中断;

⑥ 注册到IIO框架 → 生成sysfs节点。

1.3 新增设备节点

这里我们将VL53L0X测距模块接到i2c1接口,因此需要适当调整设备树。

1.3.1 i2c1

i2c1节点定义在arch/loongarch/boot/dts/loongson-2k0300.dtsi

i2c1: i2c1@0x16109000 {
        compatible = "loongson,lsfs-i2c";
        reg = <0 0x16109000 0 0x1000>;
        #address-cells = <1>;
        #size-cells = <0>;
        interrupt-parent = <&liointc0>;
        interrupts = <4 IRQ_TYPE_LEVEL_HIGH>;
        pinctrl-0 = <&i2c1_pins>;
        pinctrl-names = "default";
        clock-frequency = <200000000>;
        status = "disabled";
};

其中:

  • i2c1::标签,用于在设备树中其他地方通过&i2c1引用这个节点,方便添加属性或修改状态;
  • i2c1@0x16109000:节点名,格式为 设备名@寄存器基地址。这里i2c1是功能名,0x16109000是该I2C控制器的物理寄存器基地址。
  • compatible:驱动匹配字符串。内核通过该属性寻找能驱动此设备的驱动程序;
  • reg:寄存器地址范围。格式为 <地址高位 地址低位 长度高位 长度低位>,由于龙芯采用64位寻址,这里用两个32位数表示64位地址;
  • #address-cells#size-cells:定义该节点下子节点(即挂载的I2C从设备)的地址和长度格式。
    • #address-cells = <1>:子节点的 reg 属性中,地址部分占用132位单元。对于I2C设备,这通常是7位从设备地址;
    • #size-cells = <0>:子节点的 reg 属性中没有长度字段(因为I2C设备地址不涉及地址范围);
  • interrupt-parent:指定该设备的中断路由到哪个中断控制器。&liointc0 是龙芯2K0300内部的中断控制器节点(即龙芯I/O中断控制器)的标签;
  • interrupts:描述中断线的具体信息;
    • <4>:硬件中断编号(对应I2C1控制器在中断控制器中的编号);
    • IRQ_TYPE_LEVEL_HIGH:中断触发类型,这里定义为高电平触发。该宏在 <dt-bindings/interrupt-controller/irq.h> 中定义。
  • pinctrl-0:指定设备使用的第一组引脚配置,&i2c1_pins 是另一个设备树节点的标签,该节点描述了I2C1SCLSDA引脚应复用为I2C功能,并可能包含上拉等电气属性;
  • pinctrl-names:为引脚的配置状态命名,与 pinctrl-0 对应。"default" 是默认状态,驱动在probe时会自动应用 pinctrl-0 的配置,引脚配置设置为&i2c1_pins
  • clock-frequencyI2C控制器的输入时钟频率(单位Hz);
  • status:设备状态,此时处于禁用状态。

更多有关设备树相关的内容可以参考:《linux设备树-基础介绍》。

1.3.2 vl53l0x@29
zhengyang@ubuntu:~$ cd /opt/2k0300/build-2k0300/workspace/linux-6.12
zhengyang@ubuntu:/opt/2k0300/build-2k0300/workspace/linux-6.12$ vim arch/loongarch/boot/dts/ls2k300_99pi.dtsi

修改arch/loongarch/boot/dts/ls2k300_99pi.dtsi

// 在根节点或合适位置(通常是 &soc 内)引用并启用 I2C1
&i2c1 {
    status = "okay";

    // VL53L0X 传感器
    vl53l0x@29 {
        compatible = "st,vl53l0x";
        reg = <0x29>;
        status = "okay";
    };
};

通过 &i2c1 引用这个节点,将 status 改为 "okay",同时添加了I2C从设备节点。

其中vl53l0x@29设备节点下的compatible名称要和vl53l0x-i2c.c驱动中的vl53l0x_driver.driver.of_match_table里的名称匹配。

static const struct i2c_device_id vl53l0x_id[] = {
        { "vl53l0x" },
        { }
};
MODULE_DEVICE_TABLE(i2c, vl53l0x_id);

static const struct of_device_id st_vl53l0x_dt_match[] = {
        { .compatible = "st,vl53l0x", },
        { }
};
MODULE_DEVICE_TABLE(of, st_vl53l0x_dt_match);

static struct i2c_driver vl53l0x_driver = {
        .driver = {
                .name = "vl53l0x-i2c",
                .of_match_table = st_vl53l0x_dt_match,
        },
        .probe = vl53l0x_probe,
        .id_table = vl53l0x_id,
};

这样在适配器注册的时候会遍历i2c设备树节点下的所有子设备节点,从而实现注册该控制器下的所有I2C从设备,具体可以参考《I2C适配器注册》源码部分。

1.3.3 i2c1_pins

i2c1_pins定义在arch/loongarch/boot/dts/loongson-2k0300.dtsi

pinmux: pinmux@16000490 {
	......
	i2c1_pins: pinmux_G50_G51_as_i2c1 {
    	    pinctrl-single,bits = <0xc 0x000000f0 0x000000f0>;
	};
	......
}

这个设备树节点是使用pinctrl-single驱动来配置引脚复用功能的典型写法,这行代码会在寄存器0x1600049c7:4位写入 0xf,而其他位保持不变,用于将芯片的GPIO50GPIO51这两个引脚设置为I2C1功能。

注:pinctrl-single 是一个通用的引脚控制驱动,适用于那些引脚复用寄存器是“单寄存器位域”的芯片。当SoC没有提供复杂的pinctrl框架时,可以直接用这种方式“裸写”寄存器。

二、应用程序

接下来我们在example目录下创建子目录vl53l0x_app

zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/example$ mkdir vl53l0x_app

目录结构如下:

zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/example/vl53l0x_app$ tree .
.
├── main.c
└── Makefile

2.1 main.c

内核自带驱动把VL53L0X注册成IIO传感器,应用层直接读文件即可获取距离。下面编写一个简单的测试应用程序,用于读取距离并打印;

/*
 * 测试 VL53L0X 驱动
 */

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h> // 1. 新增:需要包含 atoi 函数的头文件

/* 设备节点路径 */
#define DEVICE_PATH             "/sys/bus/iio/devices/iio:device1/in_distance_raw"

int main(int argc, char **argv)
{
    int fd;
    char buf[16];
    int distance; // 2. 新增:用于存储转换后的整数距离

    /* 1. 打开设备 */
    fd = open(DEVICE_PATH, O_RDONLY);
    if (fd < 0) {
        perror("open");
        return -1;
    }
    printf("Opened %s successfully.\n", DEVICE_PATH);

    /* 2. 循环读取距离值 */
    while (1) {
        lseek(fd, 0, SEEK_SET);
        read(fd, buf, 16);

        // 3. 修改:将字符串转换为整数
        distance = atoi(buf);

        // 4. 修改:使用 %d 格式打印整数,输出将非常干净
        printf("Distance: %d mm\n", distance);

        /* 控制打印频率 */
        usleep(200000);  // 200ms
    }

    close(fd);
    return 0;
}

2.2 Makefile

all:
	loongarch64-linux-gnu-gcc -o main main.c
clean:
	rm -rf *.o main

2.3 编译应用程序

zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/example/vl53l0x_app$ make
loongarch64-linux-gnu-gcc -o main main.c
zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/example/vl53l0x_app$ ll

-rwxrwxr-x 1 zhengyang zhengyang 20680  3月 26 16:00 main*
-rw-rw-r-- 1 zhengyang zhengyang   740  3月 26 16:00 main.c
-rw-rw-r-- 1 zhengyang zhengyang    71  3月 24 14:24 Makefile

三、测试

3.1 烧录设备树

3.1.1 编译设备树

如果需要单独编译设备树,在linux内核根目录执行如下命令:

zhengyang@ubuntu:~$ cd /opt/2k0300/build-2k0300/workspace/linux-6.12
zhengyang@ubuntu:/opt/2k0300/build-2k0300/workspace/linux-6.12$ source ../set_env.sh && make dtbs V=1
3.1.2 更新设备树

将设备树拷贝到久久派的/opt目录;

zhengyang@ubuntu:/opt/2k0300/build-2k0300/workspace/linux-6.12$ scp arch/loongarch/boot/dts/ls2k300_99pi_wifi.dtb root@172.23.34.188:/opt

在久久派使用dd命令烧写设备树到SPI Nor Flashdtb分区;

[root@LS-GD opt]# dd if=/opt/ls2k300_99pi_wifi.dtb of=/dev/mtdblock3 bs=1
22124+0 records in
22124+0 records out
22124 bytes (22 kB, 22 KiB) copied, 0.500342 s, 44.2 kB/s

3.2 安装驱动

由于我们在内核配置环节将驱动配置到内核中了,因此需要重新编译内核并烧录内核。

3.2.1 编译内核

ubuntu宿主接重新编译内核:

zhengyang@ubuntu:/opt/2k0300/build-2k0300/workspace/linux-6.12$ source ../set_env.sh && make uImage -j$(nproc)
3.2.2 烧录内核

久久派烧录内核,即将编译生成的uImage(内核镜像)拷贝到/boot目录;

[root@LS-GD ~]# scp zhengyang@172.23.34.187:/opt/2k0300/build-2k0300/workspace/linux-6.12/arch/loongarch/boot/uImage /boot/
[root@LS-GD ~]# reboot
3.2.3 查看i2c信息

我们将龙邱Vl53l0X激光测距模块接入开发板的I2C1接口处,进入I2C总线的控制目录:

#  I2C 总线的控制目录
[root@LS-GD ~]# cd /sys/bus/i2c/

[root@LS-GD i2c]# ls -l
drwxr-xr-x  2 root root     0 Jul 24 20:49 devices
drwxr-xr-x 10 root root     0 Jul 24 20:49 drivers
-rw-r--r--  1 root root 16384 Jul 24 20:51 drivers_autoprobe
--w-------  1 root root 16384 Jul 24 20:51 drivers_probe
--w-------  1 root root 16384 Jul 24 20:49 uevent

其中:

  • devices:列出了所有连接在I2C总线上的设备;
  • drivers:列出了所有已注册的I2C驱动程序;
  • drivers_autoprobedrivers_probe:这些是控制文件,用于手动触发驱动探测或设置自动探测。

进入设备列表目录,查看具体挂载了哪些硬件:

[root@LS-GD i2c]# cd devices/

[root@LS-GD devices]# ls -l
# i2c-1:代表I2C控制器 1-0029:代表连接在 i2c-1 总线上的设备。
lrwxrwxrwx 1 root root 0 Jul 24 20:49 1-0029 -> ../../../devices/platform/2k300-soc/16109000.i2c1/i2c-1/1-0029  
lrwxrwxrwx 1 root root 0 Jul 24 20:49 i2c-1 -> ../../../devices/platform/2k300-soc/16109000.i2c1/i2c-1

进入了这个地址为0x29的设备目录:

[root@LS-GD devices]# cd 1-0029

[root@LS-GD 1-0029]# ls -l
lrwxrwxrwx 1 root root     0 Jul 24 20:52 driver -> ../../../../../../bus/i2c/drivers/vl53l0x-i2c
drwxr-xr-x 3 root root     0 Jul 24 20:49 iio:device1
-r--r--r-- 1 root root 16384 Jul 24 20:49 modalias
-r--r--r-- 1 root root 16384 Jul 24 20:49 name
lrwxrwxrwx 1 root root     0 Jul 24 20:52 of_node -> ../../../../../../firmware/devicetree/base/2k300-soc/i2c1@0x16109000/vl53l0x@29
drwxr-xr-x 2 root root     0 Jul 24 20:52 power
lrwxrwxrwx 1 root root     0 Jul 24 20:49 subsystem -> ../../../../../../bus/i2c
-rw-r--r-- 1 root root 16384 Jul 24 20:49 uevent

其中:

  • driver:绑定的驱动:内核已经设备和vl53l0x-i2c驱动绑定在了一起;
  • iio:device1:生成的输入设备节点;
  • of_node:设备树节点匹配。
3.2.4 查看iio信息

查看IIO设备相关文件:

[root@LS-GD i2c]# ls  /sys/bus/iio/devices/iio\:device1/ -l
total 0
-rw-r--r-- 1 root root 16384 Jul 24 20:56 in_distance_raw
-rw-r--r-- 1 root root 16384 Jul 24 20:56 in_distance_scale
-r--r--r-- 1 root root 16384 Jul 24 20:49 name
lrwxrwxrwx 1 root root     0 Jul 24 20:56 of_node -> ../../../../../../../firmware/devicetree/base/2k300-soc/i2c1@0x16109000/vl53l0x@29
drwxr-xr-x 2 root root     0 Jul 24 20:56 power
lrwxrwxrwx 1 root root     0 Jul 24 20:49 subsystem -> ../../../../../../../bus/iio
-rw-r--r-- 1 root root 16384 Jul 24 20:49 uevent
-r--r--r-- 1 root root 16384 Jul 24 20:56 waiting_for_supplier

查看传感器名称:

[root@LS-GD i2c]# cat  /sys/bus/iio/devices/iio\:device1/name
vl53l0x

in_distance_raw存放的是传感器的原始数据;

[root@LS-GD i2c]# cat /sys/bus/iio/devices/iio\:device1/in_distance_raw
109

3.3 应用程序测试

我们将龙邱Vl53l0X激光测距模块接入开发板的I2C1接口处,同时用手挡住遮挡激光测距模块。

在久久派开发板执行如下命令:

[root@LS-GD opt]# scp zhengyang@172.23.34.187:/opt/2k0300/loongson_2k300_lib/example/vl53l0x_app/main ./
zhengyang@172.23.34.187's password:
main                                                                          100%   20KB   1.4MB/s   00:00
[root@LS-GD opt]# ./main
Distance: 151 mm
Distance: 154 mm
Distance: 159 mm
Distance: 163 mm
Distance: 167 mm
Distance: 173 mm
Distance: 174 mm
Distance: 175 mm
Distance: 173 mm
Distance: 175 mm
Distance: 177 mm
Distance: 177 mm
Distance: 181 mm
Distance: 175 mm
Distance: 169 mm
Distance: 161 mm
Distance: 146 mm

观察输出,每隔约200ms打印一次距离(单位:毫米),按 Ctrl+C 退出。

四、代码下载

loongson_2k300_lib

参考文章

[1] LS2K0300久久派_V1.1板卡使用手册v1.2_20240705.pdf

[2] [Linux IIO驱动

[3] LinuxIIO子系统的使用与源码分析

[4] LinuxIIO子系统驱动

[5] linux IIO驱动框架开发流程说明

posted @ 2026-03-25 10:22  大奥特曼打小怪兽  阅读(117)  评论(0)    收藏  举报
如果有任何技术小问题,欢迎大家交流沟通,共同进步