您的位置:首页 > 运维架构 > Linux

linux-3.0中的触摸屏驱动讲解

2014-04-14 19:08 316 查看
一.ADC及触摸屏接口图



S3C2440有8路的ADC通道其中触摸屏控制器接口XP,XM,YP,YM与四路ADC通道复用四个IO引脚。从原理图看出8路ADC只有一个A/D转换器,通过一个8选1开关MUX来选通哪一路A/D通道进行转换。触摸屏控制会产生两个中断,一个触摸屏中断INT_TC,一个ADC_转换完成中断INT_ADC。ADC需要时钟才能工作,因为它需要设置采样率。

触摸屏工作流程:

1、选择模式

2、设置触摸屏接口到等待接口状态

3、如果中断发生,激活转换模式

4、获取坐标后,返回等待中断状态

(INT_TC中断用于按下或弹起触摸屏)

(INT_ADC用于坐标转换完成)

二.AD转换时间

当全局时钟频率为50MHz和预分频值为49时,总共10位转换时间如下:

AD转换器频率=50MHz/(49+10=1MHz

转换时间=1/(1MHz/5cycles)=1/200KHz=5us

三.ADC及触摸屏接口特殊寄存器

(1)控制寄存器(ADCCON)

(2)触摸屏控制寄存器(ADCTSC)

(3)开始延时寄存器(ADCDLY)

(4)转换数据寄存器0(ADCDAT0)

(5)转换数据寄存器1(ADCDATA1)

(6)触摸屏指针上下中断检测寄存器(ADCUPDN)

四.触摸屏接口模式:

(1)正常转换模式:通过设置ADCCON来初始化对ADCDATA0的读写操作。

(2)分离XY坐标转换模式:X坐标模式写X坐标转换数据到ADCDAT0,触摸屏接口产生中断源到中断控制器.Y坐标模式写Y坐标转换数据到ADCDAT1,触摸屏接口产生中断源到中断控制器

(3)自动XY坐标转换模式:触摸屏控制器连续转换触摸X坐标和Y坐标.在触摸控制器写X测量数据到ADCDAT0且写Y测量数据到ADCDAT1后,触摸屏接口产生中断源到自动转换模式下的中断控制器.

(4)等待中断模式:当光标按下产生中断信号(INT_TC)。触摸屏控制器的等待中断模式必须设定为触摸屏接口中触点的状态(XP,XM,YP,YM)

触摸屏可以看成是输入设备,工作原理是底层在触摸动作发送时产生一个中断,然后CPU通过外部存储器总线读取坐标,放入一个缓冲区,字符设备驱动管理该缓冲区,而驱动的read()接口让用户可以读取坐标.

Input子系统处理输入事务,任何输入设备的驱动程序都可以通过Input输入子系统提供的接口注册到内核,利用子系统提供的功能来与用户空间交互。输入设备一般包括键盘,鼠标,触摸屏等,在内核中都是以输入设备出现的。下面分析input输入子系统的结构,以及功能实现。

一. Input子系统结构与功能实现



1. Input子系统是分层结构的,总共分为三层: 硬件驱动层,子系统核心层,事件处理层。

(1)其中硬件驱动层负责操作具体的硬件设备,这层的代码是针对具体的驱动程序的,需要驱动程序的作者来编写。

(2)子系统核心层是链接其他两个层之间的纽带与桥梁,向下提供驱动层的接口,向上提供事件处理层的接口。

(3)事件处理层负责与用户程序打交道,将硬件驱动层传来的事件报告给用户程序。

2. 各层之间通信的基本单位就是事件,任何一个输入设备的动作都可以抽象成一种事件,如键盘的按下,触摸屏的按下,鼠标的移动等。事件有三种属性:类型(type),编码(code),值(value),Input子系统支持的所有事件都定义在input.h中,包括所有支持的类型,所属类型支持的编码等。事件传送的方向是 硬件驱动层-->子系统核心-->事件处理层-->用户空间

3.输入子系统设备驱动层实现原理:
在Linux中,Input设备用input_dev结构体描述,定义在input.h中。设备的驱动只需按照如下步骤就可实现了。

①、在驱动模块加载函数中设置Input设备支持input子系统的哪些事件;

②、将Input设备注册到input子系统中;

③、在Input设备发生输入操作时(如:键盘被按下/抬起、触摸屏被触摸/抬起/移动、鼠标被移动/单击/抬起时等),提交所发生的事件及对应的键值/坐标等状态。
Linux中输入设备的事件类型有(这里只列出了常用的一些,更多请看linux/input.h中):

EV_SYN 0x00 同步事件

EV_KEY 0x01 按键事件

EV_REL 0x02 相对坐标(如:鼠标移动,报告的是相对最后一次位置的偏移)

EV_ABS 0x03 绝对坐标(如:触摸屏和操作杆,报告的是绝对的坐标位置)

EV_MSC 0x04 其它

EV_LED 0x11 LED

EV_SND 0x12 声音

EV_REP 0x14 Repeat

EV_FF 0x15 力反馈

4. 以触摸屏为例说明输入子系统的工作流程:

注:s3c2440的触摸屏驱动所用驱动层对应的模块文件为:s3c2410_ts.c,事件处理层对应的模块文件为 evdev.c

(1)s3c2410_ts模块初始化函数中将触摸屏注册到了输入子系统中,于此同时,注册函数在事件处理层链表中寻找事件处理器,这里找到的是evdev,并且将驱动与事件处理器挂载。并且在/dev/input中生成设备文件event0,以后我们访问这个文件就会找的我们的触摸屏驱动程序。

(2)应用程序打开设备文件/dev/input/event0,读取设备文件,调用evdev模块中read,如果没有事件进程就会睡眠。

(3)当触摸屏按下,驱动层通过子系统核心将事件(就是X,Y坐标),传给事件处理层也就是evdev,evdev唤醒睡眠的进程,将事件传给进程处理。

触摸屏代码分析

在arch/arm/plat-s3c24xx/devs.c定义了触摸屏的资源和platform_device结构体

static struct resource s3c_ts_resource[] = {

[0] = {

.start = SAMSUNG_PA_ADC, /*控制寄存器的起始地址0X58000000*/

.end = SAMSUNG_PA_ADC + SZ_ADC - 1,

.flags = IORESOURCE_MEM,

},

[1] = {

.start = IRQ_TC,

.end = IRQ_TC,

.flags = IORESOURCE_IRQ,

},

};

struct platform_device s3c_device_ts = {

.name = "s3c2410-ts",

.id = -1,

.dev.parent = &s3c_device_adc.dev,

.num_resources = ARRAY_SIZE(s3c_ts_resource),

.resource = s3c_ts_resource,

};

EXPORT_SYMBOL(s3c_device_ts);

还有一个void __init s3c24xx_ts_set_platdata(struct s3c2410_ts_mach_info *hard_s3c2410ts_info)函数,该函数的作用是将struct s3c2410_ts_mach_info保存到触摸屏的平台数据中

s3c2410_ts_indo结构体的初始化在/arch/arm/mach-s3c2440/mach-smdk2440.c里

static struct s3c2410_ts_mach_info smdk2440_ts_cfg __initdata = {

.delay = 10000, /*ADC的延迟时间*/

.presc = 49, /*ADC的预分频系数*/

.oversampling_shift = 2, /*采样次数log2的值*/

};

在smdk2440_devices[]这个platform_device结构体数组里面添加s3c_device_ts和s3c_device_adc。内核会将该结构体了的成员全部加到platform总线上.

在smdk2440_machine_init中调用s3c24xx_ts_set_platdata(&smdk2440_ts_cfg),这样就把上面的结构体信息保存到了平台数据中.

下面分析drivers/input/touchscreen/s3c2410_ts.c

#include <linux/errno.h>

#include <linux/kernel.h>

#include <linux/module.h>

#include <linux/gpio.h>

#include <linux/input.h>

#include <linux/init.h>

#include <linux/delay.h>

#include <linux/interrupt.h>

#include <linux/platform_device.h>

#include <linux/clk.h>

#include <linux/io.h>

#include <plat/adc.h>

#include <plat/regs-adc.h>

#include <plat/ts.h>

#define TSC_SLEEP (S3C2410_ADCTSC_PULL_UP_DISABLE | S3C2410_ADCTSC_XY_PST(0))

#define INT_DOWN (0)

#define INT_UP (1 << 8)

#define WAIT4INT (S3C2410_ADCTSC_YM_SEN | \

S3C2410_ADCTSC_YP_SEN | \

S3C2410_ADCTSC_XP_SEN | \

S3C2410_ADCTSC_XY_PST(3))

