linux MCP4728 IIC 多路DAC输出芯片驱动(二)

背景

上一片讲述了直接在应用程序中使用iic设备文件操作相应的设备,对于一些没有linux驱动开发人员来说容易上手;

linux MCP4728 IIC 多路DAC输出芯片驱动(一)-CSDN博客

下一篇将讲述采用iio驱动框架重新开发驱动,直接省去一些系统函数的调用,使用cat\echo等就可以直接操作。

linux MCP4728 IIC 多路DAC输出芯片驱动(三)-CSDN博客

本次基于上一次开发,这次将改变实现思路,采用字符驱动的形式,进行开发,以下将详细讲解驱动开发流程;

1.设备树配置

设备数配置基本比较简单,本次使用zynq ps端的iic控制器,地址为:0x60; 集体如下:

&i2c1{
    mcp4728@60 {
        compatible = "microchip,mcp4728";
        status = "okay";
        reg = <0x60>;
    };
};

2.iic设备驱动框架

2.1 struct i2c_driver结构定义

/* i2c_driver结构体变量 */
static struct i2c_driver mcp4728_driver = {
	.driver = {
		.name			= "mcp4728",
		.of_match_table	= mcp4728_of_match,
	},
	.probe		= mcp4728_probe,		// probe函数
	.remove		= mcp4728_remove,		// remove函数
};

2.2 struct of_device_id匹配列表

/* 匹配列表 */
static const struct of_device_id mcp4728_of_match[] = {
	{ .compatible = "microchip,mcp4728" },
	{ /* Sentinel */ }
};

2.3 struct file_operations 定义

/*
 * file_operations结构体变量
 */
static const struct file_operations mcp4728_ops = {
	.owner = THIS_MODULE,
	.open = mcp4728_open,
	.read = mcp4728_read,
	.write = mcp4728_write,
	.release = mcp4728_release,
};

2.4 mcp4728_remove函数

static int mcp4728_remove(struct i2c_client *client)
{
	struct mcp4728_dev *mcp4728 = i2c_get_clientdata(client);

	/* 注销设备 */
	device_destroy(mcp4728->class, mcp4728->devid);

	/* 注销类 */
	class_destroy(mcp4728->class);

	/* 删除cdev */
	cdev_del(&mcp4728->cdev);

	/* 注销设备号 */
	unregister_chrdev_region(mcp4728->devid, 1);

	return 0;
}

2.5 mcp4728_probe函数

static int mcp4728_probe(struct i2c_client *client,
			const struct i2c_device_id *id)
{
	int ret;

	/* 初始化mcp4728 */
	mcp4728.client = client;

	/* 申请设备号 */
	ret = alloc_chrdev_region(&mcp4728.devid, 0, 1, DEVICE_NAME);
	if (ret)
		return ret;

	/* 初始化字符设备cdev */
	mcp4728.cdev.owner = THIS_MODULE;
	cdev_init(&mcp4728.cdev, &mcp4728_ops);

	/* 添加cdev */
	ret = cdev_add(&mcp4728.cdev, mcp4728.devid, 1);
	if (ret)
		goto out1;

	/* 创建类class */
	mcp4728.class = class_create(THIS_MODULE, DEVICE_NAME);
	if (IS_ERR(mcp4728.class)) {
		ret = PTR_ERR(mcp4728.class);
		goto out2;
	}

	/* 创建设备 */
	mcp4728.device = device_create(mcp4728.class, &client->dev,
				mcp4728.devid, NULL, DEVICE_NAME);
	if (IS_ERR(mcp4728.device)) {
		ret = PTR_ERR(mcp4728.device);
		goto out3;
	}

	i2c_set_clientdata(client, &mcp4728);


	return 0;

out3:
	class_destroy(mcp4728.class);

out2:
	cdev_del(&mcp4728.cdev);

out1:
	unregister_chrdev_region(mcp4728.devid, 1);

	return ret;
}

3.测试框架基本功能

使用:sudo insmod mcp4728.ko

输入密码;然后查看dev下的设备文件,

已生产设备文件,因此驱动基本框架已完成。

3. 测试驱动中读写

3.1 读4通道输出测试

实现读功能需要了解iic的通信,本次参考

linux MCP4728 IIC 多路DAC输出芯片驱动(一)-CSDN博客

3.1.1 iic读函数封装
/*
 * @description			: 从mcp4728设备中读取多个连续的寄存器数据
 * @param – dev			: mcp4728设备
 * @param – reg			: 要读取的寄存器首地址
 * @param – buf			: 数据存放缓存区地址
 * @param – len			: 读取的字节长度
 * @return				: 成功返回0,失败返回一个负数
 */
