字符设备的编写一

简介: 字符设备的编写一

前言

字符设备是Linux驱动中三大设备之一,字符(char)设备是个能够像字节流(类似文件)一样被访问的设备,由字符设备驱动程序来实现这种特性。字符设备驱动程序通常至少要实现open、close、read和write的系统调用。字符终端(/dev/console)和串口(/dev/ttyS0以及类似备)就是两个字符设备,它们能很好的说明“流”这种抽象概念。字符设备可以通过文件节点来访问,比如/dev/tty1和/dev/lp0等。这些设备文件和普通文件之间的唯一差别在于对普通文件的访问可以前后移动访问位置,而大多数字符设备是一个只能顺序访问的数据通道。然而,也存在具有数据区特性的字符设备,访问它们时可前后移动访问位置。


一、字符设备的编写步骤

1.实现入口函数xxx_init()和卸载函数xxx_exit()

2.申请设备号register_chrdev(与内核有关)

3.注册字符设备驱动cdev_alloc cdev_init cdev_add(与内核有关)

4.利用udev/mdev机制创建设备文件(节点),

class_create,device_create(与内核有关)

5.硬件部分初始化 io资源映射ioremap,内核提供gpio库函数(与硬件相关)

注册中断(与硬件相关)

初始化等待队列(与内核有关)

初始化定时器(与内核有关)

6.构建file_operation结构(与内核相关)

实现操作硬件方法xxx_open,xxx_read,xxx_write…(与硬件有关)

二、字符设备应用

1.视频教学中的使用

1.实现模块加载和卸载入口函数

module_init(chr_dev_init);
      module_exit(chr_dev_exit);

2.在模块加载入口函数中

A.申请主设备号(内核中用于区分和管理不通风字符设备)

