我在ARM板上写的第一个驱动程序

发布时间:2023-03-27  

摘要:搞嵌入式有两个方向,一个是嵌入式软件开发(MCU方向),另一个是嵌入式软件开发(Linux方向)。其中,MCU方向基本是裸机开发和RTOS开发,而Linux开发方向又分为驱动开发和应用开发。相较于驱动开发,应用开发相对简单一些,因为搞驱动你要和Linux内核打交道。而我们普通的单片机开发就是应用开发,和Linux开发没多大区别,单片机你去调别人写好的库,Linux应用你也是调别人的驱动程序。

很多人学习的路线是:单片机到RTOS,再到Linux,这个路线其实是非常好,循序渐进。因为你学了单片机,所以你对RTOS的学习会很容易理解,单片机+RTOS在市面上也可以找到一个很好的工作。因为你学了RTOS,你会发现Linux驱动开发其实和RT-Thread的驱动程序非常像,其实RT-Thread驱动大概率可能是仿Linux驱动而写的。所以,如果你现在在学RT-Thread,那么你后面去搞Linux驱动也是非常容易上手。

当然,做驱动去之前你还是要学习一下ubuntu操作系统、ARM裸机和linux系统移植,其目的就是为学习嵌入式linux驱动开发做准备。话不多说,先来一个hello驱动程序


在Linux中,驱动分为三大类:

  • 字符设备驱动

    • 字符设备驱动是占用篇幅最大的一类驱动,因为字符设备最多,从最简单的点灯到 I2C、SPI、音频等都属于字符设备驱动的类型。

  • 块设备驱动

    • 块设备和网络设备驱动要比字符设备驱动复杂,就是因为其复杂所以半导体厂商一般都给我们编写好了,大多数情况下都是直接可以使用的。

    • 所谓的块设备驱动就是存储器设备的驱动,比如 EMMC、NAND、SD 卡和 U 盘等存储设备,因为这些存储设备的特点是以存储块为基础,因此叫做块设备。

  • 网络设备驱动

    • 网络设备驱动很好理解,不管是有线的还是无线的,都属于网络设备驱动的范畴。一个设备可以属于多种设备驱动类型,比如 USB WIFI,其使用 USB 接口,所以属于字符设备,但是其又能上网,所以也属于网络设备驱动。

我使用的Linux内核版本为 4.1.15,其支持设备树Device tree。开发板是正点原子送的Linux-MINI板,你用其他家的板子也是一样的,没有任何影响。


1. 字符设备驱动简介

字符设备是Linux驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。

那么在Linux下的应用程序是如何调用驱动程序的呢?Linux 应用程序对驱动程序的调用如图所示:

在Linux 中一切皆为文件,驱动加载成功以后会在/dev目录下生成一个相应的文件,应用程序通过对这个名为/dev/xxx(xxx是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。写驱动的人必须要懂linux内核,因为驱动程序就是根据内核的函数去写的,写应用的人不需要懂linux内核,只需要熟悉驱动函数就可以了。比如现在有个叫做/dev/led的驱动文件,是led灯的驱动文件。应用程序使用open函数来打开文件/dev/led,使用完成以后使用close函数关闭/dev/led 这个文件。open和 close 就是打开和关闭led驱动的函数,如果要点亮或关闭led,那么就使用write 函数来操作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开led的控制参数。如果要获取led 灯的状态,就用 read 函数从驱动中读取相应的状态。应用程序运行在用户空间,而Linux 驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用open函数打开/dev/led这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。open、close、write 和read等这些函数是由C库提供的,在Linux系统中,系统调用作为C库的一部分。当我们调用 open 函数的时候流程,如下图所示:

其中关于C库以及如何通过系统调用“陷入”到内核空间这个我们不用去管,我们关注的是应用程序和具体的驱动,应用程序使用到的函数在具体驱动程序中都有与之对应的函数,比如应用程序中调用了open这个函数,那么在驱动程序中也得有一个名为open的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数。在Linux内核文件include/linux/fs.h中有个叫做file_operations的结构体,此结构体就是Linux内核驱动操作函数集合,我们可以将linux内核文件下载下来,然后用source insight打开看看。内容如下所示:点击此处下载linux内核源码:


struct file_operations {

 struct module *owner;

 loff_t (*llseek) (struct file *, loff_t, int);

 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

 ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);

 ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);

 int (*iterate) (struct file *, struct dir_context *);

 unsigned int (*poll) (struct file *, struct poll_table_struct *);

 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

 long (*compat_ioctl) (struct file *, unsigned int, unsigned long);

 int (*mmap) (struct file *, struct vm_area_struct *);

 int (*mremap)(struct file *, struct vm_area_struct *);

 int (*open) (struct inode *, struct file *);

 int (*flush) (struct file *, fl_owner_t id);

 int (*release) (struct inode *, struct file *);

 int (*fsync) (struct file *, loff_t, loff_t, int datasync);

 int (*aio_fsync) (struct kiocb *, int datasync);

 int (*fasync) (int, struct file *, int);

 int (*lock) (struct file *, int, struct file_lock *);

 ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);

 unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);

 int (*check_flags)(int);

 int (*flock) (struct file *, int, struct file_lock *);

 ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);

 ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);

 int (*setlease)(struct file *, long, struct file_lock **, void **);

 long (*fallocate)(struct file *file, int mode, loff_t offset,

     loff_t len);

 void (*show_fdinfo)(struct seq_file *m, struct file *f);