static int mcp4728_read_reg(struct mcp4728_dev *dev, u8 reg, u8 *buf, u8 len)
{
	struct i2c_client *client = dev->client;
	struct i2c_msg msg[2];
	int ret;
    pr_info("client->name:%s %d %d %s iqr:%d addr:0x%02x \n",  client->name,  client->adapter->class, client->adapter->nr, client->adapter->name, client->irq, client->addr);

	/* msg[0]: 发送消息 */
	msg[0].addr = client->addr;		// mcp4728从机地址
	msg[0].flags = !I2C_M_RD;	// 标记为写数据
	msg[0].buf = &reg;				// 要写入的数据缓冲区
	msg[0].len = 1;					// 要写入的数据长度

	/* msg[1]: 接收消息 */
	msg[1].addr = client->addr;		// mcp4728从机地址
	msg[1].flags = I2C_M_RD;// 标记为读数据
	msg[1].buf = buf;				// 存放读数据的缓冲区
	msg[1].len = len;				// 读取的字节长度

	ret = i2c_transfer(client->adapter, msg, 2);
	if (2 != ret) {
		dev_err(&client->dev, "%s: error: reg=0x%x, len=0x%x\n",
					__func__, reg, len);
		return -EIO;
	}

	return 0;
}
3.1.2 直接在prob函数中读

代码片段如下:

3.1.3 直接在prob函数中写

直接写入0x07  0x77 具体输出电压多少请参考上一篇中的计算公式。

linux MCP4728 IIC 多路DAC输出芯片驱动(一)-CSDN博客

为了一次就能看出效果,在写之后又读取一次:

3.1.4 测试在prob中函数中读写

卸载之前测试模板加载的驱动,:rmmod mcp4728.ko

重新加载新编译的驱动:sudo insmod mcp4728.ko

执行:dmesg;

查看输出信息:

4.完善驱动的open read write等函数

4.1数据结构定义:

4.2 open函数

/*
 * @description			: 打开设备
 * @param – inode		: 传递给驱动的inode
 * @param - filp		: 设备文件,file结构体有个叫做private_data的成员变量
 * 						  一般在open的时候将private_data指向设备结构体。
 * @return				: 0 成功;其他 失败
 */
static int mcp4728_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &mcp4728;
	return 0;
}

4.3 read函数

