嵌入式Linux学习笔记(三) 字符型设备驱动--LED的驱动开发
在成功构建了一个能够运行在开发板平台的系统后,下一步就要正式开始项目的实现(这里前提是有一定的C语言基础,对ARM体系的软/硬件有一定了解),根据需求分解任务,可以发现包含的外设有LED,BEEP,RS232,六轴传感(SPI接口),光环境传感器(I2C),音频输出, RTC等,如果按照这个顺序去实现驱动,一定程度其实又回归最初的模块学习的策略,如果从应用的角度,先实现基本框架,来验证能否满足预期,这比测试模块驱动的更重要,也更容易有产出感。 按照这个需求,就可以先把实际工作分解为如下几个步骤:
1.完成LED驱动,能够正常控制LED的点亮和关闭(本节完成)
2.完成RS232的驱动,能够实现串口的通讯
3.定义一套上位机、下位机之间的通讯协议(也可以使用主流工业协议如Modbus), 并在上位机和下位机编码实现通讯协议的组包和解包
4.实现一套界面化的上位机工具,带有调试功能和控制功能
既然初步的工作已经清晰,就可以开始第一步的工作,完成LED的驱动。
参考资料
1. 开发板原理图 《IMX6UL_ALPHA_V2.0(底板原理图)》 《IMX6ULL_CORE_V1.4(核心板原理图)》
2. 正点原子《Linux驱动开发指南说明V1.0》 第四十章 字符驱动设备开发
3. 宋宝华 《Linux设备驱动开发详解:基于最新的Linux 4.0内核》 第六章 字符驱动设备
4. 恩智浦官方手册 《IMX6ULL参考手册》Chapter 18:Clock Controller Module(CCM)/Chapter 28:General Purpose Input/Output (GPIO)
LED硬件配置实现
首先当然要确定原理图,下图来自底板和核心板原理图。
通过追踪就可以查看当前使用LED的引脚为GPIO1_IO3。
确定硬件后,第一步就是配置GPIO需要使用的寄存器了,对于使用过单片机的用户来说,对于GPIO这类外设,一般包含以下步骤:
1. 使能模块时钟
2. 配置模块或者相关模块的寄存器,使模块复用到需要的功能
3. 提供对外访问的接口
对于嵌入式Linux来说,这部分也没有区别,硬件初始化接口(具体寄存器可使用《IMX6ULL参考手册》查询)
/**
2 * LED硬件初始化,引脚GPIO1_IO03
3 *
4 * @param NULL
5 *
6 * @return NULL
7 */
static void led_gpio_init(void)
{
u32 value; /*1. 寄存器地址映射*/
IMX6U_CCM_CCGR1 = ioremap(0X020C406C, ); //时钟使能
SW_MUX_GPIO1_IO03 = ioremap(0X020E0068, ); //复用功能设置
SW_PAD_GPIO1_IO03 = ioremap(0X020E02F4, ); //设置PAD的输出状态
GPIO1_DR = ioremap(0X0209C000, ); //设置LED输出
GPIO1_GDIR = ioremap(0X0209C004, ); //设置GPIO的状态 /*2.时钟使能*/
value = readl(IMX6U_CCM_CCGR1);
value &= ~( << );
value |= ( << );
writel(value, IMX6U_CCM_CCGR1);
printk("led write 0"); /*3.复用功能设置*/
writel(, SW_MUX_GPIO1_IO03); /*4.引脚IO功能设置*/
writel(0x10B0, SW_PAD_GPIO1_IO03); /*5.引脚输出功能配置*/
value = readl(GPIO1_GDIR);
value |= ( << ); /* 设置新值 */
writel(value, GPIO1_GDIR); /*5.关闭LED显示,高电平关闭*/
value = readl(GPIO1_DR);
value |= ( << );
writel(value, GPIO1_DR); printk(KERN_INFO"led hardware init ok\r\n");
}
硬件资源释放.
/**
* 释放硬件资源
*
* @param NULL
*
* @return NULL
*/
static void led_gpio_release(void)
{
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
}
硬件设备管理
/**
*LED灯开关切换
*
* @param status LED开关状态,1开启,0关闭
*
* @return NULL
*/
static void led_switch(u8 status)
{
u32 value;
value = readl(GPIO1_DR); switch(status)
{
case LED_OFF:
printk(KERN_INFO"led off\r\n");
value |= ( << );
writel(value, GPIO1_DR);
break;
case LED_ON:
printk(KERN_INFO"led on\r\n");
value &= ~( << );
writel(value, GPIO1_DR);
break;
default:
printk(KERN_INFO"Invalid LED Set");
break;
}
}
至此,我们就实现了和硬件执行的接口
led_gpio_init()/led_gpio_release()/led_switch(n)
嵌入式内核模块实现
嵌入式内核模块的参考本系列的第一篇文件,主要提供加载到Linux内核,用于insmod和rmmod访问的接口,这部分因为已经讲过,如果希望理解就去看第一节内容,或者参考上面提供的资料。
Linux加载的接口:
/**
* 驱动入口函数
*
* @param NULL
*
* @return the error code, 0 on initialization successfully.
*/
static int __init led_module_init(void)
{
//此处添加设备注册的实现
//......
}
module_init(led_module_init);
Linux释放的接口:
/**
* 驱动释放函数
*
* @param NULL
*
* @return the error code, 0 on release successfully.
*/
static void __exit led_module_exit(void)
{
//此处添加设备注销的实现
//......
}
module_exit(led_module_exit);
此外,在添加驱动说明,如作者,许可证和驱动说明等
MODULE_AUTHOR("zc"); //模块作者
MODULE_LICENSE("GPL v2"); //模块许可协议
MODULE_DESCRIPTION("led driver"); //模块许描述
MODULE_ALIAS("led_driver"); //模块别名
至此本节的准备工作全部完成,下面就开始完成总线上设备的创建,这也是本章最核心的特征。
设备创建和释放
设备创建如果按照固定的结构,使用起来虽然有些困难,如果按照官方流程来实现,是有迹可循的。但是如何从应用层的访问接口open,read,write,close到底层驱动的xxx_open, xxx_read, xxx_write, xxxx_close的调用,这部分的理解在整个驱动机制的重要部分,这部分的难度当然不是一次可以讲清楚的,这里先抛砖引玉,在后面驱动的实践中会步步深入去理解。
作为熟悉C语言知识的开发者来说,可以很清楚open这一类接口是用来访问文件的,而在Linux中,字符型设备和块设备就体现了"一切都是文件"的思想,参考《Linux设备驱动开发详解:基于最新的Linux 4.0内核》第5章的说明,
通关VFS(virtual Filesytem), 将上层接口操作/dev/*下的设备文件,最后访问到驱动内部注册的实际操作硬件的接口。
想理解这部分知识,就需要理解应用层接口做了什么工作,参考这篇文章,https://www.jianshu.com/p/f3f5a33f2c59,以open为例。
open函数,这里可以简述步骤(下面所有实现在linux/fs/namei.c文件中)
1.获取一个可用的id,用于外部的记录,如fd
2.根据name名称如"/dev/led"获取file指针信息,包含设备的实际信息
3.将fd与file关联起来,后续就可以通关fd直接访问file指针的内容(设备端信息指针file),至此我们就获取设备端的信息
4.创建inode类型的数据nd,这部分就是VFS中链接到真正驱动的位置信息,其中包含的cdev *i_cdev即是和设备相关的指针,至于这部分如何链接到实际设备,等后续深入了解后在详细了解。
5.file和nd的链接则依靠file->f_path.mnt和nd->path.mnt配置相等实现
到达这一步,当然还远远不够,但目前只是初步入门,先不过度深入,下面开始驱动编写。其中在module_init中主要完成注册流程,module_exit中完成释放流程,此外还要实现访问LED的接口,具体如下:
1.访问LED的硬件接口链接
/**
* 获取LED资源
*
* @param inode
* @param filp
*
* @return the error code, 0 on initialization successfully.
*/
int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &led_driver_info;
return ;
} /**
* 释放LED设备资源
*
* @param inode
* @param filp
*
* @return the error code, 0 on initialization successfully.
*/
int led_release(struct inode *inode, struct file *filp)
{
return ;
} /**
* 从LED设备读取数据
*
* @param filp
* @param buf
* @param count
* @param f_ops
*
* @return the error code, 0 on initialization successfully.
*/
ssize_t led_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
return ;
} /**
* 向LED设备写入数据
*
* @param filp
* @param buf
* @param count
* @param f_ops
*
* @return the error code, 0 on initialization successfully.
*/
ssize_t led_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
int result;
u8 databuf[]; result = copy_from_user(databuf, buf, count);
if(result < ) {
printk(KERN_INFO"kernel write failed!\r\n");
return -EFAULT;
} /*利用数据操作LED*/
led_switch(databuf[]);
return ;
} /**
* light从设备读取状态
*
* @param filp
* @param cmd
* @param arg
*
* @return the error code, 0 on initialization successfully.
*/
long led_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
switch(cmd){
case :
led_switch();
break;
case :
led_switch();
break;
default:
printk(KERN_INFO"Invalid Cmd!\r\n");
return -ENOTTY;
} return ;
} /* 设备操作函数 */
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.unlocked_ioctl = led_ioctl,
.release = led_release,
};
2.创建设备,添加到设备总线上,这里要提到知识点,
对于一个设备的基本id,由主设备号和子设备号组成,其中主设备就是挂载在/proc/devices下的设备总线上,如果设备已经存在,则可以用register_chdev_region直接生成设备信息,则需要使用alloc_chrdev_region申请新的设备信息。
在获取设备信息结构后,可通过cdev_init将cdev,设备号以及上面的硬件操作接口函数链接起来。
最后通过cdev_add将设备信息挂载到设备总线上,这时通过cat /proc/devices就可以查看设备是否添加成功。
int result; led_driver_info.major = DEFAULT_MAJOR;
led_driver_info.minor = DEFAULT_MINOR; /*在总线上创建设备*/
/*1.申请字符设备号*/
if(led_driver_info.major){
led_driver_info.dev_id = MKDEV(led_driver_info.major, led_driver_info.minor);
result = register_chrdev_region(led_driver_info.dev_id, DEVICE_LED_CNT, DEVICE_LED_NAME);
}
else{
result = alloc_chrdev_region(&led_driver_info.dev_id, , DEVICE_LED_CNT, DEVICE_LED_NAME);
led_driver_info.major = MAJOR(led_driver_info.dev_id);
led_driver_info.minor = MINOR(led_driver_info.dev_id);
}
if(result < ){
printk(KERN_INFO"dev alloc or set failed\r\n");
return result;
}
else{
printk(KERN_INFO"dev alloc or set ok, major:%d, minor:%d\r\n", led_driver_info.major, led_driver_info.minor);
} /*2.添加设备到相应总线上*/
cdev_init(&led_driver_info.cdev, &led_fops);
led_driver_info.cdev.owner = THIS_MODULE;
result = cdev_add(&led_driver_info.cdev, led_driver_info.dev_id, DEVICE_LED_CNT);
if(result != ){
unregister_chrdev_region(led_driver_info.dev_id, DEVICE_LED_CNT);
printk(KERN_INFO"cdev add failed\r\n");
return result;
}else{
printk(KERN_INFO"device add Success!\r\n");
}
3.在/dev/下根据设备号创建设备节点,用于应用上层接口的访问,这部分和mknod /dev/led c 主设备号 从设备号功能一致,理论使用指令也可,具体如下。
/* 4、创建类 */
led_driver_info.class = class_create(THIS_MODULE, DEVICE_LED_NAME);
if (IS_ERR(led_driver_info.class)) {
printk(KERN_INFO"class create failed!\r\n");
unregister_chrdev_region(led_driver_info.dev_id, DEVICE_LED_CNT);
cdev_del(&led_driver_info.cdev);
return PTR_ERR(led_driver_info.class);
}
else{
printk(KERN_INFO"class create successed!\r\n");
} /* 5、创建设备 */
led_driver_info.device = device_create(led_driver_info.class, NULL, led_driver_info.dev_id, NULL, DEVICE_LED_NAME);
if (IS_ERR(led_driver_info.device)) {
printk(KERN_INFO"device create failed!\r\n");
unregister_chrdev_region(led_driver_info.dev_id, DEVICE_LED_CNT);
cdev_del(&led_driver_info.cdev); class_destroy(led_driver_info.class);
return PTR_ERR(led_driver_info.device);
}
else{
printk(KERN_INFO"device create successed!\r\n");
} /*硬件初始化*/
led_gpio_init();
至此,创建设备并添加到设备总线的流程实现完毕,这就是module_init中需要的所有实现。
2.释放模块
在上面我们创建设备,占用了系统资源,在卸载模块的时候,这些都要全部释放,不然就会造成内存的泄露,具体如下。
/**
* 驱动释放函数
*
* @param NULL
*
* @return the error code, 0 on release successfully.
*/
static void __exit led_module_exit(void)
{
/* 注销字符设备驱动 */
device_destroy(led_driver_info.class, led_driver_info.dev_id);
class_destroy(led_driver_info.class); cdev_del(&led_driver_info.cdev);
unregister_chrdev_region(led_driver_info.dev_id, DEVICE_LED_CNT); /*硬件资源释放*/
led_gpio_release();
}
module_exit(led_module_exit);
测试代码实现
在上面驱动代码就已经实现,但对于应用来说,实现驱动并不是结束,我们还要完成测试单元,但驱动的有效性进行测试,这部分因为并不是严格的工业化项目,所以只做简单的测试,代码如下
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h> /**
* 测试LED工作
*
* @param NULL
*
* @return NULL
*/
int main(int argc, const char *argv[])
{
unsigned char val = ;
int fd; fd = open("/dev/led", O_RDWR | O_NDELAY);
if(fd == -)
{
printf("/dev/led open error");
return -;
} if(argc > ){
val = atoi(argv[]);
} write(fd, &val, ); close(fd);
}
Makefile实现
Makefile的语法也是嵌入式Linux开发中重要知识,如果没有对bash语法有深刻的认识,且理解编译原理的那部分知识,这部分其实也十分困难,这也不是三两句可以说清楚的,等积累一段时间后专门用笔记讲解这部分内容,初步能大致看懂,修改会编译就够了。
KERNELDIR := /usr/code/linux
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
保存为Makefile后,使用make指令,就可以编译生成需要的led.ko文件,此外通过
arm-linux-gnueabihf-gcc -o led_test led_test.c也可以生成我们需要的测试文件。
文件上传和执行
可通过sd卡,ssh或者nfs系统,将上述文件添加到上章编译完成的系统中,
执行insmod /usr/driver/led.ko将驱动加载
执行lsmod查询当前加载的驱动
通过./usr/app/led_test 1或者./usr/app/led_test 0控制LED的点亮和关闭,现象如下:
总结
至此,关于LED的驱动开发基本讲解完成,虽然开发参考了部分例程用了不到2个小时,但完成这篇文档用了4个小时,为了能够将知识可以解决出来,去查询书籍,以及去查看内核代码,但是这是值得的,我感觉对驱动有了更深刻的认知,但我认为这是值得的,下节将开始Uart驱动的编写实现,整个流程算走上了正轨,不过我本身还要工作,这是因为五一才有这种效率更新,不过我已经制定了计划,希望能够顺利的去学习吧。
代码地址
相关代码在https://github.com/Imx6ull-app/remote_manage中kernal_mod/led下查看。
嵌入式Linux学习笔记(三) 字符型设备驱动--LED的驱动开发的更多相关文章
- 嵌入式Linux学习笔记之第一阶段---基础篇
嵌入式Linux学习分五个阶段 第一阶段: 01嵌入式环境搭建初期 02C语言语法概述 03C语言内存操作 04c语言函数 05linux基础 06gun基础 第二阶段: 01-linux之io系统编 ...
- 【转】嵌入式Linux学习笔记
一 嵌入式系统定义: 应用于特定环境的硬件体系. 二 两样非常重要的能力: 1. 掌握各种新概念的能力 2. 调试的能力( 包括软件, 硬件 ) 三 需要的基础知识: 1. 操作系统理论基 ...
- 嵌入式Linux学习笔记(六) 上位机QT界面实现和串口通讯实现
目录 (1).参考资料 (2).QT界面布局实现 (3).数据和操作逻辑 在上一章我们实现了下位机的协议制定,并通过串口通讯工具完成了对设备内外设(LED)的状态修改,下面就要进行上位机软件的实现了( ...
- 嵌入式Linux学习笔记(0)基础命令。——Arvin
学习记录: 到今天为止ARM裸机开发学习进程:1.2.1-1.2.14 预科班知识Linux介绍学习进程:0.2.1-0.2.6 学习内容笔记: 学习了Linux的开发方式的优劣介绍 学习了常用文件夹 ...
- Linux学习笔记(三):系统执行级与执行级的切换
1.Linux系统与其它的操作系统不同,它设有执行级别.该执行级指定操作系统所处的状态.Linux系统在不论什么时候都执行于某个执行级上,且在不同的执行级上执行的程序和服务都不同,所要完毕的工作和所要 ...
- 嵌入式Linux学习笔记 NAND Flash控制器
一.NAND Flash介绍和NAND Flash控制器的使用 NAND Flash在嵌入式系统中的作用,相当于PC上的硬盘 常见的Flash有NOR Flash和NAND Flash,NOR Fla ...
- 嵌入式Linux学习笔记之第二阶段---文件I/O
1.文件IO的四个函数 一些术语: 不带缓冲的I/O: 每个read和write都调用内核中的一个系统调用. 文件描述符: 一个非负整数,对内核而言,所以打开的文件都通过文件描述符引用. ①打开或创建 ...
- 嵌入式linux学习笔记1—内存管理MMU之虚拟地址到物理地址的转化
一.内存管理基本知识 1.S3C2440最多会用到两级页表:以段的方式进行转换时只用到一级页表,以页的方式进行转换时用到两级页表.页的大小有三种:大页(64KB),小页(4KB),极小页(1KB).条 ...
- linux学习笔记三:防火墙设置
请注意:centOS7和7之前的版本在防火墙设置上不同,只有正确的设置防火墙才能实现window下访问linux中的web应用. centOS6添加端口: vi /ets/sysconfig/ipta ...
随机推荐
- 【物理】AABB物理碰撞检测
什么是AABB? AABB,指轴对齐包围盒(Axis-aligned bounding boxes).在3D空间中,AABB是一个长方体,在2D空间中是一个长方形.特征是面法线皆平行于坐标轴,即当物体 ...
- ubuntu 虚拟机复制后打开蓝屏解决办法
sudo apt-get install xserver-xorg-lts-utopic sudo dpkg-reconfigure xserver-xorg-lts-utopic reboot
- Python爬虫系列(一):从零开始,安装环境
在上一个系列,我们学会使用rabbitmq.本来接着是把公司的celery分享出来,但是定睛一看,celery4.0已经不再支持Windows.公司也逐步放弃了服役多年的celery项目.恰好,公司找 ...
- tf.nn.relu6 激活函数
tf.nn.relu6(features,name=None) 计算校正线性6:min(max(features, 0), 6) 参数: features:一个Tensor,类型为float,doub ...
- Java 泛型、通配符? 解惑
Java 泛型通配符?解惑 分类: JAVA 2014-05-05 15:53 2799人阅读 评论(4) 收藏 举报 泛型通配符上界下界无界 目录(?)[+] 转自:http://www.linux ...
- G - Messy codeforces1262C
题目大意: 输入n和m,n是n个字符,m是m个前缀.对前缀的规定可以配对的括号.比如(),,((()))等等.在输入n个括号字符,对这个n个字符,通过交换使其满足m个前缀.交换次数不限,规则想当与re ...
- js拼接onclick方法字符串参数解决方法
onclick = contentmap("'+useridarr[i]+'")
- 详解 JDK8 新增的日期时间类
JDK8 新增的日期时间类 在本人之前的博文<处理时间的类 -- System类.Date类 .SimpleDateFormat类 与 Calendar类>中,讲到过表示时间的类,有三类: ...
- selenium 窗口的切换
窗口切换需要用到一个关键词:句柄,每个窗口唯一的标识 获取句柄的方法:driver.getWindowHandle(); 下面的例子是点击京东页面,跳转到京东手机页面,然后关闭京东页面 driver. ...
- [YII2] Activeform表单部分组件使用方法
文本框:textInput(); 密码框:passwordInput(); 单选框:radio(),radioList(); 复选框:checkbox(),checkboxList(); 下拉框:dr ...