二、点亮LED
接着上一章,本章来实现控制LED的亮灭操作:
一、驱动框架
#include <linux/fs.h>
#include <linux/init.h> /* 定义文件内私有结构体 */
struct led_device {
struct cdev cdev;
int stat; /* 用于保存LED状态,0为灭,1为亮 */
}; /* LED write()函数 */
static ssize_t led_write(struct file *filep, const char __user * buf, size_t len, loff_t *ppos)
{
return ;
} /* LED open()函数 */
static int led_open(struct inode *inodep, struct file *filep)
{
return ;
} /* 把定义的函数接口集合起来,方便系统调用 */
static const struct file_operations led_fops = {
.open = led_open,
.write = led_write,
}; /* 驱动初始化函数 */
static int __init led_init(void)
{
return ;
} /* 驱动卸载函数 */
static void __exit led_exit(void)
{
} /* 声明段属性 */
module_init(led_init);
module_exit(led_exit); MODULE_LICENSE("GPL");
我们在驱动程序实现的write()和open()函数的格式必须遵循struct file_operations里面的函数指针:
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 *);
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 *);
...
};
通常我们不会实现struct file_operations里面的所有函数,只会实现一些针对某些设备需要用到的函数
驱动中定义的led_init()和led_exit()函数需要实现向上层注册字符设备、struct file_operations等
这两个函数所使用到的__init和__exit,在此以__init为例展开:
#define __init __attribute__((".init.text")) \
__attribute__((__cold__)) \
__attribute__((no_instrument_function))
可以看到led_init()函数代码会被定位到.init.text段中
这个段定义在include/asm-generic/vmlinux.lds.h中
#define INIT_TEXT_SECTION(inittext_align) \
. = ALIGN(inittext_align); \
.init.text : AT(ADDR(.init.text) - LOAD_OFFSET) { \
VMLINUX_SYMBOL(_sinittext) = .; \
INIT_TEXT \
VMLINUX_SYMBOL(_einittext) = .; \
}
在arch/arm/kernel/vmlinux.lds.S中使用
INIT_TEXT_SECTION()
驱动程序中调用的module_init()和module_exit()函数用于向上层注册led_init()和led_exit()
#define module_init(x) __initcall(x)
#define __initcall(fn) device_initcall(fn) ... #define core_initcall(fn) __define_initcall("1",fn,1)
#define core_initcall_sync(fn) __define_initcall("1s",fn,1s)
#define postcore_initcall(fn) __define_initcall("2",fn,2)
#define postcore_initcall_sync(fn) __define_initcall("2s",fn,2s)
#define arch_initcall(fn) __define_initcall("3",fn,3)
#define arch_initcall_sync(fn) __define_initcall("3s",fn,3s)
#define subsys_initcall(fn) __define_initcall("4",fn,4)
#define subsys_initcall_sync(fn) __define_initcall("4s",fn,4s)
#define fs_initcall(fn) __define_initcall("5",fn,5)
#define fs_initcall_sync(fn) __define_initcall("5s",fn,5s)
#define rootfs_initcall(fn) __define_initcall("rootfs",fn,rootfs)
#define device_initcall(fn) __define_initcall("6",fn,6)
#define device_initcall_sync(fn) __define_initcall("6s",fn,6s)
#define late_initcall(fn) __define_initcall("7",fn,7)
#define late_initcall_sync(fn) __define_initcall("7s",fn,7s) ... #define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn; \
LTO_REFERENCE_INITCALL(__initcall_##fn##id)
最终,led_init()函数的地址会被定位到.initcall6.init段中
那么initcall为什么要分成这么多段呢?
系统的初始化时,所有的东西都必须按照一定的顺序初始化
对于驱动注册,是在上面的initcall6里面实现的。而要实现设备驱动的注册,必须要在设备驱动模型初始化完之后才能进行,否则如果设备驱动的管理程序都还没初始化,则驱动的注册肯定就有问题了。而要想让初始化阶段先初始化驱动的管理程序,如果靠函数依次调用,因为内核的内容太庞大,这明显不可能实现。所以初始化阶段,内核按先后顺序分了16个子阶段阶段
通常越靠前的是越底层越核心的初始化,通常后面的初始化对前面的都有一定的依赖
总结起来就是:
1. __init修饰的函数,表示把该函数放入init.text这个代码段
2. module_init修饰的函数,表示把init.text代码段中的函数地址,存到init.data段
3. 内核启动时,会根据initcall后面的数字大小,分层进行调用初始化
驱动程序中的MODULE_LICENSE("GPL");用于表示许可证,不需要深度了解
现在我们在框架的基础上完成注册字符设备、struct file_operations等操作
二、完成init()函数和exit()函数
... static int g_major;
module_param(g_major, int, S_IRUGO); static struct led_device* dev;
static struct class* scls;
static struct device* sdev; ... static int __init led_init(void)
{
int ret;
dev_t devt; /* 1. 申请设备号 */
if (g_major) {
devt = MKDEV(g_major, );
ret = register_chrdev_region(devt, , "led");
}
else
ret = alloc_chrdev_region(&devt, , , "led");
if (ret)
return ret; /* 2. 申请文件内私有结构体 */
dev = kzalloc(sizeof(struct led_device), GFP_KERNEL);
if (dev == NULL) {
ret = -ENOMEM;
goto fail_malloc;
} /* 3. 注册字符设备驱动 */
cdev_init(&dev->cdev, &led_fops); /* 初始化cdev并链接file_operations和cdev */
ret = cdev_add(&dev->cdev, devt, ); /* 注册cdev */
if (ret)
return ret; /* 4. 创建类设备,insmod后会生成/dev/led设备文件 */
scls = class_create(THIS_MODULE, "led");
sdev = device_create(scls, NULL, devt, NULL, "led"); return ; fail_malloc:
unregister_chrdev_region(devt, ); return ret;
} static void __exit led_exit(void)
{
/* 镜像注销 */
dev_t devt = MKDEV(g_major, ); device_destroy(scls, devt);
class_destroy(scls); cdev_del(&(dev->cdev));
kfree(dev); unregister_chrdev_region(devt, );
} ...
代码中第4行:module_param(g_major, int, S_IRUGO)表示int型变量g_major可以通过外部向内核传递值
S_IRUGO表示数值的权限为0444
函数原型如下,此函数用于在加载模块时或者模块加载以后传递参数给模块
module_param(name,type,perm);
函数参数:
name:模块参数的名称
type:模块参数的数据类型,如bool、charp(字符指针)、short、int、long、ulong(无符号long)
perm:模块参数的访问权限
代码中第15行:dev_t devt定义了设备号,为32位,其中高12位为主设备号,低20位为次设备号
主设备号用来表示一个特定的驱动程序;次设备号用来表示使用该驱动程序的各设备。例如TINY4412,有4个LED,每个LED都可以独立的打开或者关闭。那么,这个LED的字符设备驱动程序,可以将其主设备号注册成5号设备,次设备号分别为1、2、3和4。这里,次设备号就分别对应4个LED
设备文件通常都在/dev目录下:
如上图的/dev/tty,它的主设备号是5,次设备号是0
使用以下宏可以从dev_t中获取主设备号和次设备号:
MAJOR(dev_t dev)
MINOR(dev_t dev)
使用以下宏则可以通过主设备号和次设备号生成dev_t:
MKDEV(int major, int minor)
代码中第20行和第23行:register_chrdev_region()和alloc_chrdev_region()用于向系统申请设备号,这两个函数原型为:
int register_chrdev_region(dev_t from, unsigned count, const char *name) int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
register_chrdev_region()函数用于已知起始设备的设备号情况;而alloc_chrdev_region()函数用于设备号未知的情况,由系统分配并返回分配对的设备号
释放设备号函数原型为:
void unregister_chrdev_region(dev_t from, unsigned count)
代码中第28行:kzalloc()用于申请一片内核内存,并清空内存数据,详细了解可查看:Linux驱动函数解读第一节
Linux内核提供了一组函数操作cdev结构体:
cdev_init()用于初始化cdev的成员,并建立cdev和file_operations之间的链接
cdev_alloc()用于动态申请一个cdev内存,本节代码使用的申请内存函数为kzalloc()
cdev_add()函数和cdev_del()函数分别向系统添加和删除一个cdev,完成字符设备的注册和注销
代码中第7行:struct class用于表示一个类,类是一个设备的高层视图,它抽象出了低层的实现细节,大概意思就是抽象出了一个通用的接口,类似于C++的面向对象的编程方式
代码中第8行:struct device用于表示一个设备,关于device的注册过程可以查看Linux驱动函数解读第二节
我们可以把类当作一个班级,设备当作学生。班级用于容纳学生,当老师来上课时,老师只需要讲一遍,学生就都可以听到(函数抽象)
三、完成write()函数、open()函数和release()函数
static volatile unsigned long *gpm4con;
static volatile unsigned long *gpm4dat; /* LED write()函数 */
static ssize_t led_write(struct file *filep, const char __user * buf, size_t len, loff_t *ppos)
{
struct led_device *dev = filep->private_data; if (copy_from_user(&(dev->stat), buf, ))
return -EFAULT; if (dev->stat == )
*gpm4dat &= ~(( << ) | ( << ) | );
else
*gpm4dat |= (( << ) | ( << ) | ); return ;
} /* LED open()函数 */
static int led_open(struct inode *inodep, struct file *filep)
{
struct led_device *dev; dev = container_of(inodep->i_cdev, struct led_device, cdev);
// 放入私有数据中
filep->private_data = dev; // 映射LED
gpm4con = ioremap(0x110002E0, );
gpm4dat = gpm4con + ;
// 设为输入引脚,灭灯
*gpm4con = 0x1111;
*gpm4dat |= (( << ) | ( << ) | ); return ;
} static int led_close(struct inode *inodep, struct file *filep)
{
iounmap(gpm4con); return ;
} /* 把定义的函数接口集合起来,方便系统调用 */
static const struct file_operations led_fops = {
.owner = THIS_MODULE,
.write = led_write,
.open = led_open,
.release = led_close,
};
代码中第5行:write()函数使用了文件私有数据(filp->private_data)。实际上,大多数Linux驱动遵循一个“潜规则”,那就是将文件的私有数据private_data指向设备结构体,再用read()、write()等函数通过private_data访问设备结构体
需要注意的是,用户空间不能直接访问内核空间的内存,因此在read()函数中一般使用copy_to_user(),在write()函数中一般使用copy_from_user()来完成用户空间和内核空间的数据复制,两函数原型为:
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n) unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
函数参数以及返回值:
to:复制到的地址
from:待复制的地址
n:复制字节数
返回值:两函数均不返回被复制的字节数,成功返回0,失败返回负值
代码中第25行:container_of()函数可以参考:Linux驱动函数解读第三节
在Linux系统中,开启MMU后,我们就不能直接使用寄存器的硬件地址(或者说我们不知道,寄存器硬件地址被映射到哪块内存了),所以我们只能使用虚拟地址来操纵寄存器。而目前我们不知道虚拟地址,只知道物理地址
所以内核给我们提供了一个接口函数ioremap()。它会建立一个新的页表,可以通过寄存器的物理地址得到寄存器的虚拟地址。
void __iomem *ioremap(phys_addr_t offset, unsigned long size)
函数参数以及返回值:
offset:物理地址
size:寄存器大小
返回值:成功返回虚拟地址,失败返回-1
ioremap()函数对应的释放函数为iounmap():
void iounmap(void __iomem *addr)
函数参数:
addr:ioremap()函数返回的虚拟地址
四、完整代码
led源代码:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/device.h> #include <asm/uaccess.h>
#include <asm/io.h> /* 定义文件内私有结构体 */
struct led_device {
struct cdev cdev;
int stat; /* 用于保存LED状态,0为灭,1为亮 */
}; static int g_major;
module_param(g_major, int, S_IRUGO); static struct led_device* dev;
static struct class* scls;
static struct device* sdev; static volatile unsigned long *gpm4con;
static volatile unsigned long *gpm4dat; /* LED write()函数 */
static ssize_t led_write(struct file *filep, const char __user * buf, size_t len, loff_t *ppos)
{
struct led_device *dev = filep->private_data; if (copy_from_user(&(dev->stat), buf, ))
return -EFAULT; if (dev->stat == )
*gpm4dat &= ~(( << ) | ( << ) | );
else
*gpm4dat |= (( << ) | ( << ) | ); return ;
} /* LED open()函数 */
static int led_open(struct inode *inodep, struct file *filep)
{
struct led_device *dev; dev = container_of(inodep->i_cdev, struct led_device, cdev);
// 放入私有数据中
filep->private_data = dev; // 映射LED
gpm4con = ioremap(0x110002E0, );
gpm4dat = gpm4con + ;
// 设为输出引脚,灭灯
*gpm4con = 0x1111;
*gpm4dat |= (( << ) | ( << ) | ); return ;
} static int led_close(struct inode *inodep, struct file *filep)
{
iounmap(gpm4con); return ;
} /* 把定义的函数接口集合起来,方便系统调用 */
static const struct file_operations led_fops = {
.write = led_write,
.open = led_open,
.release = led_close,
}; static int __init led_init(void)
{
int ret;
dev_t devt; /* 1. 申请设备号 */
if (g_major) {
devt = MKDEV(g_major, );
ret = register_chrdev_region(devt, , "led");
}
else {
ret = alloc_chrdev_region(&devt, , , "led");
g_major = MAJOR(devt);
}
if (ret)
return ret; /* 2. 申请文件内私有结构体 */
dev = kzalloc(sizeof(struct led_device), GFP_KERNEL);
if (dev == NULL) {
ret = -ENOMEM;
goto fail_malloc;
} /* 3. 注册字符设备驱动 */
cdev_init(&dev->cdev, &led_fops); /* 初始化cdev并链接file_operations和cdev */
ret = cdev_add(&dev->cdev, devt, ); /* 注册cdev */
if (ret)
return ret; /* 4. 创建类设备,insmod后会生成/dev/led设备文件 */
scls = class_create(THIS_MODULE, "led");
sdev = device_create(scls, NULL, devt, NULL, "led"); return ; fail_malloc:
unregister_chrdev_region(devt, ); return ret;
} static void __exit led_exit(void)
{
/* 镜像注销 */
dev_t devt = MKDEV(g_major, ); device_destroy(scls, devt);
class_destroy(scls); cdev_del(&(dev->cdev));
kfree(dev); unregister_chrdev_region(devt, );
} /* 声明段属性 */
module_init(led_init);
module_exit(led_exit); MODULE_LICENSE("GPL");
Makefile:
KERN_DIR = /work/tiny4412/tools/linux-3.5 all:
make -C $(KERN_DIR) M=`pwd` modules clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order obj-m += led.o
测试文件:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h> int main(int argc, char** argv)
{
if (argc != ) {
printf("Usage: \n");
printf("%s <on|off>\n", argv[]);
return -;
} int fd;
fd = open("/dev/led", O_RDWR);
if (fd < ) {
printf("can't open /dev/led\n");
return -;
} char stat;
if ( == strcmp(argv[], "off")) {
stat = ;
write(fd, &stat, );
} else {
stat = ;
write(fd, &stat, );
}
close(fd); return ;
}
需要注意的是,Makefile中的KERN_DIR = /work/tiny4412/tools/linux-3.5需要改成自己的linux内核路径。
执行make命令编译.ko驱动程序
执行arm-linux-gcc test.c -o test_led
将驱动程序和测试程序复制到文件系统中,完成后如下图:
启动开发板,执行:
[root @ lioker / ] #cd /my_driver/dong/01.led/
挂载模块insmod:
[root @ lioker 01.led ] #insmod led.ko
[root @ lioker 01.led ] #./test_led on
[root @ lioker 01.led ] #./test_led off
卸载模块rmmod:
[root @ lioker 01.led ] #rmmod led.ko
可看到对应现象
其实源代码中的读写寄存器方式并不是值得推荐的,内核给我们提供了封装好的函数,如:
#define readb(c) ({ u8 __v = readb_relaxed(c); __iormb(); __v; })
#define readw(c) ({ u16 __v = readw_relaxed(c); __iormb(); __v; })
#define readl(c) ({ u32 __v = readl_relaxed(c); __iormb(); __v; }) #define writeb(v,c) ({ __iowmb(); writeb_relaxed(v,c); })
#define writew(v,c) ({ __iowmb(); writew_relaxed(v,c); })
#define writel(v,c) ({ __iowmb(); writel_relaxed(v,c); })
函数使用可查看Linux驱动函数解读第四节
下一章 三、中断分析以及按键中断
二、点亮LED的更多相关文章
- zigbee学习之路(二)点亮LED
一.前言 今天,我来教大家如何点亮led,这也是学习开发板最基础的步骤了. 二.原理分析 cc2530芯片跟虽然是51的内核,但是它跟51单片机还是有区别的,51单片机不需要对IO口进行配置,而cc2 ...
- 字符型设备驱动程序-first-printf以及点亮LED灯(三)
根据 字符型设备驱动程序-first-printf以及点亮LED灯(二) 学习 修改函数 中的printf 为 printk. #include <linux/module.h> /* ...
- JZ2440裸机点亮LED【学习笔记】
平台:jz2440 作者:庄泽彬(欢迎转载,请注明作者) 说明:韦东山一期视频学习笔记 一.我们首先来做第一个实验,用汇编语言点亮板子上的LED. 1.1 LED的原理图 从下面的原理图可知LED1是 ...
- Mini2440上的第一个程序——点亮Led
手头的Mini2440搁置了两年半之后,我再次决定拿出它,重新尝试嵌入式Linux的学习. 我使用的是友善之臂的Mini2440开发板.韦东山的<嵌入式Linux应用开发完成手册>及其视频 ...
- S3C2440—3.用点亮LED来熟悉裸机开发的详细流程
文章目录 一.硬件知识 1.LED原理图 2.芯片手册 Ⅰ.找LED原理图 Ⅱ.找对应引脚 Ⅲ.在芯片手册中查找引脚信息 Ⅳ.查看寄存器说明 Ⅴ.配置寄存器 二.S3C2440框架与启动过程 三.要用 ...
- Arduino 极速入门系列–1 点亮 LED
本篇内容为,使用 Arduino 点亮 LED 的做法示范.很简单的一个入门示范.我们让 LED 闪. 本篇使用到的工具和材料 Arduino Mini Pro 1 PCS Mini USB 数据线 ...
- STM32学习笔记——点亮LED
STM32学习笔记——点亮LED 本人学习STM32是直接通过操作stm32的寄存器,使用的开发板是野火ISO-V2版本: 先简单的介绍一下stm32的GPIO: stm32的GPIO有多种模式: 1 ...
- STM32F407第一步之点亮LED
STM32F407第一步之点亮LED. 要点亮LED,首先了解一下F4的GPIO模块.首先看一下STM32F4数据手册,GPIO模块的内部结构图 看上去有点复杂,不要怕,慢慢理解就可以了.对外引脚那里 ...
- ARM学习篇一 点亮LED
要点亮LED,先决条件是什么,当然得有相应的硬件设施.板子的整个电路图比较大,我就直接取相关部分. 给发光二级管加上3.3v电压后,通过1k电阻,直接与S3C2440连接.至于为什么要加电阻,大家应该 ...
随机推荐
- nginx location rewrite 禁止访问某个目录
Location 指令,是用来为匹配的 URI 进行配置 http://www.baidu.com/test/index.php?a=1&b=ture 这里面/test/index.php ...
- 走进JavaWeb技术世界14:通过项目逐步深入了解Mybatis(一)
通过项目逐步深入了解Mybatis(一) 2017-06-12 文章导航 Mybatis 和 SpringMVC 通过订单商品案例驱动 官方中文地址:http://www.mybatis.org/my ...
- IDEA的版本控制
参考:https://blog.csdn.net/qq_35246620/article/details/70792861 1.从远程仓库下载项目 2.提交项目到远程仓库
- Hibernate 基本使用
Hibernate框架概述 一.什么是框架 软件的一个半成品,已经帮你完成了部分功能. 把一些不确定的东西,按照框架要求,达到相应的功能 Hibernate是JavaEE技术三层架构所用到的技术 二. ...
- CentOS7 修改设置静态IP和DNS
最近因为学习Puppet,用虚拟机装了个CentOS,使用的NAT的网络模式,为了防止再次启动系统的时候网络IP发生变化,因此设置静态IP和DNS. 由于CentOS是最小化安装,没有ifconfig ...
- 详谈mysqldump数据导出的问题
1,使用mysqldump时报错(1064),这个是因为mysqldump版本太低与当前数据库版本不一致导致的. mysqldump: Couldn't execute 'SET OPTION SQL ...
- PHP uploadify io error错误如何解决?
首先说结论吧,这个问题不是很好解决,因为因素很多,只能一步一步排除. 一般原因是: 方法一: 用php+apache上传文件的时候,由于文件过大,容易导致上传失败,解决办法: 修改php.ini中: ...
- Vrms、Vpk、W、dBm、dBW、dBuV、dBm/Hz
负载阻抗Z 在做这些单位转换前第一个需要提到的就是负载阻抗(Z, Ohm),我们在测试测量中说某个量为上面的某一个单位时候,都包含了一个前提条件,那就是负载阻抗,离开了负载阻抗你说的这些总带有一丝耍流 ...
- 【419】C语言语句
判断语句 C 语言提供了以下类型的判断语句.点击链接查看每个语句的细节. 语句 描述 if 语句 一个 if 语句 由一个布尔表达式后跟一个或多个语句组成. if...else 语句 一个 if 语句 ...
- Delphi ADOQuery的速度优化
今天终于把纠缠了几天的问题改完了,说到底只是一个很小的问题,就是ADOQuery的一个小属性. 把控件DBGridEh的一列的checkbox设为true,将其绑定DataSource和ADOQuer ...