首页 » 技术分享 » AM335X——hwmon和input子系统

AM335X——hwmon和input子系统

 

CSDN仅用于增加百度收录权重,排版未优化,日常不维护。请访问:www.hceng.cn 查看、评论。
本博文对应地址: https://hceng.cn/2019/01/18/AM335X——hwmon和input子系统/#more

记录两个SPI设备分别采用hwmon子系统和input子系统。

刚开始学Linux驱动的时候,就看了input子系统,现在都忘得差不多了,不过回忆起来也还快,这里再记录一下。

为什么要用各种子系统框架,就目前的理解,一是为了向应用层提供统一的接口,二是简化了编写驱动的流程
各种子系统它们是通过一层一层的函数传递与封装,实现了设备驱动的注册,定义了file_operations结构体里面的各种函数操作,不需要在单独的设备驱动代码中进行注册、定义,直接调用相应的的子系统即可。

1. hwmon子系统简介

hwmon即硬件监控(Hardware monitor),它是用于检测设备状态的一类传感器设备接口,比如CPU温度、风扇转速、模数转换等。

Hwmon子系统的核心代码是drivers/hwmon/hwmon.c
通过同路径下的Kconfig文件,可以得知它在make menuconfig中的配置名字为Hardware Monitoring support
通过hwmon.c中的EXPORT_SYMBOL_GPL()符号,可知对外提供如下几组接口函数:

hwmon_device_register_with_groups() / hwmon_device_register()
hwmon_device_unregister()

devm_hwmon_device_register_with_groups()
devm_hwmon_device_unregister()

1.1 AD7705简介

AD7705是十六位分辨率的A/D转换器,2个通道全差分模拟输入。
内部有8个寄存器,常用的就Communication RegisterSetup registerClock registerData register
在每次做任何操作前,都要写Communication Register来设置即将操作的是哪一个寄存器、是读还是写操作、操作哪一个通道。
其操作流程如下:

  1. 拉低复位引脚,硬件复位;
  1. 在至少32个时钟周期里连续发送高脉冲,以同步时钟;
  2. 配置AD7705时钟(时钟源、分频系数等);
  3. 自校准,并等待就绪引脚拉低;
  4. 从数据寄存器里读取数据;

1.2 完整代码及效果

  • 设备树:
    {% codeblock lang:dts [am335x-evm.dts] %}
    spidev@0 {
    compatible = “titan,ad7705”;
    spi-max-frequency = <2500000>;
    reg = <2>;
    };
    {% endcodeblock %}

  • 驱动代码:
    {% codeblock lang:c [ad7705_drv.c] %}
    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <linux/delay.h>
    #include <linux/gpio.h>
    #include <linux/spi/spi.h>
    #include <linux/hwmon.h>
    #include <linux/hwmon-sysfs.h>

#define CHANNEL_NUM (2) //AD7705 channel number
#define DRDY_PIN (12) //AD7705 DRDY Pin(GPIO0_12)
#define RESET_PIN (13) //AD7705 RESET Pin(GPIO0_13)

//Communication Register
enum
{
REG_COMM = (0 << 4), //Communication Register RS2:RS1:RS0 = [0:0:0]
REG_SETUP = (1 << 4), //Setup register RS2:RS1:RS0 = [0:0:1]
REG_CLOCK = (2 << 4), //Clock register RS2:RS1:RS0 = [0:1:0]
REG_DATA = (3 << 4), //Data register RS2:RS1:RS0 = [0:1:1]
REG_TEST = (4 << 4), //Test register RS2:RS1:RS0 = [1:0:0]
REG_RESE = (5 << 4), //No operation RS2:RS1:RS0 = [1:0:1]
REG_OFFSET = (6 << 4), //Offset register RS2:RS1:RS0 = [1:1:0]
REG_GAIN = (7 << 4), //Gain register RS2:RS1:RS0 = [1:1:1]

CMD_WRITE        = (0 << 3), //write operation
CMD_READ         = (1 << 3), //read  operation

CH_1             = 0,        //Register Pair 0 (AIN1+  AIN1-)
CH_2             = 1,        //Register Pair 1 (AIN2+  AIN2-)
CH_3             = 2,        //Register Pair 0 (AIN1-  AIN1-)
CH_4             = 3         //Register Pair 2 (AIN1-  AIN2-)

};