#define AUTOPST (S3C2410_ADCTSC_YM_SEN | \

S3C2410_ADCTSC_YP_SEN | \

S3C2410_ADCTSC_XP_SEN | \

S3C2410_ADCTSC_AUTO_PST | \

S3C2410_ADCTSC_XY_PST(0))

#define FEAT_PEN_IRQ (1 << 0) /* HAS ADCCLRINTPNDNUP */

struct s3c2410ts {

struct s3c_adc_client *client; /*触摸屏驱动是利用ADC转换坐标值 所以 要将触摸屏驱动挂接到ADC上 即 我们这里要说的将ADC作为server 将触摸屏做为client */

struct device *dev;

struct input_dev *input;

struct clk *clock;

void __iomem *io;

unsigned long xp;

unsigned long yp;

int irq_tc;

int count;

int shift;

int features;

};

static struct s3c2410ts ts;

/*

#define S3C2410_ADCDAT0_UPDOWN (1<<15)

取ADCDAT0的第15位与ADCDAT1的第15位相与的结果。只有当两个寄存器的第15位t同时为0时才返回1,否则返回0.从s3c2440的手册查得ADCDAT的第15位表示对与等待中断模式的光标按下或提起状态,0表示光标被按下状态,1表示光标被提起状态.所以此函数返回1表示按下.

*/

