天天看點

為linux3.4.2核心編寫LED驅動

開發環境

  • JZ2440 V3開發闆
  • Linux-3.4.2核心
  • 主控端:Ubuntu 16.04_64位
  • 交叉編譯器:arm-linux-gcc (version 4.3.2 )

1、linux字元裝置驅動架構

  • 使用者應用程式通過調用C庫裡已經實作的 open 、read、write等庫函數來操作檔案(在Linux中,一切皆檔案,所有硬體裝置在核心看來均是檔案)。
  • 庫函數(open等)的調用引發作業系統(Linux 核心)産生一個異常中斷(軟中斷:swi val),于是CPU控制權交給核心,進入核心異常處理。
  • 核心根據發生中斷的原因(中斷号),調用相應處理函數(sys_open,sys_read等)。
  • sys_open等函數會根據中斷類型,調用相關裝置(字元裝置、塊裝置、網絡裝置等)的驅動程式裡的相應函數,如led_open,led_read等。
為linux3.4.2核心編寫LED驅動

一個簡單的字元驅動程式架構主要包括以下幾個部分:

  1. 與C庫函數相對應的裝置操作函數,例如led_open,led_read等
static int first_drv_open(struct inode *inode, struct file *file)
{
  printk("first_drv_open\n");
  return 0;
}
 
static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
  printk("first_drv_write\n");
  return 0;
}      
  1. 用一個結構體封裝上述函數
static struct file_operations first_drv_fops = {
    .owner  =   THIS_MODULE,    /* 指向編譯子產品時自動建立的__this_module變量的宏 */
    .open   =   first_drv_open,     
    .write    =    first_drv_write,       
};      
  1. 通過驅動注冊函數将上述結構體告知核心,使裝置的各種操作函數與庫函數對應起來
int major;    //主裝置号
static int first_drv_init(void)
{
    major = register_chrdev(0, "first_drv", &first_drv_fops); // 注冊, 告訴核心
    firstdrv_class = class_create(THIS_MODULE, "firstdrv");
    firstdrv_class_dev = device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz"); /* /dev/xyz */

    printk("first_drv_init\n");
    return 0;
}

static void first_drv_exit(void)
{
    unregister_chrdev(major, "first_drv"); // 解除安裝
}      
  • 以下兩個函數實作了在核心中加入裝置資訊,mdev就可以根據/sys下生成的裝置資訊,自動建立裝置節點:
  • class_create會在/sys下建立 firstdrv這個類,
  • firstdrv_class_dev會在firstdrv類下建立xyz這個裝置,
  • mdev會自動建立一個dev/xyz裝置節點。
  1. 指定子產品加載函數、解除安裝函數和授權資訊等
module_init(first_drv_init);
module_exit(first_drv_exit);

MODULE_LICENSE("GPL");      

當使用者應用程式使用open函數打開某個裝置(/dev/led)的時候,系統首先檢視出該裝置是字元裝置,然後根據其主裝置号,去字元裝置檔案操作指針數組中找到對應的file_operation結構體位址。

驅動注冊的過程就是上述過程的逆過程:建立裝置的主、次裝置号major、minor,并以major為索引,将裝置的file_operation結構體指針寫進裝置對應的檔案操作指針數組中去,實作注冊。

  1. Makefile編寫
KERN_DIR = /home/leon/linux-3.4.2  //主控端上編譯到開發闆上的linux核心的根目錄
 
all:
  make -C $(KERN_DIR) M=`pwd` modules 
 
clean:
  make -C $(KERN_DIR) M=`pwd` modules clean
  rm -rf modules.order
 
obj-m  += first_drv.o      
  1. 編譯驅動子產品

驅動編譯依賴于核心檔案,是以需要解壓核心源碼後再編譯。

并且使用的交叉編譯工具也要和編譯核心鏡像uImage檔案一緻,用到的核心版本也要和闆子上運作的一緻。編譯成功後生成.ko檔案。

  1. 加載驅動子產品

啟動開發闆進入linux系統指令行,并将編譯好的驅動子產品first_dri.ko複制到開發闆的根檔案系統中,執行加載指令:

JZ2440 # insmod first_dri.ko      

執行​

​cat /proc/devices​

​可以檢視裝置驅動是否成功加載到系統(主裝置号+裝置名稱)

執行​

​cat /proc/modular​

​可以檢視驅動子產品是否成功加載到系統

  1. 編寫驅動測試函數

編寫驅動測試函數,在主控端上交叉編譯後得到可執行檔案,并将其複制到開發闆根檔案系統中,運作後檢視結果。要注意的是,編譯驅動測試程式的工具鍊版本和檔案系統的版本要一緻,不然應用程式可能會缺少某些庫而無法運作

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
 
/* firstdrvtest on
  * firstdrvtest off
  */
int main(int argc, char **argv)
{
  int fd;
  int val = 1;
  fd = open("/dev/xyz", O_RDWR);  //打開裝置節點名,而不是驅動程式
  if (fd < 0)
  {
    printf("can't open!\n");
  }
  write(fd, &val, 4);
  return 0;
}      
arm-linux-gcc  firstdrvtest.c -o firstdrvtest  //虛拟機上編譯,得到可執行測試檔案

cp      
JZ2440 # ./firstdrvtest on //開發闆上運作測試程式,檢視結果
first_drv_init
first_drv_open
first_drv_write
JZ2440 # 
JZ2440 # rmsmod first_dri.ko //移除驅動子產品
JZ2440 # rm /dev/xyz     //解除安裝裝置節點      

2、LED字元裝置驅動編寫