//Setup register
enum
{
MD_NORMAL = (0 << 6), //Normal Mode.
MD_CAL_SELF = (1 << 6), //Self-Calibration
MD_CAL_ZERO = (2 << 6), //Zero-Scale System Calibration
MD_CAL_FULL = (3 << 6), //Full-Scale System Calibration

GAIN_1           = (0 << 3), //Gain 1  
GAIN_2           = (1 << 3), //Gain 2  
GAIN_4           = (2 << 3), //Gain 4  
GAIN_8           = (3 << 3), //Gain 8  
GAIN_16          = (4 << 3), //Gain 16 
GAIN_32          = (5 << 3), //Gain 32 
GAIN_64          = (6 << 3), //Gain 64 
GAIN_128         = (7 << 3), //Gain 128

BIPOLAR          = (0 << 2), //Bipolar Operation  
UNIPOLAR         = (1 << 2), //Unipolar Operation

BUF_NO           = (0 << 1), //Buffer Control Off
BUF_EN           = (1 << 1), //Buffer Control On

FSYNC_0          = (0 << 0), //Filter Synchronization Normal
FSYNC_1          = (1 << 0)  //Filter Synchronization Disable

};

//Clock register
enum
{
CLKDIS_0 = (0 << 4), //Master Clock Enable (Use crystal clock source)
CLKDIS_1 = (1 << 4), //Master Clock Disable(Use an external clock source)

CLKDIV_0         = (0 << 3), //Clock Divider 0
CLKDIV_1         = (1 << 3), //Clock Divider 2 (4.9152Mhz/2=2.4576Mhz)

CLK_0            = (0 << 2), //Clock Bit(If master clock 1MHz(CLKDIV = 0) or 2MHz(CLKDIV = 1)) 
CLK_1            = (1 << 2), //Clock Bit(If master clock 2.4576MHz(CLKDIV = 0) or 4.9152MHz(CLKDIV = 1)) 
//If Clock Bit = 0
UPDATE_20        = (0 << 0), //Filter Selection Bits
UPDATE_25        = (1 << 0),
UPDATE_100       = (2 << 0),
UPDATE_200       = (3 << 0),
//If Clock Bit = 1      
UPDATE_50        = (0 << 0), //Filter Selection Bits
UPDATE_60        = (1 << 0),
UPDATE_250       = (2 << 0),
UPDATE_500       = (3 << 0)

};

struct ad7705 {
struct device *hwmon_dev;
struct mutex lock;
};

//Reset
static void ad7705_reset(void)
{
gpio_direction_output(RESET_PIN, 1);
msleep(1);
gpio_direction_output(RESET_PIN, 0);
msleep(2);
gpio_direction_output(RESET_PIN, 1);
msleep(1);
}

//Synchronous SPI timing
static void ad7705_sync_spi(struct spi_device *spi)
{
u8 tx_buf[6]; //Write logic “1” to DIN for at least 32 clocks
memset(tx_buf, 0xFF, sizeof(tx_buf));
spi_write(spi, tx_buf, sizeof(tx_buf));

msleep(1);

}

//Waiting for DRDY pin signal
static int ad7705_wait_DRDY(void)
{
int i = 0;
int time_cnt = 500*1000;

for (i=0; i<time_cnt; i++)
{
    if (0 == gpio_get_value(DRDY_PIN))
        break;
    udelay(1);
}

if (i >= time_cnt)
    return -1; 
else
    return 0;

}

//Self-Calibration
static void ad7705_calib_self(struct spi_device *spi, u8 channel)
{
u8 tx_buf[2] = {0};

tx_buf[0] = REG_SETUP | CMD_WRITE | channel;
tx_buf[1] = MD_CAL_SELF | GAIN_1 | UNIPOLAR | BUF_EN | FSYNC_0;
spi_write(spi, tx_buf, sizeof(tx_buf));

ad7705_wait_DRDY(); //Waiting for the internal operation to complete, the time is long, about 180ms

msleep(50);

}

