嵌入式Linux之我行,主要讲述和总结了本人在学习嵌入式linux中的每个步骤。一为总结经验,二希望能给想入门嵌入式Linux的朋友提供方便。如有错误之处,谢请指正。
一、开发环境
- 主 机:VMWare--Fedora 9
- 开发板:Mini2440--64MB Nand, Kernel:2.6.30.4
- 编译器:arm-linux-gcc-4.3.2
二、相关概念
1、平台设备及平台设备驱动:
这个在前面篇幅:
S3C2440上RTC时钟驱动开发实例讲解
中已经讲过了。这里只需了解一下系统为我们定义的看门狗(Watchdog)平台设备及资源情况,在arch/arm/plat-s3c24xx/devs.c中,如下:
de>
/* Watchdog */
de>
de>
/*定义了Watchdog平台设备使用的资源,这些资源在驱动程序中都会用到*/
static
struct
resource s3c_wdt_resource[
]
=
{
[
0]
=
{ /*Watchdog所使用IO端口资源范围*/
.
start =
S3C24XX_PA_WATCHDOG,
.
end =
S3C24XX_PA_WATCHDOG +
S3C24XX_SZ_WATCHDOG -
1,
.
flags =
IORESOURCE_MEM,
}
,
[
1]
=
{ /*Watchdog中断资源*/
.
start =
IRQ_WDT,
.
end =
IRQ_WDT,
.
flags =
IORESOURCE_IRQ,
}
}
;
de>
de>
/*定义了Watchdog平台设备*/
struct
platform_device s3c_device_wdt =
{
.
name =
"s3c2410-wdt"
, /*设备名称*/
.
id =
-
1,
.
num_resources =
ARRAY_SIZE(
s3c_wdt_resource)
, /*资源数量*/
.
resource =
s3c_wdt_resource, /*引用上面定义的资源*/
}
;
EXPORT_SYMBOL(
s3c_device_wdt)
;
de>
2、混杂设备(misc设备)
misc设备是Linux定义的主设备号为10的特殊字符设备,因为不符合字符设备的范畴,所以被归纳为
misc设备,在Linux中有很多这种设备,例如:LED设备、Watchdog设备等等,系统会根据设备的次设备号来区分具体是哪个设备,通常这些次
设备号被定义在include/linux/miscdevice.h中。在Linux中用miscdevice结构体来描述一个misc设备,这就意味
着被定义为misc设备的驱动中就要实现该结构体中的接口函数。该结构体也定义在miscdevice.h中,如下:
de>
struct
miscdevice {
int
minor;
const
char
*
name;
const
struct
file_operations *
fops;
struct
list_head list
;
struct
device *
parent;
struct
device *
this_device;
}
;
de>
三、实例讲解
1、Watchdog硬件结构图分析:
我们从结构图和数据手册得知,看门狗Watchdog主要是实现系统自动复位的功能,他是利用芯片内部的定时器,定时输出连接到电路的复位端,程序
在一定时间范围内对定时器清零(俗称“喂狗”),所以程序在正常工作时,定时器总是不能溢出,也就不能产生复位信号;如果程序出现错误,不在定时周期内复
位看门狗,那么定时器就会溢出而产生复位信号使系统复位。
S3C2440的Watchdog模块提供了三个寄存器来对Watchdog进行操作,他们分别是:定时器控制寄存器WTCON、定时器数据寄存器
WTDAT和定时器计数寄存器WTCNT。注意:在对定时器数据寄存器WTDAT进行操作时必须在定时器控制寄存器WTCON使能之前写入一个计数目标
值,当Watchdog使能开启后,WTDAT中的值会自动被加载到计数寄存器WTCNT中,然后Watchdog从CPU内部的时钟分频和时钟除数因子
得到一个工作周期,当每个周期结束时计数寄存器WTCNT中的值会1,直到递减为0时,如果还不重新往WTCNT中写入新的计数目标值(即“喂狗”),则
Watchdog就产生复位信号使系统复位。关于这些寄存器的功能和寄存器的各个位的操作值请参考数据手册。
2、Watchdog驱动程序具体实现步骤(建立驱动文件my2440_watchdog.c):
注意:在每步中,为了让代码逻辑更加有条理和容易理解,就没有考虑代码的顺序,比如函数要先定义后调用。如果要编译此代码,请严格按照C语言的规范来调整代码的顺序。
①、
依然是驱动程序的最基本结构:Watchdog
驱动的初始化和卸载部分及其他,如下:
de>
#
include
<
linux/
kernel.
h>
#
include
<
linux/
module.
h>
#
include
<
linux/
init.
h>
#
include
<
linux/
platform_device.
h>
de>
de>
de>
de>
/*Watchdog平台驱动结构体,平台驱动结构体定义在platform_device.h中,该结构体内的接口函数在第②、④步中实现*/
static
struct
platform_driver watchdog_driver =
{
.
probe =
watchdog_probe,
/*Watchdog探测函数,在第②步中实现*/
.
remove
=
__devexit_p(
watchdog_remove)
,
/*Watchdog移除函数, 在第④步中实现*/
.
shutdown
=
watchdog_shutdown,
/*Watchdog关闭函数,在第④步中实现*/
.
suspend =
watchdog_suspend,
/*Watchdog挂起函数,在第④步中实现*/
.
resume =
watchdog_resume,
/*Watchdog恢复函数,在第④步中实现*/
.
driver =
{
/*注意这里的名称一定要和系统中定义平台设备的地方一致,这样才能把平台设备与该平台设备的驱动关联起来*/
.
name =
"s3c2410-wdt"
,
.
owner =
THIS_MODULE,
}
,
}
;
static
int
__init watchdog_init(
void
)
{
/*将Watchdog注册成平台设备驱动*/
return
platform_driver_register(
&
watchdog_driver)
;
}
static
void
__exit watchdog_exit(
void
)
{
/*注销Watchdog平台设备驱动*/
platform_driver_unregister(
&
watchdog_driver)
;
}
module_init(
watchdog_init)
;
module_exit(
watchdog_exit)
;
/*驱动程序模块参数,如果在加载驱动模块时没有设定这些参数,则这些参数将采用默认值,
这些参数在接下来的步骤中将被一一用到,参数具体作用也将在各步骤中来说明*/
module_param(
tmr_margin,
int
,
0)
;
module_param(
tmr_atboot,
int
,
0)
;
module_param(
nowayout,
int
,
0)
;
module_param(
soft_noboot,
int
,
0)
;
MODULE_LICENSE(
"GPL"
)
;
MODULE_AUTHOR(
"Huang Gang"
)
;
MODULE_DESCRIPTION(
"My2440 Watchdog Driver"
)
;
de>
②、
Watchdog平台驱动结构中探测函数watchdog_probe的实现。探测就意味着在系统总线中去检测设备的存在,然后获取设备有用的相关资源信息,以便我们使用这些信息。代码如下:
de>
#
include
<
linux/
irq.
h>
#
include
<
asm
/
io.
h>
#
include
<
linux/
clk.
h>
#
include
<
linux/
ioport.
h>
#
include
<
plat/
regs-
watchdog.
h>
#
include
<
linux/
miscdevice.
h>
/*定义了一个用来保存watchdog的IO端口占用的IO空间和经过虚拟映射后的内存地址*/
static
struct
resource *
wdt_mem;
static
void
__iomem *
wdt_base;
/*保存watchdog中断号,NO_IRQ宏定义在irq.h中*/
static
int
wdt_irqno =
NO_IRQ;
/*保存从平台时钟队列中获取watchdog的时钟*/
static
struct
clk *
wdt_clock;
#
define
CONFIG_WATCHDOG_ATBOOT (
0)
#
define
CONFIG_WATCHDOG_DEFAULT_TIME (
15)
static
int
tmr_atboot =
CONFIG_WATCHDOG_ATBOOT;
static
int
tmr_margin =
CONFIG_WATCHDOG_DEFAULT_TIME;
static
int
soft_noboot;
static
unsigned
int
wdt_count;
/*用于保存经计算后得到的计数寄存器WTCNT的计数值*/
/*申明并初始化一个自旋锁wdt_pie_lock,对Watchdog资源进行互斥访问*/
static
DEFINE_SPINLOCK(
wdt_pie_lock)
;
static
int
__devinit watchdog_probe(
struct
platform_device *
pdev)
{
int
ret;
int
started =
0;
struct
resource *
res;
/*定义一个资源,用来保存获取的watchdog的IO资源*/
/*在系统定义的watchdog平台设备中获取watchdog中断号
platform_get_irq定义在platform_device.h中*/
wdt_irqno =
platform_get_irq(
pdev,
0)
;
if
(
wdt_irqno <
0)
{
/*获取watchdog中断号不成功错误处理
dev_err定义在device.h中,在platform_device.h中已经引用,所以这里就不需再引用了*/
dev_err(
&
pdev-
>
dev,
"no irq for watchdog/n"
)
;
return
-
ENOENT;
}
/*申请Watchdog中断服务,这里使用的是快速中断:IRQF_DISABLED。
中断服务程序为:wdt_irq,将Watchdog平台设备pdev做参数传递过去了*/
ret =
request_irq(
wdt_irqno,
wdt_irq,
IRQF_DISABLED,
pdev-
>
name,
pdev)
;
if
(
ret)
{
/*错误处理*/
dev_err(
dev,
"IRQ%d error %d/n"
,
wdt_irqno,
ret)
;
return
ret;
}
/*获取watchdog平台设备所使用的IO端口资源,注意这个IORESOURCE_MEM标志和watchdog平台设备定义中的一致*/
res =
platform_get_resource(
pdev,
IORESOURCE_MEM,
0)
;
if
(
res =
=
NULL
)
{
/*错误处理*/
dev_err(
&
pdev-
>
dev,
"failed to get memory region resource/n"
)
;
return
-
ENOENT;
}
/*从平台时钟队列中获取watchdog的时钟,这里为什么要取得这个时钟,因为看门狗定时器的工作周期是由这个
时钟和时钟除数因子得到的。注意这里的watchdog参数要与系统中定义的时钟名称一致才能获取得到,也就是
说,系统必须先定义得有watchdog。系统的一些时钟定义在arch/arm/plat-s3c24xx/s3c2410-clock.c中*/
wdt_clock =
clk_get(
&
pdev-
>
dev,
"watchdog"
)
;
if
(
IS_ERR(
wdt_clock)
)
{
/*错误处理*/
dev_err(
&
pdev-
>
dev,
"failed to find watchdog clock source/n"
)
;
return
PTR_ERR(
wdt_clock)
;
}
/*时钟获取后要使能后才可以使用,clk_enable定义在arch/arm/plat-s3c/clock.c中*/
clk_enable(
wdt_clock)
;
/*申请watchdog的IO端口资源所占用的IO空间(要注意理解IO空间和内存空间的区别),
request_mem_region定义在ioport.h中*/
wdt_mem =
request_mem_region(
res-
>
start,
res-
>
end -
res-
>
start +
1,
pdev-
>
name)
;
if
(
wdt_mem =
=
NULL
)
{
/*错误处理*/
dev_err(
&
pdev-
>
dev,
"failed to reserve memory region/n"
)
;
ret =
-
ENOENT;
goto
err_noclk;
}
/*将watchdog的IO端口占用的这段IO空间映射到内存的虚拟地址,ioremap定义在io.h中。
注意:IO空间要映射后才能使用,以后对虚拟地址的操作就是对IO空间的操作,*/
wdt_base =
ioremap(
res-
>
start,
res-
>
end -
res-
>
start +
1)
;
if
(
wdt_base =
=
NULL
)
{
/*错误处理*/
dev_err(
&
pdev-
>
dev,
"failed ioremap()/n"
)
;
ret =
-
EINVAL;
goto
err_noreq;
}
/*好了,通过上面的步骤已经将watchdog的资源都准备好了,下面就开始使用啦*/
/*这里是计算并设置看门狗定时器时钟周期值,wdt_set_heartbeat定义在下面。符合数据手册中要求的“在看门狗定时
器开始工作之前,一个初始值必须先写入看门狗定时器计数寄存器WTCNT中”。其实这里就是初始化看门狗定时器*/
if
(
wdt_set_heartbeat(
pdev,
tmr_margin)
)
{
/*这里调用两次的意思是看能不能设置成期望的值,如果不能就设置默认的值*/
started =
wdt_set_heartbeat(
pdev,
CONFIG_WATCHDOG_DEFAULT_TIME)
;
/*打印设置的值信息*/
if
(
started =
=
0)
dev_info(
&
pdev-
>
dev,
"tmr_margin value out of range, default %d used/n"
,
CONFIG_WATCHDOG_DEFAULT_TIME)
;
else
dev_info(
&
pdev-
>
dev,
"default timer value is out of range, cannot start/n"
)
;
}
/*device_init_wakeup该函数定义在pm_wakeup.h中,定义如下:
static inline void device_init_wakeup(struct device *dev, int val){
dev->power.can_wakeup = dev->power.should_wakeup = !!val;}
显然这个函数是让驱动支持电源管理的,这里只要知道,can_wakeup为1时表明这个设备可以被唤醒,设备驱动为了支持
Linux中的电源管理,有责任调用device_init_wakeup()来初始化can_wakeup,而should_wakeup则是在设备的电源状态
发生变化的时候被device_may_wakeup()用来测试,测试它该不该变化,因此can_wakeup表明的是一种能力,
而should_wakeup表明的是有了这种能力以后去不去做某件事。好了,我们没有必要深入研究电源管理的内容了,
要不就扯远了,电源管路以后再讲*/
device_init_wakeup(
&
pdev-
>
dev,
1)
;
/*把看门狗设备又注册成为misc设备,misc_register定义在miscdevice.h中
wdt_miscdev结构体定义及内部接口函数在第③步中讲*/
ret =
misc_register(
&
wdt_miscdev)
;
if
(
ret)
{
/*错误处理*/
dev_err(
&
pdev-
>
dev,
"cannot register miscdev on minor=%d (%d)/n"
,
WATCHDOG_MINOR,
ret)
;
goto
err_nomap;
}
/*函数wdt_start_or_stop定义在下面*/
if
(
tmr_atboot &
&
started =
=
0)
{
wdt_start_or_stop(
1)
;
/*参数1表示启动看门狗定时器*/
}
else
if
(
!
tmr_atboot)
{
wdt_start_or_stop(
0)
;
/*参数0表示停止看门狗定时器*/
}
return
0;
//以下是上面错误处理的跳转点
err_noclk:
clk_disable(
wdt_clock)
;
clk_put(
wdt_clock)
;
err_noreq:
release_resource(
wdt_mem)
;
kfree(
wdt_mem)
;
err_nomap:
iounmap(
wdt_base)
;
return
ret;
}
/*看门狗定时器中断服务程序*/
static
irqreturn_t wdt_irq(
int
irq,
void
*
argv)
{
/*主要要做的事情是在看门狗定时器计数寄存器值递减到0之前重新写入新值(即:“喂狗”)*/
wdt_keepalive(
)
;
return
IRQ_HANDLED;
}
/*看门狗定时器“喂狗”*/
static
void
wdt_keepalive(
void
)
{
spin_lock(
&
wdt_lock)
;
/*获取自旋锁保护临界区资源*/
writel(
wdt_count,
wdt_base +
S3C2410_WTCNT)
;
/*往计数寄存器WTCNT重新写入计数值*/
spin_unlock(
&
wdt_lock)
;
/*释放自旋锁,即解锁*/
}
/*计算并设置看门狗定时器时钟周期值并初始化看门狗定时器*/
static
int
wdt_set_heartbeat(
struct
platform_device *
pdev,
int
timeout)
{
unsigned
int
freq =
clk_get_rate(
wdt_clock)
;
unsigned
int
count
;
unsigned
int
divisor =
1;
unsigned
long
wtcon;
if
(
timeout <
1)
return
-
EINVAL;
freq /
=
128;
count
=
timeout *
freq;
if
(
count
>
=
0x10000)
{
for
(
divisor =
1;
divisor <
=
0x100;
divisor+
+
)
{
if
(
(
count
/
divisor)
<
0x10000)
break
;
}
if
(
(
count
/
divisor)
>
=
0x10000)
{
dev_err(
&
pdev-
>
dev,
"timeout %d too big/n"
,
timeout)
;
return
-
EINVAL;
}
}
tmr_margin =
timeout;
count
/
=
divisor;
wdt_count =
count
;
wtcon =
readl(
wdt_base +
S3C2410_WTCON)
;
/* wtcon=1000000000100001 这是控制寄存器的默认值,看数据手册得到*/
wtcon &
=
~
S3C2410_WTCON_PRESCALE_MASK;
/* wtcon=1000000000100001 & ~1111111100000000 = 0000000000100001 */
/*S3C2410_WTCON_PRESCALE宏是将divisor-1的值向左位移8位,也就是说该值的右8位都为0,则计算如下:
wtcon=0000000000100001 | (xxxxxxxx)00000000 = (xxxxxxxx)00100001*/
wtcon |
=
S3C2410_WTCON_PRESCALE(
divisor-
1)
;
/*设置看门狗定时器数据寄存器WTDAT的值,然后WTDAT的值会自动加载到WTCNT中*/
writel(
count
,
wdt_base +
S3C2410_WTDAT)
;
/*根据数据手册和上面计算的wtcon值可得,下面是设置看门狗定时控制寄存器WTCON为:
看门狗定时器输出使能有效、一个保留位默认0、中断使能无效、时钟除数因子为16、
看门狗定时器使能有效、两个保留位默认00、预定标器值为xxxxxxxx的内容。*/
writel(
wtcon,
wdt_base +
S3C2410_WTCON)
;
return
0;
}
/*根据标志flag的值来启动或者停止看门狗定时器,1表示启动,0表示停止*/
static
void
wdt_start_or_stop(
int
flag)
{
unsigned
long
wtcon;
spin_lock(
&
wdt_pie_lock)
;
/*获取自旋锁保护临界区资源*/
/*停止看门狗定时器,以下各寄存器的位操作请参照数据手册*/
wtcon =
readl(
wdt_base +
S3C2410_WTCON)
;
wtcon &
=
~
(
S3C2410_WTCON_ENABLE |
S3C2410_WTCON_RSTEN)
;
writel(
wtcon,
wdt_base +
S3C2410_WTCON)
;
if
(
!
flag)
{
wtcon =
readl(
wdt_base +
S3C2410_WTCON)
;
wtcon |
=
S3C2410_WTCON_ENABLE |
S3C2410_WTCON_DIV128;
if
(
soft_noboot)
{
wtcon |
=
S3C2410_WTCON_INTEN;
wtcon &
=
~
S3C2410_WTCON_RSTEN;
}
else
{
wtcon &
=
~
S3C2410_WTCON_INTEN;
wtcon |
=
S3C2410_WTCON_RSTEN;
}
writel(
wdt_count,
wdt_base +
S3C2410_WTDAT)
;
writel(
wdt_count,
wdt_base +
S3C2410_WTCNT)
;
writel(
wtcon,
wdt_base +
S3C2410_WTCON)
;
}
spin_unlock(
&
wdt_pie_lock)
;
/*释放自旋锁,即解锁*/
}
de>
③
、
实现misc设备中对设备文件的操作,代码如下:
de>
#
include
<
linux/
fs.
h>
#
include
<
linux/
interrupt.
h>
#
include
<
linux/
watchdog.
h>
/*申明并初始化一个信号量open_clock,对Watchdog资源进行互斥访问,注意:这里的信号量和第②步中的
自旋锁的区别,虽然都是达到资源互斥访问的目的,但信号量是进程级的,也就是说信号量是用在多个进程
中对同一资源的互斥访问,下面的使用会在wdt_open和wdt_release两个进程中对Watchdog资源进行互斥访问。
对于自旋锁和信号量的具体区别,请在网上找,这里不再多说了*/
static
DECLARE_MUTEX(
open_clock)
;
/*用来表示Linux内核配置选项中配不配置CONFIG_WATCHDOG_NOWAYOUT项,WATCHDOG_NOWAYOUT定义在watchdog.h中*/
static
int
nowayout =
WATCHDOG_NOWAYOUT;
typedef
enum
close_state
{
CLOSE_STATE_NOT,
CLOSE_STATE_ALLOW =
0x4021
}
close_state_t;
static
close_state_t allow_close;
/*用于记录看门狗定时器的当前的操作状态*/
/*misc设备结构体实现*/
static
struct
miscdevice wdt_miscdev =
{
.
minor =
WATCHDOG_MINOR,
/*WATCHDOG_MINOR次设备号定义在miscdevice.h中为130*/
.
name =
"watchdog"
,
/*设备名称*/
.
fops =
&
wdt_fops,
/*实现字符设备的相关操作*/
}
;
/*字符设备的相关操作实现*/
static
const
struct
file_operations wdt_fops =
{
.
owner =
THIS_MODULE,
.
open
=
wdt_open,
.
release =
wdt_release,
.
write
=
wdt_write,
.
ioctl =
wdt_ioctl,
.
llseek =
no_llseek,
/*定义为不可定位,即屏蔽seek操作,no_llseek定义在fs.h中*/
}
;
/*看门狗设备驱动的打开接口函数*/
static
int
wdt_open(
struct
inode *
inode,
struct
file
*
file
)
{
/*试着获取信号量(即:加锁),如果获取不成功,说明其他进程此时占用了,就返回忙*/
if
(
down_trylock(
&
open_clock)
)
{
return
-
EBUSY;
}
if
(
nowayout)
{
/*如果内核配置了CONFIG_WATCHDOG_NOWAYOUT项,则使模块使用计数加1*/
__module_get(
THIS_MODULE)
;
}
/*开始记录看门狗定时器的当前操作状态为:无状态*/
allow_close =
CLOSE_STATE_NOT;
/*启动看门狗定时器*/
wdt_start_or_stop(
1)
;
/*表示返回的这个设备文件是不可以被seek操作的,nonseekable_open定义在fs.h中*/
return
nonseekable_open(
inode,
file
)
;
}
/*看门狗设备驱动的关闭接口函数*/
static
int
wdt_release(
struct
inode *
inode,
struct
file
*
file
)
{
/*如果判断到当前操作状态是可以关闭看门狗定时器时就关闭,否则就是“喂狗”状态*/
if
(
allow_close =
=
CLOSE_STATE_ALLOW)
{
wdt_start_or_stop(<