这学期课设过的挺顺利,总结一下学到的东西。

一、Linux驱动开发

1.Linux驱动开发基础

驱动程序的功能

  • 对设备初始化以及资源释放
  • 内核与硬件之间的数据(操作、读写内容等)的转换与传送
  • 读取应用程序传送给设备文件的数据和回送应用程序请求的数据
  • 检测和处理设备出现的错误

应用程序、库、驱动程序和内核的关系

  • 应用程序调用一系列函数库,以文件形式访问各种硬件设备
  • 某些库函数无需内核的支持,由库函数内部通过代码实现。另外一些库函数涉及到硬件操作或内核的支持,由内核完成对应功能,我们称之为系统调用
  • 内核处理系统调用,根据设备文件,调用设备驱动程序
  • 驱动程序直接与硬件通信,为系统提供硬件接口

驱动程序与应用程序的区别

  • 应用程序

    • 工作在用户态
    • 可以使用glibc等C标准函数库
    • 以main开始
    • 从头到尾执行
  • 驱动程序

    • 是内核的一部分,工作在内核态
    • 不能使用标准库
    • 没有main
    • 以一个模块初始化函数作为入口,完成初始化之后不再运行,等待系统调用

2.Linux内核模块

  Linux内核是单内核,而驱动程序作为操作系统设备管理中的一部分,就需要被编译进内核。如果直接在现有的内核中加入新的驱动,就不得不重新编译内核,效率非常低,同时如果编译的模块不是很完善,很可能会造成内核崩溃。这也是单内核的缺点,可扩展性和可维护性相对较差。

  因此,Linux提供了内核模块机制来弥补上述缺陷。内核模块是Linux内核向外部提供的一个插口,全称为动态可加载内核模块(Loadable Kernel Module,LKM),简称为模块。模块在运行时被链接到内核作为内核的一部分在内核空间运行,可以实现内核本身并不含有的功能。

  显然,内核模块非常适合用于Linux驱动程序的开发。但是模块并不是驱动的必要形式,驱动不一定必须是模块,有些驱动是直接编译进内核的,同时模块也不全是驱动。

3.LED驱动

硬件原理

  LED硬件原理如图所示。LED 1-4对应的GPIO引脚为GPB5、GPB6、GPB7、GPB8。由LED电路可知,每个LED的一端连接电阻,同时有VDD33V提供高电平,另一端则直接连接控制芯片。当控制芯片提供低电平时,即数据寄存器置0,电流通过,LED被点亮。

驱动设计

  驱动需要实现的功能有:设备初始化,包括有定义设备引脚、设置引脚功能、定义设备名、创建设备文件、设备注册、设备状态初始化等;给出系统调用ioctl函数的驱动实现;驱动模块卸载时的操作,包括卸载设备以及删除设备文件等。

代码实现

/** myleds.c **/
#include <linux/miscdevice.h>
#include <linux/delay.h>
#include <asm/irq.h>
#include <mach/regs-gpio.h>
#include <mach/hardware.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/delay.h>
#include <linux/moduleparam.h>
#include <linux/slab.h>
#include <linux/errno.h>
#include <linux/ioctl.h>
#include <linux/cdev.h>
#include <linux/string.h>
#include <linux/list.h>
#include <linux/pci.h>
#include <linux/gpio.h>
#include <asm/uaccess.h>
#include <asm/atomic.h>
#include <asm/unistd.h>

#define DEVICE_NAME "myleds"

static unsigned long led_table[]={ //定义led的GPIO引脚
    S3C2410_GPB(5),
    S3C2410_GPB(6),
    S3C2410_GPB(7),
    S3C2410_GPB(8)
};

static unsigned int led_cfg_table[]={ //设置led引脚功能为输出
    S3C2410_GPIO_OUTPUT,
    S3C2410_GPIO_OUTPUT,
    S3C2410_GPIO_OUTPUT,
    S3C2410_GPIO_OUTPUT
};