//Initialize the specified channel
static void ad7705_config_channel(struct spi_device *spi, u8 channel)
{
u8 tx_buf[2] = {0};

tx_buf[0] = REG_CLOCK | CMD_WRITE | channel;
tx_buf[1] = CLKDIS_0 | CLKDIV_1 | CLK_1 | UPDATE_50;
spi_write(spi, tx_buf, sizeof(tx_buf));

ad7705_calib_self(spi, channel); 

}

//Reset and initialize the specified channel
static void ad7705_reset_and_reconfig(struct spi_device *spi)
{
ad7705_reset();

msleep(5);
ad7705_sync_spi(spi);
msleep(5);

ad7705_config_channel(spi, CH_1);
ad7705_config_channel(spi, CH_2);

}

//Read the specified channel value
static int ad7705_read_channel(struct device *dev, u8 channel, u16 *data)
{
struct spi_device *spi = to_spi_device(dev);
struct ad7705 *adc = spi_get_drvdata(spi);
int ret = -1;
u16 value = 0;
u8 tx_buf[1] = {0};
u8 rx_buf[2] = {0};

if (mutex_lock_interruptible(&adc->lock))
    return -ERESTARTSYS;

if (ad7705_wait_DRDY() < 0)
{
    printk(KERN_ERR "[%s] ad7705_wait_DRDY() time out.\n", __FUNCTION__);
    goto fail;
}

tx_buf[0] = REG_DATA | CMD_READ | channel;
ret = spi_write_then_read(spi, tx_buf, sizeof(tx_buf), rx_buf, sizeof(rx_buf));
//printk("channel:%d rx_buf[0]=%d  rx_buf[1]=%d \n", channel, rx_buf[0], rx_buf[1]);
if (0 > ret)
{
    printk(KERN_ERR "[%s] ad7705_read_channel() fail. ret=%d\n", __FUNCTION__, ret);
    goto fail;
}
value = (rx_buf[0] << 8) + rx_buf[1];

#if 0
if (0xFFFF == value)
{
ret = -1;
goto fail;
}
#endif
*data = value;

fail:
mutex_unlock(&adc->lock);

return ret;

}

//sysfs hook function
static ssize_t ad7705_get_sensor_value(struct device *dev, struct device_attribute *devattr, char *buf)
{
struct spi_device *spi = to_spi_device(dev);
struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr);
u16 ad = 0;
int i, ret = -1;
int vol1, vol2;

//After switching channels, the value of the other channel is read for the first time.
for (i=0; i<2; i++) 
{
    ret = ad7705_read_channel(dev, attr->index, &ad);
        continue;
    if (0 > ret)
    {
        ad7705_reset_and_reconfig(spi);
        return ret;
    }
}

#if 0
ret = sprintf(buf, “%u\n”, ad);
#else
vol1 = ad5/65535; //Voltage integral part
vol2 = (ad
51000/65535) - (vol11000);//Voltage fraction part
ret = sprintf(buf, “vol = %d.%dV\n”,vol1, vol2);
#endif

return ret;

}

static struct sensor_device_attribute ad_input[] = {
SENSOR_ATTR(ad7705_ch1, S_IRUGO, ad7705_get_sensor_value, NULL, CH_1),
SENSOR_ATTR(ad7705_ch2, S_IRUGO, ad7705_get_sensor_value, NULL, CH_2),
};