/*
 * @description			: 从设备读取数据 
 * @param – filp		: 要打开的设备文件(文件描述符)
 * @param - buf			: 返回给用户空间的数据缓冲区
 * @param - cnt			: 要读取的数据长度
 * @param – off			: 相对于文件首地址的偏移
 * @return				: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t mcp4728_read(struct file *filp, char __user *buf,
			size_t cnt, loff_t *off)
{
    struct mcp4728_dev *dev = filp->private_data;
    struct i2c_client *client = dev->client;
	struct mcp4728_value value = {0};
    u8 readbuf[32] = {0};
	int ret, i;
    
    //解析用户读取的通道数量
	ret = copy_from_user(&value, buf, cnt);	
	if(0 > ret)
		return -EFAULT;
    
    //判断通道数量是否有效
    if((value.channels <= 0) || (value.channels > OUT_CHANNEL_CNT_MAX))
    {
        dev_err(&client->dev, "%s: error: Invalid channnelCnt %d\n",
					__func__, value.channels);
		return -1;
    }
    
    //读取多路信息
    for(i = 0; i < value.channels; i++)
    {
        if(0 != mcp4728_read_reg(dev, gchannelAddr[i], readbuf, 3))
        {
            dev_err(&client->dev, "mcp4728_read_reg\n");
            return -1;
        } 
        
        //计算电压值
        value.hvOut[i] = readbuf[1]*256 + readbuf[2];
        pr_info("====>read channel:%d value:0x%04x \n", i, value.hvOut[i]);        

    }
    
	return copy_to_user(buf, &value, sizeof(struct mcp4728_value));
}

4.4 write函数

/*
 * @description			: 向设备写数据 
 * @param – filp		: 设备文件,表示打开的文件描述符
 * @param - buf			: 要写给设备写入的数据
 * @param - cnt			: 要写入的数据长度
 * @param - offt		: 相对于文件首地址的偏移
 * @return				: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t mcp4728_write(struct file *filp, const char __user *buf,
			size_t cnt, loff_t *offt)
{
    struct mcp4728_dev *dev = filp->private_data;
    struct i2c_client *client = dev->client;
	struct mcp4728_value value = {0};
    u8 sedBuf[8] = {0};
	int ret, i;

    // 得到应用层传递过来的数据
	ret = copy_from_user(&value, buf, cnt);	
	if(0 > ret)
		return -EFAULT;
    
    //判断通道数量是否有效
    if((value.channels <= 0) || (value.channels > OUT_CHANNEL_CNT_MAX))
    {
        dev_err(&client->dev, "%s: error: Invalid channnelCnt %d\n",
					__func__, value.channels);
		return -1;
    }

    //循环写多路
    for(i = 0; i < value.channels; i++)
    {
       //解析数据
        sedBuf[0] = (value.hvOut[i] >> 8) % 0xff;
        sedBuf[1] = (value.hvOut[i]) % 0xff;
        
        pr_info("====>write channel:%d value:0x%04x \n", i, value.hvOut[i]);
        if(0 != mcp4728_write_reg(dev, gchannelAddr[i], sedBuf, 2))
        {
            dev_err(&client->dev, "%s:  write channel:%d is failed \n", __func__, i);
            return -1;
        }
    }

	return sizeof(struct mcp4728_value);
}

5.编译报错

5.1问题分析

将电压和adc值的转换放到驱动中结果编译报错

原因是不支持浮点数计算,决定将转换放到应用程序中,调整数据结构以及读写函数。

5.2修改后数据结构

5.3修改后写函数

6.编写测试程序

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

struct mcp4728_value {
	unsigned int channels;
    unsigned short hvOut[4];	// 输出高压A
};

/*
 * @description		: main主程序
 * @param - argc	: argv数组元素个数
 * @param - argv	: 具体参数
 * @return			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd, ret;
	struct mcp4728_value value = {0};

	/* 打开设备 */
	fd = open(argv[1], O_RDWR);
	if(0 > fd) {
		printf("Error: file %s open failed!\n", argv[1]);
		return -1;
	}

    float out = 1.1;//初始电压1.1v
    while(1)
    {
        //读取4个通道
        value.channels = 4;
        ret = read(fd, &value, sizeof(struct mcp4728_value));
		if (0 > ret) {
			printf("Error: file %s read failed!\n", argv[1]);
		} 
       
       int i = 0;
       for(i = 0; i < value.channels; i++)
       {
           float readV = value.hvOut[i] /(4096.00 / 3.3);
           printf("read channel:%d out:%f \n", i, readV);
       }
       
       
        /* 将时间写入RTC */
        printf("write out:%f \n", out);
        float outV = (float)((4096.00 / 3.3) * out);
        for(i = 0; i < value.channels; i++)
        {
            value.hvOut[i] = outV;
        }

		ret = write(fd, &value, sizeof(struct mcp4728_value));
		if (sizeof(struct mcp4728_value) > ret) {
			printf("Error: file %s write failed!\n", argv[1]);
		} 
        
        out += 0.5;
        if(out >= 3.0)
        {
          out = 0.0;  
        }

        sleep(2);
    }
    
	/* 关闭文件 */
	close(fd);
	return ret;
}

7.测试结果

7.1读写驱动报错:

原因:驱动中read\write函数中判断边界值有问题:

7.2修改驱动代码

修改后代码段:

7.3复测

至此已经完成了驱动开发和软件中的使用,后续将使用iio驱动模型再次实现该功能。

8.补充

在测试过程中发现每次只有一路能够正常输出,其他的都没法输出,进过问题排查才发现,我们可能希望通过单次写入的方式分别控制各路DAC输出,但是这里经过测试发现,如果我们写入某一DAC后,立刻再写入,也就是说想要连续多次进行“单次写入”功能是会失败的,这个原因就是EEPROM的RDY状态,如果想要实现执行多次“单次写入”操作,我们需要检测RDY引脚状态,或者我们做好延时。


8.1使用延时

加延时后能成功,但是应用程序急需要阻塞一会,不是想要的效果。

8.2.使用gpio监测EEPROM的RDY状态

该方案不再细说,

8.3使用写多个通道指令

8.3.1指令格式

8.3.2代码片段
    //一次写入多路
    int index = 0;
    for(i = 0; i < value.channels; i++)
    {
       //解析数据
        sedBuf[index++] = (value.hvOut[i] >> 8) % 0xff;
        sedBuf[index++] = (value.hvOut[i]) % 0xff;
        pr_info("====>write channel:%d sedBuf:0x%02x%02x %d \n", i, sedBuf[i*2 + 0],sedBuf[i*2 + 0 + 1], value.hvOut[i]);
        
    }
    
    if(0 != mcp4728_write_reg(dev, 0x50, sedBuf, value.channels * 2))
    {
        dev_err(&client->dev, "%s:  write channel:%d is failed \n", __func__, i);
        return -1;
    }

测试各通道输出正常。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值