register_chrdev(unsigned int major, const char * name, const struct file_operations * fops    

B.创建设备节点文件(为用户提供哟个课操作到文件接口—open());

struct class class_create(owner, name)
struct device *device_create(struct class *class, struct device *parent,
           dev_t devt, void *drvdata, const char *fmt, ...)

C.硬件的初始化

1.地址的映射

gpx2conf = ioremp(GPX2_CON,GPX2_SIZE);

2.中断的申请

3.实现硬件的寄存器的初始化

//需要1配置gpio功能为输出

*gpx2conf &= ~(0xf<<28);
    *gpx2conf |=(0x1<<28);

D.实现file_operations

const struct file_operations my_fops= {
  .open = chr_drv_open,
  .read = chr_drv_read,
  .write = chr_drv_write,
  .release = chr_drv_close,
};

规范:

1.

//0,实例化全局的设备对象–分配空间

//GFP_KERNEL如果当前空间内存不够用的时候,该函数会一直阻塞(休眠)

//#include <linux/slab.h>
led_dev = kmalloc(sizeof(struct  led_desc), GFP_KERNE);
If(led_dev == NULL)
{
printk(KERN_ERR “malloc error\n”);
return -ENOMEM;
}
led_dev->dev_major = 250;

2.做出错处理

在某个位置出错了,要将之前申请到的资源进行释放

led_dev->dev_major = register_che_dev(0, “led_dev_test”,&my_fops);
if(led_dev->dev_major < 0)
{
printk(KERN_ERR “register_chrdev error\n”);
ret = -ENODEV;
goto ↓err_0;
}
err_0:
kfree(led_dev);
return ret;

2.个人观点

按照视频的来最终在使用时,可能会遇到一些bug,比如:

在使用kzalloc申请内存的时候总是申请失败,在参考网上的解决办法后依旧未能解决,实例化对象需要用到kzalloc这一种方法在我学习的时候,并没有得到验证,始终卡在内存申请这儿,让后我使用传统方法,才达到自己想要的结果。


总结

字符设备驱动编写方法有多种,其中区别主要在于硬件资源的获取:传统字符设备驱动编写;平台总线;设备树。前面两种本质上区别其实并不大,但引入了分层的思想。后续将慢慢和大家分享。

附录

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/slab.h>
/* 寄存器物理地址 */
#define CCM_CCGR1_BASE        (0X020C406C)  
#define SW_MUX_GPIO1_IO03_BASE    (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE    (0X020E02F4)
#define GPIO1_DR_BASE       (0X0209C000)
#define GPIO1_GDIR_BASE       (0X0209C004)
/* 映射后的寄存器虚拟地址指针 */
static volatile unsigned int  *IMX6U_CCM_CCGR1;
static volatile unsigned int  *SW_MUX_GPIO1_IO03;
static volatile unsigned int  *SW_PAD_GPIO1_IO03;
static volatile unsigned int  *GPIO1_DR;
static volatile unsigned int  *GPIO1_GDIR;
//静态指定
struct led_desc {
  unsigned int dev_major;//设备号
  struct class *cls;
  struct device *dev;//创建设备文件
  /* 映射后的寄存器虚拟地址指针 */
};
 struct led_desc *led_dev;
static int kernel_val = 555;
//read(fd,buf,size);
ssize_t chr_drv_read(struct file *filp, char __user *buf, size_t count, loff_t *fops)
{
  int ret;
  printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
  ret = copy_to_user(buf, &kernel_val, count);
  if(ret > 0)
  {
    printk("failed copy_to_user\r\n");
    return -EFAULT;
  }
  return 0;
}
ssize_t chr_drv_write (struct file *filp, const char __user *buf, size_t count, loff_t *fops)
{
  int ret;
  int value;
  printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
  ret = copy_from_user(&value, buf, count);
  if(ret > 0)
  {
    printk("failed copy_from_user\r\n");
    return -EFAULT;
  }
  if(value){
    *GPIO1_DR |= (1 << 3);
  }else{
    *GPIO1_DR &= ~(1 << 3);
  }
  return 0;
}
int chr_drv_open (struct inode *inode, struct file *filp)
{
  printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
  return 0;
}
int chr_drv_close (struct inode *inode, struct file *filp)
{
  printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
  return 0;
}
const struct file_operations my_fops  = {
  .open = chr_drv_open,
  .read = chr_drv_read,
  .write = chr_drv_write,
  .release = chr_drv_close,
};
/*
②在模块加载入口函数中
  a  申请主设备号(内核中用于区分和管理不同字符设备)
    register_chrdev()
  b  创建设备节点文件(为用户提供一个可操作到文件接口--open())
    struct class *class_create()
    struct device *device)create()
  c  硬件初始化
    1.地址映射
    2.中断申请
    3.实现硬件的寄存器的初始化
  d  实现file_operation
*/
static int __init chr_dev_init(void)
{
  int ret;
  //0 实例化全局的设备对象
  led_dev = kzalloc(sizeof(struct led_desc), GFP_KERNEL);
  if(led_dev == NULL);
  {
    printk("kmalloc failed\r\n");
    return -ENOMEM;
  }
  //一般都是申请设备号资源
  //1.申请设备号
  led_dev->dev_major = register_chrdev(0, "chr_dev_test", &my_fops);
  if(led_dev->dev_major < 0){
    printk(KERN_ERR"register_chrdev failed\n");
    ret = -ENODEV;
    goto err_0;
  }
  //2.创建设备文件
  led_dev->cls = class_create(THIS_MODULE, "chr_cls");
  if(IS_ERR(led_dev->cls))
  {
    printk(KERN_ERR"class_create failed\n");
    ret = PTR_ERR(led_dev->cls);//将指针出错原因转换为一个出错码
    goto err_1;
  }
  // /dev/led0
  led_dev->dev = device_create(led_dev->cls, NULL, MKDEV(led_dev->dev_major, 0), NULL, "led%d", 0);
  if(IS_ERR(led_dev->dev))
  {
    printk(KERN_ERR"device_create failed\n");
    ret = PTR_ERR(led_dev->dev);//将指针出错原因转换为一个出错码
    goto err_2;
  }
  //3,硬件初始化
  /* 初始化LED */
  /* 1、寄存器地址映射 */
    IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
  SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
    SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
  GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
  GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
  /* enable GPIO1
   * configure GPIO1_io3 as gpio
   * configure GPIO1_io3 as output 
   */
  *IMX6U_CCM_CCGR1 |= 0x0C000000;
  *SW_MUX_GPIO1_IO03 &= 0xfffffff0;
  *SW_MUX_GPIO1_IO03 |= 0x5;
  *SW_PAD_GPIO1_IO03 &= 0xfffffff0;
  *SW_PAD_GPIO1_IO03 |=  0x10B0;
  *GPIO1_GDIR &= 0xfffffff0;
  *GPIO1_GDIR |= 0x8;
/*
readl   writel
static inline u32 readl(const volatile void __iomem *addr)
static inline void writel(u32 value, void *addr)
 2、使能GPIO1时钟 
  val = readl(IMX6U_CCM_CCGR1);
  val &= ~(3 << 26);   清楚以前的设置 
  val |= (3 << 26); 设置新值 
  writel(val, IMX6U_CCM_CCGR1);
   3、设置GPIO1_IO03的复用功能,将其复用为
      GPIO1_IO03,最后设置IO属性。
  writel(5, SW_MUX_GPIO1_IO03);
  寄存器SW_PAD_GPIO1_IO03设置IO属性
   bit 16:0 HYS关闭
   bit [15:14]: 00 默认下拉
     bit [13]: 0 kepper功能
     bit [12]: 1 pull/keeper使能
     bit [11]: 0 关闭开路输出
     bit [7:6]: 10 速度100Mhz
     bit [5:3]: 110 R0/6驱动能力
     bit [0]: 0 低转换率
  writel(0x10B0, SW_PAD_GPIO1_IO03);
   4、设置GPIO1_IO03为输出功能 
  val = readl(GPIO1_GDIR);
  val &= ~(1 << 3);  清除以前的设置 
  val |= (1 << 3);   设置为输出 
  writel(val, GPIO1_GDIR);
   5、默认关闭LED 
  val = readl(GPIO1_DR);
  val |= (1 << 3);  
  writel(val, GPIO1_DR);
writel(readl(led_dev->GPIO1_DR) | (1 << 3),led_dev->GPIO1_DR);
*/
  return 0;
err_2:
  class_destroy(led_dev->cls);
err_1:
  unregister_chrdev(led_dev->dev_major, "chr_dev_test");
err_0:
  kfree(&led_dev);
  return  ret;
  return ret;
}
static void __exit chr_dev_exit(void)
{
  //一般都是释放资源
    /* 取消映射 */
  iounmap(IMX6U_CCM_CCGR1);
  iounmap(SW_MUX_GPIO1_IO03);
  iounmap(SW_PAD_GPIO1_IO03);
  iounmap(GPIO1_DR);
  iounmap(GPIO1_GDIR);
  device_destroy(led_dev->cls, MKDEV(led_dev->dev_major, 0));
  class_destroy(led_dev->cls);
  unregister_chrdev(led_dev->dev_major, "chr_dev_test");
}
//①实现模块的加载和卸载入口函数
module_init(chr_dev_init);
module_exit(chr_dev_exit);
MODULE_LICENSE("GPL");




目录
相关文章
|
6月前
|
Linux 程序员 芯片
【Linux驱动】普通字符设备驱动程序框架
【Linux驱动】普通字符设备驱动程序框架
|
Linux
字符设备驱动编程①
字符设备驱动编程①
153 0
|
Linux C语言
Linux驱动 | debugfs接口创建
Linux驱动 | debugfs接口创建
字符设备的编写二
字符设备的编写二
71 0
|
存储 API Windows
驱动开发:内核遍历文件或目录
在笔者前一篇文章`《驱动开发:内核文件读写系列函数》`简单的介绍了内核中如何对文件进行基本的读写操作,本章我们将实现内核下遍历文件或目录这一功能,该功能的实现需要依赖于`ZwQueryDirectoryFile`这个内核API函数来实现,该函数可返回给定文件句柄指定的目录中文件的各种信息,此类信息会保存在`PFILE_BOTH_DIR_INFORMATION`结构下,通过遍历该目录即可获取到文件的详细参数,如下将具体分析并实现遍历目录功能。
1443 0
|
存储 API
驱动开发:内核文件读写系列函数
在应用层下的文件操作只需要调用微软应用层下的`API`函数及`C库`标准函数即可,而如果在内核中读写文件则应用层的API显然是无法被使用的,内核层需要使用内核专有API,某些应用层下的API只需要增加Zw开头即可在内核中使用,例如本章要讲解的文件与目录操作相关函数,多数ARK反内核工具都具有对文件的管理功能,实现对文件或目录的基本操作功能也是非常有必要的。
280 0
|
Linux
linux驱动开发 使用设备树编写一个led驱动程序
linux驱动开发 使用设备树编写一个led驱动程序
253 0
字符设备驱动代码的编写
传统方法,优点简单缺点就是不易宽展,硬件更换了板子都要重新写代码重新比编译,硬件的每次改动都要重新修改软件版本增加工作量。
|
Linux
树莓派内核驱动编写——添加与调用
树莓派内核驱动编写——添加与调用
473 0
|
Linux 编译器 芯片
嵌入式Linux开发: 编写EEPROM驱动_采用杂项字符设备框架
嵌入式Linux开发: 编写EEPROM驱动_采用杂项字符设备框架
334 0
嵌入式Linux开发: 编写EEPROM驱动_采用杂项字符设备框架