背景
上一片讲述了直接在应用程序中使用iic设备文件操作相应的设备,对于一些没有linux驱动开发人员来说容易上手;
linux MCP4728 IIC 多路DAC输出芯片驱动(一)-优快云博客
下一篇将讲述采用iio驱动框架重新开发驱动,直接省去一些系统函数的调用,使用cat\echo等就可以直接操作。
linux MCP4728 IIC 多路DAC输出芯片驱动(三)-优快云博客
本次基于上一次开发,这次将改变实现思路,采用字符驱动的形式,进行开发,以下将详细讲解驱动开发流程;
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输出芯片驱动(一)-优快云博客
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 = ® // 要写入的数据缓冲区
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输出芯片驱动(一)-优快云博客
为了一次就能看出效果,在写之后又读取一次:
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;
}
测试各通道输出正常。