static int ad7705_probe(struct spi_device *spi)
{
struct ad7705 *adc = NULL;
int i, status;

adc = kzalloc(sizeof *adc, GFP_KERNEL);
if (!adc)
    return -ENOMEM;

mutex_init(&adc->lock);
mutex_lock(&adc->lock);

spi_set_drvdata(spi, adc);
for (i=0; i<CHANNEL_NUM; i++)
{
    status = device_create_file(&spi->dev, &ad_input[i].dev_attr);
    if (status)
    {
        dev_err(&spi->dev, "device_create_file() failed.\n");
        goto fail_crete_file;
    }
}

adc->hwmon_dev = hwmon_device_register(&spi->dev);
if (IS_ERR(adc->hwmon_dev))
{
    dev_err(&spi->dev, "hwmon_device_register() fail.\n");
    status = PTR_ERR(adc->hwmon_dev);
    goto fail_crete_file;
}

status = gpio_request(DRDY_PIN, "ad7705_drdy");   //ad7705 DRDY Pin
if (status)
{
    dev_err(&spi->dev, "gpio_request(AD705_DRDY_PIN) fail.\n");
    goto fail_device_register;
}
gpio_direction_input(DRDY_PIN);

status = gpio_request(RESET_PIN, "ad7705_reset");  //ad7705 RESET Pin
if (status)
{
    dev_err(&spi->dev, "gpio_request(RESET_PIN) fail.\n");
    goto fail_request_drdy_pin;
}
gpio_direction_output(RESET_PIN, 1);

ad7705_reset_and_reconfig(spi);

mutex_unlock(&adc->lock);
return 0;

fail_request_drdy_pin:
gpio_free(DRDY_PIN);
fail_device_register:
hwmon_device_unregister(adc->hwmon_dev);
fail_crete_file:
for (i–; i>=0; i–)
device_remove_file(&spi->dev, &ad_input[i].dev_attr);

spi_set_drvdata(spi, NULL);
mutex_unlock(&adc->lock);
kfree(adc);

return status;

}

static int ad7705_remove(struct spi_device *spi)
{
int i;
struct ad7705 *adc = spi_get_drvdata(spi);

mutex_lock(&adc->lock);

gpio_free(DRDY_PIN);
gpio_free(RESET_PIN);
hwmon_device_unregister(adc->hwmon_dev);
for (i=0; i<CHANNEL_NUM; i++)
    device_remove_file(&spi->dev, &ad_input[i].dev_attr);
spi_set_drvdata(spi, NULL);

mutex_unlock(&adc->lock);

kfree(adc);

return 0;

}

static const struct of_device_id of_match_spi[] = {
{ .compatible = “titan,ad7705”, .data = NULL },
{ /* sentinel */ }
};

static struct spi_driver ad7705_driver = {
.driver = {
.name = “ad7705”,
.owner = THIS_MODULE,
.of_match_table = of_match_spi,
},
.probe = ad7705_probe,
.remove = ad7705_remove,
};

module_spi_driver(ad7705_driver);

MODULE_LICENSE(“GPL”);
MODULE_AUTHOR(“hceng huangcheng.job@foxmail.com”);
MODULE_DESCRIPTION(“TI am335x board spi device: ad7705 driver.”);
MODULE_VERSION(“v1.0”);
{% endcodeblock %}

  • 测试代码:
    因为是子系统的关系,可以在应用层使用cat /sys/class/hwmon/hwmon0/device/ad7705_ch1直接得到结果。
    这里再使用该节点,写了个应用程序,以便进行连续访问:
    {% codeblock lang:c [ad7705_app.c] %}
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <fcntl.h>

void read_channel(char *dev_file_path)
{
int fd = 0;
int ret = 0;
unsigned char buff[128] = {0};

fd = open(dev_file_path, O_RDONLY);
if (-1 == fd)
{
    printf("Can't open device file fail\n");
    return ;
}

memset(buff, 0, 128);
ret = read(fd, buff, 128);
if (0 > ret)
{
    printf("Can't read data\n");
}

printf("%s", buff);

close(fd);

}

int main(void)
{
char ch1_path[] = {"/sys/class/hwmon/hwmon0/device/ad7705_ch1"};
char ch2_path[] = {"/sys/class/hwmon/hwmon0/device/ad7705_ch2"};
//char ch1_path[] = {"/sys/bus/spi/drivers/ad7705/spi1.2/ad7705_ch1"};
//char ch2_path[] = {"/sys/bus/spi/drivers/ad7705/spi1.2/ad7705_ch2"};

while (1)
{
    read_channel(ch1_path);
    usleep(1000*1000);

    read_channel(ch2_path);
    usleep(1000*1000);
}

}
{% endcodeblock %}

