1.裸板驱动和linux驱动的异同点
裸板驱动:
uart驱动程序:
uart_init
uart_puts
uart_gets
i2c控制器驱动:
i2c_start
i2c_stop
i2c_tx
i2c_rx
g-sensor驱动
mma8653_read_id
mma8653_read_acc
linux下驱动:英语的完型填空
需要的知识:
1) 硬件的知识
读懂电路原理图
阅读芯片的数据手册
熟悉计算机中常用的接口
UART
I2C
SPI
CAN
USB
...
2) 驱动程序属于内核态的一部分 运行于内核态
内核态编程的规则
用户态和内核态的数据交互
内核模块编程的基本框架
解决竞态
...
3) 驱动编程框架
字符设备驱动程序框架
读写以字节为单位 读写顺序固定
例如:键盘
块设备驱动程序框架
读写以块 (多字节) 为单位 读写顺序不固定
例如: USB 硬盘 ......
网络设备驱动程序框架
读写以帧(多字节)为单位 读写顺序固定
例如: Gamc...
2.内核态驱动编程的学习方法
内核态编程最好的老师就是内核源码
1) 看看内核中其他模块如何使用该函数
2) 研究该函数的源码分析及使用功能
经典书籍:
内核: Linux内核的设计与实现
驱动: LDD3
精通Linux设备驱动程序开发
3.搭建linux驱动开发环境
3.1 安装交叉编译工具
3.2 编译linux内核
cd /home/liuyang/driver
mkdir kernel 编译通过的内核源码
cp /mnt/hgfs/driver/env/kernel.tar.bz2 driver/
tar xf kernel.tar.bz2
cd kernel
cp arch/arm/configs/x6818_config .config
vi Makefile
ARCH
CROSS_COMPILE
make uImage
让开发板使用新编译的内核
cp uImage /tftpboot
tftp 48000000 uImage
mmc write 48000000 800 3000
3.3 让开发板上的内核挂载/home/liuyang/driver/rootfs根文件系统
mkdir rootfs 根文件系统
cp porting/busybox-1.23.2/_install/ driver/rootfs -a
//注意是将_intsall 换成了名字为rootfs的目录,并不是将_install拷贝到rootfs目录下
vi /etc/exports
+ /home/liuyang/driver/rootfs *(rw,sync,no_root_squash)
/etc/init.d/nfs-kernel-server restart
setenv bootcmd mmc read 48000000 800 3000 \; bootm 48000000
setenv bootargs root=/dev/nfs nfsroot=192.168.1.8:/home/liuyang/driver/rootfs
ip=192.168.1.6:192.168.1.8:192.168.1.1:255.255.255.0 maxcpus=1
init=/linuxrc console=ttySAC0 lcd=wy070ml tp=gslx680
saveenv
4.内核态编程的特点
4.1 内核代码的特点
1)内核中用到的所有函数都是自身实现的
2) 内核源码使用的是GNU C
GNU C 是标准C的扩展版
3) 更多的关注代码的执行效率 以及代码的可移植性
4.2 内核编程时需要注意的问题
1)内核编程时使用的虚地址为3G~4G
2)内核中不允许做浮点运算
3) 每个线程对应独立的栈空间
每个线程有两个栈,用户态的栈 和内核态的栈
5.编写独立的ko文件
vi hello.c
#include //在指定的内核源码目录下找该头文件
#include
int __init hello_world_init(void)
{
printk("helloworld\n");
return 0;
}
void __exit hello_world_exit(void)
{
printk("bye bye!\n");
}
module_init(helloworld_init);
module_exit(helloworld_exit);
vi Makefile
//编译hello.c生成内核模块文件hello.ko
obj-m += hello.o
KERNEL_PATH=/home/liuyang/driver/kernel/
all:
make -C $(KERNEL_PATH) M=$(PWD) modules
//-C:指定进入哪个目录进行编译
//指定内核源码目录 并且该源码是编译通过的
//M=$(PWD),指定当前目录
//modules,编译内核模块的固定写法
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
make
cp hello.ko /home/liuyang/driver/rootfs
insmod hello.ko
rmmod hello
int __init hello_world_init(void)
module_init(helloworld_init);
//如果模块在安装时,希望某个函数被调用,以上是标准写法
//安装模块时被调用的函数,其形式必须为:
//int xxxx(void)
//__init --->本身是一个宏,位于linux/init.h
//规定hello_world_init对应的代码放入 ".init.text" 段
//放入".init.text"段的代码执行一次 其占用的空间就释放
//被module_init 修饰的函数在模块被安装的时候被调用
void __exit hello_world_exit(void)
module_exit(helloworld_exit);
//如果模块卸载时,希望某个函数被调用 以上是标准写法
//被module_init修饰的函数在卸载模块时被调用
//__exit被其修饰的函数,链接时被放入".exit.text"段
//放入该段的代码执行一次,占据的存储空间就释放
在insmod hello.ko的时候,报hello: module license 'unspecified' taints kernel
污染了内核.
所以应该也声明为开源的
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EFI Test Driver");
MODULE_AUTHOR("yang Liu ");
6 导出符号
它的作用是解决模块与模块之间的函数调用问题
应用编程
a.c
int add (int x, int y)
{
return x+y;
}
a.h
extern int add (int, int)
b.c
#include "a.h"
res = add (a,b);
内核编程
a.c
int add (int x, int y)
{
return x+y;
}
EXPORT_SYMBOL(add);/EXPORT_SYMBOL_GPL(add);
a.h
extern int add (int, int)
b.c
#include "a.h"
res = add (a,b);
vi export.c
#include
#include
MODULE_LICENSE("GPL");
int add (int x, int y)
{
return x + y;
}
EXPORT_SYMBOL(add);
vi export.h
#ifndef __EXPORT_H_
#define __EXPORT_H_
extern int add(int , int);
#endif
vi import.c
#include
#include
#include "export.h"
MODULE_LICENSE("GPL");
int __init import_init(void)
{
int res = add (10,20);
printk ("res = %d\n", res);
return 0;
}
void __exit import_exit(void)
{
printk("bye bye\n");
}
module_init(import_init);
module_exit(import_exit);
vi Makefile
obj-m += import.o export.o
KERNEL_PATH=/home/liuyang/driver/kernel/
all:
make -C $(KERNEL_PATH) M=$(PWD) modules
cp *.ko ../../rootfs
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
注意安装和卸载的顺序:
insmod export.ko
lsmod
insmod import.ko
lsmod
rmmod import
lsmod
rmmod export
lsmod
EXPORT_SYMBOL 和 EXPORT_SYMBOL_GPL的区别:
EXPORT_SYMBOL导出的符号所有的模块都可以使用
EXPORT_SYMBOL_GPL导出的符号 只有遵循"GPL"的模块才能够使用
7.关于printk
printk 的标准使用方法
printk("<0/1/2/3../7>" "res = %d\n", res);
特殊情况
pritnk ("res = %d\n", res); //使用的是默认优先级
linux内核将打印优先级设置为8级
值越小优先级越高
printk 输出的信息先到内核维护的缓冲区
缓冲区的信息能不能输出到控制台是有限制的
printk调用时使用的优先级 > 优先级阈值的 能够输出到终端
可以通过dmesg 输出缓冲区中的所有打印信息
实际项目中有debug 和release版本
proc,基于内存的虚拟文件系统
板子上的proc和pc上的proc目录不会做同步
用于向用户空间输出内核的执行状态
cat /proc/进程ID/map
cat /proc/cpuinfo
cat /proc/meminfo
cat /proc/sys/kernel/printk
7 4 1 7
第一个值,优先级阈值开关
优先级高于该值的printk信息可以输出到终端
第二个值, 默认优先级
printk ("res = %d\n",res);
vi printkall.c
#include
#include
MODULE_LICENSE("GPL");
int __init printkall_init(void)
{
printk("<0>" "level 0\n");
printk("<1>" "level 1\n");
printk("<2>" "level 2\n");
printk("<3>" "level 3\n");
printk("<4>" "level 4\n");
printk("<5>" "level 5\n");
printk("<6>" "level 6\n");
printk("<7>" "level 7\n");
return 0;
}
void __exit printkall_exit(void)
{
printk("<0>" "level 0\n");
printk("<1>" "level 1\n");
printk("<2>" "level 2\n");
printk("<3>" "level 3\n");
printk("<4>" "level 4\n");
printk("<5>" "level 5\n");
printk("<6>" "level 6\n");
printk("<7>" "level 7\n");
}
module_init(printkall_init);
module_exit(printkall_exit);
vi Makefile
obj-m += printkall.o
KERNEL_PATH=/home/liuyang/driver/kernel/
all:
make -C $(KERNEL_PATH) M=$(PWD) modules
cp *.ko ../../rootfs
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
insmod
修改打印优先级阈值的方式一:
echo 4 >/proc/sys/kernel/printk
rmmod printkall
dmesg
重启之后无效,是因为/proc下是基于内存的文件系统
修改打印优先级阈值的方式二:
setenv bootargs ...... loglevel=数字 (优先级阈值)
8.模块参数
用户态:
./a.out xxx yyy zzz
int main (int argc, char **argv)
内核态:
安装内核模块时给其传递参数
内核中模块参数的使用步骤:
1) 定义全局变量
2) 通过特定的宏将其声明为模块参数
module_param (name, type, perm);
name, 要声明为模块参数的变量名称
type, 变量的类型
int long short charp (char *)
perm,权限
module_param_array(name, type, nump, perm);
name, 数组的名称
type, 数组元素的数据类型
nump, 数组元素个数指针
perm, 权限
3) 使用模块参数
insmod moduleparam.ko
insmod moduleparam.ko irq=10 str="loongson" fish=1,2,3,4,5,6
#include
#include
MODULE_LICENSE ("GPL");
int irq = 0;
char *str = "liuyang";
int fish[10] = 0;
int num = 10;
module_param(irq, int, 0644);
module_param(str, charp, 0);
module_param_array(fish, int, &num, 0600);
int __init moduleparam_init (void)
{
int i = 0;
printk ("irq = %d\n", irq);
printk ("str = %s\n", str);
for (; i < num; i++)
{
printk ("fish[%d] = %d\n", i, fish[i]);
}
return 0;
}
void __exit moduleparam_exit (void)
{
int i = 0;
printk ("irq = %d\n", irq);
printk ("str = %s\n", str);
for (; i < num; i++)
{
printk ("fish[%d] = %d\n", i, fish[i]);
}
}
module_init (moduleparam_init);
module_exit (moduleparam_exit);
ls /sys/module/moduleparam/parameters/
ls fish -l
ls irq -l
因为所有的权限都是0,所以没有str文件
cat /sys/module/moduleparam/parameters/irq
echo 100 > /sys/module/moduleparam/parameters/irq
echo 11,22,33,44,55 >/sys/module/moduleparam/parameters/irq/fish
看其在内核模块中的值发生了改变没?
rmmod moduleparam
发现发生了改变,神奇吧?!!!! 我们在用户空间改变了内核空间的值耶! 我就说神奇吧!哈哈
4) 模块参数的使用场景:
驱动代码调试
VAR声明为模块参数
REG=VAR
9 系统调用
谈谈对系统调用的理解?
9.1 系统调用的意义:
是用户空间调用内核空间函数的一种方式
由用户态进入内核态的一种方式
注: 由用户态进入内核态的另一种方式是中断
它保证了内核的安全
允许应用程序调用内核中的函数
以安全方式调用内核中的函数
9.2 系统调用是如何实现的?
应用程序首先使用适当的值填充寄存器
然后调用特殊的指令
该指令的执行 导致跳转到内核的某个位置去执行
在该位置根据填充到寄存器中的值
来找到内核中对应的函数 并调用该函数
函数执行完毕后 原路返回到用户空间
open ("a.txt", O_RDWR);
适当的值: arch/arm/include/asm/unistd.h
#define __NR_open 5
如果是open系统调用 5
寄存器 : r7
特殊的指令:
软中断指令 ARM swi
X86 int
固定的位置 : 软中断异常产生后 跳转到异常向量表
再跳转到软中断异常处理函数中
arch/arm/kernel/entry-common.S
ENTRY(vector_swi)
Get the system call number.//获取系统调用号
adr tbl, sys_call_table
@ load syscall table pointer
sys_call_table [r7]
sys_call_table 存在于calls.S
sys_call_table:
.long sys_restart_syscall /* 0 - old "setup()" system call */
.long sys_exit
.long sys_fork
.long sys_read
.long sys_write
.long sys_open /* 5 */
...
总结:
open (.....)
5
swi
------------------------
sys_call_table[5]
等价于
sys_open(...)
{
.......
}
9.3 增加一个新的系统调用 演示用户空间如何调用内核空间的函数
cd /home/liuyang/driver/kernel
1) 在内核下增加一个新函数
vi arch/arm/kernel/sys_arm.c
asmlinkage int sys_add(int x, int y)
{
printk ("enter %s\n", __func__);
return x + y;
}
2) 增加新的系统调用号
vi arch/arm/include/asm/unistd.h
#define __NR_add (__NR_SYSCALL_BASE + 378)
3) 更新系统调用表
vi arch/arm/kernel/calls.S
CALL(sys_add)
4) 编译内核 让开发板使用新内核
make uImage
cp arch/arm/boot/uImage /tftpboot
tftp 48000000 uImage
bootm 48000000
5) 编写应用程序 在用户空间调用sys_add
#include
int main (void)
{
//open --> 5 --> r7 ---> swi
//open ("a.txt", O_RDWR);
//syscall (5, "a.txt", O_RDWR); //syscall是间接的系统调用
//这种写法和上面的效果一样
syscall (378, 10, 20);
pritnf ("res = %d\n", res);
return 0;
}
arm-cortex_a9-linux-gnueabi-gcc test.c -o test
cp test ../../rootfs/
./test 报没有libgcc_s.so.1 这个库,去交叉编译工具的安装包里找
find-name /opt/arm-cortex_a9-eabi-4.7-eglibc-2.18/ -name libgcc_s.so.1
cp /opt/arm-cortex_a9-eabi-4.7-eglibc-2.18/arm-cortex_a9-linux-guneabi/sysroot/lib/libgcc_s.so.*
/home/liuyang/driver/rootfs/lib/ -a
./test