跳转至

最简单的驱动程序

点亮LED

GPIO的操作方法

1.GPIO 模块一般结构(Registers)

  • 使能(Enable):GPIO电源/时钟控制
  • 模式(Mode):配置GPIO复用
  • 方向:选择GPIO是输出还是输入
  • 数值:设置或读取引脚的电平状态

(1)设置CCM向GPIO模块提供时钟

Clock Controller Mpdule (CCM) 这里有7个寄存器CCM_CCGR0–CCM_CCGR6,这7个寄存器控制着I.MX6U的所有外设时钟开关,以 CCM_CCGR0 为例来看

(Address: 20C_4000h base + 68h offset = 20C_4068h)

img

img

img

该寄存器每两位控制着一个外设时钟,取值含义如下:

img

  • 00:该GPIO模块全程被关闭
  • 01:该GPIO模块在CPU run mode情况下是使能的;在WAIT或STOP模式下,关闭
  • 10:保留
  • 11:该GPIO模块全程使能

如果要打开GPIO2的时钟:

CCM_CCGR0=3<<30

(2)设置IOMUXC选择引脚的模式

选择GPIO功能(IOMUXC_SW_MUX_CTL)

(Address: 20E_0000h base + 60h offset = 20E_0060h GPIOx的base address看参考手册chapter2 : Memonry Maps 如:)

img

img

img

设置上下拉电阻等参数(IOMUXC_SW_PAD_CTL)

(3)设置引脚方向(GPIOx_GDIR)

(Address: Base address + 4h GPIOx的base address看参考手册chapter2 : Memonry Maps)

img

该寄存器某一位的值代表某一个引脚

(4)设置输出引脚的电平(GPIOx_DR)

(Address: Base address + 0h offset GPIOx的base address看参考手册chapter2 : Memonry Maps)

img

该寄存器某一位的值代表某一个引脚

(5)读取引脚的电平(GPIOx_PSR)

(Address: Base address + 8h offset GPIOx的base address看参考手册chapter2 : Memonry Maps)

img

该寄存器某一位的值代表某一个引脚

2.GPIO 寄存器操作

(1)直接读写:读出、修改对应位、写入

/*要设置某一位 bit n*/
val = data_reg;
val = val | (1<<n);
data_reg = val;
/*要清除某一位 bit n*/
val = data_reg;
val = val & ~(1<<n);
data_reg = val;

(2)set-and-clear protocol:

/*set_reg, clr_reg, data_reg 三个寄存器对应的是同一个物理寄存器*/
/*要设置某一位*/
 bit nset_reg = (1<<n);
/*要清除某一位*/
 bit nclr_reg = (1<<n);

驱动程序

应用函数与驱动函数一 一对应,应用需要什么,驱动就提供什么

img

驱动编程要点

  • 确定主设备号或让内核分配
  • 定义file_operations结构体
  • 实现对应的open/read/write等函数,填入file_operations结构体
  • 把file_operations结构体告诉内核:注册驱动程序
  • 入口函数:注册驱动程序 , 安装驱动程序时,就会去调用这个入口函数
  • 出口函数:卸载驱动程序时,就会去调用这个出口函数
  • 提供设备信息,自动创建设备节点

标准的驱动程序模板

/* Regsiters 地址*/
/*
    LInux中驱动程序无法直接访问寄存器的物理地址
    需要经过ioremap的虚拟映射才能访问
    映射过程在入口函数中进行
*/
/*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3地址: 配置GPIO*/
static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
/*GPIO5_GDIR地址: 配置GPIO方向*/
static volatile unsigned int *GPIO5_GDIR
/*GPIO5_DR地址: 配置GPIO输出电平*/
static volatile unsigned int *GPIO5_DR


/* 1. 确定主设备号                                                                 */
static int major = 0;               //主设备号为零时将由系统自动分配
static struct class *led_class;


/* 2. 定义自己的file_operations结构体                                              */
static struct file_operations led_drv = {
    .owner   = THIS_MODULE,
    .open    = led_drv_open,
    .read    = led_drv_read,
    .write   = led_drv_write,
    .release = led_drv_close,
};