//ioctl函数的驱动实现
static int leds_ioctl(struct inode *inode,struct file *file,unsigned int cmd,unsigned long arg)  
{
    if(cmd==0 || cmd==1){
        if(arg>4) return -EINVAL;
            s3c2410_gpio_setpin(led_table[arg],!cmd); //设置相应的引脚状态
        return 0;
    }
    else
        return -EINVAL;
}

static struct file_operations dev_fops={ //文件结构体
    .owner=THIS_MODULE,                  //防止使用过程中被卸载
    .ioctl=leds_ioctl                    //ioctl函数实现
};

static struct miscdevice misc={ //混杂设备结构体
    .minor=MISC_DYNAMIC_MINOR,  //动态分配次设备号
    .name=DEVICE_NAME,          //驱动名为DEVICE_NAME,即myleds
    .fops=&dev_fops             //文件操作指针
};

static int __init dev_init(void) //模块加载时的初始化函数
{
  int ret,i;
    
    printk(KERN_ALERT"\"Myleds\" installed\n");

    for (i=0;i<4;i++){
        s3c2410_gpio_cfgpin(led_table[i],led_cfg_table[i]);
        //配置led引脚的功能
        s3c2410_gpio_setpin(led_table[i],0);
        //初始化led状态为点亮
    }

    ret=misc_register(&misc);  
    //设备注册,创建设备文件,并miscdevice结构挂载到misc_list列表
    printk ("\"Myleds\" initialized\n");

  return ret;
}

static void __exit dev_exit(void) //模块卸载时的退出函数
{
    misc_deregister(&misc);
    //卸载设备,从mist_list中删除miscdevice结构,删除设备文件
    printk("\"Myleds\" removed\n");
}