前面已經寫好了字元裝置驅動程式架構,現在實作led字元裝置驅動隻需要往架構裡填充函數就行了。整體設想就是:

  • open中實作端口初始化(輸入輸出配置)
  • 入口函數中完成虛拟位址映射
  • write中實作LED操作
  • copy_from_user 使用者空間到核心空間傳遞資料。
  • copy_to_user(); 核心空間到使用者空間傳遞資料

完整驅動代碼如下:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
 
static struct class *firstdrv_class;
static struct class_device    *firstdrv_class_dev;
 
volatile unsigned long *gpfcon = NULL;
volatile unsigned long *gpfdat = NULL;
 
 
static int first_drv_open(struct inode *inode, struct file *file)
{
    /* 配置GPF4,5,6為輸出 */
    *gpfcon &= ~((0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)));
    *gpfcon |= ((0x1<<(4*2)) | (0x1<<(5*2)) | (0x1<<(6*2)));
    return 0;
}
 
static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
    int val;
 
    copy_from_user(&val, buf, count); //  copy_to_user();
 
    if (val == 1)
    {
        // 點燈
        *gpfdat &= ~((1<<4) | (1<<5) | (1<<6));
    }
    else
    {
        // 滅燈
        *gpfdat |= (1<<4) | (1<<5) | (1<<6);
    }
    
    return 0;
}
 
static struct file_operations first_drv_fops = {
    .owner  =   THIS_MODULE,    
    .open   =   first_drv_open,     
    .write    =    first_drv_write,       
};
 
 
int major;
static int first_drv_init(void)
{
    major = register_chrdev(0, "first_drv", &first_drv_fops);    //0表示主裝置号由系統指定
 
    firstdrv_class = class_create(THIS_MODULE, "firstdrv");
 
    firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz");
 
    gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
    gpfdat = gpfcon + 1;
 
    return 0;
}
 
static void first_drv_exit(void)
{
    unregister_chrdev(major, "first_drv");
 
    class_device_unregister(firstdrv_class_dev);
    class_destroy(firstdrv_class);
    iounmap(gpfcon);
}
 
module_init(first_drv_init);
module_exit(first_drv_exit);
 
MODULE_LICENSE("GPL");      
  • 位址映射函數ioremap的了解:

由于驅動是屬于核心的一部分,是不能直接通路硬體的實體位址的,是以對于Soc上的實體位址,我們需要進行位址映射以後才能夠通路。

程序的虛拟位址和實體記憶體的位址關系如下所示:

為linux3.4.2核心編寫LED驅動

在驅動中進行位址映射隻要用ioremap函數就可以了。

ioremap宏定義在asm/io.h内:

#define      

__ioremap函數原型為(arm/mm/ioremap.c):

void __iomem * __ioremap(unsigned long phys_addr, size_t size, unsigned long flags);      

phys_addr:要映射的起始的實體位址;

size:為映射的位址長度,

flags:要映射的位址空間和權限有關的标志(我們不需要關心)

由于我們的目的是點亮LED燈,我們隻需要操作控制寄存器(設定引腳輸出)和資料寄存器(設定引腳電平)就可以了,我們先在檔案頭部對要操作的寄存器進行定義:

volatile unsigned long *gpfcon = NULL;
volatile unsigned long *gpfdat = NULL;      
volatile是一個特征修飾符,volatile的作用是作為指令關鍵字,確定本條指令不會因編譯器的優化而省略,且要求每次直接讀值。

檢視JZ2440開發闆原理圖:

為linux3.4.2核心編寫LED驅動

本次涉及的GPIO管腳為GPF4,GPF5,GPF6。

再檢視主晶片S3C2440A的晶片使用者手冊,查的GPFCON和GPFDAT的實體位址如下:

為linux3.4.2核心編寫LED驅動

然後我們就可以在入口函數中調用ioremap函數對實體位址進行映射了,由于GPFCON和GPFDAT在實體位址上是緊挨着的,我們友善起見,隻需要用映射GPFCON的位址,而GPFDAT隻需要在GPFCON的指針上+1就可以了。

同樣,在出口函數裡也要對映射的位址進行釋放,釋放映射的位址我們用到了iounmap函數,參數隻要将映射的指針傳進去就可以了。由于隻對GPFCON位址進行了映射操作,這裡隻對GPFCON進行釋放就可以了。

  • GPIOF的設定

需要将GPIOF4、5、6引腳設定為輸出引腳,點亮和熄滅LED隻需要設定GPFDAT的第4到6位為0和1就行:

為linux3.4.2核心編寫LED驅動
為linux3.4.2核心編寫LED驅動

3、LED字元裝置驅動的改進

改進思路:利用次裝置号,實作LED的精準控制。(原來是3個燈同時亮滅,現在想可以獨立控制)

/*
 * 執行insmod指令時就會調用這個函數 
 */
static int __init s3c24xx_leds_init(void)
//static int __init init_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 register major number\n");
      return ret;
    }
 
    leds_class = class_create(THIS_MODULE, "leds");
    if (IS_ERR(leds_class))
        return PTR_ERR(leds_class);
     
    leds_class_devs[0] = class_device_create(leds_class, NULL, MKDEV(LED_MAJOR, 0), NULL, "leds"); /* /dev/leds */
    
    for (minor = 1; minor < 4; minor++)  /* /dev/led1,2,3 */
    {
        leds_class_devs[minor] = class_device_create(leds_class, NULL, MKDEV(LED_MAJOR, minor), NULL, "led%d", minor);
        if (unlikely(IS_ERR(leds_class_devs[minor])))
            return PTR_ERR(leds_class_devs[minor]);
    }
        
    printk(DEVICE_NAME " initialized\n");
    return 0;
}      
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
 
/*
  *  ledtest <dev> <on|off>
  */
 
void print_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);
}
int main(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;
}