/* 3. 实现对应的open/read/write等函数,填入file_operations结构体                   */
static ssize_t led_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
    /*APP 调用 read 函数时,把驱动中保存的数据返回给 APP*/
}

/* write(fd, &val, 1); */
static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
    /*APP 调用 write 函数时,传入的数据保存在驱动中*/

    char val
   /* copy_from_user : get date from app */
   copy_from_user(&val,buf,1) //把数据从用户空间拷贝到内核空间
   /* to set gpio register : out 1/0 */
   if(val == 1)
   {
     /* set led on */
     *GPIO5_DR &= ~(1<<3);
   }
   else
   {
     /* set led off */
      *GPIO5_DR |= (1<<3);
   }
}

static int led_drv_open (struct inode *node, struct file *file)
{
   /* 当设备文件打开时调用
    * enable gpio(默认已经开启)

    * configure pin as gpio
    * configure gpio as output  */
   *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 &= ~0xf; //清除低四位,防止之前被别的函数修改
   *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 |= 0x05; //赋值后四位0101
   *GPIO5_GDIR |= (1<<3);//GPIO5的第三位置为1,即将GPIO5_3设为输出引脚
}

static int led_drv_close (struct inode *node, struct file *file)
{

}

/* 4. file_operations结构体告诉内核:注册驱动程序                                */
/* 5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 */
static int __init led_init(void)
{
     printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); //确保入口函数被执行

    /*  register_chrdev   :注册字符设备驱动程序
     *  ioremap           : 无法直接访问物理寄存器需要对其虚拟映射
     *  class_create      :创建设备节点的辅助信息
     *  device_creat      : 创建设备驱动节点
     * */

    major = register_chrdev(0, "myled", &led_drv);               /* /dev/led  */

    /*ioremap*/
    IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x02290000 + 0x14)
    GPIO5_GDIR = ioremap(0x020AC004 + 0x14)
    GPIO5_DR   = ioremap(0x020AC000 + 0x14)

    led_class = class_create(THIS_MODULE, "myled");
    device_creat(led_class,NULL,MKDEV(major,0),NULL,"myled")

    return 0;
}

/* 6. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数           */
static void __exit led_exit(void)
{

 /* 入口函数加载的全都卸载 */

    iounmap(IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3);
    iounmap(GPIO5_GDIR);
    iounmap(GPIO5_DR);

   device_destroy(led_class, MKDEV(major, i)); /* /dev/100ask_led0,1,... */
   device_destroy(led_class, MKDEV(major, 0));
   class_destroy(led_class);
   unregister_chrdev(major, "myled");
}


/* 7. 其他完善:提供设备信息,自动创建设备节点                                     */

module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");

应用测试

/*  应用函数的用法:
 => argv[0]/argv[1]/argv[2]

    ledtest/dev/myled on
    ledtest/dev/myled off
*/

/*
 * @description     : main主程序
 * @param - argc    : argv数组元素个数
 * @param - argv    : 具体参数
 * @return          : 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
    int fd, status =0;
    char *filename;
    unsigned char databuf[1];

    if(argc != 3){ //如果参数个数不为3的话,则输入错误
        printf("Usage: %s <dev> <on|off>\n",argv[0]);
        printf( "eg: %s /dev/myled on \n"  ,argv[0]);
        printf( "eg: %s /dev/myled off\n"  ,argv[0]);
        return -1;
    }

    filename = argv[1];

    /* OPEN */
    fd = open(filename, O_RDWR);
    if(fd < 0){
        printf("file %s open failed!\r\n", argv[1]);
        return -1;
    }

    /* write */
    if(strcmp(argv[2], "on") == 0)
    {
        status = 1;
    }
     write(fd, &status ,1);

    return 0;
}

Makefile

KERNELDIR := /home/yangwentao/IMX6ULL/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek  #内核存放目录
CURRENT_PATH := $(shell pwd)

obj-m := led.o

build: kernel_modules

kernel_modules:
    $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules

clean:
    $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean