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 Register
、Setup register
、Clock register
、Data register
。
在每次做任何操作前,都要写Communication Register
来设置即将操作的是哪一个寄存器、是读还是写操作、操作哪一个通道。
其操作流程如下:
- 拉低复位引脚,硬件复位;
- 在至少32个时钟周期里连续发送高脉冲,以同步时钟;
- 配置AD7705时钟(时钟源、分频系数等);
- 自校准,并等待就绪引脚拉低;
- 从数据寄存器里读取数据;
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 = (ad51000/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.c
和am335x-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/
下创建文件夹,并按照hwmon0
、hwmon1
、hwmon2
等顺序编号
第二个参数是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 = (ad51000/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子系统简介
输入子系统是对不同类型的输入设备进行统一处理的驱动程序。
一个输入事件,如按键,是通过设备驱动层->系统核心层->事件处理层->用户空间的顺序到达用户空间并传给应用程序使用。