从单片机步入Linux驱动开发(概念和Demo)

单片机的裸跑能实现很多功能,代码量也不大,但是在嵌入式领域不学习Linux,那写再多的代码也枉然,因为有无数的大牛给Linux添砖加瓦,并且Linux有开放源码,易于移植,资源丰富,免费等优点。

首先我们看一个Linux系统的内容:可以分为应用程序、库、操作系统、驱动程序。一共五个层次,如下图:
在这里插入图片描述

应用程序层是直接使用open read write ioctl这些库函数。Linux驱动开始是要根据项目需求编写具体的驱动程序,也就是open read write ioctl这些函数的具体内容。不同的外设有不同的open read…内容。

Linux驱动程序的分类:
可以分为3类。1、字符设备(character device) 2、块设备(block device) 3、网络接口(network interface)。

字符设备是能够像字节流一样被访问的设备,就是说对它的读写是以字节为单位的。比如串口,和简单的点灯驱动程序。

块设备上的数据以块的形式存放,比如NAND Flash上的数据就是以页为单位存放的。

网络接口同时具有字符设备,块设备的部分特点。网络设备有小到几个字节的,也有大到几千个字节的。UNIX式的操作系统访问网络接口的方法是给他们分配一个唯一的名字(比如eht0),但这个名字在文件系统中不存在对应的节点项。应用程序,内核和网络驱动程序间的通信完全不同于字符设备,块设备,库、内核提供了一套和数据包传输相关的函数,而不是open,read等函数。

驱动程序的加载和卸载:
可以将驱动程序编译进内核中,也可以将它作为模块在使用时再加载。在配置内核时,如果某个配置项被设为m,就表示它将会被编译成一个模块。

当使用insmod加载模块时,模块的初始化函数被调用,它用来向内核注册驱动程序;当使用rmmod卸载模块时,模块的清除函数被调用。在驱动代码中,这两个函数要么取固定的名字:init_module 和 cleanup_module,要么使用以下两行来标记它们

module_init(demo_init);
module_exit(demo_cleanup);

记住,这是固定格式。

Linux操作系统将所有的设备都看成文件,以操作文件的方式访问设备。应用程序不能直接操作硬件,而是使用统一的接口函数调用硬件驱动程序。这组接口被称为系统调用。

对于字符设备,open read write这些函数在集合在file_operation类型的数据结构中。file_operation结构在Linux内核的include/linux/fs.h文件中定义,整个结构体如下:

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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	int (*readdir) (struct file *, void *, filldir_t);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
	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 (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, struct dentry *, 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 (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);
	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 (*dir_notify)(struct file *filp, unsigned long arg);
	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);
};

当应用程序使用open函数打开某个设备时,上面结构体中的open成员就会调用。所以从这个角度来说,编写字符设备的驱动程序就是为具体硬件的file_operation结构编写具体的函数内容。不用全部函数实现,需要什么函数就编写什么函数。

每个设备都有一个file_operation,都用用open函数来调用这些设备,那他们之间有什么区别呢。有两个区分要点:
1、设备文件有主/次设备号。在PC上运行命令ls /dev/ttyS0 /dev/hda1 -l可以看到以下信息:

brw-rw----    1   root    disk   3,  1, Jan  30  2003  /dev/hda1
crw-rw----    1   root    uucp   4,  64, Jan 30  2003  /dev/ttyS0

brw-rw----中的b表示/dev/had1是个块设备,它的主设备号为3,次设备号为1;
crw-rw----中的c表示/dev/ttyS0是个字符设备,它的主设备号为4,次设备号为64.

2、模块初始化时,将主设备号与file_operation结构一起向内核注册。在初始化一个设备时,就会向内核注册,用register_chrdev函数注册。

所以编写一个(简单的)字符驱动程序的过程就可以分为两步:
1、编写驱动程序初始化函数。
2、构造file_operation结构中要用到的各个成员函数。