#ifndef CONFIG_MMU

 unsigned (*mmap_capabilities)(struct file *);

#endif

};



  • 第 1589 行,owner 拥有该结构体的模块的指针,一般设置为THIS_MODULE。

  • 第 1590 行,llseek函数用于修改文件当前的读写位置。

  • 第 1591 行,read函数用于读取设备文件。

  • 第 1592 行,write函数用于向设备文件写入(发送)数据。

  • 第 1596 行,poll是个轮询函数,用于查询设备是否可以进行非阻塞的读写。

  • 第 1597 行,unlocked_ioctl函数提供对于设备的控制功能,与应用程序中的ioctl函数对应。

  • 第 1598 行,compat_ioctl函数与unlocked_ioctl函数功能一样,区别在于在64位系统上,32位的应用程序调用将会使用此函数。在32位的系统上运行32位的应用程序调用的是unlocked_ioctl。

  • 第 1599 行,mmap函数用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如LCD驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。

  • 第 1601 行,open 函数用于打开设备文件。

  • 第 1603 行,release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应。

  • 第 1604 行,fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。

  • 第 1605 行,aio_fsync函数与 fasync 函数的功能类似,只是aio_fsync是异步刷新待处理的数据。

2. 字符设备驱动开发

学习裸机或者STM32的时候关于驱动的开发就是初始化相应的外设寄存器,在Linux驱动开发中肯定也是要初始化相应的外设寄存器,这个是毫无疑问的。只是在Linux驱动开发中我们需要按照其规定的框架来编写驱动,所以说学Linux驱动开发重点是学习其驱动框架。

2.1 APP打开的文件在内核中如何表示

APP使用open函数打开文件时,可以得到一个整数,这个整数被称为文件句柄。对于APP的每一个文件句柄,在内核里面都有一个struct file与之对应。

struct file

我们使用open打开文件时,传入的 flags、mode等参数会被记录在内核中对应的struct file结构体里(f_flags、f_mode):

int open(const char *pathname, int flags, mode_t mode);

去读写文件时,文件的当前偏移地址也会保存在struct file结构体的f_pos成员里。

open->struct file

打开字符设备节点时,内核中也有对应的struct file注意这个结构体中的结构体:struct file_operations *f_op,这是由驱动程序提供的。

驱动程序的 open/read/write

结构体struct file_operations的定义如下,上面也讲过了。

2.2 编写驱动程序的步骤

  • 1、确定主设备号,也可以让内核分配。

  • 2、定义自己的file_operations结构体。

  • 3、实现对应的drv_open/drv_read/drv_write等函数,填入file_operations结构体。

  • 4、把file_operations结构体告诉内核:register_chrdev。

  • 5、谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数。

  • 6、有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev。

  • 7、其他完善:提供设备信息,自动创建设备节点:class_create,device_create。

2.3 试验程序编写

应用程序调用open函数打开hello_drv这个设备,打开以后可以使用write 函数向hello_drv的写缓冲区writebuf中写入数据(不超过 100 个字节),也可以使用read函数读取读缓冲区readbuf中的数据操作,操作完成以后应用程序使用close函数关闭chrdevbase设备。

hello_drv.c

#include


#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include


/* 1. 确定主设备号*/

static int major = 200;

static char kernel_buf[1024];

static struct class *hello_class;



#define MIN(a, b) (a < b ? a : b)


/* 3. 实现对应的open/read/write等函数,填入file_operations结构体  */

static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)

{

 int err;

 printk("%s %s line %dn", __FILE__, __FUNCTION__, __LINE__);

 err = copy_to_user(buf, kernel_buf, MIN(1024, size));

 return MIN(1024, size);

}


static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)