值得注意的是,通过read()返回的数据不再是整型数据,而是字符串,且返回的字符串样式,在驱动里定义。

  • 效果:
cat /sys/class/hwmon/hwmon0/device/ad7705_ch1
vol = 2.1V

cat /sys/class/hwmon/hwmon0/device/ad7705_ch2
vol = 0.0V


./ad7705_app
vol = 2.1V
vol = 0.0V

1.3 详细分析

1.3.1 驱动与设备树匹配

ad7705_drv.cam335x-evm.dts各自中的compatible属性名字相同时,即调用probe()函数,进入一切的开端。

1.3.2 设置结构体

这里定义了一个ad7705的结构体,包含设备指针和互斥锁。
{% codeblock lang:c %}
struct ad7705 {
struct device *hwmon_dev;
struct mutex lock;
};
{% endcodeblock %}
probe()里定义和分配该结构体:
{% codeblock lang:c %}
struct ad7705 *adc = NULL;

adc = kzalloc(sizeof *adc, GFP_KERNEL);
if (!adc)
    return -ENOMEM;

{% endcodeblock %}
补充下内核中内存申请的相关知识:
内核中常用的内存申请函数有:kmalloc()kzalloc()vmalloc()

  • void *kmalloc(size_t size, gfp_t flags); void kfree(const void *objp);
  • kmalloc()申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因为存在较简单的转换关系,所以对申请的内存大小有限制,不能超过128KB。
  • 常用的flags:
      GFP_ATOMIC —— 分配内存的过程是一个原子过程,分配内存的过程不会被(高优先级进程或中断)打断;
      GFP_KERNEL —— 正常分配内存;
      GFP_DMA —— 给 DMA 控制器分配内存,需要使用该标志(DMA要求分配虚拟地址和物理地址连续;
  • void *kzalloc(size_t size, gfp_t flags); void kfree(const void *objp);
  • kzalloc()就是调用的kmalloc(),多加了个__GFP_ZERO标志,即分配内存的时候还会将其清零;
  • 常用的flags:
      同kmalloc()
  • void *vmalloc(unsigned long size); void vfree(const void *addr);
  • vmalloc()会在虚拟内存空间给出一块连续的内存区,但这片连续的虚拟内存在物理内存中并不一定连续。由于vmalloc()没有保证申请到的是连续的物理内存,因此对申请的内存大小没有限制,如果需要申请较大的内存空间就可以用此函数了。vmalloc()vfree()可以睡眠,因此不能从中断上下文调用。

1.3.3 设置互斥锁

为了并发控制,加入互斥锁独占资源。
我好像写驱动都没加锁的习惯,以后改正。
互斥锁的使用比较简单,先初始化,再加锁,执行要做的内容,最后解锁。
{% codeblock lang:c %}
mutex_init(&adc->lock);
mutex_lock(&adc->lock);
……
mutex_unlock(&adc->lock);
{% endcodeblock %}

1.3.4 设置私有变量

将定义的ad7705结构体adc,保存到spi_device:
{% codeblock lang:c %}
spi_set_drvdata(spi, adc);
{% endcodeblock %}
后面其它函数就可以通过device得到spi_device,再提取到私有数据:
{% codeblock lang:c %}
struct spi_device *spi = to_spi_device(dev);
struct ad7705 *adc = spi_get_drvdata(spi);
{% endcodeblock %}

1.3.5 创建sysfs属性文件

首先解释一下sysfs
sysfs是Linux所提供的一种虚拟档案系统;
在设备模型中,sysfs文件系统用来表示设备的结构,将设备的层次结构形象的反应到用户空间中,从而可以通过修改sysfs中的文件属性来修改设备的属性值;
sysfs被挂载到根目录下的/sys文件夹下。

使用函数device_create_file()将会调用到sysfs_create_file(),将在/sys/class/hwmon/下创建文件夹,并按照hwmon0hwmon1hwmon2等顺序编号

第二个参数是struct device_attribute结构体,通过定义的sensor_device_attribute结构体成员传给它。
{% codeblock lang:c %}
static struct sensor_device_attribute ad_input[] = {
SENSOR_ATTR(ad7705_ch1, S_IRUGO, ad7705_get_sensor_value, NULL, CH_1),
SENSOR_ATTR(ad7705_ch2, S_IRUGO, ad7705_get_sensor_value, NULL, CH_2),
};
{% endcodeblock %}
参数含义分别是节点名字、节点访问权限(S_IRUGO:用户、组、其它成员都可读)、读函数(cat命令时将调用)、写函数(echo命令时将调用)、索引。

1.3.6 实现device_attribute的函数

前面的sensor_device_attribute只提供了读函数ad7705_get_sensor_value,并未提供写函数NULL,这里就只实现读函数即可。
{% codeblock lang:c %}
static ssize_t ad7705_get_sensor_value(struct device *dev, struct device_attribute *devattr, char *buf)
{
struct spi_device *spi = to_spi_device(dev);
struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr);
u16 ad = 0;
int i, ret = -1;
int vol1, vol2;

//After switching channels, the value of the other channel is read for the first time.
for (i=0; i<2; i++) 
{
    ret = ad7705_read_channel(dev, attr->index, &ad);
        continue;
    if (0 > ret)
    {
        ad7705_reset_and_reconfig(spi);
        return ret;
    }
}

#if 0
ret = sprintf(buf, “%u\n”, ad);
#else
vol1 = ad5/65535; //Voltage integral part
vol2 = (ad
51000/65535) - (vol11000);//Voltage fraction part
ret = sprintf(buf, “vol = %d.%dV\n”,vol1, vol2);
#endif

return ret;

}
{% endcodeblock %}
AD7705有两个ADC通道,它们都调用ad7705_get_sensor_value(),但通过sensor_device_attribute结构体中的index成员,将会读指定通道值。
sprintf()将指定格式的数据传给用户层。

