版權所有,轉載必須說明轉自 http://my.csdn.net/weiqing1981127
原創作者:南京郵電大學 通信與信息系統專業 研二 魏清
一.Backlight背光子系統概述
我們的LCD屏常常需要一個背光,調節LCD屏背光的亮度,這裏所說的背光不是僅僅亮和不亮兩種,而是根據用戶的需求,背光亮度是可以任意調節。Linux內核中有一個backlight背光子系統,該系統就是爲滿足用戶這種需求設計的,用戶只要根據自己的LCD背光電路中PWM輸出引腳,對內核backlight子系統代碼進行相應的配置,就可以實現LCD的背光。
LCD的背光原理主要是由核心板的一根引腳控制背光電源,一根PWM引腳控制背光亮度組成,應用程序可以通過改變PWM的頻率達到改變背光亮度的目的。
我們這裏主要講解基於backlight子系統的蜂鳴器驅動,其實簡單的使得蜂蜜器發聲的驅動很簡單,這裏只是把蜂鳴器作爲一種設備,而且這種設備原理類似背光的原理,都是基於pwm的,而我們的終極目的是使用backlight背光子系統。綜上所述,backlight子系統是基於pwm核心的一種驅動接口,如果你使用的一種設備也是基於pwm的,並且需要用戶可以調節pwm的頻率以達到諸如改變背光亮度,改變蜂鳴器頻率的效果,那麼你可以使用這個backlight背光子系統。
二.PWM核心驅動
我們先講解下PWM核心
先熟悉下pwm核心代碼在/arch/arm/plat-s3c/pwm.c
查看/arch/arm/plat-s3c/Makefile
obj-$(CONFIG_HAVE_PWM) += pwm.o
查看/arch/arm/plat-s3c/Konfig,發現同目錄的Konfig中無對應HAVE_PWM選項
查看/arch/arm/plat-s3c24xx/Konfig
config S3C24XX_PWM
bool "PWM device support"
select HAVE_PWM
help
Support for exporting the PWM timer blocks via the pwm device
system.
所以配置內核make menuconfig時,需要選中這一項。
好了,我們看看pwm.c,它是pwm核心驅動,該驅動把設備和驅動沒有分離開來,都寫在了這個pwm.c中,我們先看看pwm.c中的驅動部分
static int __init pwm_init(void)
{
int ret;
clk_scaler[0] = clk_get(NULL, "pwm-scaler0"); //獲取0號時鐘
clk_scaler[1] = clk_get(NULL, "pwm-scaler1"); //獲取1號時鐘
if (IS_ERR(clk_scaler[0]) || IS_ERR(clk_scaler[1])) {
printk(KERN_ERR "%s: failed to get scaler clocks\n", __func__);
return -EINVAL;
}
ret = platform_driver_register(&s3c_pwm_driver); //註冊pwm驅動
if (ret)
printk(KERN_ERR "%s: failed to add pwm driver\n", __func__);
return ret;
}
跟蹤下s3c_pwm_driver的定義
static struct platform_driver s3c_pwm_driver = {
.driver = {
.name = "s3c24xx-pwm", //驅動名
.owner = THIS_MODULE,
},
.probe = s3c_pwm_probe, //探測函數
.remove = __devexit_p(s3c_pwm_remove),
};
我們看看探測函數s3c_pwm_probe
static int s3c_pwm_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct pwm_device *pwm;
unsigned long flags;
unsigned long tcon;
unsigned int id = pdev->id;
int ret;
if (id == 4) {
dev_err(dev, "TIMER4 is currently not supported\n");
return -ENXIO;
}
pwm = kzalloc(sizeof(struct pwm_device), GFP_KERNEL); //分配pwm設備空間
if (pwm == NULL) {
dev_err(dev, "failed to allocate pwm_device\n");
return -ENOMEM;
}
pwm->pdev = pdev;
pwm->pwm_id = id;
pwm->tcon_base = id == 0 ? 0 : (id * 4) + 4; //計算TCON中控制哪個定時器
pwm->clk = clk_get(dev, "pwm-tin"); //獲取預分頻後的時鐘
if (IS_ERR(pwm->clk)) {
dev_err(dev, "failed to get pwm tin clk\n");
ret = PTR_ERR(pwm->clk);
goto err_alloc;
}
pwm->clk_div = clk_get(dev, "pwm-tdiv");
if (IS_ERR(pwm->clk_div)) { //獲取二次分頻後的時鐘
dev_err(dev, "failed to get pwm tdiv clk\n");
ret = PTR_ERR(pwm->clk_div);
goto err_clk_tin;
}
local_irq_save(flags);
tcon = __raw_readl(S3C2410_TCON);
tcon |= pwm_tcon_invert(pwm); //信號反轉輸出
__raw_writel(tcon, S3C2410_TCON);
local_irq_restore(flags);
ret = pwm_register(pwm); //註冊pwm設備
if (ret) {
dev_err(dev, "failed to register pwm\n");
goto err_clk_tdiv;
}
pwm_dbg(pwm, "config bits %02x\n",
(__raw_readl(S3C2410_TCON) >> pwm->tcon_base) & 0x0f);
dev_info(dev, "tin at %lu, tdiv at %lu, tin=%sclk, base %d\n",
clk_get_rate(pwm->clk),
clk_get_rate(pwm->clk_div),
pwm_is_tdiv(pwm) ? "div" : "ext", pwm->tcon_base);
platform_set_drvdata(pdev, pwm);
return 0;
err_clk_tdiv:
clk_put(pwm->clk_div);
err_clk_tin:
clk_put(pwm->clk);
err_alloc:
kfree(pwm);
return ret;
}
下面看看註冊pwm設備的函數pwm_register
static LIST_HEAD(pwm_list);
static int pwm_register(struct pwm_device *pwm)
{
pwm->duty_ns = -1;
pwm->period_ns = -1;
mutex_lock(&pwm_lock);
list_add_tail(&pwm->list, &pwm_list); //把pwm設備掛到pwm_list鏈表上
mutex_unlock(&pwm_lock);
return 0;
}
剩下來,我們看看這個pwm.c給我們提供了哪些接口函數
struct pwm_device *pwm_request(int pwm_id, const char *label)
int pwm_config(struct pwm_device *pwm, int duty_ns, int period_ns)
int pwm_enable(struct pwm_device *pwm)
void pwm_free(struct pwm_device *pwm)
EXPORT_SYMBOL(pwm_request); //申請PWM設備
EXPORT_SYMBOL(pwm_config); //配置PWM設備,duty_ns爲空佔比,period_ns爲週期
EXPORT_SYMBOL(pwm_enable); //啓動Timer定時器
EXPORT_SYMBOL(pwm_disable); //關閉Timer定時器
上面這個函數,只要知道API,會調用就行了,在此,我分析下最難的一個配置PWM函數,這個函數主要是根據週期period_ns,計算TCNT,根據空佔比duty_ns,計算TCMP,然後寫入相應寄存器。
int pwm_config(struct pwm_device *pwm, int duty_ns, int period_ns)
{
unsigned long tin_rate;
unsigned long tin_ns;
unsigned long period;
unsigned long flags;
unsigned long tcon;
unsigned long tcnt;
long tcmp;
if (period_ns > NS_IN_HZ || duty_ns > NS_IN_HZ)
return -ERANGE;
if (duty_ns > period_ns)
return -EINVAL;
if (period_ns == pwm->period_ns &&
duty_ns == pwm->duty_ns)
return 0;
tcmp = __raw_readl(S3C2410_TCMPB(pwm->pwm_id));
tcnt = __raw_readl(S3C2410_TCNTB(pwm->pwm_id));
period = NS_IN_HZ / period_ns; //計算週期
pwm_dbg(pwm, "duty_ns=%d, period_ns=%d (%lu)\n",
duty_ns, period_ns, period);
if (pwm->period_ns != period_ns) {
if (pwm_is_tdiv(pwm)) {
tin_rate = pwm_calc_tin(pwm, period);
clk_set_rate(pwm->clk_div, tin_rate);
} else
tin_rate = clk_get_rate(pwm->clk);
pwm->period_ns = period_ns;
pwm_dbg(pwm, "tin_rate=%lu\n", tin_rate);
tin_ns = NS_IN_HZ / tin_rate;
tcnt = period_ns / tin_ns; //根據週期求TCNT,n=To/Ti
} else
tin_ns = NS_IN_HZ / clk_get_rate(pwm->clk);
tcmp = duty_ns / tin_ns; //根據空佔比求TCMP
tcmp = tcnt - tcmp; //根據佔空比求TCMP
if (tcmp == tcnt)
tcmp--;
pwm_dbg(pwm, "tin_ns=%lu, tcmp=%ld/%lu\n", tin_ns, tcmp, tcnt);
if (tcmp < 0)
tcmp = 0;
local_irq_save(flags);
__raw_writel(tcmp, S3C2410_TCMPB(pwm->pwm_id)); //寫入TCMP
__raw_writel(tcnt, S3C2410_TCNTB(pwm->pwm_id)); //寫入TCNT
tcon = __raw_readl(S3C2410_TCON);
tcon |= pwm_tcon_manulupdate(pwm);
tcon |= pwm_tcon_autoreload(pwm); //自動加載
__raw_writel(tcon, S3C2410_TCON);
tcon &= ~pwm_tcon_manulupdate(pwm); //更新TCNT和TCMP
__raw_writel(tcon, S3C2410_TCON);
local_irq_restore(flags);
return 0;
}
下面說說這個週期是怎麼設計的
我們定時器的輸出頻率fi=PCLK/(prescaler value+1)/(divider value),這個可以獲得確定值
我們需要寫入一個初值n給TCNT,這樣就可以獲得一個頻率,爲什麼呢?
根據初值n=fi/fo,那麼n=To/Ti
所以當用戶給pwm_config函數傳遞一個週期period_ns,其實就是To=period_ns
這樣根據前面公式n=To/Ti= period_ns/fi,然後將這個初值n寫入TCNT就可以改變週期了
接着我再補充說明下pwm_config函數裏代碼註釋關於自動加載怎麼回事?
定時器工作原理其實是TCNT的值在時鐘到來時,減一計數,每次減一完後,拿當前TCNT與TCMP比較,如果TCNT=TCMP,那麼信號電平反向輸出,然後TCNT繼續減一計數,知道TCNT減到零後,如果有自動加載功能那麼此時將由TCNTB把計數初值再次寫給TCNTP,同時TCMPB把比較值給TCMP,這樣就完成一次初值重裝,然後繼續進行計數。我們給這種加載模式起了個名字叫雙緩衝機制,其中TCMPB和TCNTB就是Buffer緩存。
前面說pwm.c集驅動和設備於一體,那麼下面我們看看設備相關的代碼
#define TIMER_RESOURCE_SIZE (1)
#define TIMER_RESOURCE(_tmr, _irq) \
(struct resource [TIMER_RESOURCE_SIZE]) { \
[0] = { \
.start = _irq, \
.end = _irq, \
.flags = IORESOURCE_IRQ \
} \
}
#define DEFINE_S3C_TIMER(_tmr_no, _irq) \
.name = "s3c24xx-pwm", \
.id = _tmr_no, \
.num_resources = TIMER_RESOURCE_SIZE, \
.resource = TIMER_RESOURCE(_tmr_no, _irq), \
struct platform_device s3c_device_timer[] = {
[0] = { DEFINE_S3C_TIMER(0, IRQ_TIMER0) },
[1] = { DEFINE_S3C_TIMER(1, IRQ_TIMER1) },
[2] = { DEFINE_S3C_TIMER(2, IRQ_TIMER2) },
[3] = { DEFINE_S3C_TIMER(3, IRQ_TIMER3) },
[4] = { DEFINE_S3C_TIMER(4, IRQ_TIMER4) },
};
上面的代碼就是設備部分代碼,其實就是五個定時器的資源,我們把目光放在DEFINE_S3C_TIMER宏上,你會發現其設備名是"s3c24xx-pwm",而我們在pwm.c中定義的驅動名也是"s3c24xx-pwm",這樣如果我們把設備註冊到內核,那麼設備"s3c24xx-pwm"和驅動"s3c24xx-pwm"就會匹配成功。所以如果你用到定時器0,那麼你只要在BSP中添加s3c_device_timer[0]就可以了。我們現在做的是蜂鳴器驅動,使用的是Timer0定時器,我們就在mini2440的BSP文件mach-mini2440.c中添加如下代碼
static struct platform_device *mini2440_devices[] __initdata = {
……
&s3c_device_timer[0], //添加
};
這樣我們就分析完pwm核心層的代碼了。