{

 int err;

 printk("%s %s line %dn", __FILE__, __FUNCTION__, __LINE__);

 err = copy_from_user(kernel_buf, buf, MIN(1024, size));

 return MIN(1024, size);

}


static int hello_drv_open (struct inode *node, struct file *file)

{

 printk("%s %s line %dn", __FILE__, __FUNCTION__, __LINE__);

 return 0;

}


static int hello_drv_close (struct inode *node, struct file *file)

{

 printk("%s %s line %dn", __FILE__, __FUNCTION__, __LINE__);

 return 0;

}


/* 2. 定义自己的file_operations结构体*/

static struct file_operations hello_drv = {

 .owner  = THIS_MODULE,

 .open    = hello_drv_open,

 .read    = hello_drv_read,

 .write   = hello_drv_write,

 .release = hello_drv_close,

};


/* 4. 把file_operations结构体告诉内核:注册驱动程序                */

/* 5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 */

static int __init hello_init(void)

{

 int retvalue;

 

 printk("%s %s line %dn", __FILE__, __FUNCTION__, __LINE__);

 retvalue = register_chrdev(major, "hello_drv", &hello_drv);  /* /dev/hello */

 if(retvalue < 0){

  printk("chrdevbase driver register failedrn");

 }

 printk("chrdevbase init!rn");

 return 0;

}


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

static void __exit hello_exit(void)

{

 printk("%s %s line %dn", __FILE__, __FUNCTION__, __LINE__);

 unregister_chrdev(major, "hello_drv");

}



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

module_init(hello_init);

module_exit(hello_exit);


MODULE_LICENSE("GPL");

MODULE_AUTHOR("zhiguoxin");


2.4 测试程序编写

驱动编写好以后是需要测试的,一般编写一个简单的测试APP,测试APP运行在用户空间。测试APP很简单通过输入相应的指令来对hello_drv设备执行读或者写操作。


hello_drv_test.c

#include 
#include 
#include 
#include 
#include 
#include 

/*
app测试
./hello_drv_test -w www.zhiguoxin.cn
./hello_drv_test -r
*/
int main(int argc, char **argv)
{
 int fd;
 char buf[1024];
 int len;
 
 /* 1. 判断参数 */
 if (argc n", argv[0]);
  printf("       %s -rn", argv[0]);
  return -1;
 }

 /* 2. 打开文件 */
 fd = open("/dev/hello", O_RDWR);
 if (fd == -1)
 {
  printf("can not open file /dev/hellon");
  return -1;
 }

 /* 3. 写文件或读文件 */
 if ((0 == strcmp(argv[1], "-w")) && (argc == 3))
 {
  len = strlen(argv[2]) + 1;
  len = len 

文章来源于:电子工程世界    原文链接
本站所有转载文章系出于传递更多信息之目的,且明确注明来源,不希望被转载的媒体或个人可与我们联系,我们将立即进行删除处理。

我们与500+贴片厂合作,完美满足客户的定制需求。为品牌提供定制化的推广方案、专属产品特色页,多渠道推广,SEM/SEO精准营销以及与公众号的联合推广...详细>>

利用葫芦芯平台的卓越技术服务和新产品推广能力,原厂代理能轻松打入消费物联网(IOT)、信息与通信(ICT)、汽车及新能源汽车、工业自动化及工业物联网、装备及功率电子...详细>>

充分利用其强大的电子元器件采购流量,创新性地为这些物料提供了一个全新的窗口。我们的高效数字营销技术,不仅可以助你轻松识别与连接到需求方,更能够极大地提高“闲置物料”的处理能力,通过葫芦芯平台...详细>>

我们的目标很明确:构建一个全方位的半导体产业生态系统。成为一家全球领先的半导体互联网生态公司。目前,我们已成功打造了智能汽车、智能家居、大健康医疗、机器人和材料等五大生态领域。更为重要的是...详细>>

我们深知加工与定制类服务商的价值和重要性,因此,我们倾力为您提供最顶尖的营销资源。在我们的平台上,您可以直接接触到100万的研发工程师和采购工程师,以及10万的活跃客户群体...详细>>

凭借我们强大的专业流量和尖端的互联网数字营销技术,我们承诺为原厂提供免费的产品资料推广服务。无论是最新的资讯、技术动态还是创新产品,都可以通过我们的平台迅速传达给目标客户...详细>>

我们不止于将线索转化为潜在客户。葫芦芯平台致力于形成业务闭环,从引流、宣传到最终销售,全程跟进,确保每一个potential lead都得到妥善处理,从而大幅提高转化率。不仅如此...详细>>