static inline bool get_down(unsigned long data0, unsigned long data1)

{

/* returns true if both data values show stylus down */

return (!(data0 & S3C2410_ADCDAT0_UPDOWN) &&

!(data1 & S3C2410_ADCDAT0_UPDOWN));

}

static void touch_timer_fire(unsigned long data)

{

unsigned long data0;

unsigned long data1;

bool down;

data0 = readl(ts.io + S3C2410_ADCDAT0); /*获取ADCDAT0里的值*/

data1 = readl(ts.io + S3C2410_ADCDAT1); /*获取ADCDAT1里的值*/

down = get_down(data0, data1);

if (down) { /*如果光标为按下状态*/

if (ts.count == (1 << ts.shift)) { /*这里的ts.shift=2,在下面的s3c2410ts_probe里有ts.shift = info->oversampling_shift,我们前面在初始化s3c2410_ts_mach_info是将oversampling_shift设置为2,所以这里的条件是采样次数等于4时*/

ts.xp >>= ts.shift; /*求平均值*/

ts.yp >>= ts.shift;

dev_dbg(ts.dev, "%s: X=%lu, Y=%lu, count=%d\n",

__func__, ts.xp, ts.yp, ts.count);

/*提交X,Y的绝对坐标*/

input_report_abs(ts.input, ABS_X, ts.xp);

input_report_abs(ts.input, ABS_Y, ts.yp);

input_report_key(ts.input, BTN_TOUCH, 1); /*提交按键事件,键值为1表示触摸屏对应的按键被按下*/

input_report_abs(ts.input, ABS_PRESSURE, 1); /* 提交触摸屏的状态,1表示触摸屏被按下Add by guowenxue, 2012.03.30 */

input_sync(ts.input); /*等待接受方收到数据后回复确认,用于同步*/

ts.xp = 0;

ts.yp = 0;

ts.count = 0; /*当转换次数为4时,将count重新置为0*/

}

s3c_adc_start(ts.client, 0, 1 << ts.shift); /*开启AD转换*/

} else {

ts.xp = 0;

ts.yp = 0;

ts.count = 0;

input_report_key(ts.input, BTN_TOUCH, 0);

input_report_abs(ts.input, ABS_PRESSURE, 0); /* Add by guowenxue, 2012.03.30 */

input_sync(ts.input);

writel(WAIT4INT | INT_DOWN, ts.io + S3C2410_ADCTSC); /*设置成等待中断模式*/

}

}

/*这里为什么要添加这个内核定时器.因为当我们按下一次,只会产生一个中断,如果我们滑动的话,就不会产生任何效果.为了处理滑动的情况,我们就启用定时器,每隔一段时间就启用一次AD转换,这样就可以得到按下点的信息了。*/

static DEFINE_TIMER(touch_timer, touch_timer_fire, 0, 0); /*创建内核定时器*/

/**

* stylus_irq - touchscreen stylus event interrupt

* @irq: The interrupt number

* @dev_id: The device ID.

*

* Called when the IRQ_TC is fired for a pen up or down event.

*/

