目录

(1).参考资料

(2).LED硬件配置实现

(3).嵌入式内核模块实现

(4).设备创建和释放

(5).测试代码实现

(6).Makefile实现

(7).文件上传和执行

(8).总结

(9).代码地址

 在成功构建了一个能够运行在开发板平台的系统后,下一步就要正式开始项目的实现(这里前提是有一定的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的驱动开发的更多相关文章

  1. 嵌入式Linux学习笔记之第一阶段---基础篇

    嵌入式Linux学习分五个阶段 第一阶段: 01嵌入式环境搭建初期 02C语言语法概述 03C语言内存操作 04c语言函数 05linux基础 06gun基础 第二阶段: 01-linux之io系统编 ...

  2. 【转】嵌入式Linux学习笔记

    一  嵌入式系统定义: 应用于特定环境的硬件体系. 二  两样非常重要的能力: 1.  掌握各种新概念的能力 2.  调试的能力( 包括软件, 硬件 ) 三  需要的基础知识: 1.  操作系统理论基 ...

  3. 嵌入式Linux学习笔记(六) 上位机QT界面实现和串口通讯实现

    目录 (1).参考资料 (2).QT界面布局实现 (3).数据和操作逻辑 在上一章我们实现了下位机的协议制定,并通过串口通讯工具完成了对设备内外设(LED)的状态修改,下面就要进行上位机软件的实现了( ...

  4. 嵌入式Linux学习笔记(0)基础命令。——Arvin

    学习记录: 到今天为止ARM裸机开发学习进程:1.2.1-1.2.14 预科班知识Linux介绍学习进程:0.2.1-0.2.6 学习内容笔记: 学习了Linux的开发方式的优劣介绍 学习了常用文件夹 ...

  5. Linux学习笔记(三):系统执行级与执行级的切换

    1.Linux系统与其它的操作系统不同,它设有执行级别.该执行级指定操作系统所处的状态.Linux系统在不论什么时候都执行于某个执行级上,且在不同的执行级上执行的程序和服务都不同,所要完毕的工作和所要 ...

  6. 嵌入式Linux学习笔记 NAND Flash控制器

    一.NAND Flash介绍和NAND Flash控制器的使用 NAND Flash在嵌入式系统中的作用,相当于PC上的硬盘 常见的Flash有NOR Flash和NAND Flash,NOR Fla ...

  7. 嵌入式Linux学习笔记之第二阶段---文件I/O

    1.文件IO的四个函数 一些术语: 不带缓冲的I/O: 每个read和write都调用内核中的一个系统调用. 文件描述符: 一个非负整数,对内核而言,所以打开的文件都通过文件描述符引用. ①打开或创建 ...

  8. 嵌入式linux学习笔记1—内存管理MMU之虚拟地址到物理地址的转化

    一.内存管理基本知识 1.S3C2440最多会用到两级页表:以段的方式进行转换时只用到一级页表,以页的方式进行转换时用到两级页表.页的大小有三种:大页(64KB),小页(4KB),极小页(1KB).条 ...

  9. linux学习笔记三:防火墙设置

    请注意:centOS7和7之前的版本在防火墙设置上不同,只有正确的设置防火墙才能实现window下访问linux中的web应用. centOS6添加端口: vi /ets/sysconfig/ipta ...

随机推荐

  1. Spring XML Bean 定义的加载和注册

    前言 本篇文章主要介绍 Spring IoC 容器怎么加载 bean 的定义元信息. 下图是一个大致的流程图: 第一次画图,画的有点烂.

  2. Spring Cloud 系列之 Consul 注册中心(二)

    本篇文章为系列文章,未读第一集的同学请猛戳这里:Spring Cloud 系列之 Consul 注册中心(一) 本篇文章讲解 Consul 集群环境的搭建. Consul 集群 上图是一个简单的 Co ...

  3. POj3017 dp+单调队列优化

    传送门 解题思路: 大力推公式:dp[i]=min(dp[k]+max(k+1,i)){k>=0&&k<i},max(j,i)记为max(a[h]){h>k& ...

  4. Java14:你需要知道的新特性

    基本介绍 2020 年 3 月 17 日,JDK / Java 14 正式 GA(General Available).这是自从 Java 采用六个月一次的发布周期之后的第五次发布. 此版本包含的 J ...

  5. Java读源码之ReentrantLock(2)

    前言 本文是 ReentrantLock 源码的第二篇,第一篇主要介绍了公平锁非公平锁正常的加锁解锁流程,虽然表达能力有限不知道有没有讲清楚,本着不太监的原则,本文填补下第一篇中挖的坑. Java读源 ...

  6. Python递归爬取头条用户的所有文章、视频

    前言 文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. PS:如有需要Python学习资料的小伙伴可以加点击下方链接自行获取http ...

  7. Cyclic Nacklace 杭电3746

    CC always becomes very depressed at the end of this month, he has checked his credit card yesterday, ...

  8. 详解 缓冲区(Buffer 抽象类)

    在本篇博文中,本人主要讲解NIO 的两个核心点 -- 缓冲区(Buffer) 和 通道 (Channel)之一的 缓冲区(Buffer), 有关NIO流的其他知识点请观看本人博文<详解 NIO流 ...

  9. Chrome 浏览器安装 ChroPath 插件

    1.下载地址 http://www.cnplugins.com/devtool/chropath/download.html 2.安装方法 a.把下载的文件更改后缀名变为压缩包,然后解压到本地:如下图 ...

  10. Springboot:员工管理之环境准备(十(1))

    1:静态资源 下载静态资源:https://files.cnblogs.com/files/applesnt/ztzy.zip 项目下载:https://files.cnblogs.com/files ...