这一块主要是了解linux系统驱动部分,编译镜像相关的知识,这里记录一下。
使用板子如下:
教程用的这一个版本:
1、基本环境搭建
这个比较简单,只是注意一下就是正点原子的教程用了一个NFS文件系统,简单来讲就是linux移植不是有三大块吗,uboot,linux内核和文件系统,正点原子教程里面大部分这个文件系统是放在虚拟机里面的,然后通过nfs的方式来访问的。
所以这里要关注一下
学习过程中我建了一个文件夹,然后nfs的文件也就一样放到里面去了
我的nfs路径为:
然后其他的部分参考教程吧,都是比较基础的部分,这里不再赘述。
2、linux系统移植
现在裸机开发的很少了,然后我本身也是做的应用层的开发,这块就接触的更少了,所以我觉得前面那些类似stm32一样去写linux驱动的这种可以直接略过不看,对后面的没有影响,直接跳到下面的章节来。
2.1 编译uboot并使用
这里先编译正点原子改好的uboot,就是把压缩包拿过来,然后解压,之后给了一个编译脚本进行编译
脚本内容如下
#!/bin/bash
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_14x14_ddr512_emmc_defconfig
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4
执行脚本就可以编译(记得给脚本提权限),编译结果如下
编译出来的目录里面的uboot.bin就是我们需要的uboot文件了
之后烧录到sd卡里面,然后通过sd卡来启动,这里也有个烧录脚本,烧录脚本直接去裸机例程那边拿过来用就行,路径如下所示:
查看sd卡路径
执行烧录命令
之后选择sd卡启动,启动方式看这里
执行如下所示,可以看到内核是最近编译的
教程后面还介绍了一些uboot使用相关的知识,这里暂时没有需求,先跳过,后面有空再看。
2.2 移植ubbot
这个过程就是改一些官方uboot的参数,使其适配正点原子的这个板子,这里也不赘述了,主要还是参考文档吧
这里重点关注这两个参数,这两个决定了uboot启动后会引导什么东西的问题
最后归结起来就是这两个
setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw'
setenv bootcmd 'mmc dev 1; fatload mmc 1:1 80800000 zImage; fatload mmc 1:1 83000000 imx6ull-alientek-emmc.dtb; bootz 80800000 - 83000000;'
saveenv
可以测试一下,因为之前emmc里面已经刷过系统了
成功启动
上面的方式是引导emmc里面的镜像和文件系统,我们可以通过tftp的方式获取系统,命令如下(注意:上面的命令是保存到了flash里面的,就是上电后会自动执行,所以如果要重新使用新的方式,就上电后按enter按键,重新进入uboot页面)
先确认一下我们的tftp路径下有需要的东西
接下来先设置一下网络参数
setenv ipaddr 192.168.1.3
setenv ethaddr b8:ae:1d:01:00:00
setenv gatewayip 192.168.1.1
setenv netmask 255.255.255.0
setenv serverip 192.168.1.4
之后输入
setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw'
setenv bootcmd 'tftp 80800000 zImage; tftp 83000000 imx6ull-alientek-emmc.dtb; bootz 80800000 - 83000000'
saveenv
正常启动,可以看到他是先用uboot拉了一下镜像在启动的
最后正常启动
上面的方式还可以进一步简化成nfs来挂载文件系统,这样开发的时候更方便,文件系统和我们的虚拟机里面的代码可以实时更新,使用的启动命令如下
如下所示
setenv bootcmd 'tftp 80800000 zImage;tftp 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000'
setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.1.4:/home/lx/IMX6ULL/nfs/rootfs,proto=tcp rw ip=192.168.1.3:192.168.1.4:192.168.1.1:255.255.255.0::eth0:off'
saveenv
他也是拉一个镜像过来,之后是这样的,我们可以尝试在虚拟机的文件系统里面做一下修改
进入串口,可以看到同步的修改来了
2.3 编译linux内核
上面已经介绍了uboot的编译和一些启动相关的内容,下面开始正式内核的移植,请直接跳到这一章来看
这里大部分工作也是修改imx官方的内核工程,适配我们现在这块板子(具体过程这里就不赘述了),之后也是用一个板子进行编译,移植的过程我觉得这个总结的还是可以的
编译命令如下
#!/bin/sh
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_alientek_emmc_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j4
编译结果如下所示
在下面的两个文件夹中拿到镜像和设备树(之后烧录就是前面说的内容了)
2.4 编译busybox
这里根文件系统这块正点原子用的是busybox来实现的,这部分请直接跳到这个章节来看
按照教程里面的进行修改,之后输入make
进行编译
之后输入下面的命令安装到我们的nfs目录下面去
make install CONFIG_PREFIX=/home/lx/IMX6ULL/nfs/rootfs
后面的步骤根据教程添加需要的库进去
最后是这样的
之后是按照教程创建了这个文件来测试
编译
执行程序
至此文件系统就也OK了
3、系统烧写
进入这个路径,找到这个
前面我们编译好的linux系统几个大件都能找到下面需要的材料
但是我实测应该是改成这个样子(可以根据自己实际需求来改吧)
接下来就是用我们的文件替换掉 NXP 官方的文件,先将图中的 zImage、 u-bootimx6ull14x14evk_emmc.imx 和 zImage-imx6ull-14x14-evk-emmc.dtb 这三个文件拷贝到 mfgtoolswith-rootfs/mfgtools/Profiles/Linux/OS Firmware/firmware 目录中,替换掉原来的文件。然后将图中的所有 4 个文件都拷贝到 mfgtools-with-rootfs/mfgtools/Profiles/Linux/OS Firmware/files目录中,这两个操作完成以后我们就可以进行烧写了。
之后进入USB模式,然后就点击开始
烧录过程串口会同步信息,如下所示:
之后选择emmc启动,至此,烧录到emmc完成(别忘了设置成emmc启动)
3、linux驱动开发
根据教程,linux驱动开发有三大类,分别是字符设备驱动,块设备驱动和网络设备驱动,字符设备驱动是占用篇幅最大的一类驱动,因为字符设备最多,从最简单的点灯到 I2C、 SPI、音频等都属于字符设备驱动的类型。这部分一般可以自己来实现,块设备驱动和网络设备驱动一般比字符设备驱动复杂,但是这部分一般是供应商那边实现,然后这边调用一下就行。块设备驱动一般值储存设备的驱动,例如EMMC,NAND,SD卡和U盘这一类。网络设备驱动就是网络驱动,不管是有线网络还是无线网络,都属于网络设备驱动。一个设备可以同时属于不同驱动,比如USB网卡,使用了USB接口,那么是字符设备驱动,但是又有wifi功能,也属于网络设备驱动。
3.1 字符设备开发基本知识
linux的应用程序驱动程序的流程如下所示,从下面的图中也可以更深刻的了解linux下一切皆文件的思想,驱动加载成功后会在/dev目录下面生成一个相应的文件,通过对这个文件进行操作即可实现对硬件的操作。
这里还需要注意就是我们的应用程序是运行在用户空间的,但是linux驱动是运行在内核空间的,属于内核的一部分,我们在用户空间想要实现对内核的操作,就需要实现open函数来打开这个设备,这个就是系统调用,open、 close、 write 和 read 等这些函数是由 C 库提供的,在 Linux 系统中,系统调用作为 C 库的一部分,可以直达内核,他的具体流程如下所示:
linux驱动一般可以有两种方式,一种是直接将驱动编进内核,一种是编成内核模块,我们调试的时候编成内核模块即可(内核模块就是.ko文件)下面是一个简单的内核模块示例
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, world!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, world!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("LX2035");
MODULE_DESCRIPTION("A simple kernel module");
编写一下makefile,注意一下下面的路径换成自己的即可
KERNELDIR := /home/lx/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH := $(shell pwd)
obj-m := my_module.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
可以看到编译后生成ko文件
将内核模块放到我们的板子里面加载然后卸载,效果如下所示,说明内核模块成功弄好了
3.2 字符设备开发示例
前面已经介绍了字符设备开发的基本示例,下面看一下如何使用那些文件操作函数来实现字符设备的开发(其实就是点灯!)
下面先直接贴代码
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#define CHRDEVBASE_MAJOR 200 /*主设备号*/
#define CHRDEVBASE_NAME "chrdevbase" /*设备名*/
static char readbuf[100];
static char writebuf[100];
static char kerneldata[] = {"kernel data!"};
/*打开设备*/
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
printk("chrdevbase open!\r\n");
return 0;
}
/*从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue = 0;
memcpy(readbuf, kerneldata, sizeof(kerneldata));
/*copy_to_user 内核空间的数据到用户空间的复制*/
retvalue = copy_to_user(buf, readbuf, cnt);
if (retvalue == 0)
{
printk("kernel senddata ok!\r\n");
}
else
{
printk("kernel senddata failed!\r\n");
}
return 0;
}
/*向设备写入数据
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t chrdevbase_write(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue = 0;
/*copy_from_user 接收用户空间传递给内核的数据*/
retvalue = copy_from_user(writebuf, buf, cnt);
if (retvalue == 0)
{
printk("kernel recevdata:%s\r\n", writebuf);
}
else
{
printk("kernel recevdata failed!\r\n");
}
return 0;
}
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
return 0;
}
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
static int __init chrdevbase_init(void)
{
int retvalue = 0;
retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);
if (retvalue < 0)
{
printk("chrdevbase driver register failed\r\n");
}
printk("chrdevbase_init()\r\n");
return 0;
}
static void __exit chrdevbase_exit(void)
{
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
printk("chrdevbase_exit()\r\n");
}
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("liuxing");
上述代码中关注一下初始化函数,就做了这些事,除了打开,然后读写函数之外还有一个设备号和设备名称的概念
retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);
关于设备号:为了方便管理, Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。
我们可以查看当前系统中已经使用了的设备号,输入下面命令
设备号可以静态分配,也可以动态分配,比如上面我用的就是静态分配的方式实现的,如果要使用动态分配就是(静态分配存在的问题是如果没有看系统已有的设备号,自己随便写的一个设备号可能和已有的设备号冲突)
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
但是注意就是取消的时候也要注销这个设备号
void unregister_chrdev_region(dev_t from, unsigned count)
另外还可以关注两个函数copy_to_user
和copy_from_user
,前面一个函数是完成内核空间的数据到用户空间的复制,后面一个函数是完成用户空间的数据到内核空间的复制。这两个函数在用户空间和内核空间的数据传递有很重要的作用。
下面编写一个内核的测试程序
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
static char usrdata[] = {"user data!"};
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
char readbuf[100], writenuf[100];
if (argc != 3)
{
printf("error usage\r\n");
return -1;
}
filename = argv[1];
fd = open(filename, O_RDWR);
if (fd < 0)
{
printf("Can't open file %s\r\n", filename);
return -1;
}
if (atoi(argv[2]) == 1)
{
retvalue = read(fd, readbuf, 50);
if (retvalue < 0)
{
printf("read file %s failed!\r\n", filename);
}
else
{
printf("read data:%s\r\n", readbuf);
}
}
if (atoi(argv[2]) == 2) /*写文件*/
{
memcpy(writenuf, usrdata, sizeof(usrdata));
retvalue = write(fd, writenuf, 50);
if(retvalue < 0)
{
printf("write file %s failed!\r\n", filename);
}
}
retvalue = close(fd);
if(retvalue < 0)
{
printf("Can't close file %s\r\n", filename);
return -1;
}
return 0;
}
编译程序还是用之前的那个命令,用交叉编译工具来编译
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
下面开始实验
创建一个设备节点
在应用程序给他发消息过去
如果不用了就卸载
3.3 字符设备led开发示例
对外设的驱动,到最后都是归结到寄存器的配置上,也就是配置到相应的硬件寄存器,对于I.MX6U-ALPHA 开发板,板子上的 LED 连接到 I.MX6ULL 的 GPIO1_IO03 这个引脚上。
真实情况下我们要写寄存器只要写那个地址就行了,但是linux还不一样,linux有MMU,这个会整出来一个虚拟内存的东西,作用是让内存变的大一些,这个在linux中是很普遍的,导致我们在linux中的写的地址不是真的地址,因此需要先了解一下MMU这个东西,MMU完成的功能如下所示:
- 完成虚拟空间到物理空间的映射。
- 内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。
内存映射可以从下面的表中看出,对于32位的处理器来说,虚拟地址的范围是 2^32=4GB,开发板上有 512MB 的 DDR3,这 512MB 的内存就是物理内存,经过 MMU 可以将其映射到整个 4GB 的虚拟空间,这也就意味着会有多个虚拟地址映射到同一个物理地址的情况,这个情况是由处理器来处理的,这个问题我们暂且不关注,我们目前只需要关注真实的硬件地址对应的是虚拟内存的哪个位置就行了。
Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚拟地址。比 如 I.MX6ULL 的 GPIO1_IO03 引 脚 的 复 用 寄 存 器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03的地址为 0X020E0068。如果没有开启 MMU 的话直接向 0X020E0068 这个寄存器地址写入数据就可以配置 GPIO1_IO03 的复用功能。现在开启了 MMU,并且设置了内存映射,因此就不能直接向 0X020E0068 这个地址写入数据了。我们必须得到 0X020E0068 这个物理地址在 Linux 系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数:ioremap和iounmap。
这两个函数的作用也比较清晰:
- ioremap:获取指定物理地址空间对应的虚拟地址空间
- iounmap:卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射
接下来看一下IO内存访问函数,这里的IO是输入输出的意思,涉及到IO端口和IO内存,当外部寄存器或者内存映射到IO空间的时候,称为IO端口,当外部寄存器或者内存映射到内存空间的时候,称为IO内存。对于ARM体系来说目前只有IO内存这个概念,使用 ioremap 函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
主要是下面的几个函数,分别是对8,16,32bit的读操作和写操作
- u8 readb(const volatile void __iomem *addr)
- u16 readw(const volatile void __iomem *addr)
- u32 readl(const volatile void __iomem *addr)
- void writeb(u8 value, volatile void __iomem *addr)
- void writew(u16 value, volatile void __iomem *addr)
- void writel(u32 value, volatile void __iomem *addr)
下面就可以正式开始写代码了,直接贴代码出来看看
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define LED_MAJOR 200 /*主设备号*/
#define LED_NAME "led" /*设备名*/
#define LEDOFF 0
#define LEDON 1
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)
/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;
void led_switch(u8 sta)
{
u32 val = 0;
if (sta == LEDON)
{
val = readl(GPIO1_DR);
val &= ~(1 << 3);
writel(val, GPIO1_DR);
}
else if (sta == LEDOFF)
{
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
}
}
static int led_open(struct inode *inode, struct file *filp)
{
return 0;
}
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
static ssize_t led_write(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue;
unsigned char databuf[1];
unsigned char ledstat;
retvalue = copy_from_user(databuf, buf, cnt);
if (retvalue < 0)
{
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstat = databuf[0]; /*获取状态值*/
if (ledstat == LEDON)
{
led_switch(LEDON);
}
else if (ledstat == LEDOFF)
{
led_switch(LEDOFF);
}
return 0;
}
static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
static int __init led_init(void)
{
int retvalue = 0;
u32 val = 0;
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
/*使能时钟*/
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26); /* 清除以前的设置 */
val |= (3 << 26); /* 设置新值 */
writel(val, IMX6U_CCM_CCGR1);
/*设置复用*/
writel(5, SW_MUX_GPIO1_IO03);
/*设置属性*/
writel(0x10B0, SW_PAD_GPIO1_IO03);
/*设置输出功能*/
val = readl(GPIO1_GDIR);
val &= ~(1 << 3); /* 清除以前的设置 */
val |= (1 << 3); /* 设置为输出 */
writel(val, GPIO1_GDIR);
/*默认关闭led*/
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
/* 6、注册字符设备驱动 */
retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
if (retvalue < 0)
{
printk("register chrdev failed!\r\n");
return -EIO;
}
return 0;
}
static void __exit led_exit(void)
{
/* 取消映射 */
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
unregister_chrdev(LED_MAJOR, LED_NAME);
printk("led_exit()\r\n");
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("liuxing");
其实基本也就是上面一个字符设备驱动修改了一点实现的,加了内存映射的部分,实现这里还是写寄存器,就是裸机开发那一套
下面看一下测试app,测试app这里我们只要写0和1即可,下面是完整代码
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#define LEDOFF 0
#define LEDON 1
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
unsigned char databuf[1];
if (argc != 3)
{
printf("error usage\r\n");
return -1;
}
filename = argv[1];
fd = open(filename, O_RDWR);
if (fd < 0)
{
printf("Can't open file %s\r\n", filename);
return -1;
}
databuf[0] = atoi(argv[2]);
retvalue = write(fd, databuf, sizeof(databuf));
if(databuf < 0)
{
printf("led control failed \r\n");
close(fd);
return -1;
}
retvalue = close(fd);
if(retvalue < 0)
{
printf("file close error \r\n");
return -1;
}
return 0;
}
加载到板子里面看看,执行下面的命令可以看到板子的led亮灭变化
最后不要忘了卸载驱动
3.4 新字符设备开发示例
register_chrdev
和 unregister_chrdev
这两个函数是老版本驱动使用的函数,现在新的字符设备驱动已经不再使用这两个函数,而是使用Linux内核推荐的新字符设备驱动API函数。
先说一下之前用的那个字符设备驱动的问题
使用 register_chrdev
函数注册字符设备的时候只需要给定一个主设备号即可,但是这样会带来两个问题:
- 需要我们事先确定好哪些主设备号没有使用。
- 会将一个主设备号下的所有次设备号都使用掉,比如现在设置 LED 这个主设备号为200,那么 0~1048575(2^20-1)这个区间的次设备号就全部都被 LED 一个设备分走了。这样太浪费次设备号了!一个 LED 设备肯定只能有一个主设备号,一个次设备号。
解决这个问题就是采用前面提到过的使用动态设备号分配的方案alloc_chrdev_region
,子设备号一般直接为0,另外字符设备也有新的注册方式,如下所示:
这里面有两个重要的成员变量:ops
和 dev
,这两个就是字符设备文件操作函数集合file_operations
以及设备号dev_t
。
之后使用专门的初始话函数初始话和添加节点,注意还在最后使用class_create
来实现自动创建设备节点,之前都是通过mknod /dev/led c 200 0
来创建的。
关于自动创建设备节点,使用的是mdev的机制,mdev是busybox创建的一个udev的简化版本,udev是可以检测系统中的硬件设备状态,从而来创建或者删除设备文件,比如使用modprobe 命令成功加载驱动模块以后就自动在/dev 目录下创建对应的设备节点文件,使用rmmod 命令卸载驱动模块以后就删除掉/dev 目录下的设备节点文件。
要实现这个,只需要在rcs下面最底部加入这个命令即可:
具体反馈到应用上来看就是使用上面的说的class_create
函数创建类,之后用device_create
函数创建设备就行,下面直接看源代码:
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define NEWCHRLED_CNT 1 /* 设备号个数 */
#define NEWCHRLED_NAME "newchrled" /* 名字 */
#define LEDOFF 0 /* 关灯 */
#define LEDON 1 /* 开灯 */
/* 寄存器物理地址 */
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)
/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;
struct newchrled_dev
{
dev_t devid;
struct cdev cdev;
struct class *class;
struct device *device;
int major;
int minor;
};
struct newchrled_dev newchrled;
void led_switch(u8 sta)
{
u32 val = 0;
if (sta == LEDON)
{
val = readl(GPIO1_DR);
val &= ~(1 << 3);
writel(val, GPIO1_DR);
}
else if (sta == LEDOFF)
{
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
}
}
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &newchrled;
return 0;
}
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
static ssize_t led_write(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue;
unsigned char databuf[1];
unsigned char ledstat;
retvalue = copy_from_user(databuf, buf, cnt);
if (retvalue < 0)
{
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstat = databuf[0]; /*获取状态值*/
if (ledstat == LEDON)
{
led_switch(LEDON);
}
else if (ledstat == LEDOFF)
{
led_switch(LEDOFF);
}
return 0;
}
static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}
static struct file_operations newchrled_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
static int __init led_init(void)
{
int retvalue = 0;
u32 val = 0;
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
/*使能时钟*/
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26); /* 清除以前的设置 */
val |= (3 << 26); /* 设置新值 */
writel(val, IMX6U_CCM_CCGR1);
/*设置复用*/
writel(5, SW_MUX_GPIO1_IO03);
/*设置属性*/
writel(0x10B0, SW_PAD_GPIO1_IO03);
/*设置输出功能*/
val = readl(GPIO1_GDIR);
val &= ~(1 << 3); /* 清除以前的设置 */
val |= (1 << 3); /* 设置为输出 */
writel(val, GPIO1_GDIR);
/*默认关闭led*/
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
/* 6、注册字符设备驱动 */
if (newchrled.major)
{
newchrled.devid = MKDEV(newchrled.major, 0);
register_chrdev_region(newchrled.devid,
NEWCHRLED_CNT,
NEWCHRLED_NAME);
}
else
{
alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME);
newchrled.major = MAJOR(newchrled.devid);
newchrled.minor = MAJOR(newchrled.devid);
}
printk("newcheled major=%d,minor=%d\r\n", newchrled.major, newchrled.minor);
/*初始化cdev*/
newchrled.cdev.owner = THIS_MODULE;
cdev_init(&newchrled.cdev, &newchrled_fops);
/*添加一个cdev*/
cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);
/*创建类*/
newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
if (IS_ERR(newchrled.class))
{
return PTR_ERR(newchrled.class);
}
/*创建设备*/
newchrled.device = device_create(newchrled.class,
NULL,
newchrled.devid,
NULL,
NEWCHRLED_NAME);
if (IS_ERR(newchrled.device))
{
return PTR_ERR(newchrled.device);
}
return 0;
}
static void __exit led_exit(void)
{
/* 取消映射 */
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
cdev_del(&newchrled.cdev);
unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);
device_destroy(newchrled.class, newchrled.devid);
class_destroy(newchrled.class);
printk("led_exit()\r\n");
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("liuxing");
编译后加载到开发板,可以看到自动运行起来了
可以正常实现开关灯控制
3.5 设备树开发示例
在新版本的linux中,arm相关的驱动都采用了设备树,很多新的CPU也都是基于设备树,因此学习了解设备树还是很有必要的。下面部分来了解一下设备数相关的知识
设备树起源
描述设备树的文件叫DTS,这个DTS采用树的结构来描述板级设备,也就是开发板上的信息,例如CPU 数量、 内存基地址、 IIC 接口上接了哪些设备、 SPI 接口上接了哪些设备这些内容,如下图所示:
树的主干就是系统总线,IIC 控制器、GPIO 控制器、SPI 控制器等都是接到系统主线上的分支。IIC 控制器有分为 IIC1 和 IIC2 两种,其中 IIC1 上接了 FT5206 和 AT24C02这两个 IIC 设备, IIC2 上只接了 MPU6050 这个设备。 DTS 文件的主要功能就是按照图所示的结构来描述板子上的设备信息, DTS 文件描述设备信息是有相应的语法规则要求的,也就是说只要掌握了这个规则,就能清楚怎么编写设备树。
设备树前后写驱动区别
这个部分直接参考原教程吧,愿教程举了个例子,如下所示
就是说每个板子为了支持这些芯片都得写这样一个文件到里面去,然后linux有需要很好的兼容性,这些东西都会被加入到linux内核中,导致linux内核越来越大,之后就引入了设备树这个概念
DTS、DTB 和 DTC
这里也直接看教程就行
因此我们如果仅仅编译设备树的话,只要在源码目录下输入即可,编译完整的zImage
才使用make all
make dtbs
具体到我们这个板子上来,是那个设备树可以看这个
这里面这个就是我们需要的了
DTS语法
设备树很大,一般不会从头到尾重写一个dts文件,大多数时候是直接在SOC厂商的dts下面修改,教程中以imx6ull-alientek-emmc.dts为例学习一下dts语法,这个文件在这个路径下面
可以看到他开头就引用了原厂的dts文件
打开原厂的dts文件,就可以看到他描述的一些外设信息
这里我们可以进入到can的描述里面看一下
原教程根据这个得出了一个设备树的基本模版:
/ {
aliases {
can0 = &flexcan1;
};
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
};
};
intc: interrupt-controller@00a01000 {
compatible = "arm,cortex-a7-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x00a01000 0x1000>,
<0x00a02000 0x100>;
};
}
具体说明如下图,另外还有很多信息,比较多,还是参考原教程吧,这里不过多赘述
设备树在板子中的体现
3.5 设备树驱动led测试
先修改设备树,设备树我们现在用的是这个
在跟节点加入下面内容:
源代码如下:
alphaled {
#address-cells = <1>;
#size-cells = <1>;
compatible = "atkalpha-led";
status = "okay";
reg = < 0X020C406C 0X04 /* CCM_CCGR1_BASE */
0X020E0068 0X04 /* SW_MUX_GPIO1_IO03_BASE */
0X020E02F4 0X04 /* SW_PAD_GPIO1_IO03_BASE */
0X0209C000 0X04 /* GPIO1_DR_BASE */
0X0209C004 0X04 >; /* GPIO1_GDIR_BASE */
};
之后编写基于设备树的驱动代码,可以和上一章的代码进行对比,基本就是寄存器的信息在这个程序中看不到了,变成了从设备树中读取代码。
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define dtsled_CNT 1 /* 设备号个数 */
#define dtsled_NAME "dtsled" /* 名字 */
#define LEDOFF 0 /* 关灯 */
#define LEDON 1 /* 开灯 */
/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;
struct dtsled_dev
{
dev_t devid;
struct cdev cdev;
struct class *class;
struct device *device;
int major;
int minor;
struct device_node *nd; /*设备节点*/
};
struct dtsled_dev dtsled;
void led_switch(u8 sta)
{
u32 val = 0;
if (sta == LEDON)
{
val = readl(GPIO1_DR);
val &= ~(1 << 3);
writel(val, GPIO1_DR);
}
else if (sta == LEDOFF)
{
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
}
}
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &dtsled;
return 0;
}
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
static ssize_t led_write(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue;
unsigned char databuf[1];
unsigned char ledstat;
retvalue = copy_from_user(databuf, buf, cnt);
if (retvalue < 0)
{
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstat = databuf[0]; /*获取状态值*/
if (ledstat == LEDON)
{
led_switch(LEDON);
}
else if (ledstat == LEDOFF)
{
led_switch(LEDOFF);
}
return 0;
}
static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}
static struct file_operations dtsled_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
static int __init led_init(void)
{
u32 val = 0;
int ret = 0;
u32 regdata[14];
const char *str;
struct property *proper;
/*获取设备树中的属性数据*/
dtsled.nd = of_find_node_by_path("/alphaled");
if (dtsled.nd == NULL)
{
printk("alphaled node can not found!\r\n");
return -EINVAL;
}
else
{
printk("alphaled node has been found!\r\n");
}
proper = of_find_property(dtsled.nd, "compatible", NULL);
if (proper == NULL)
{
printk("compatible property find failed\r\n");
}
else
{
printk("compatible = %s\r\n", (char *)proper->value);
}
ret = of_property_read_string(dtsled.nd, "status", &str);
if (ret < 0)
{
printk("status read failed!\r\n");
}
else
{
printk("status = %s\r\n", str);
}
ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 10);
if (ret < 0)
{
printk("reg property read failed!\r\n");
}
else
{
u8 i = 0;
printk("reg data:\r\n");
for (i = 0; i < 10; i++)
printk("%#X ", regdata[i]);
printk("\r\n");
}
#if 0 /*下面两种都是可以的*/
/* 1、寄存器地址映射 */
IMX6U_CCM_CCGR1 = ioremap(regdata[0], regdata[1]);
SW_MUX_GPIO1_IO03 = ioremap(regdata[2], regdata[3]);
SW_PAD_GPIO1_IO03 = ioremap(regdata[4], regdata[5]);
GPIO1_DR = ioremap(regdata[6], regdata[7]);
GPIO1_GDIR = ioremap(regdata[8], regdata[9]);
#else
IMX6U_CCM_CCGR1 = of_iomap(dtsled.nd, 0);
SW_MUX_GPIO1_IO03 = of_iomap(dtsled.nd, 1);
SW_PAD_GPIO1_IO03 = of_iomap(dtsled.nd, 2);
GPIO1_DR = of_iomap(dtsled.nd, 3);
GPIO1_GDIR = of_iomap(dtsled.nd, 4);
#endif
/*使能时钟*/
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26); /* 清除以前的设置 */
val |= (3 << 26); /* 设置新值 */
writel(val, IMX6U_CCM_CCGR1);
/*设置复用*/
writel(5, SW_MUX_GPIO1_IO03);
/*设置属性*/
writel(0x10B0, SW_PAD_GPIO1_IO03);
/*设置输出功能*/
val = readl(GPIO1_GDIR);
val &= ~(1 << 3); /* 清除以前的设置 */
val |= (1 << 3); /* 设置为输出 */
writel(val, GPIO1_GDIR);
/*默认关闭led*/
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
/* 6、注册字符设备驱动 */
if (dtsled.major)
{
dtsled.devid = MKDEV(dtsled.major, 0);
register_chrdev_region(dtsled.devid,
dtsled_CNT,
dtsled_NAME);
}
else
{
alloc_chrdev_region(&dtsled.devid, 0, dtsled_CNT, dtsled_NAME);
dtsled.major = MAJOR(dtsled.devid);
dtsled.minor = MAJOR(dtsled.devid);
}
printk("newcheled major=%d,minor=%d\r\n", dtsled.major, dtsled.minor);
/*初始化cdev*/
dtsled.cdev.owner = THIS_MODULE;
cdev_init(&dtsled.cdev, &dtsled_fops);
/*添加一个cdev*/
cdev_add(&dtsled.cdev, dtsled.devid, dtsled_CNT);
/*创建类*/
dtsled.class = class_create(THIS_MODULE, dtsled_NAME);
if (IS_ERR(dtsled.class))
{
return PTR_ERR(dtsled.class);
}
/*创建设备*/
dtsled.device = device_create(dtsled.class,
NULL,
dtsled.devid,
NULL,
dtsled_NAME);
if (IS_ERR(dtsled.device))
{
return PTR_ERR(dtsled.device);
}
return 0;
}
static void __exit led_exit(void)
{
/* 取消映射 */
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
cdev_del(&dtsled.cdev);
unregister_chrdev_region(dtsled.devid, dtsled_CNT);
device_destroy(dtsled.class, dtsled.devid);
class_destroy(dtsled.class);
printk("led_exit()\r\n");
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("liuxing");
测试程序可以用之前的ledApp,直接拿过来用
放到文件里面,最后运行如下所示:
4、pinctrl 和 gpio 子系统
前面提到的那些驱动来实现功能都需要直接对寄存器来操作,前面的都还只是针对某个GPIO,要是一些复杂的外设,寄存器多的数不胜数,肯定是很麻烦,linux肯定也考虑到了这一点,因此linux内核提供了pinctrl 和 gpio 子系统用于gpio驱动。
先看一下前面的驱动led的实现:
- 修改设备树, 添加相应的节点,节点里面重点是设置 reg 属性, reg 属性包括了 GPIO
相关寄存器。 - 获 取 reg 属 性 中 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 和IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 这两个寄存器地址,并且初始化这两个寄存器,这两个寄存器用于设置 GPIO1_IO03 这个 PIN 的复用功能、上下拉、速度等。
- 设置 GPIO1_IO03来完成GPIO的输出
这个流程和stm32的那种逻辑驱动是很类似的,因此对于这种通用的行为,就可以做成一个系统来用。
pinctrl 子系统主要工作内容如下
- 1、获取设备树中 pin 信息。
- 2、根据获取到的 pin 信息来设置 pin 的复用功能
- 3、根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等
5、并发和竞争
5、1原子操作
5、2自旋锁
5、3自旋锁的衍生
读写自旋锁
顺序锁