static irqreturn_t stylus_irq(int irq, void *dev_id)

{

unsigned long data0;

unsigned long data1;

bool down;

data0 = readl(ts.io + S3C2410_ADCDAT0);

data1 = readl(ts.io + S3C2410_ADCDAT1);

down = get_down(data0, data1);

/* TODO we should never get an interrupt with down set while

* the timer is running, but maybe we ought to verify that the

* timer isn't running anyways. */

if (down) /*如果触摸屏被按下开启AD转换*/

s3c_adc_start(ts.client, 0, 1 << ts.shift); /*设置client中的channel(使用通道为)0,nr_samples为4*/

else

dev_dbg(ts.dev, "%s: count=%d\n", __func__, ts.count);

if (ts.features & FEAT_PEN_IRQ) {

/* Clear pen down/up interrupt */

writel(0x0, ts.io + S3C64XX_ADCCLRINTPNDNUP);

}

return IRQ_HANDLED;

}

**

* s3c24xx_ts_conversion - ADC conversion callback

* @client: The client that was registered with the ADC core.

* @data0: The reading from ADCDAT0.

* @data1: The reading from ADCDAT1.

* @left: The number of samples left.

*

* Called when a conversion has finished.

*/

/*s3c24xx_ts_conversion的作用就是累加每次采样后得到的x,y坐标值,并记录采样次数,

在其它函数中,可以取ts.xp和ts.yp的平均值,得到传递给用户空间的x,y坐标值。*/

static void s3c24xx_ts_conversion(struct s3c_adc_client *client,

unsigned data0, unsigned data1,

unsigned *left)

{

dev_dbg(ts.dev, "%s: %d,%d\n", __func__, data0, data1);

ts.xp += data0;

ts.yp += data1;

ts.count++;

/* From tests, it seems that it is unlikely to get a pen-up

* event during the conversion process which means we can

* ignore any pen-up events with less than the requisite

* count done.

*

* In several thousand conversions, no pen-ups where detected

* before count completed.

*/

}

/**

* s3c24xx_ts_select - ADC selection callback.

* @client: The client that was registered with the ADC core.

* @select: The reason for select.

*

* Called when the ADC core selects (or deslects) us as a client.

*/

static void s3c24xx_ts_select(struct s3c_adc_client *client, unsigned select)

{

/*select为非0时设置为自动连续测量x坐标和y坐标模式*/

if (select) {

writel(S3C2410_ADCTSC_PULL_UP_DISABLE | AUTOPST,

ts.io + S3C2410_ADCTSC); /*设置成自动连续测量X坐标和Y坐标*/

} else {

mod_timer(&touch_timer, jiffies+1);

writel(WAIT4INT | INT_UP, ts.io + S3C2410_ADCTSC); /*设置成抬起触发中断*/

}

}

/**

* s3c2410ts_probe - device core probe entry point

* @pdev: The device we are being bound to.

*

* Initialise, find and allocate any resources we need to run and then

* register with the ADC and input systems.

*/

static int __devinit s3c2410ts_probe(struct platform_device *pdev)

