【什么是驱动程序】
uart_init
uart_puts
uart_gets
这就是裸板上的串口驱动
linux中将串口驱动封装了一套框架
其中实现了大量硬件无关的代码
只需要编程实现硬件相关的代码
linux下完成驱动相关的开发,需要三方面的知识:
1)硬件相关的知识
电路原理图
芯片的数据手册
RS232通信协议
I2C通信协议
2)内核的知识
内核驱动属于内核的一部分,它运行在内核态
内存管理
解决竞争状态
...
3)驱动框架的知识
内核中已经实现了大量的硬件驱动,完成了驱动的框架编程,只需要驱动工程师编程实现硬件相关的代码就可以了。
1、搭建linux驱动开发环境
1.1 安装交叉编译工具
'安装包 - 解压缩
// 位置:~/workdir/arm-linux-gcc/arm-cortex_a9-eabi-4.7-eglibc-2.18.tar.gz
'PATH修改
// :/opt/arm-cortex_a9-eabi-4.7-eglibc-2.18/bin
1.2 拿到uboot、编译、烧写进开发板
1.3 拿到kernel、编译、烧写到开发板
1.4 制作根文件系统,通过nfs方式让开发板可以加载该文件系统
$:'sudo vi /etc/exports
/home/tarena/driver/rootfs *(rw,sync,no_root_squash)
$:'sudo /etc/init.d/nfs-kernel-server restart
#:'setenv bootcmd mmc read 0x48000000 0x800 0x3000 \; bootm 0x48000000
#:'setenv bootargs root=/dev/nfs nfsroot=192.168.1.8:/home/tarena/jy/driver/rootfs ip=192.168.1.6:192.168.1.8:192.168.1.1:255.255.255.0 lcd=vs070cxn tp=gslx680-linux console=ttySAC0,115200 maxcpus=1 init=/linuxrc
#:'saveenv
driver/kernel:编译内核源码 // 确保编译通过
driver/rootfs:根文件系统数据(能安装卸载模块,能远程登录开发板,让开发板可以挂载该文件系统)
2、内核驱动开发的基本知识
如何学习驱动编程?
1)最好的老师是 内核源码
man open // 以前用法
要去使用某个陌生函数:
'查看内核中关于该函数的定义,注释;
'查看内核中其他模块是如何使用该函数的。
2)经典的书籍
内核开发:《linux内核设计与实现 - 第3版》.pdf
驱动开发:
LDD3 - 《linux设备驱动 - 第3版》.chm // 概念性的,没什么错误。
《精通linux设备驱动程序开发》.pdf // 比较细的。
关于linux内核:
1)linux中使用的函数都是自身实现的,它肯定不会调用c库中的函数
strcmp memcpy
2)linux中绝大部分代码是用GNU c语言完成的,少部分是用汇编实现的
GNU c可以理解为标准C的扩展版。
// 可变参数的实现原理,后补查询了解。
内核态编程需要注意的问题:
1)内核态不能做浮点数运算
2)用户空间的每一个进程都有独立的0~3G
所有的进程共享同一个内核
内核使用的地址空间是3G~4G
3)'每个线程有独立的栈空间
用户态的栈
内核态的栈:当产生了系统调用,进入内核时;
该栈空间非常小,一般只有一个内存页(4K)
3、hello,world驱动模块
cd driver
mkdir day01/01 -p
cd day01/01
vi helloworld.c
'make -C /home/tarena/jy/driver/kernel M=$(PWD) modules
-C:指定进入哪个目录进行编译
helloworld.c
M: M=当前路径
指定要编译的模块文件所在的路径
modules: 是编译内核模块的固定写法
>>>>模块可以安装卸载,看不到打印信息的话,需要配置此项:
$:'make menuconfig
Kernel hacking --->
[ 4 ]Default message log level (1-7) '修改为4,默认打印优先级
$:'make uImage -j4
// 烧写新的内核到开发板,再次安装、卸载模块。
【规律】
安装模块时 helloworld_init 被调用;
卸载模块时 helloworld_exit 被调用。
helloworld: module license 'unspecified' taints kernel
模块许可未指定污染了内核。
是由于 helloworld.c没有声明 license
GNU linux
MODULE_LICENSE ("GPL");// 声明为开源软件【必须添加的项】
MODULE_DESCRIPTION("Intel Footbridge (21285) serial driver");
// 描述
MODULE_AUTHOR(SERIAL_21285_MAJOR, SERIAL_21285_MINOR);
// 作者
MODULE_VERSION(TW_DRIVER_VERSION);
// 版本
$:'modinfo helloworld.ko
3.1 关于helloworld.c文件
#include <linux/init.h>
#include <linux/module.h>
// 以上两个头文件位于内核源码在 kernel/include/linux/ 下。
MODULE_LICENSE ("GPL");
// 声明为开源软件【必须添加的项】,GPL:版权,GNU copyleft
如果该模块在'安装'时希望该模块中的某个函数被调用:
linux内核中追求的是:
更高的执行效率、更好的可移植性、可扩展性。
4、 导出符号
~ #:'lsmod
import 817 0 - Live 0xbf00c000 (O)
export 663 1 import, Live 0xbf008000 (O)
模块名 大小 引用次数 被引用函数 存在地址
'安装卸载模块在有调用函数的时候,有其先后顺序。
#:' insmod export.ko
#:' insmod import.ko
#:'rmmod import
#:'rmmod export
5、打印优先级控制
printf 和 printk
printk ("<优先级>" "%s: helloworld!\n", __FILE__);// 中间是空格。
与printf相比多了打印优先级的控制。
printk ("helloworld!\n");// 使用的是默认的优先级
函数的实现原型在内核的printk.c文件中:
linux内核的打印优先级分为 8 级。
验证8个优先级的具体效果:
7 4 1 7
第一项:打印优先级的阈值,printk中指定的优先级高于该值,可以正常打印;
优先级低于该值,打印信息被屏蔽。
第二项:默认打印优先级
printk ("helloworld!\n");// 没加打印优先级的时候,是默认值 4
'修改打印控制级别
方式一:
#:'echo 4 >/proc/sys/kernel/printk
// 该方式不能影响内核启动时打印信息的输出
方式二:
#:'setenv bootargs ... loglevel=数字
setenv bootargs root=/dev/nfs nfsroot=192.168.1.8:/home/tarena/jy/driver/rootfs ip=192.168.1.6:192.168.1.8:192.168.1.1:255.255.255.0 lcd=vs070cxn tp=gslx680-linux console=ttySAC0,115200 maxcpus=1 loglevel=4
#:'saveenv
// 修改uboot的环境变量
uart_init
uart_puts
uart_gets
这就是裸板上的串口驱动
linux中将串口驱动封装了一套框架
其中实现了大量硬件无关的代码
只需要编程实现硬件相关的代码
linux下完成驱动相关的开发,需要三方面的知识:
1)硬件相关的知识
电路原理图
芯片的数据手册
RS232通信协议
I2C通信协议
2)内核的知识
内核驱动属于内核的一部分,它运行在内核态
内存管理
解决竞争状态
...
3)驱动框架的知识
内核中已经实现了大量的硬件驱动,完成了驱动的框架编程,只需要驱动工程师编程实现硬件相关的代码就可以了。
1、搭建linux驱动开发环境
1.1 安装交叉编译工具
'安装包 - 解压缩
// 位置:~/workdir/arm-linux-gcc/arm-cortex_a9-eabi-4.7-eglibc-2.18.tar.gz
'PATH修改
// :/opt/arm-cortex_a9-eabi-4.7-eglibc-2.18/bin
1.2 拿到uboot、编译、烧写进开发板
1.3 拿到kernel、编译、烧写到开发板
1.4 制作根文件系统,通过nfs方式让开发板可以加载该文件系统
$:'sudo vi /etc/exports
/home/tarena/driver/rootfs *(rw,sync,no_root_squash)
$:'sudo /etc/init.d/nfs-kernel-server restart
#:'setenv bootcmd mmc read 0x48000000 0x800 0x3000 \; bootm 0x48000000
#:'setenv bootargs root=/dev/nfs nfsroot=192.168.1.8:/home/tarena/jy/driver/rootfs ip=192.168.1.6:192.168.1.8:192.168.1.1:255.255.255.0 lcd=vs070cxn tp=gslx680-linux console=ttySAC0,115200 maxcpus=1 init=/linuxrc
#:'saveenv
driver/kernel:编译内核源码 // 确保编译通过
driver/rootfs:根文件系统数据(能安装卸载模块,能远程登录开发板,让开发板可以挂载该文件系统)
2、内核驱动开发的基本知识
如何学习驱动编程?
1)最好的老师是 内核源码
man open // 以前用法
要去使用某个陌生函数:
'查看内核中关于该函数的定义,注释;
'查看内核中其他模块是如何使用该函数的。
2)经典的书籍
内核开发:《linux内核设计与实现 - 第3版》.pdf
驱动开发:
LDD3 - 《linux设备驱动 - 第3版》.chm // 概念性的,没什么错误。
《精通linux设备驱动程序开发》.pdf // 比较细的。
关于linux内核:
1)linux中使用的函数都是自身实现的,它肯定不会调用c库中的函数
strcmp memcpy
2)linux中绝大部分代码是用GNU c语言完成的,少部分是用汇编实现的
GNU c可以理解为标准C的扩展版。
// 可变参数的实现原理,后补查询了解。
内核态编程需要注意的问题:
1)内核态不能做浮点数运算
2)用户空间的每一个进程都有独立的0~3G
所有的进程共享同一个内核
内核使用的地址空间是3G~4G
3)'每个线程有独立的栈空间
用户态的栈
内核态的栈:当产生了系统调用,进入内核时;
该栈空间非常小,一般只有一个内存页(4K)
3、hello,world驱动模块
cd driver
mkdir day01/01 -p
cd day01/01
vi helloworld.c
'make -C /home/tarena/jy/driver/kernel M=$(PWD) modules
-C:指定进入哪个目录进行编译
helloworld.c
M: M=当前路径
指定要编译的模块文件所在的路径
modules: 是编译内核模块的固定写法
>>>>模块可以安装卸载,看不到打印信息的话,需要配置此项:
$:'make menuconfig
Kernel hacking --->
[ 4 ]Default message log level (1-7) '修改为4,默认打印优先级
$:'make uImage -j4
// 烧写新的内核到开发板,再次安装、卸载模块。
/** 代码演示 - helloworld.c **/
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE ("GPL"); // 声明为开源软件
int __init helloworld_init (void) {
printk ("func: %s, line: %d\n", __func__, __LINE__);
return 0;
}
void __exit helloworld_exit (void) {
printk ("func: %s, line: %d\n", __func__, __LINE__);
return 0;
}
module_init (helloworld_init);
module_exit (helloworld_exit);
/** 代码演示 - Makefile **/
obj-m += helloworld.o
all:
make -C /home/tarena/jy/driver/kernel M=$(PWD) modules
cp *.ko ../../rootfs -v
clean:
make -C /home/tarena/jy/driver/kernel M=$(PWD) clean
【规律】
安装模块时 helloworld_init 被调用;
卸载模块时 helloworld_exit 被调用。
helloworld: module license 'unspecified' taints kernel
模块许可未指定污染了内核。
是由于 helloworld.c没有声明 license
GNU linux
MODULE_LICENSE ("GPL");// 声明为开源软件【必须添加的项】
MODULE_DESCRIPTION("Intel Footbridge (21285) serial driver");
// 描述
MODULE_AUTHOR(SERIAL_21285_MAJOR, SERIAL_21285_MINOR);
// 作者
MODULE_VERSION(TW_DRIVER_VERSION);
// 版本
$:'modinfo helloworld.ko
3.1 关于helloworld.c文件
#include <linux/init.h>
#include <linux/module.h>
// 以上两个头文件位于内核源码在 kernel/include/linux/ 下。
MODULE_LICENSE ("GPL");
// 声明为开源软件【必须添加的项】,GPL:版权,GNU copyleft
如果该模块在'安装'时希望该模块中的某个函数被调用:
int __init xxx (void) { // 函数必须是int返回值,参数为void
/* ... */
return 0;
}
module_init (xxx); // 函数必须被宏module_init修饰,注册自己提供的服务,加载函数执行完毕,模块依旧存在。
如果该模块在'卸载'时希望该模块中的某个函数被调用:
int __exit yyy (void) { // 函数必须是int返回值,参数为void
/* ... */
return 0;
}
module_exit (yyy);
// 函数必须被宏module_exit修饰,内核调用,模块消失。
__init:
#define __init __section(.init.text)
__exit:
#define __exit __section(.exit.text)
// __init 和 __exit修饰的函数在执行一次后对应的内存空间就会释放。
linux内核中追求的是:
更高的执行效率、更好的可移植性、可扩展性。
4、 导出符号
"a.c"
int func (int a, int b) {
return a + b;
}
EXPORT_SYMBOL (func); // 其他模块使用a.c的函数的话,需要加导出符号
EXPORT_SYMBOL_GPL (func);
// EXPORT_SYMBOL和EXPORT_SYMBOL_GPL的区别:
EXPORT_SYMBOL 导出符号可以被任意引用;
EXPORT_SYMBOL_GPL 导出的符号只能被遵循"GPL"的模块所引用。
"a.h"
extern int func (int, int);
"b.c"
extern int func (int, int);
#include "a.h"
func (10, 20);
/** 代码演示 - export.h **/
#ifndef _EXPORT_H
#define _EXPORT_H
extern int my_func (int, int);
#endif //_EXPORT_H
/** 代码演示 - export.c **/
include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE ("GPL");
int my_func (int a, int b) {
return a + b;
}
// 添加导出符号
EXPORT_SYMBOL (my_func);
/** 代码演示 - import.c **/
#include <linux/init.h>
#include <linux/module.h>
#include "export.h"
static int __init import_init (void) {
int res = 0;
res = my_func (10, 20);
printk ("%s: res = %d\n", __func__, res);
return 0;
}
static void __exit import_exit (void) {
printk ("enter %s\n", __func__);
}
module_init (import_init);
module_exit (import_exit);
MODULE_LICENSE ("GPL");
/** 代码演示 - Makefile **/
obj-m += export.o
obj-m += import.o
all:
make -C /home/tarena/jy/driver/kernel M=$(PWD) modules
cp *.ko ../../rootfs -v
clean:
make -C /home/tarena/jy/driver/kernel M=$(PWD) clean
~ #:'lsmod
import 817 0 - Live 0xbf00c000 (O)
export 663 1 import, Live 0xbf008000 (O)
模块名 大小 引用次数 被引用函数 存在地址
'安装卸载模块在有调用函数的时候,有其先后顺序。
#:' insmod export.ko
#:' insmod import.ko
#:'rmmod import
#:'rmmod export
5、打印优先级控制
printf 和 printk
printk ("<优先级>" "%s: helloworld!\n", __FILE__);// 中间是空格。
与printf相比多了打印优先级的控制。
printk ("helloworld!\n");// 使用的是默认的优先级
函数的实现原型在内核的printk.c文件中:
/** 代码演示 - prink.c **/
asmlinkage int printk(const char *fmt, ...)
{
va_list args;
int r;
#ifdef CONFIG_KGDB_KDB
if (unlikely(kdb_trap_printk)) {
va_start(args, fmt);
r = vkdb_printf(fmt, args);
va_end(args);
return r;
}
#endif
va_start(args, fmt);
r = vprintk(fmt, args);
va_end(args);
return r;
}
linux内核的打印优先级分为 8 级。
#define KERN_EMERG "<0>" /* system is unusable */
#define KERN_ALERT "<1>" /* action must be taken immediately */
#define KERN_CRIT "<2>" /* critical conditions */
#define KERN_ERR "<3>" /* error conditions */
#define KERN_WARNING "<4>" /* warning conditions */
#define KERN_NOTICE "<5>" /* normal but significant condition */
#define KERN_INFO "<6>" /* informational */
#define KERN_DEBUG "<7>" /* debug-level messages */
验证8个优先级的具体效果:
/** 代码演示 - **/
#include <linux/init.h>
#include <linux/module.h>
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);
#:'cat /proc/sys/kernel/printk
7 4 1 7
第一项:打印优先级的阈值,printk中指定的优先级高于该值,可以正常打印;
优先级低于该值,打印信息被屏蔽。
第二项:默认打印优先级
printk ("helloworld!\n");// 没加打印优先级的时候,是默认值 4
'修改打印控制级别
方式一:
#:'echo 4 >/proc/sys/kernel/printk
// 该方式不能影响内核启动时打印信息的输出
方式二:
#:'setenv bootargs ... loglevel=数字
setenv bootargs root=/dev/nfs nfsroot=192.168.1.8:/home/tarena/jy/driver/rootfs ip=192.168.1.6:192.168.1.8:192.168.1.1:255.255.255.0 lcd=vs070cxn tp=gslx680-linux console=ttySAC0,115200 maxcpus=1 loglevel=4
#:'saveenv
// 修改uboot的环境变量