module_init(dev_init);
module_exit(dev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Boys Next Door"); // 🤪

4.按键驱动

硬件原理

  按键硬件原理如图所示。按键1-6对应的GPIO引脚为GPG0、GPG3、GPG5、GPG6、GPG7、GPG11,同时对应有外部中断EINT8、EINT11、EINT13、EINT14、EINT15、EINT19。由按键电路可知,当按键按下时,VDD33V高电平被拉低,从而触发产生相应的中断。所以按键驱动采用中断方式实现对设备的控制。

驱动设计

  驱动需要实现的功能有:按键按下即触发中断,进入中断服务程序,获取按键状态;设备初始化,包括有定义设备引脚、设置引脚功能、定义设备名、创建设备文件、设备注册、设备状态初始化等;给出系统调用open函数、close函数、read函数、poll函数的驱动实现;驱动模块卸载时的操作,包括卸载设备以及删除设备文件等。

  这里引入等待队列和中断标志:当按键按下产生中断时,唤醒等待队列,设置中断标志,以便read函数获取按键状态并传递给用户程序;当按键没有按下时,系统不会轮询按键状态,节省时钟资源。中断服务程序实现按键状态的获取、设置中断标志并唤醒等待队列。

  涉及按键的系统调用有open函数、close函数、read函数、poll与select函数。驱动对各函数的底层实现如下:

  • open函数:实现中断注册并设置中断标志。中断类型采用双沿触发,这样能够更加有效的判断按键状态。

  • close函数:实现释放中断并注销中断处理函数。

  • read函数:当按键未按下时,若程序使用非阻塞方式读取则返回错误,若程序使用阻塞方式读取则将进程挂在等待队列,等待按键按下唤醒。被唤醒后就将中断处理程序获取的一组按键状态传递给用户程序。

  • poll与select函数:将进程挂在等待队列,当进程被唤醒时返回数据可读标识。

代码实现

/** mybuttons.c **/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/poll.h>
#include <linux/irq.h>
#include <asm/irq.h>
#include <linux/interrupt.h>
#include <asm/uaccess.h>
#include <mach/regs-gpio.h>
#include <mach/hardware.h>
#include <linux/platform_device.h>
#include <linux/cdev.h>
#include <linux/miscdevice.h>
#include <linux/sched.h>
#include <linux/gpio.h>

#define DEVICE_NAME "mybuttons"

struct button_irq_desc{  //中断用结构体
    int irq;             //中断号
    int pin;             //GPIO引脚
    int pin_setting;     //引脚设置
    int number;          //键值
    char *name;          //按键名称
};

static struct button_irq_desc button_irqs[]={
    {IRQ_EINT8,S3C2410_GPG(0),S3C2410_GPG0_EINT8,0,"KEY1"},
    {IRQ_EINT11,S3C2410_GPG(3),S3C2410_GPG3_EINT11,1,"KEY2"},
    {IRQ_EINT13,S3C2410_GPG(5),S3C2410_GPG5_EINT13,2,"KEY3"},
    {IRQ_EINT14,S3C2410_GPG(6),S3C2410_GPG6_EINT14,3,"KEY4"},
    {IRQ_EINT15,S3C2410_GPG(7),S3C2410_GPG7_EINT15,4,"KEY5"},
    {IRQ_EINT19,S3C2410_GPG(11),S3C2410_GPG11_EINT19,5,"KEY6"}
};

//按键状态
static volatile char key_values[]={'0','0','0','0','0','0'};

//创建等待队列,中断产生时唤醒队列,设置中断标志,以便read函数读取键值,没有中断时,系统不会轮询按键状态,节省时钟资源
static DECLARE_WAIT_QUEUE_HEAD(button_waitq);

//中断标志
static volatile int ev_press=0;

//中断服务程序
static irqreturn_t buttons_interrupt(int irq,void *dev_id)
{
  struct button_irq_desc *button_irqs=(struct button_irq_desc *)dev_id;
  int down;

    down=!s3c2410_gpio_getpin(button_irqs->pin);
    //获取被按下的按键状态

    if(down!=(key_values[button_irqs->number] & 1)){
        key_values[button_irqs->number]='0'+down;
        ev_press=1;
        //设置中断标志
        wake_up_interruptible(&button_waitq);
        //唤醒等待队列
    }
    
  return IRQ_RETVAL(IRQ_HANDLED);
}

//open函数驱动实现
static int buttons_open(struct inode *inode,struct file *file) 
{
  int i;
  int err=0;
    
    for(i=0;i<sizeof(button_irqs)/sizeof(button_irqs[0]);i++){
        if(button_irqs[i].irq<0)
            continue;
        err=request_irq(button_irqs[i].irq,buttons_interrupt,IRQ_TYPE_EDGE_BOTH,button_irqs[i].name,(void *)&button_irqs[i]);
        //注册中断,中断类型为IRQ_TYPE_EDGE_BOTH(双沿触发),能够更加有效的判断按键状态
        if(err) break;
    }

    if(err){ //如果出错则释放已注册的中断
        i--;
        while(i>=0){
            if(button_irqs[i].irq<0)
                continue;
            disable_irq(button_irqs[i].irq);
            free_irq(button_irqs[i].irq,(void *)&button_irqs[i]);
            i--;
        }
        return -EBUSY;
    }
	
    ev_press=1; //中断注册成功,设置中断标志
    
  return 0;
}

//close函数驱动实现
static int buttons_close(struct inode *inode,struct file *file)
{
  int i;
    
    for(i=0;i<sizeof(button_irqs)/sizeof(button_irqs[0]);i++){
        if (button_irqs[i].irq<0)
            continue;
        free_irq(button_irqs[i].irq,(void *)&button_irqs[i]);
        //释放中断,并注销中断处理函数
    }

  return 0;
}

//read函数驱动实现
static int buttons_read(struct file *filp,char __user *buff,size_t count,loff_t *offp)
{
  unsigned long err;

    if(!ev_press)//中断标志为0时
        if(filp->f_flags & O_NONBLOCK) //以非阻塞方式打开时,返回
            return -EAGAIN;
        else
            wait_event_interruptible(button_waitq, ev_press); 
        //以阻塞方式打开时,挂在等待队列,等待被唤醒
    
    ev_press=0;//清中断标志
    err=copy_to_user(buff,(const void *)key_values,min(sizeof(key_values),count));
    //将一组按键状态传递到用户层

  return err?-EFAULT:min(sizeof(key_values),count);
}

static unsigned int buttons_poll(struct file *file,struct poll_table_struct *wait)
//poll函数驱动实现
{
  unsigned int mask=0;

    poll_wait(file,&button_waitq,wait);
    //将进程挂到等待队列,以便被唤醒
    if(ev_press)
        mask|=POLLIN | POLLRDNORM;

  return mask;
}

static struct file_operations dev_fops={ //文件结构体
    .owner=THIS_MODULE,                  //防止使用过程中被卸载
    .open=buttons_open,                  //open函数实现
    .release=buttons_close,              //close函数实现
    .read=buttons_read,                  //read函数实现
    .poll=buttons_poll                   //poll函数实现
};

static struct miscdevice misc={
    .minor=MISC_DYNAMIC_MINOR, //动态分配次设备号
    .name=DEVICE_NAME,         //驱动名为DEVICE_NAME,即mybuttons
    .fops=&dev_fops            //文件操作指针
}; 

static int __init dev_init(void) //模块加载时的初始化函数
{
  int ret;

    printk(KERN_ALERT"\"Mybuttons\" installed\n");
    ret = misc_register(&misc);
    //设备注册,创建设备文件,并miscdevice结构挂载到misc_list列表

  return ret;
}

static void __exit dev_exit(void) //模块卸载时的退出函数
{
    misc_deregister(&misc);
    //卸载设备,从mist_list中删除miscdevice结构,删除设备文件
    printk("\"Mybuttons\" removed\n");
}

module_init(dev_init);
module_exit(dev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Boys Next Door");

5.驱动模块的编译与安装

  编译模块的Makefile

PWD = $(shell pwd)
# 模块源文件路径
KDIR = /home/***/Desktop/kernel/linux-2.6.32.2/
# 内核目录
obj-m := myleds.o mybuttons.o
# obj-m表示编译目标为模块,指定编译的源文件
all:
    $(MAKE) -C $(KDIR) M=$(PWD) CONFIG_DEBUG_SECTION_MISMATCH=y
    # 切换到内核目录进行编译
clean:
    rm -rf *.o *~core.depend. *.cmd *.ko *.mod.c .tmp_versions
    rm -rf *.order Module.*
active:
    echo -e "$(MAKE) \n"
    $(MAKE) -C $(KDIR) M=$(PWD)

  Led驱动模块的安装与卸载,按键的也一样

insmod myleds.ko
#加载led驱动模块
lsmod
#查看当前已加载的模块
rmmod myleds
#卸载led驱动模块

二、内核裁剪与编译

  由于原系统自带的LED和按键驱动已经直接编译进内核,为了测试自己编写的驱动,需要对内核进行配置,裁剪去掉原有的驱动模块。Linux内核配置系统系统由以下三个部分组成。

  • 内核源代码各层目录中的Makefile

  • 配置文件

  • 配置工具:make config(字符界面)、make menuconfig(ncurses图形界面)、make xconfig(Xwindows图形界面)

  运行 make menuconfig 对现有的Linux内核配置进行修改。在 Devices Drivers 菜单中,选择进入 Character devices,找到并选中LEDs和Buttons驱动。可以看到驱动名称前的标识符为<*>,意思是模块直接编译进内核,修改为< >对应的模块将不被编译进内核,内核裁剪完成。

  再执行make zImage,根据配置文件编译生成Linux内核映像文件arch/arm/boot/zImage

三、Qt/E

  (Qt/E开发板上GUI程序待续……)

  在Qtopia-2.2.0中运行Qt程序,需要在“设置”的“关机”中点击Terminate Server以关闭Qtopia。

  还需要执行以下命令。

source /bin/setqt4env
# 使用自带脚本设置环境变量
./gui -qws
# 使用-qws选项运行程序,将程序设置为服务程序

四、参考