{

struct s3c2410_ts_mach_info *info;

struct device *dev = &pdev->dev;

struct input_dev *input_dev;

struct resource *res;

int ret = -EINVAL;

/* Initialise input stuff */

memset(&ts, 0, sizeof(struct s3c2410ts));

ts.dev = dev;

info = pdev->dev.platform_data;

if (!info) {

dev_err(dev, "no platform data, cannot attach\n");

return -EINVAL;

}

dev_dbg(dev, "initialising touchscreen\n");

ts.clock = clk_get(dev, "adc"); /*获取ADC时钟*/

if (IS_ERR(ts.clock)) {

dev_err(dev, "cannot get adc clock source\n");

return -ENOENT;

}

clk_enable(ts.clock); /*时钟使能*/

dev_dbg(dev, "got and enabled clocks\n");

ts.irq_tc = ret = platform_get_irq(pdev, 0); /*获取中断号*/

if (ret < 0) {

dev_err(dev, "no resource for interrupt\n");

goto err_clk;

}

res = platform_get_resource(pdev, IORESOURCE_MEM, 0); /*获取资源*/

if (!res) {

dev_err(dev, "no resource for registers\n");

ret = -ENOENT;

goto err_clk;

}

ts.io = ioremap(res->start, resource_size(res)); /*I/O内存映射*/

if (ts.io == NULL) {

dev_err(dev, "cannot map registers\n");

ret = -ENOMEM;

goto err_clk;

}

/* inititalise the gpio */

if (info->cfg_gpio) /*如果有cfg_gpio,就调用,初始化gpio*/

info->cfg_gpio(to_platform_device(ts.dev));

/*注册ADC设备*/

ts.client = s3c_adc_register(pdev, s3c24xx_ts_select,

s3c24xx_ts_conversion, 1);

if (IS_ERR(ts.client)) {

dev_err(dev, "failed to register adc client\n");

ret = PTR_ERR(ts.client);

goto err_iomap;

}

/* Initialise registers */

/*设置ADC的延时寄存器*/

if ((info->delay & 0xffff) > 0)

writel(info->delay & 0xffff, ts.io + S3C2410_ADCDLY);

writel(WAIT4INT | INT_DOWN, ts.io + S3C2410_ADCTSC); /*设置成等待中断模式*/

input_dev = input_allocate_device(); /分配一个input_dev*/

if (!input_dev) {

dev_err(dev, "Unable to allocate the input device !!\n");

ret = -ENOMEM;

goto err_iomap;

}

/*初始化input*/

ts.input = input_dev;

ts.input->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_ABS) | BIT(EV_SYN); /* Modify by

guowenxue, 2012.03.30, 每一种类型的事件都在input_dev.evbit中用一位来表示,构成一个位图,如果某一位为1,表示支持该事件,如果该位为0,表示不支持该事件*/

ts.input->keybit[BIT_WORD(BTN_TOUCH)] = BIT_MASK(BTN_TOUCH); /*设置所支持的按键,触摸屏可以看成是只有一个按键的设备*/

/* 设置ad转换的XY坐标,范围是0-0x3FF

因为mini2440的AD转换出的数据最大为10位,所以不会超过0x3ff。

*/

input_set_abs_params(ts.input, ABS_X, 0, 0x3FF, 0, 0);

input_set_abs_params(ts.input, ABS_Y, 0, 0x3FF, 0, 0);

input_set_abs_params(ts.input, ABS_PRESSURE, 0, 1, 0, 0); /* Add by guowenxue, 2012.03.

30 */

ts.input->name = "S3C24XX TouchScreen";

ts.input->id.bustype = BUS_HOST;

ts.input->id.vendor = 0xDEAD;

ts.input->id.product = 0xBEEF;

ts.input->id.version = 0x0102;

ts.shift = info->oversampling_shift;

ts.features = platform_get_device_id(pdev)->driver_data;

ret = request_irq(ts.irq_tc, stylus_irq, IRQF_DISABLED,

"s3c2410_ts_pen", ts.input);

if (ret) {

dev_err(dev, "cannot get TC interrupt\n");

goto err_inputdev;

}

dev_info(dev, "driver attached, registering input device\n");

/* All went ok, so register to the input system */

ret = input_register_device(ts.input);

if (ret < 0) {

dev_err(dev, "failed to register input device\n");

ret = -EIO;

goto err_tcirq;

}

return 0;

err_tcirq:

free_irq(ts.irq_tc, ts.input);

err_inputdev:

input_free_device(ts.input);

err_iomap:

iounmap(ts.io);

err_clk:

del_timer_sync(&touch_timer);

clk_put(ts.clock);

return ret;

}

static int __devexit s3c2410ts_remove(struct platform_device *pdev)

{

free_irq(ts.irq_tc, ts.input);

del_timer_sync(&touch_timer);

clk_disable(ts.clock);

clk_put(ts.clock);

input_unregister_device(ts.input);

iounmap(ts.io);

return 0;

}

#ifdef CONFIG_PM

static int s3c2410ts_suspend(struct device *dev)

{

writel(TSC_SLEEP, ts.io + S3C2410_ADCTSC);

disable_irq(ts.irq_tc);

clk_disable(ts.clock);

return 0;

}

static int s3c2410ts_resume(struct device *dev)