1.3.7 注册hwmon子系统

使用hwmon_device_register注册hwmon设备。

1.3.8 引脚GPIO初始化

AD7705除了SPI引脚,还用到了一个就绪引脚和复位引脚。
这两个脚的信息可以从设备树中指定,再获取,这里直接写死在驱动里面了。
{% codeblock lang:c %}
status = gpio_request(DRDY_PIN, “ad7705_drdy”); //ad7705 DRDY Pin
if (status)
{
dev_err(&spi->dev, “gpio_request(AD705_DRDY_PIN) fail.\n”);
goto fail_device_register;
}
gpio_direction_input(DRDY_PIN);

status = gpio_request(RESET_PIN, "ad7705_reset");  //ad7705 RESET Pin
if (status)
{
    dev_err(&spi->dev, "gpio_request(RESET_PIN) fail.\n");
    goto fail_request_drdy_pin;
}
gpio_direction_output(RESET_PIN, 1);

{% endcodeblock %}
GPIO常见的操作:

1.判断引脚是否合法
 int gpio_is_valid(int number);
2.申请该引脚
 int gpio_request(unsigned gpio, const char *label);
3.设置为输入还是输出
 int gpio_direction_input(unsigned gpio);
 int gpio_direction_output(unsigned gpio, int value);
4.获取/设置引脚电平
 int gpio_get_value(unsigned gpio);
 void gpio_set_value(unsigned gpio, int value);
5.申请作为中断引脚/释放中断
 int gpio_to_irq(unsigned gpio);
 void free_irq(unsigned int irq, void *dev_id);
6.导出到用户态(/sys/class/gpio/gpioN)
 int gpio_export(unsigned gpio, bool direction_may_change);
 void gpio_unexport(unsigned gpio);

1.3.9 复位和配置AD7705

调用ad7705_reset()硬件复位,调用ad7705_sync_spi()同步下时钟,调用ad7705_config_channel()配置时钟和自校准。

2. input子系统简介

输入子系统是对不同类型的输入设备进行统一处理的驱动程序。
一个输入事件,如按键,是通过设备驱动层->系统核心层->事件处理层->用户空间的顺序到达用户空间并传给应用程序使用。

转载自原文链接, 如需删除请联系管理员。

原文链接:AM335X——hwmon和input子系统,转载请注明来源!

0