//裸机程序下载
1)使用DNW软件下载,需安装驱动(过程省略,只有XP和WIN7驱动),连接USB-OTG接口、调试串口到PC机;
2)重启开发板,输入:dnw 40008000;(下载到该地址)
3)在DNW软件中,选择“USB Port->Transmit->Transmit”,选择*.bin裸机程序;
4)在dnw软件上输入“go 40008000”使cpu从0x40008000地址开始运行;
=====================================
//linux驱动开发
驱动的概念:
应用程序--》操作系统--》驱动程序--》外设;
应用程序与驱动程序相互独立,应用程序通过函数间接调用外设。
有了操作系统,系统更加稳定可靠。
字符设备,块设备,网络设备的概念;
MCU与MPU的重要区别:内存管理单元(MMU);
===========================
GPIO的控制:
GPIO的配置寄存器,数据寄存器,驱动强度寄存器。
语法:
unsigned int * p;//32位指针
p=0x11000104;//指向寄存器地址
*p=1;//寄存器赋值
=========================
ARM架构及发展:
1)哈佛结构:数据与程序分开;
2)高速缓存:平衡CPU与内存的性能差距;
3)虚拟内存:基于操作系统和虚拟地址访问,使内存地址范围扩大,访问外设也通过虚拟地址。通过ioremap函数实现虚拟地址与物理地址的映射。
4)ARM cortex的后缀:-A:综合性能较强,适用于平板电脑和手机;-R:实时性较好;-M:替代单片机。
5)内存管理单元MMU:实现虚拟内存和多任务管理;
6)RISC指令集:芯片开发者与软件工程师的桥梁。精简指令集便于实现高性能。
7)几种处理器架构:ARM,X86,POWERPC,MIPS;
==========================
LINUX目录:
通过“系统调用”和“硬件中断“来完成用户空间到内核空间的转移;
内核System Call Interface (SCI层):为用户空间提供了一套标准的系统调用函数来访问Linux内核空间。
内核支持的所有CPU架构,在arch目录下都有对应的子目录。
例如:
平台文件:mach*.c;
boot目录:内核镜像文件;
kernel目录:内核核心功能源码,特性实现方式,如:信号处理;
lib目录:硬件相关库函数;
tools目录:将.C文件编译成目标文件,生成镜像的工具;
binary目录:没有源码的二进制程序;
block目录:部分块设备驱动程序;
documentation目录:内核使用说明文档;
drivers目录:设备驱动程序;90%的重要性;
firmware目录;固件接口;
fs目录:存放文件系统实现代码;
include/linux目录:通用的头文件;
init目录:内核初始化代码;(由厂家提供)
ipc目录:进程通信源码;
virt目录:内核虚拟机;
mm目录:内存管理;
net目录:各种网络协议;
samples目录:内核编程范例,核心代码;(初期不需要研究)
scripts目录:配置裁剪内核的工具脚本;(menu config源码)
security目录:Linux安全模型代码;
sound目录:音频设备驱动程序;
usr目录:临时文件;
========================
设备驱动分离;驱动分层;
内存式访问;
驱动接口;
========================
驱动程序编译:
源码头文件:#include <linux/module.h>;#include <linux/init.h>;
声明GPL协议,否则模块将无法在Linux中使用:
– MODULE_LICENSE(_license)//添加遵循GPL协议,必须的!例如:– MODULE_LICENSE("Dual BSD/GPL");//声明是开源的,没有内核版本限制
– MODULE_AUTHOR(_author)//代码作者,可不写
模块接口函数:
– 入口函数module_init(x)
– 出口函数module_exit(x)
其中,x是自己写的功能函数;
语法:printk(KERN_EMERG "Hello World enter!\n");//在串口终端以最高优先级打印;
Linux的驱动可以和Linux源码放在一起编译,也可以单独拿出来编译;单独编译驱动需要写一个Makefile文件;
拷贝makefile文件和.c源文件到ubuntu,执行“make”命令,编译生成.KO文件;
makefile文件中的all和clean参数后面的必须添加Tab键,否则会报错;(输入“make clean”执行,而不只是clean)
加载模块、查看模块、卸载模块命令:
– insmod加载模块命令:insmod 路径/file.ko
– lsmod查看模块命令:或者 cat /proc/modules命令
– rmmod卸载模块命令:rmmod 模块名(没有.ko)
使VI编辑器能显示中文:在/etc/vim/vimrc文件结尾,添加“set fencs=utf-8,GB18030,ucs-bom,default,latin1”;
无法卸载模块的解决办法:
按照提示去创建相应目录,然后再卸载;
========================
内核配置:
menu config命令:通过菜单界面进行内核配置,生成.config文件;linux编译器通过.config文件确认哪些代码编译进内核,哪些被裁减掉;
Kconfig文件:决定菜单界面是否出现该驱动,及可选项;每一级目录下均有相应的Kconfig文件;Kconfig文件能够仿写即可;
源码中带的config_for_xxx都是针对不同的操作系统或者功能裁减的.config文件;
常用的配置基本在drivers目录下;
在内核源码根目录下输入命令:make menuconfig,可以进入配置界面;
– #make config(基于文本的最为传统的配置界面,不推荐使用)
– #make menuconfig(基于文本菜单的配置界面)
– #make xconfig(要求QT被安装)
– #make gconfig(要求GTK+被安装)
配置菜单常用操作:
– 上下选择→按键“上下方向键”
– 左右选择→按键“左右方向键”
– 进入下级界面→按键“回车”
– 返回上级界面→选择“Exit”+按键“回车”
– 帮助→选择“help”+按键“回车”
– 界面输入“/”,输入查找关键词,输入“回车”即可搜索
– “M”编译成模块(Kconfig文件中为“tristate”,可选为“M”状态)
– “空”不编译状态
– “*”编译进内核
===========================
内核编译:
1)在源码根目录下,打开Makefile文件,设置好编译器路径;
2)在drivers/Makefile文件中,类似obj-y后面没有选项的,都将被强制编译进内核;
3)类似obj-$(CONFIG_LEDS_CTL),则只在定义了宏CONFIG_LEDS_CTL以后才会编译;(宏定义在驱动目录的Kconfig文件中)
4)如果存在相互依赖的驱动,则修改Kconfig文件,并在menu config时全选中;(此为“依赖编译”)
5)在前述配置好内核后,使用“make zImage”命令,生成镜像文件;
查看当前运行的系统是否已有相应驱动程序:ls /dev/node_name(此处应该是相应设备节点的名称)
============================
查看总线和设备号:
查看总线的命令:ls /sys/bus/
查看设备号的命令:cat /proc/devices(查看的是主设备号)
查看杂项设备号的命令:cat /proc/misc(会列出从设备号)
由于设备太多,Linux现在是先注册驱动,设备来了再注册设备;
驱动注册流程:通过platform虚拟平台总线,将驱动与设备对应起来,通过platform_match系统函数比较名称,对比成功则进行初始化,分配节点号等等操作;对比失败,则放到总线链表的结尾。
=============================
注册设备:(这种方法不用去调用注册设备的函数)
1)在迅为提供的platform平台.c文件中,添加自定义设备的platform_device结构体和链表单元;(name是设备名称,将在/sys/devices目录显示,id为-1代表只有1个设备)
2)在内核驱动目录的Kconfig文件中定义好相应的驱动和宏;
3)menu config,并配置好内核;(此处没选中设备驱动,则设备不会注册)
4)make zImage,生成镜像,并刷到板子里;此后,系统运行时会自动注册该设备;
查看注册设备命令:ls /sys/devices/platform/;(注意:与dev目录下的显示不同,那个可能是所有支持的设备)
查看结构体:/linux/platform_device.h;
=============================
注册驱动:(生成一个.ko文件并加载该模块)
1)在.c文件中自定义probe,remove,shutdown,suspend,resume等函数;(这些函数的形参为platform_device结构体指针)
2)例化一个platform_driver结构体,并给其中的成员赋值;(其中的.driver成员的name必须是前面设备的名称)
3)在自定义的init函数中调用platform_driver_register函数,注册这个例化的platform_driver结构体,注册函数成功时返回0;
4)在自定义的exit函数中调用platform_driver_unregister函数,注销这个例化的结构体,以支持卸载;
5)输入“make”命令,生成.ko文件;
6)在开发板上使用“insmode file.ko”命令加载这个.ko文件,即运行驱动;
=============================
注册设备:(通过platform_device_register函数注册,调试设备更方便,不用每次编译内核)
1)创建.c文件,修改makefile文件;(makefile文件中含有编译输出信息)
2)在.c文件中,例化一个platform_device结构体,并初始化其名称,id,release信息;(需自定义一个release函数,否则注销的时候会报错)
例如:
static void leds_release(struct device * dev)//release函数
{
}
static struct platform_device leds_device = {//例化的结构体
.name = "my_code_led",
.id = -1,
.dev = {
.release = leds_release, },
};
3)在自定义的init函数中调用platform_device_register函数,注册这个例化的platform_device结构体;
4)在自定义的exit函数中调用platform_device_unregister函数,注销这个例化的结构体,以支持卸载;
5)执行“make”命令,编译;
6)在测试目录下,执行“insmode file.ko”命令,加载该设备;(可以到/sys/devices/platform/查看该设备是否注册)
7)执行“rmmod file”命令,卸载该设备;(注意该命令没有.ko后缀)
===============================
注册驱动,同时获取设备信息:(要求设备提前注册好,否则只是注册驱动,不会获取到设备信息)
1)按照前述注册一个设备;
2)修改驱动,在probe函数中,通过其platform_device指针,获取并打印该设备信息;
3)如果没有提前注册设备,则驱动的probe函数不会执行,初始化会失败;
==============================
杂项设备注册及驱动注册:
1)驱动注册,在.c文件中例化platform_driver结构体,并在init和exit函数中注册和注销该结构体;//注册函数platform_driver_register,注销函数platform_driver_unregister;
2)设备节点注册,在.c文件中例化miscdevice结构体,并在驱动的probe和remove函数中注册和注销该节点;//此处是生成设备节点,此结构体中的name是设备节点名称,与设备名称不同,所以与驱动结构体中的名称可以不一致,注册函数misc_register,注销函数misc_deregister;
– .minor设备号,若随机分配,则赋值MISC_DYNAMIC_MINOR;
– .name生成设备节点的名称
– .fops指向设备节点的文件结构体
3)文件例化,在.c文件中例化file_operations结构体,并自定义open,release,ioctl函数;//这应该是对上层应用程序的接口,文件的结构体file_operations参数很多,根据需求选择。必选的是参数是:
– .owner一般是THIS_MODULE
– .open打开文件函数
– .release关闭文件函数
Linux到2.6版本的时候,改动巨大,现在2.6版本以前的基本都废弃了,不用管了,学了也没用。
#include <linux/miscdevice.h>//注册杂项设备头文件
#include <linux/fs.h>//注册设备节点的文件结构体
==============================
在上层应用程序调用驱动:
应用程序示例:
#include <stdio.h>
#include <sys/types.h>//基本系统数据类型。系统的基本数据类型在32位编译环境中保持为32位值,并会在64位编译环境中增长为64位值。
#include <sys/stat.h>//系统调用函数头文件。可以调用普通文件,目录,管道,socket,字符,块的属性
#include <fcntl.h>//定义了open函数
#include <unistd.h>//定义了close函数
#include <sys/ioctl.h>//定义了ioctl函数
//这些头文件应该都在编译器的include目录下。
main(){
int fd;//文件标识符
char *hello_node = "/dev/hello_ctl123";//指定设备节点及路径
if((fd = open(hello_node,O_RDWR|O_NDELAY))<0){//执行,对应驱动中的文件OPEN函数,O_RDWR只读打开,O_NDELAY非阻塞方式
printf("APP open %s failed",hello_node);//不执行,因为打开驱动文件成功
}
else{
printf("APP open %s success",hello_node);//执行,但优先级较低,在最后输出
ioctl(fd,1,6);//执行,优先输出,对应驱动中的文件iotcl函数
}
close(fd);//执行,优先输出,对应驱动中的文件release函数
}
注意:需要先加载驱动及注册设备,再运行上述应用程序。
在当前目录及子目录下查找文件:find ./ -name fcntl.h
编译命令:arm-none-linux-gnueabi-gcc -o invoke_hello invoke_hello.c -static
命令 类型 目标文件名 源文件名 用编译器自带库运行,无需linux系统支持
输入命令:应用程序目标文件名,即运行该应用程序。(可能需要先修改权限,chmod 777 文件名)
====================================
设备节点,设备注册,驱动注册:
1)生成节点的代码可以放到任何地方,和驱动注册和设备注册的关系不是那么严密,甚至没有设备注册和驱动注册,也是可以生成设备节点的。
2)一般情况下,是将设备节点注册放到probe中,但是放到init函数中的驱动也是有的。
3)示例:在驱动代码中只生成设备节点,没有注册设备和驱动。然后加载该模块,并运行上层应用程序。
语法:printk(KERN_EMERG "HELLO WORLD enter!\n");//在串口终端打印HELLO WORLD enter!
====================================
虚拟地址和物理地址:
1)虚拟地址空间被划分为页的单位,相应的物理地址空间被划分为页帧;
2)MMU本质上是一个表格,一边是CPU发送指令对应的虚拟地址,一边存储的是物理地址;
3)在ARM处理器中,内部寄存器也是通过虚拟地址和物理地址映射之后才拿来使用的;
4)CPU通过虚拟地址,管理超过内存空间的程序;32位CPU可以管理4GB空间的虚拟地址,64位对应16GGB的虚拟地址。
5)上层程序员(包括驱动工程师)不用关心物理地址和虚拟地址具体是多少,只需要对一组宏定义(对应虚拟地址)操作,就是对4412内部寄存器操作;
====================================
地址概念:
iROM:存储三星的固化程序,uboot运行之前的代码;
iRAM:运行三星的固化程序;
rom概念扩展:泛指固化存储器。
虚拟地址范围:与内存物理地址重合。内核函数ioremap返回的是内存地址,一定在内存物理地址范围内。
====================================
GPIO初始化:
1)VA代表虚拟地址,PA代表物理地址;
2)通过宏定义,取得结构体数据,从而控制IO;
3)宏定义对应虚拟地址,通过映射数组对应到物理地址,这个映射数组是由程序定义的,放在MMU中,由ioremap函数调用。
4)物理地址和虚拟地址都是由平台文件分别定义好的。
=====================================
GPIO的使用:(使用杂项设备举例)
Linux中申请GPIO的头文件:include/linux/gpio.h
三星平台的GPIO配置函数头文件:arch/arm/plat-samsung/include/plat/gpio-cfg.h(包括三星所有处理器的配置函数)
三星平台EXYNOS系列平台,GPIO配置参数宏定义头文件:arch/arm/mach-exynos/include/mach/gpio.h(GPIO管脚拉高拉低配置参数等等)
配置参数的宏定义在arch/arm/plat-samsung/include/plat/gpio-cfg.h文件中;
三星4412处理器所有的GPIO的宏定义头文件:arch/arm/mach-exynos/include/mach/gpio-exynos4.h(已经被include在上面的头文件gpio.h中)
程序示例:
1)在驱动的probe函数中初始化GPIO
gpio_request(EXYNOS4_GPL2(0),"LEDS");//申请,失败返回值为-1,LEDS取名随意。
s3c_gpio_cfgpin(EXYNOS4_GPL2(0),S3C_GPIO_OUTPUT);//配置,设为输出脚
gpio_set_value(EXYNOS4_GPL2(0),0);//赋值,初始值为0
2)修改驱动中的iotcl函数,使之能识别上层应用命令
gpio_set_value(EXYNOS4_GPL2(0),cmd);//根据cmd设置GPIO的值
3)在自定义的remove函数中释放该gpio:
gpio_free(led_gpios[i]);//led_gpios[i]是int类型,内部也是个宏定义地址
4)修改上层应用程序,通过iotcl函数实现控制,此处不再给出程序。
5)内核的LED驱动要事先卸载掉,并重新刷内核到开发板。
6)修改makefile文件,执行make命令,生成驱动模块文件;
7)编译应用程序,命令:arm-none-linux-gnueabi-gcc -o invoke_leds invoke_leds.c -static
8)将生成的文件复制到开发板,加载驱动模块,然后运行应用程序。
其它函数:free_irq(unsigned int irq,void *dev_id);//在自定义的exit函数中调用
======================================
在加载模块的同时给模块参数赋值:(不是在上层应用程序中赋值)
module_param(name,type,perm)函数:(单个参数接收)
– name:变量名(将接收的参数赋值给该变量)
– type: 变量的数据类型(支持int long short uint ulong ushort类型)
– perm: 变量的访问权限(S_IRUSR表示所有文件所有者可读)
module_param_array(name, type, nump, perm)函数:(多个参数接收)
– name:变量的名称
– type: 变量的数据类型(支持int long short uint ulong ushort类型)
– nump:保存变量个数的变量的地址
– perm: 变量的访问权限(S_IRUSR表示所有文件所有者可读)
参数perm表示此参数在sysfs文件系统中所对应的文件节点的属性,其权限在include/linux/stat.h中有定义。
– #define S_IRUSR 00400//文件所有者可读,此处是八进制表达
– #define S_IWUSR 00200//文件所有者可写
– #define S_IXUSR 00100//文件所有者可执行
– #define S_IRGRP 00040//与文件所有者同组的用户可读
– #define S_IWGRP 00020
– #define S_IXGRP 00010
– #define S_IROTH 00004//与文件所有者不同组的用户可读
– #define S_IWOTH 00002
– #define S_IXOTH 00001
数字最后三位转化为二进制:xxx xxx xxx,高位往低位依次看,第一位为1可读,第二位可写,第三位可执行;
接下来三位表示同组用户的权限;再下来三位为不同组用户权限;
示例:(在驱动程序内)
#include <linux/moduleparam.h>//定义module_param module_param_array的头文件
#include <linux/stat.h>//定义module_param module_param_array中perm的头文件
static int module_arg1,module_arg2;//声明这些参数变量
static int int_array[50];
static int int_num;
module_param(module_arg1,int,S_IRUSR);//接收赋值,其中的int有些多余,因为声明变量时已经有了类型。
module_param(module_arg2,int,S_IRUSR);
module_param_array(int_array,int,&int_num,S_IRUSR);
编译完成后执行:insmod /mnt/udisk/module_param.ko module_arg1=10 module_arg2=20 int_array=11,12,13,14,15,16,17,18
即在加载的同时传递参数了。
加载后查询参数值:cat /sys/module/module_param/parameters/参数名
===========================================
静态申请主次设备号:(通过手动给驱动模块参数赋值的方式,自己指定设备号和卸载设备,举例为字符设备)
程序示例:(在驱动程序内)
#include <linux/fs.h>//内含三个字符设备注册函数
#include <linux/kdev_t.h>//转换设备号数据类型的宏定义MKDEV
#include <linux/cdev.h>//定义字符设备的结构体
#define DEVICE_NAME "scdev"//设备名称,将与设备号并列显示
#define DEVICE_MINOR_NUM 2//从设备号的数量
#define DEV_MAJOR 0//主设备号,为零则后续应使用动态申请
#define DEV_MINOR 0//从设备号,为零则后续应使用动态申请
int numdev_major = DEV_MAJOR;//初始赋值
int numdev_minor = DEV_MINOR;
//此处,由前述手动给驱动模块赋值,给numdev_major和numdev_minor传参数,省略相关代码。
dev_t num_dev;//定义dev_t类型的综合设备号,必须是此类型才可以。实际可能就是int类型。
num_dev = MKDEV(numdev_major,numdev_minor);//经过宏定义处理,将int类型的主设备号和次设备号合并为综合设备号
register_chrdev_region(num_dev,DEVICE_MINOR_NUM,DEVICE_NAME);//注册该设备,参数依次为:综合设备号,次设备号数量,设备名称
unregister_chrdev_region(MKDEV(numdev_major,numdev_minor),DEVICE_MINOR_NUM);//注销该设备,在卸载命令时发挥作用
使用命令“cat /proc/devices”查看已经被注册的主设备号;
编译运行,输入命令:insmod /mnt/udisk/request_cdev_num.ko numdev_major=9 numdev_minor=0;
输入命令卸载:rmmod request_cdev_num numdev_major=9 numdev_minor=0;
==============================================
动态申请主次设备号:(由Linux系统分配设备号)
程序示例:(在驱动程序内,其余参照静态申请部分)
dev_t num_dev;
alloc_chrdev_region(&num_dev,numdev_minor,DEVICE_MINOR_NUM,DEVICE_NAME);
//动态申请主次设备号,参数依次为综合设备号存储地址,次设备号,次设备号数量,设备名,申请成功后num_dev会被赋值。
numdev_major = MAJOR(num_dev);//通过宏定义,可获得主设备号
编译运行,输入命令:insmod /mnt/udisk/file.ko,即实现设备号分配;
=============================================
注册字符类设备:
#include <linux/slab.h>//分配内存空间函数头文件
大概流程:
1)在自定义的驱动init函数中,注册设备号(可静态或动态申请),使用kmalloc函数申请设备属性结构体的空间,并用memset函数清零这些空间;
2)使用kmalloc函数给设备属性结构体中的data缓冲区申请空间,并用memset函数清零这些空间;
3)在自定义的注册函数中,使用系统的cdev_init函数,初始化设备属性结构体和文件指针,并用cdev_add函数将该结构体中的cdev结构体注册到系统;(使用综合设备号,并事先声明好设备结构体和设备文件指针)
4)在自定义的驱动exit函数中,使用系统的cdev_del函数,卸载设备结构体中的cdev结构体,然后卸载设备号;
程序较乱,此处省略;
cdev_init函数:
void cdev_init(struct cdev *,const struct file_operations *);
-参数1指向设备属性结构体中的cdev结构体;
-参数2指向设备文件的指针;
例如:cdev_init(&dev->cdev,&my_fops);
cdev_add函数:
int cdev_add(struct cdev *,dev_t,unsigned);
- 参数1:cdev字符设备属性结构体
- 参数2:综合设备号
- 参数3:设备范围大小,为1应该就是1个设备
例如:cdev_add(&dev->cdev,devno,1);
cdev_del函数:
void cdev_del(struct cdev *);
例如:cdev_del(&(my_devices[i].cdev));
=============================================
生成字符类设备节点:
字符类设备节点在生成前,必须先创建设备的类,这与杂项设备不同,杂项设备在创建节点时已封装好了相应类。
此处的类(class),是一种结构体:struct class;这与C++中的类不是一个概念;
头文件:#include <linux/device.h>(含有类结构体和相应函数)
查看类:ls /sys/class
查看设备节点:ls /dev/node_name
步骤:(在驱动文件中)
1)例化一个类:static struct class *myclass;
2)在自定义的Init函数中,创建这个设备类;myclass = class_create(THIS_MODULE,DEVICE_NAME);
3)在自定义的Init函数中,创建设备节点:device_create(myclass,NULL,MKDEV(numdev_major,numdev_minor+i),NULL,DEVICE_NAME"%d",i);
4)在自定义的exit函数中,销毁设备节点:device_destroy(myclass,MKDEV(numdev_major,numdev_minor+i));
5)在自定义的exit函数中,销毁设备的类:class_destroy(myclass);
6)在自定义的exit函数中,释放内存:kfree(my_devices);
这中间伴随着设备号的注册和初始化,提供给上层文件接口等等,不再赘述,只能再看例程。
老版本linux创建设备class函数:class_device_create(不推荐使用)
使用手动命令生成设备节点:(不推荐使用)
mknod dev/test0 c 249 0(c代表字符设备,主设备号249,从设备号0)
==============================================
在上层应用程序调用字符类驱动:
1)在驱动中,增加自定义的文件操作函数;(chardevnode_open等等)
定位函数:loff_t (*llseek) (struct file *, loff_t, int)//改变文件中的当前读写位置,将新的位置作为返回值,错误返回负数
2)在驱动中,将自定义的操作函数赋值给file_operations结构体;
struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = chardevnode_open,
.release = chardevnode_release,
.unlocked_ioctl = chardevnode_ioctl,
.read = chardevnode_read,
.write = chardevnode_write,
.llseek = chardevnode_llseek,
};
3)在驱动中,如果需要不同的设备节点有不同的功能,只需要在注册设备的时候添加不同的file_operations结构体即可;
==============================================
使用字符类设备的GPIO:
1)与前述字符类设备操作类似;
2)在自定义的io_ctl函数中执行IO操作;//gpio_set_value(led_gpios[arg], cmd);
3)在自定义的init函数中执行GPIO的申请和初始化;//示例中居然没有用到probe函数
4)在自定义的exit函数中释放GPIO;//gpio_free(led_gpios[i]);
通过上层应用程序传递控制命令参数,进而控制GPIO,还包括前述申请设备号,生成设备节点,注册设备等等。
===============================================
proc文件系统:(伪文件系统,用于查看内核运行状态的信息)
1)查看内存:cat proc/meminfo;
2)查看CPU:cat /proc/cpuinfo;
3)查看中断:cat /proc/interrupts;
一个文档《proc参数介绍》
==============================================
GPIO输入:(GPC,GPX,GPL等等都可以作为GPIO)
1)设置引脚方向,设置上下拉寄存器;(到drivers目录下寻找其虚拟地址的宏定义)
2)相关函数:申请gpio_request,读取gpio_get_value,配置s3c_gpio_cfgpin,上下拉s3c_gpio_setpull,释放gpio_free;
(到arch目录下平台文件去找函数)
3)在上层应用程序中使用read函数访问驱动模块,调用驱动中的ioctl函数,通过里面的gpio_get_value函数获得输入值。
举例:
gpio_request(EXYNOS4_GPC0(3),"SWITCH3");
gpio_get_value(EXYNOS4_GPC0(3));
s3c_gpio_cfgpin(EXYNOS4_GPX0(6),S3C_GPIO_INPUT);
s3c_gpio_setpull(EXYNOS4_GPX0(6),S3C_GPIO_PULL_NONE);
==============================================
ioremap函数的使用:
1)在驱动中不能使用物理地址操作,只能使用虚拟地址;
2)ioremap函数将物理地址转为虚拟地址,其返回值是虚拟首地址,第一个形参是物理基地址,第二个是地址段长度,例如0x10;(ioummap没做举例)
ioremap(unsigned long phy_addr,unsigned long size);
此时,虚拟地址+偏移量,等价于物理基地址+偏移量。
3)使用unsigned long型的指针,指向虚拟地址,进行寄存器操作;
4)在驱动中没有main函数,只需自定义init和exit函数即可。
==============================================
并发与竞态的概念:
1)多个CPU核之间,单CPU核的多个程序之间,对某资源的访问可能产生竞态;
2)临界区:访问共享资源的代码区,称为临界区;
3)互斥机制:原子操作,自旋锁,信号量,互斥体;
==============================================
整型原子操作:(使用变量保证程序之间互斥访问公共资源)
1)宏定义:
atomic_t:代表int类型,用于声明原子变量;
atomic_read(&value_atomic):返回原子变量的值,形参是原子变量地址;
atomic_inc(&value_atomic):原子变量加1,形参是原子变量地址;
atomic_dec(&value_atomic):原子变量减1,形参是原子变量地址;
ATOMIC_INIT(0):用于给原子变量赋值,形参是所赋的值;例如:value_atomic=ATOMIC_INIT(0);
用valotile关键词声明的变量:从内存存取,保证稳定访问,但速度较慢。否则可能被优化成CPU寄存器。
2)举例:
使用两个上层应用程序,调用一个设备的驱动模块;
在驱动模块中的自定义open函数中,使用atomic_read判断原子变量的值,并用atomic_inc改值,以便互斥访问;
如果两个程序同时执行,则必然只有一个成功访问设备;(实际并没有演示如何让两个程序同时执行)
在自定义的release函数中,使用atomic_dec改值,允许其它程序访问。
===============================================
位原子操作:(了解即可,与整型原子用法类似)
1)宏定义:
test_bit(0,&value_bit):返回value_bit的第0位的值;
set_bit(0,&value_bit):将value_bit的第0位置1;
clear_bit(0,&value_bit):将value_bit的第0位清零;
其中,value_bit使用了unsigned long int类型。
2)举例:
与整型原子类似,在驱动自定义的open函数中,判断位原子的值,如果非0,则置1,以屏蔽其它程序访问;
在自定义的release函数中,将位原子清零,允许其它程序访问。
================================================
485驱动:(485本身只是个电气接口,将串口单端信号变为差分信号)
1)驱动部分只控制485的收发使能,是一个GPIO的控制,驱动注册为杂项设备,接受上层应用程序的输入命令;//驱动程序:max485_ctl.c
2)串口设备节点使用ttySAC1,直接在应用程序内调用;
3)举例:
使用了两块开发板,一个发送,另一个接收,分别运行各自的应用程序。
使用了两个串口终端窗口,来显示各自开发板的状态。
输入命令:./test_485 /dev/ttySAC1 0(接收)
输入命令:./test_485 /dev/ttySAC1 1 (发送)
4)硬件连接:两侧485芯片的A对A,B对B。芯片型号:SN65HVD3082;
5)一种宏定义用法:(在define的时候什么也没指定,后续依然可以作为判断依据,决定代码段是否执行)
#define MAX485_CONTROL
代码段
#ifdef MAX485_CONTROL
代码段
#endif
6)串口头文件:#include <termios.h>
串口配置函数:int set_opt(int fd,int nSpeed, int nBits, char nEvent, int nStop)
fd:用open函数打开串口设备节点所返回的句柄;
nSpeed:波特率,2400,4800,9600,115200,460800,921600;
nBits:位数,7或8;应该是有效数据位数。
nEvent:校验方式,奇校验'O',偶校验'E',无校验'N';
nStop:停止位,1或2;含义不详。
例如:set_opt(fd1, 9600, 8, 'N', 1);
================================================
PWM定时器输出:
1)基本原理:CPU上带有PWM管脚,可由定时器控制输出;
通过二级分频,将系统时钟提供给定时器;
定时器通过TCNTn和TCMPn两个寄存器进行计时,比较,决定何时翻转输出电平;
TCNTn为全周期,TCMPn为高电平周期;TCNTn做减1计数,与TCMPn相等时输出高电平,开始计数和归零后输出低电平;
通过更新设置,来加载上述两个寄存器;(事先要给相应缓冲寄存器赋值,缓冲寄存器为TCNTBn和TCMPBn)
开启翻转功能,允许在TCNTn和TCMPn相等时输出高电平;
2)定时器的控制寄存器:TCON;(引脚设置为PWM输出,则是通过GPIO的控制寄存器)
================================================
PWM驱动:
1)语法:struct { } *p1;//定义一个结构体类型的指针,结构体的数据结构在括号内,该结构体没有起名。
2)举例:根据寄存器物理地址,使用ioremap函数映射为虚拟地址;//使用volatile unsigned long类型变量存储物理和虚拟地址
自定义一个结构体及指针,结构体内的数据结构与实际寄存器的位数和地址排列相同;//使用了unsigned int类型变量对应寄存器
通过自定义结构体的指针,指向虚拟首地址,操作这些寄存器;
示例仅仅定义了init和exit函数。
3)头文件:使用了GPIO类似的头文件;
示例仅手动加载了驱动,没有上层应用程序;
================================================
查询GPIO按键:
1)对内核make menuconfig,取消内核原来的中断按键驱动,重新生成和烧写内核;
Device Drivers--->Input device support--->Keyboards--->GPIO buttons
2)在arch/name/name/name.c平台文件中注册新设备,增加相应的结构体和引用,类比LEDS;
3)修改驱动和上层应用程序,手动加载运行;//“应用程序名 &”代表在后台运行!?
在上层应用中open设备后,通过read函数反复读取,从而实现查询。
4)CPU占用率较高,会占用1个CPU内核,更高效的方式:中断,或异步通信;
===============================================
GPIO按键的中断:
1)流程:中断申请--->中断释放;(伴随中断处理函数)
2)中断申请函数:
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev_id)
参数1:宏定义形式的中断号。(与平台架构相关,GPIO对应的号码查找datasheet,宏定义查找平台文件)
参数2:自定义的中断处理函数名(自定义的中断处理函数最后需要return IRQ_HANDLED)
参数3:中断标志。上升/下降沿,高/低电平(下降沿触发的宏定义:IRQ_TYPE_EDGE_FALLING)
参数4:中断名称。(在窗口下的查看方式:cat /proc/interrupts)
参数5:指向设备的结构体指针或者NULL;//例如pdev(使用struct platform_device *pdev声明)
3)中断释放函数:
free_irq(unsigned int irq,void *dev_id)
参数1:宏定义形式的中断号。
参数2:指向设备的结构体指针或者NULL。
4)开发板的中断号宏定义:IRQ_EINT(x),其中x需根据GPIO所在引脚查找手册上的号码;
5)内核准备,在平台文件中添加相应结构体和引用;需要在内核去除其它调用该GPIO的驱动;
6)//中断头文件
#include <linux/irq.h>
#include <linux/interrupt.h>
7)中断处理函数:
static irqreturn_t eint10_interrupt(int irq,void *dev_id)//eint10_interrupt是自定义名称
8)举例:在自定义的probe函数中申请中断,在remove函数中释放中断
宏定义:DPRINTK函数;宏定义:-EINVAL;-ENOMEM;
================================================
I2C总线设备驱动:
1)外设驱动通过API调用主机驱动,主机驱动直接控制寄存器,一般是定型的;
2)外设驱动面向不同的外设,进行不同的配置;例如,设备名称,设备的I2C地址;
3)API接口:注册i2c设备:i2c_board_info;
驱动注册和卸载函数以及结构体:i2c_del_driver/i2c_add_driver,i2c_driver
读写函数和结构体:i2c_transfer,i2c_msg
4)查询i2c设备地址:ls /sys/bus/i2c/devices/,查询结果“3-0038”,代表第三通道I2C的0038设备地址;
查询i2c设备名称:cat /sys/bus/i2c/devices/3-0038/name
5)内核的make menuconfig中去除占用I2C通道的相应设备,然后在平台文件的i2c_borad_info结构体中增加自定义设备;
例如:{ I2C_BOARD_INFO("i2c_test", 0x70>>1),},//指定了设备名和I2C地址
=====================================
I2C驱动注册:(由触摸屏驱动修改而来)
1)例化i2c_driver类型的结构体,并指定函数和成员初始值;
2)使用late_initcall调用自定义init函数,而不是使用module_init;//它比module_init晚些执行,两者只在集成到内核时有区别,手动加载驱动无区别
3)在自定义remove函数中,使用i2c_set_clientdata函数,移除I2C结构体数据;(移除i2c_client结构体)
4)结构体:static const struct i2c_device_id i2c_test_id[] = {
{ "i2c_test", 0 },//0代表硬件版本号,可以是1,2等
{ }
};//该结构体用于给例化的i2c_driver类型的结构体的id_table成员赋值。
5)在自定义的init函数中,使用i2c_add_driver函数注册驱动;例如:i2c_add_driver(&i2c_test_driver);
6)在自定义的exit函数中,使用i2c_del_driver函数注销驱动;例如:i2c_del_driver(&i2c_test_driver);
语法:printk("==%s:\n", __FUNCTION__);//打印的字符串就是函数名
=======================================
I2C驱动调用:(头文件#include <linux/i2c.h>)
1)过程:在probe和remove函数中,利用其形参i2c_client结构体,将该结构体指针提供给i2c_transfer函数使用,完成读写;
2)i2c_transfer函数:
int i2c_transfer(struct i2c_adapter *adap,struct i2c_msg *msgs,int num)
-形参1:指向i2c_client结构体中的adapter,是i2c的通道地址;
-形参2:指向i2c_msg结构体;该结构体内部定义了每次操作的从机地址,读写标志,字节数,读写数据指针等等;如下:
struct i2c_msg {
__u16 addr; //slave address,通道地址,不是外部设备的I2C寄存器地址
__u16 flags;
#define I2C_M_RD 0x0001 //read data, from slave to master
__u16 len; //msg length,如果长度大于1,则连续读写后续寄存器地址
__u8 *buf; // pointer to msg data
};
-形参3:指向读写缓冲区;在写的时候该缓冲区首地址存的是寄存器地址,读的时候是存储读到的数据。
3)举例:(在自定义的读函数中)
u8 buf1[4] = { 0 };
u8 buf2[4] = { 0 };//存储读取的数据
struct i2c_msg msgs[] = {
{
.addr = client->addr, //0x38,通道地址
.flags = 0, //写操作
.len = 1, //要写的数据的长度
.buf = buf1, //存储寄存器地址的指针
},
{
.addr = client->addr,
.flags = I2C_M_RD,//读操作
.len = 1,
.buf = buf2,//读取的数据的指针
},
};
buf1[0] = addr;//写寄存器地址
i2c_transfer(client->adapter, msgs, 2);//操作两次,根据msgs指针的结构体内容决定读写方式和寄存器等等。
=======================================
应用层对i2c的读和写:省略,一般不这样用,用法与前述应用程序类似。
=======================================
SPI驱动概述:
1)也是设备驱动,控制器驱动部分和核心驱动不用管;
2)开发板限制:对于安卓4.0,使用SPI接口就不能用WIFI;安卓4.4没有这个限制;另外,精英板默认配置为RFID的SPI接口,多了一个复位信号。全能板默认配置为CAN的SPI接口。修改配置,则要make menuconfig,重配内核选项。具体配置路径省略。
(RFID对应RC522模块,CAN对应MCP251X)
3)设备节点:用于上层应用程序进行文件操作;
=======================================
SPI设备注册:
1)在平台文件的spi_board_info类型的结构体数组中,添加自定义的设备,指定相关参数;
结构体数据结构:
.modalias = "rc522", //初始化设备的名称
.platform_data = NULL, //指向初始化函数的指针
.max_speed_hz = 10*1000*1000, //初始化传输速率
.bus_num = 2, //控制器编号
.chip_select = 0, //控制器片选的编号
.mode = SPI_MODE_0, //spi的模式,0--3,0和2上跳沿采样,1和3下跳沿采样
.controller_data = &spi2_csi[0], //作为片选的GPIO信息
2)在平台文件中,会调用设备注册函数spi_register_board_info,将上述结构体注册;
3)设备查询:cat sys/bus/spi/devices/spi2.0/modalias //此时查询的是设备名称
4)修改平台文件后,需要重配内核,将RFID和CAN驱动取消,然后重新烧写内核。
5)杂项设备相当于在字符设备基础上,封装好了子设备号。否则字符设备非常有限。
6)多个SPI设备可以使用同一个SPI总线,但需要用不同的片选信号。使用不同的GPIO作为片选。
========================================
SPI驱动注册:
1)例化一个spi_driver类型的结构体,指定相关参数、指向自定义probe和remove函数;
2)在init和exit函数中,调用spi_register_driver和spi_unregister_driver注册和注销该结构体;
3)头文件:
#include <linux/spi/spi.h>
#include <linux/spi/spidev.h>
========================================
在SPI驱动中复位、读写RFID设备:
1)修改已有驱动程序,涉及到中断和互斥访问的部分省略;
2)在复位中用到gpio_request_one函数:配置GPIO后释放,直到下一次被配置,状态才改变;
例如:gpio_request_one(RC522_RESET_PIN, GPIOF_OUT_INIT_HIGH, "RC522_RESET");
3)例化一个spi_device类型的结构体,在自定义probe函数中,(先执行复位函数,操作GPIO)用形参为该结构体赋值,然后执行读写函数;
4)自定义读写函数:例化spi_transfer类型的结构体,及spi_message类型的结构体,使用spi_message_init及spi_message_add_tail函数初始化传输数据,例如:
struct spi_transfer t = {
.tx_buf = buffer,//发送缓冲
.len = len,
};
struct spi_message m;
spi_message_init(&m);
spi_message_add_tail(&t, &m);
DECLARE_COMPLETION_ONSTACK(done);
m.complete = complete;
m.context = &done;
5)使用spi_async函数执行收发操作,例如:spi_async(my_spi,&m),执行成功返回0;读、写调用方式相同。
6)其它函数:wait_for_completion(&done);//等待收发完成
7)标志:m.status;//收发完成后为0
8)标志:m.actual_length;//收发完成后返回收发数据长度,可能是字节数
===========================================
通过上层应用读写SPI设备:
1)例化file_operations结构体,内部指向驱动中自定义的读、写、打开SPI函数;
2)通过应用中的读、写、iotcl函数,给驱动传递参数。应用程序较复杂,具体省略。
3)例化miscdevice类型的结构体,并在自定义的probe函数中将其注册为杂项设备节点,在remove中注销节点;
//misc_register(&rc522_dev);
//misc_deregister(&rc522_dev);
4)驱动中的其它函数:copy_from_user(tx_buf,buf,count);//将buf中的count数据量复制到tx_buf;
5)驱动中的其它函数:copy_to_user(buf,&rx_buf,status);//将rx_buf中的status数据量复制到buf;
6)应用中的其它函数:pabort("can't set max speed hz");//打印信息,在异常返回的时候
============================================
定时器概述
1)时钟中断:即Linux的0号中断,由定时器产生,送入CPU,以更新系统日期和时间、记帐、监督系统工作以及确定未来的调度优先级等。
2)时钟中断频率通过CONFIG_HZ来设置,范围是100-1000,单位HZ;
3)内核的全局变量jiffies:即内核启动以来产生的时钟中断数;内核启动以来的秒数=jiffies/HZ;
============================================
定时器驱动:
1)头文件
#include "linux/timer.h"
#include "linux/jiffies.h"
2)例化一个timer_list类型的结构体,调用setup_timer初始化该结构体,指定中断处理函数和待处理的数据;(没有设置超时时间)
例如:setup_timer(&demo_timer,time_func,(unsigned long) "demo_timer!");
3)给结构体的超时时间赋值;例如:demo_timer.expires = jiffies + 1*HZ;(当前时间加1秒)
4)增加定时器中断;例如:add_timer(&demo_timer);//将结构体添加到虚拟平台总线
以上都是在自定义的init函数中。
5)在中断处理函数里调用mod_timer函数,修改定时器中断触发条件;例如:mod_timer(&demo_timer,jiffies + 5*HZ);
(增加5秒,删除前面的定时器中断,重新添加该中断)
6)在自定义的exit函数中,删除定时器中断;例如:del_timer(&demo_timer);(将结构体删除)
7)其它概念:双向链表。虚拟平台总线实际是一个双向链表,各种驱动的结构体以链表方式连接在一起。双向,就是链表单元含有两个指针,分别指向前面和后面。
8)timer_list类型的结构体成员:
struct list_head entry:双向链表指针。
unsigned long expires;超时中断数,与jiffies同单位,超过这个数就产生中断。
struct tvec_base *base;管理时钟的结构体,在内核初始化时已完成
void (*function)(unsigned long);中断处理函数指针
unsigned long data;传给中断处理函数的数据
============================================
定时器内核代码分析:(不需要掌握)
1)内核代码中函数名以下划线开头:局部作用函数;
2)双向链表操作函数都在include/linux/list.h文件中;
3)每个CPU核都有自己的定时器双向链表及基地址,将定时器插入到相应链表中;
4)定时器双向链表分了几组,根据超时时间决定例化的定时器链表单元插入到哪一组;
5)内核C语言运行起点:init/main.c----start_kernel函数;(前面是汇编代码运行)
6)内核对定时器的操作:初始化,判断超时,调用中断处理函数,释放定时器;(由mod_timer函数实现定时器再次运行)
============================================
中断基础知识:
1)硬件中断与软件中断:硬中断是外部设备对CPU的中断;软中断通常是硬中断服务程序对内核的中断;
2)外部中断与内部中断:都属于硬中断;外部中断由外设产生;内部中断由CPU自身启动,或由软中断指令启动;内部中断通常具有更高的优先级;
3)中断向量:存储中断处理程序的入口地址;
4)向量中断与非向量中断:向量中断直接跳转到中断处理程序,速度较快;非向量中断是先到解析程序,根据相应的中断状态寄存器,再跳转到处理程序;
5)中断处理程序架构:上半部分---硬件处理;下半部分---软件处理;两者通过软中断连接。
6)FIQ与IRQ:IRQ即前述中断;FIQ(快速中断请求)则是在中断向量位置直接铺设程序,而不需要跳转。如果有多个FIQ时,按照非向量中断方式处理。(但是少了第一次跳转)另外,FIQ的寄存器资源更多,中断恢复更快。
============================================
触摸屏驱动:(一般由屏幕厂家提供,做少量修改)
1)4根线:I2C信号两根,中断信号1根,复位信号1根;
2)复位信号需要在驱动中配置GPIO;
3)捕捉到触摸中断后,通过I2C将数据传给上层应用程序;
4)函数:gpio_direction_output(EXYNOS4_GPX0(3),0);//直接将GPIO设为0电平输出,等价于原来的方向和赋值两个函数。
5)注册i2c驱动,并在自定义的的probe函数中,通过平台文件将GPIO和坐标初始值传给驱动中的结构体,开启中断。在中断服务中创建工作队列,处理屏幕数据。
6)函数:gpio_to_irq(),将括号内的GPIO设置为中断;
7)函数:disable_irq_nosync,关闭中断,不用等待中断服务执行完。此后转到工作队列。捕捉到屏幕数据时,使用它来关闭中断。
8)函数:disable_irq,关闭中断,但要等待服务执行完。
9)工作队列:调用读取和汇报函数,将屏幕数据传给内核。每次触摸点数,决定每组I2C数据个数。
10)调试:打印出坐标信息,修改汇报函数中的坐标生成公式。
============================================
触摸屏分辨率及开机图片修改:
1)屏幕电源:3.3V;其它屏幕可能不是此值;
2)背光电源:7寸屏为LED+(芯片RT8059),9.7寸屏为LED+和LED-(芯片TP3984),由电源芯片将5V转换得到;
3)屏幕亮度控制信号:PWM信号;(7寸由开发板直接给屏幕提供信号,9.7寸是由开发板先送到TP3984再给屏幕)
4)其它信号:时钟和数据LVDS差分对;
5)屏幕驱动路径:/drivers/video/samsung/name.c
6)屏幕驱动内容:分辨率,刷新率(freq),颜色位宽(bpp),视频信号时序,行场信号极性;
7)背光驱动路径:arch/arm/mach-exynos/name.c
8)背光驱动内容:设置ARM芯片GPIO引脚为LCD颜色输出模式,设置时钟信号开关,设置背光开关;
9)LOGO图片路径:/drivers/video/samsung/name.h,里面的第二个数组;
10)LOGO图片要求:bmp格式,480×640,(素材图要更高分辨率才清晰),使用软化工具转为C语言数组;
11)LOGO位置调整:/drivers/video/samsung/name.c
12)美图秀秀:可将图片转化为bmp格式;