下面开始分析一个LED驱动程序Demo 。Demo是运行在s3c24c10芯片上的。
首先需要分析原理图,我们假设:
要点亮一个LED,引脚输出0 。
要熄灭一个LED,引脚输出1 。
模块的初始化函数和卸载函数如下:

static int __init s3c24xx_leds_init(void)
{
    int ret;

    /* 注册字符设备驱动程序
     * 参数为主设备号、设备名字、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;
    }
    
    printk(DEVICE_NAME " initialized\n");
    return 0;
}

/*
 * 执行”rmmod s3c24xx_leds.ko”命令时就会调用这个函数 
 */
static void __exit s3c24xx_leds_exit(void)
{
    /* 卸载驱动程序 */
    unregister_chrdev(LED_MAJOR, DEVICE_NAME);
}

/* 这两行指定驱动程序的初始化函数和卸载函数 */
module_init(s3c24xx_leds_init);
module_exit(s3c24xx_leds_exit);

注意看最后两个函数:module_init和module_exit这里就将具体的设备对应起来了。执行“insmod s3c24xx_leds.ko”命令时就会调用s3c24xx_leds_init函数,这个函数的核心就是register_chrdev函数,将主设备号LED_MAJOR与file_operations结构s3c24xx_leds_fops联系起来。执行rmmod s3c24xx_leds.ko命令时就会调用s3c24xx_leds_exit函数,它进而调用unregister_chrdev函数卸载驱动程序,功能与register_chrdev函数相反。

下面看看s3c24xx_leds_fops的组成

/* 这个结构是字符设备驱动程序的核心
 * 当应用程序操作设备文件时所调用的open、read、write等函数,
 * 最终会调用这个结构中指定的对应函数
 */
static struct file_operations s3c24xx_leds_fops = {
    .owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open   =   s3c24xx_leds_open,     
    .ioctl  =   s3c24xx_leds_ioctl,
};

file_operations 类型的s3c24xx_leds_fops 结构是驱动中最重要的数据结构,编写字符设备驱动程序的主要工作也是填充其中的各个成员。比如在字符设备里的open ioctl成员被设为s3c24xx_leds_open s3c24xx_leds_ioctl函数。

s3c24xx_leds_open 的代码如下:

/* 应用程序对设备文件/dev/leds执行open(...)时,
 * 就会调用s3c24xx_leds_open函数
 */
static int s3c24xx_leds_open(struct inode *inode, struct file *file)
{
    int i;
    
    for (i = 0; i < 4; i++) {
        // 设置GPIO引脚的功能:本驱动中LED所涉及的GPIO引脚设为输出功能
        s3c2410_gpio_cfgpin(led_table[i], led_cfg_table[i]);
    }
    return 0;
}

在应用程序执行open(…)系统调用时,s3c24xx_leds_open将被调用。用来使能GPIO为输出模式。s3c2410_gpio_cfgpin是用来配置GPIO引脚模式的,与单片机类似。

s3c24xx_leds_ioctl函数的代码如下:

/* 应用程序对设备文件/dev/leds执行ioclt(...)时,
 * 就会调用s3c24xx_leds_ioctl函数
 */
static int s3c24xx_leds_ioctl(
    struct inode *inode, 
    struct file *file, 
    unsigned int cmd, 
    unsigned long arg)
{
    if (arg > 4) {
        return -EINVAL;
    }
    
    switch(cmd) {
    case IOCTL_LED_ON:
        // 设置指定引脚的输出电平为0
        s3c2410_gpio_setpin(led_table[arg], 0);
        return 0;

    case IOCTL_LED_OFF:
        // 设置指定引脚的输出电平为1
        s3c2410_gpio_setpin(led_table[arg], 1);
        return 0;

    default:
        return -EINVAL;
    }
}

应用程序执行系统调用ioclt时,s3c24xx_leds_ioctl函数将被调用。

驱动程序编译:
将写好的驱动程序s3c24xx_leds.c文件放入Linux内核drivers/char子目录下,在drivers/char/Makefile中增加一行
obj-m += s3c24xx_leds.o//将驱动程序加入编译的队列
然后在内核根目录下执行"make modules",就可以生成模块drivers/char/s3c24xx_leds.ko。然后将该.ko文件放入你的嵌入式板子根文件系统的/lib/modules/2.6.22.6/目录下,就可以使用“insmod s3c24xx_leds”、“rmmod s3c24xx_leds”命令进行加载、卸载了。

最后一步
驱动程序测试:
首先需要在你的电脑里编写一个测试主程序:led_test.c。如下:

int main(int argc, char **argv)
{
    unsigned int led_no;
    int fd = -1;
    
    if (argc != 3)
        goto err;
        
    fd = open("/dev/leds", 0);  // 打开设备
    if (fd < 0) {
        printf("Can't open /dev/leds\n");
        return -1;
    }
    
    led_no = strtoul(argv[1], 0, 0) - 1;    // 操作哪个LED?
    if (led_no > 3)
        goto err;
    
    if (!strcmp(argv[2], "on")) {
        ioctl(fd, IOCTL_LED_ON, led_no);    // 点亮它
    } else if (!strcmp(argv[2], "off")) {
        ioctl(fd, IOCTL_LED_OFF, led_no);   // 熄灭它
    } else {
        goto err;
    }
    
    close(fd);
    return 0;
    
err:
    if (fd > 0) 
        close(fd);
    usage(argv[0]);
    return -1;
}

其中open.ioctl最终会调用驱动程序中的s3c24xx_leds_open、s3c24xx_leds_ioctl函数。
还需要在你的电脑里编写Makefile文件,如下:

CROSS=arm-linux-

all: led_test

led_test: led_test.c
	$(CROSS)gcc -o $@ led_test.c -static

clean:
	@rm -rf led_test *.o

然后自己电脑里执行make命令生成可执行程序led_test,将这个可执行文件放入板子根系统/usr/bin/目录。

然后,在板子跟文件系统建立如下设备文件:

mknod /dev/leds c 231 0//231是主设备编号,0是次设备编号。

接下来就可以直接运行les_test命令来操作LED了,点亮和熄灭如下:

led_test 1 on
led_test 1 off

到目前为止,一个简单LED驱动程序就编写好了,有问题可以交流一下,以上的内容学习自韦山东的书籍~

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
单片机转向Linux开发可以按照以下步骤进行: 1. 学习Linux基础知识:了解Linux操作系统的基本原理、文件系统、进程管理等概念和机制,可以通过阅读相关书籍、教程或参加培训课程来学习。 2. 选择合适的开发平台:选择一个适合进行Linux开发的硬件平台,例如Raspberry Pi、BeagleBone等,这些平台都支持Linux操作系统。 3. 安装Linux操作系统:将选择的Linux发行版安装到开发平台上,可以通过官方镜像或社区提供的镜像文件进行安装。 4. 学习Linux应用开发:学习Linux上的应用开发,掌握C/C++等编程语言以及相关工具链的使用,了解Linux下的文件操作、进程通信、网络编程等技术。 5. 移植嵌入式代码:将原有的单片机代码移植到Linux平台上,可以使用嵌入式开发工具链和相关库函数进行移植。 6. 开发应用程序:根据需求开发应用程序,可以利用Linux提供的丰富的开发资源和库函数进行开发,例如使用Qt进行图形界面开发。 7. 调试和测试:在开发过程中进行调试和测试,确保应用程序在Linux平台上的稳定性和正常运行。 8. 部署和发布:将开发完成的应用程序部署到Linux平台上,并进行发布,可以通过打包成软件包、制作镜像等方式进行分发。 需要注意的是,从单片机转向Linux开发需要掌握更多的知识和技术,因此需要花费一定的时间和精力进行学习和实践。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值