字符设备是能够像字节流(比如文件)一样被访问的设备,对它的访问是以字节为单位的。例如利用串口收发数据时就是一个一个字节进行的,在编写驱动程序时,我们可以在驱动程序中设置缓冲区来存放数据以提高效率,但是串口本身并无此要求。字符设备驱动程序中运用到了open 、close、read、write等系统调用,应用程序可以通过设备文件来访问字符设备。
在寒假期间,我着重看了一下简单的LED字符设备驱动,以三星的开发板S3c24xx为例的LED驱动。首先是模块的初始化和卸载函数:
static int __init leds_init(void)
{
leds_class = class_create(THIS_MODULE, "leds");
if (IS_ERR(leds_class))
return PTR_ERR(leds_class);
leds_class->suspend = led_suspend;
leds_class->resume = led_resume;
leds_class->dev_attrs = led_class_attrs;
return 0;
}
static void __exit leds_exit(void)
{
class_destroy(leds_class);
}
subsys_initcall(leds_init);
module_exit(leds_exit);
在初始化函数主要做了一件事情,那就是调用class_create函数注册了一个class。
然后是led_classdev_register():
/**
*led_classdev_register - register a new object of led_classdev class.
* @parent: Thedevice to register.
* @led_cdev: theled_classdev structure for this device.
*/
intled_classdev_register(struct device *parent, struct led_classdev *led_cdev)
{
led_cdev->dev = device_create(leds_class, parent, 0, led_cdev,
"%s", led_cdev->name);
if(IS_ERR(led_cdev->dev))
return PTR_ERR(led_cdev->dev);
#ifdef CONFIG_LEDS_TRIGGERS
init_rwsem(&led_cdev->trigger_lock);
#endif
/* add tothe list of leds */
down_write(&leds_list_lock);
list_add_tail(&led_cdev->node, &leds_list);
up_write(&leds_list_lock);
if(!led_cdev->max_brightness)
led_cdev->max_brightness = LED_FULL;
led_update_brightness(led_cdev);
init_timer(&led_cdev->blink_timer);
led_cdev->blink_timer.function = led_timer_function;
led_cdev->blink_timer.data = (unsigned long)led_cdev;
#ifdefCONFIG_LEDS_TRIGGERS
led_trigger_set_default(led_cdev);
#endif
printk(KERN_DEBUG "Registered led device: %s\n",
led_cdev->name);
return 0;
}
EXPORT_SYMBOL_GPL(led_classdev_register);
在led_classdev_register函数中主要做了以下一些事情:
1.在2.6版本内核中是自动在/dev目录下创建设备节点,而在4.33中最后调用了device_register函数向系统注册了一个struct device(可以在/sys/class/leds/目录下找到这个设备,比如lcd背光驱动的lcd-backlight)。
2. 加入led_cdev加入到链表leds_list中。
3. 调用led_update_brightness函数更新led亮度值。
4. 调用init_timer初始化一个定时器。
/**
*led_classdev_unregister - unregisters a object of led_properties class.
*@led_cdev: the led device to unregister
*
*Unregisters a previously registered via led_classdev_register object.
*/
void led_classdev_unregister(structled_classdev *led_cdev)
{
#ifdef CONFIG_LEDS_TRIGGERS
down_write(&led_cdev->trigger_lock);
if (led_cdev->trigger)
led_trigger_set(led_cdev,NULL);
up_write(&led_cdev->trigger_lock);
#endif
/* Stop blinking */
led_brightness_set(led_cdev, LED_OFF);
device_unregister(led_cdev->dev);
down_write(&leds_list_lock);
list_del(&led_cdev->node);
up_write(&leds_list_lock);
}
EXPORT_SYMBOL_GPL(led_classdev_unregister);
led_classdev_unregister()函数的主要作用是注销led的这个class。 down_write(&led_cdev->trigger_lock)用于关闭led。然后调用device_unregister注销设备。list_del(&led_cdev->node)来从链表leds_list中删除节点。
在Linux2.66中,模块初始化和卸载函数如下:
/*
* 执行insmod命令时就会调用这个函数
*/
static int __init s3c24xx_leds_init(void)
//static int __initinit_module(void)
{
int ret;
int minor = 0;
gpio_va = ioremap(0x56000000, 0x100000);
if (!gpio_va) {
return -EIO;
}
/* 注册字符设备
* 参数为主设备号、设备名字、file_operations结构;
* 这样,主设备号就和具体的file_operations结构联系起来了,
* 操作主设备为LED_MAJOR的设备文件时,就会调用s3c24xx_leds_fops中的相关成员函数
* LED_MAJOR可以设为0,表示由内核自动分配主设备号
*/
ret = register_chrdev(LED_MAJOR,DEVICE_NAME, &s3c24xx_leds_fops);
if (ret < 0) {
printk(DEVICE_NAME " can't registermajor number\n");
return ret;
}
printk(DEVICE_NAME "initialized\n");
return 0;
}
/*
* 执行rmmod命令时就会调用这个函数
*/
static void __exit s3c24xx_leds_exit(void)
{
int minor;
/* 卸载驱动程序 */
unregister_chrdev(LED_MAJOR, DEVICE_NAME);
for (minor = 0; minor < 4; minor++)
{
class_device_unregister(leds_class_devs[minor]);
}
class_destroy(leds_class);
iounmap(gpio_va);
}
/* 这两行指定驱动程序的初始化函数和卸载函数 */
module_init(s3c24xx_leds_init);
module_exit(s3c24xx_leds_exit);
执行“insmod s3c24xx_led.ko”命令时就会调用s3c24xx_leds_init函数。它调用register_chrdev函数向内核注册驱动程序:将主设备号LED_MAJOR与file_operation结构s3c24xx_leds_fops联系起来。以后应用程序操作主设备号为LED_MAJOR与file_operation结构s3c24xx_leds_fops联系起来。当应用程序操作主设备为LED_MAJOR的设备文件时,就会调用s3c24xx_leds_fops中的相关成员函数,比如open、read、write、ioctl,对应于s3c24xx_leds_fops中有其对应的函数,就会调用这些函数。
而执行“remmod s3c24xx_led.ko”时,就会调用s3c24xx_leds_exit函数,它进而调用unregister_chrdev函数卸载驱动程序,功能与register_chrdev函数相反。
s3c24xx_leds_fops的组成为:
/* 这个结构是字符设备驱动程序的核心
* 当应用程序操作设备文件时所调用的open、read、write等函数,
* 最终会调用这个结构中指定的对应函数
*/
static struct file_operationss3c24xx_leds_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = s3c24xx_leds_open,
.read = s3c24xx_leds_read,
.write = s3c24xx_leds_write,
};
file_operation类型的s3c24xx_leds_fops结构是驱动中最重要的数据结构,字符设备驱动就是要填充其中各个成员。如open、ioctl成员被设为s3c24xx_leds_open和s3c24xx_leds_ioctl函数。s3c24xx_leds_open用于 初始化LED所用的GPIO引脚,s3c24xx_leds_ioctl用来根据用户传入的参数设置GPIO输出电平。s3c24xx_leds_open函数代码如下:
static ints3c24xx_leds_open(struct inode *inode, struct file *file)
{
int i;
for (i=0;i<4;i++){
//设置GPIO引脚的功能,为输出功能
s3c2410_gpio_cfgpin(led_table[i],led_cfg_table[i]);
}
return 0;
}
s3c2410_gpio_cfgpin函数是内核里实现的,它用来选择引脚的功能。其实就是设置GPIO的控制寄存器。
S3c24xx_leds_ioctl代码如下:
/* 应用程序对设备文件/dev/leds执行ioctl(...)时,
* 就会调用s3c24xx_leds_ioctl函数
*/
static int s3c2440_leds_ioctl(structinode *inode,
structfile *file,
unsignedint cmd,
unsignedlong arg)
{
if(arg > 4){
return–EINVAL;
}
switch(cmd){ //通过命令设置LED引脚电平状态
case IOCTL_LED_ON:
//设定指定引脚的输出电平为0
s3c2410_gpio_setpin(led_table[arg],0);
return 0;
case IOCTL_LED_OFF:
//设定指定引脚的输出电平为1
s3c2410_gpio_setpin(led_table[arg],1);
return 0;
default:
return –EINVAL;
}
}
应用程序执行系统调用ioctl(fd,cmd,arg)时,s3c2440_leds_ioctl函数被调用,cmd,arg参数调用s3c2410_gpio_setpinhans,来设置引脚的输出电平,输出0时点亮LED,输出1时熄灭LED。
在Linux4.33的led_s3c24xx驱动中还有两个主要的设备属性文件brightness和max_brightness。
brightness有操作函数led_brightness_show和led_birghtness_shore,led_brightness_show用于显示led的亮度值,而led_birghtness_shore用于设置led的亮度值,所以可以通过echo 120 > brightness的方式设置lcd的背光亮度为120。
而max_brightness只有一个操作函数为led_max_brightness_show用于显示led的最大亮度值。
还有led_timer_function函数
static void led_timer_function(unsigned longdata)
{
struct led_classdev *led_cdev = (void *)data;
unsigned long brightness;
unsigned long delay;
if (!led_cdev->blink_delay_on || !led_cdev->blink_delay_off) {
led_set_brightness(led_cdev,LED_OFF);
return;
}
brightness = led_get_brightness(led_cdev);
if (!brightness) {
/* Time to switch the LED on.*/
brightness =led_cdev->blink_brightness;
delay =led_cdev->blink_delay_on;
} else {
/* Store the currentbrightness value to be able
* to restore it when thedelay_off period is over.
*/
led_cdev->blink_brightness= brightness;
brightness = LED_OFF;
delay =led_cdev->blink_delay_off;
}
led_set_brightness(led_cdev, brightness);
mod_timer(&led_cdev->blink_timer, jiffies +msecs_to_jiffies(delay));
}
此函数运用定时器来控制led灯。首先判断led_cdev中的blink_delay_on和blink_delay_off,如果这两个值为0,那么直接设置led亮度值为0,并返回。
再读取brightness,这里有两个分支,如果brightness值为0,即led是关闭的,那么设置brightness为led_cdev->blink_brightness,然后设置led的亮度值,修改定时器,是它延长led_cdev->blink_delay_on再执行led_timer_function这个函数,从这里可以看到blink_delay_on为led灯亮的时间。如果brightness不为0,也就是led是亮着的,先将之前的led亮度值保存在变量led_cdev->blink_brightness中,然后关闭led,延长blink_delay_off这么长时间,所以blink_delay_off是led关闭的时间。
从这里我们可以看出,blink_delay_on和blink_delay_off是用于调节led亮和灭的时间长度,单位是毫秒,如果它们都是500毫秒,那么就会出现led500毫秒一亮一灭,这样设计的目的是为了节约用电,比如控制lcd的背光(注意:PWM控制的LCD背光不需要blink_delay_on和blink_delay_off这两个参数,因为PWM本身就具有这个功能)。
注意:每个调用led_classdev_register注册到内核的struct led_classdev都有个定时器,而且他们的定时器操作函数都是相同的,他们各自管理自己的delay_on、dealy_off。初始情况下led_timer_function是不会被执行的,除非有外界条件去触发它,例如调用了mod_timer。
led_set_brightness用来设置led亮度,在led.h中:
static inline void led_set_brightness(struct led_classdev *led_cdev,
enum led_brightness value)
{
if (value >led_cdev->max_brightness)
value =led_cdev->max_brightness;
led_cdev->brightness =value;
if (!(led_cdev->flags& LED_SUSPENDED))
led_cdev->brightness_set(led_cdev, value);
}
此函数中调用led_classdev中的brightness_set,在led_core.c中,有led_brightness_set()、led_blink_set两个接口函数。
void led_brightness_set(struct led_classdev *led_cdev,
enumled_brightness brightness)
{
led_stop_software_blink(led_cdev);
led_cdev->brightness_set(led_cdev, brightness);
}
EXPORT_SYMBOL(led_brightness_set);
void led_blink_set(struct led_classdev *led_cdev,
unsigned long*delay_on,
unsigned long*delay_off)
{
del_timer_sync(&led_cdev->blink_timer);
if (led_cdev->blink_set&&
!led_cdev->blink_set(led_cdev, delay_on, delay_off))
return;
/* blink with 1 Hz as defaultif nothing specified */
if (!*delay_on &&!*delay_off)
*delay_on = *delay_off = 500;
led_set_software_blink(led_cdev, *delay_on, *delay_off);
}
EXPORT_SYMBOL(led_blink_set);
Linux2.66中创建一个file_operation结构体。填充这个结构体,指定操作设备文件时的函数是什么,例如open、read、write等。而在Linux4.33中,如果去写一个led驱动,首先得定义一个struct led_classdev,它得有两个成员,brightness_set和blink_set,最后调用led_classdev_register函数去完成注册。以leds-s3c24xx为例:
平台驱动部分为:
staticstruct platform_driver s3c24xx_led_driver = {
.probe = s3c24xx_led_probe,
.remove = s3c24xx_led_remove,
.driver = {
.name = "s3c24xx_led",
.owner = THIS_MODULE,
};
module_platform_driver(s3c24xx_led_driver);
staticint s3c24xx_led_probe(struct platform_device *dev)
{
struct s3c24xx_led_platdata *pdata =dev->dev.platform_data;
struct s3c24xx_gpio_led *led;
int ret;
led = kzalloc(sizeof(structs3c24xx_gpio_led), GFP_KERNEL);
if (led == NULL) {
dev_err(&dev->dev,"No memory for device\n");
return -ENOMEM;
}
platform_set_drvdata(dev, led);
led->cdev.brightness_set =s3c24xx_led_set;
led->cdev.default_trigger =pdata->def_trigger;
led->cdev.name = pdata->name;
led->cdev.flags |=LED_CORE_SUSPENDRESUME;
led->pdata = pdata;
/* no point in having a pull-up if weare always driving */
if (pdata->flags &S3C24XX_LEDF_TRISTATE) {
s3c2410_gpio_setpin(pdata->gpio,0);
s3c2410_gpio_cfgpin(pdata->gpio, S3C2410_GPIO_INPUT);
} else {
s3c2410_gpio_pullup(pdata->gpio, 0);
s3c2410_gpio_setpin(pdata->gpio, 0);
s3c2410_gpio_cfgpin(pdata->gpio, S3C2410_GPIO_OUTPUT);
}
/* register our new led device */
ret =led_classdev_register(&dev->dev, &led->cdev);
if (ret < 0) {
dev_err(&dev->dev,"led_classdev_register failed\n");
kfree(led);
return ret;
}
return 0;
}
在probe函数中首先申请内存,其中就包括struct led_classdev,然后是对它的一些赋值操作,比如brightness_set为s3c24xx_led_set。再然后是初始化gpio口,最后是调用led_classdev_register函数完成注册。
下面编写一个测试程序测试它:
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
/*
* ledtest <dev> <on|off>
*/
voidprint_usage(char *file)
{
printf("Usage:\n");
printf("%s <dev><on|off>\n",file);
printf("eg. \n");
printf("%s /dev/leds on\n",file);
printf("%s /dev/leds off\n",file);
printf("%s /dev/led1 on\n",file);
printf("%s /dev/led1 off\n",file);
}
intmain(int argc, char **argv)
{
int fd;
char* filename;
char val;
if (argc != 3)
{
print_usage(argv[0]);
return 0;
}
filename = argv[1];
fd = open(filename, O_RDWR);
if (fd < 0)
{
printf("error, can't open%s\n", filename);
return 0;
}
if (!strcmp("on", argv[2]))
{
// 亮灯
val = 0;
write(fd, &val, 1);
}
else if (!strcmp("off", argv[2]))
{
// 灭灯
val = 1;
write(fd, &val, 1);
}
else
{
print_usage(argv[0]);
return 0;
}
return 0;
}
运行 :./ledtest /dev/led1 on
可用。