{

struct platform_device *pdev = to_platform_device(dev);

struct s3c2410_ts_mach_info *info = pdev->dev.platform_data;

clk_enable(ts.clock);

enable_irq(ts.irq_tc);

/* Initialise registers */

if ((info->delay & 0xffff) > 0)

writel(info->delay & 0xffff, ts.io + S3C2410_ADCDLY);

writel(WAIT4INT | INT_DOWN, ts.io + S3C2410_ADCTSC);

return 0;

}

static struct dev_pm_ops s3c_ts_pmops = {

.suspend = s3c2410ts_suspend,

.resume = s3c2410ts_resume,

};

#endif

static struct platform_device_id s3cts_driver_ids[] = {

{ "s3c2410-ts", 0 },

{ "s3c2440-ts", 0 },

{ "s3c64xx-ts", FEAT_PEN_IRQ },

{ }

};

MODULE_DEVICE_TABLE(platform, s3cts_driver_ids);

static struct platform_driver s3c_ts_driver = {

.driver = {

.name = "samsung-ts",

.owner = THIS_MODULE,

#ifdef CONFIG_PM

.pm = &s3c_ts_pmops,

#endif

},

.id_table = s3cts_driver_ids,

.probe = s3c2410ts_probe,

.remove = __devexit_p(s3c2410ts_remove),

};

static int __init s3c2410ts_init(void)

{

return platform_driver_register(&s3c_ts_driver);

}

static void __exit s3c2410ts_exit(void)

{

platform_driver_unregister(&s3c_ts_driver);

}

module_init(s3c2410ts_init);

module_exit(s3c2410ts_exit);

MODULE_AUTHOR("Arnaud Patard <arnaud.patard@rtp-net.org>, "

"Ben Dooks <ben@simtec.co.uk>, "

"Simtec Electronics <linux@simtec.co.uk>");

MODULE_DESCRIPTION("S3C24XX Touchscreen driver");

MODULE_LICENSE("GPL v2");

下面总结一下驱动原理:当触摸屏被按下时,首先会触发触摸屏中断,该中断是在s3c2410ts_probe函数中申请的ret = request_irq(ts.irq_tc, stylus_irq, IRQF_DISABLED,

"s3c2410_ts_pen", ts.input);中断处理函数为stylus_irq,在该函数内首先判断屏幕是否被按下,按下则调用s3c_adc_start函数开始AD转换。转换完成后又会触发ADC中断,进入ADC的中断服务程序s3c_adc_irq.在ADC的中断服务程序中会调用客服端的convert_cb函数,即上面的s3c24xx_ts_conversion函数,该函数的作用就是累计X,Y坐标的值。然后判断转换次数到了没,前面我们把转换次数设置成了4,每次进入这个函数时就将转换次数减一,没到就调用client->select_cb(client,
1),即上面的s3c24xx_ts_select,设置成自动连续测量X坐标和Y坐标函数,然后进行ADC转换,转换后又产生ADC中断,又进入到ADC中断服务程序,如此循环直到进行了4次AD转换.最后调用(client->select_cb)(client, 0),进入定时器处理函数,这里说明一下为什么能调用到s3c24xx_ts_conversion和convert_cb的,因为在触摸屏驱动中我们向server端注册了一个client sc32410_ts.c中的probe函数中ts.client
= s3c_adc_register(pdev, s3c24xx_ts_select,s3c24xx_ts_conversion, 1);。定时器处理函数 首先判断触摸屏是按下还是抬起 如果是按下则算出x轴和y轴坐标值的平均值(ts.xp >>= ts.shift;ts.yp
>>= ts.shift;注意右移2位表示除以4) 并把结果上报给input子系统 然后又继续调用s3c_adc_start函数开始ADC转换 这样一直循环 如果是抬起 则进入触摸屏中断服务程序stylus_irq(因为前面已经把触摸屏中断触发方式改为抬起了)

到这里 触摸屏驱动基本上就分析完了 其实 如果要扩展的话 东西太多 大家有兴趣也可以自己研究 个人认为 原理很简单 代码花点时间也能看明白 而最难的最关键的我认为还是驱动架构 为什么linux要这么做这个驱动 这么做有什么好处 就触摸屏驱动而言 因为他要用到ADC 而还有很多地方也要用到ADC 比如 HWMON驱动 那么如果把ADC单独拿出来作为server的话 其他使用ADC的设备作为client 这样就不会形成累赘的代码 可移植性也越好 PWM也是一样
LCD 背光 蜂鸣器都要用到PWM 因此也就采用这种server和client结构
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: