一. arm基础知识
基础:c语言 具有一定硬件基础
特点---》前后联系
arm目标:
看懂简单的汇编代码
会看电路图、芯片手册
学会如何用软件控制硬件思想
解决问题的办法
谈谈对嵌入式的理解?
以计算应用为中心,软硬件可裁剪的专用计算机系统。对功耗、体积、性能,成本等有一定要求。
特点:专用性强,专用的计算机;运行环境差异性大;比通用PC机资源少(适用性,够用就好);功耗低,体积小,集成度高,成本低;具有较长的生命周期
学习arm重点学习两方面:程序运行原理、硬件控制原理。
1. 计算机基本理论
计算机系统中用高、低电平来表示逻辑1和0
数据在计算机中的存储、传输、运算(对数据的处理方式)都是以二进制的形式进行的
过数据的传输通总线真正传递的是电信号,高低电平(0、1)。内存只有高低电平。运算在电路中运行的,集成电路中完成运算。
2. 计算机的组成
输入设备、输出设备、存储器、运算器、控制器共同组成
1.输入设备:将其他信号转换为计算机可以识别的信号(电信号) 。
2.输出设备:将电信号(0、1)转为人或其他设备能理解的信号。
3.存储器:存放程序和数据的部件,也是计算机能够实现“存储程序控制”的基础。
程序:指令的有序集合 //汇编指令
ROM: flash (EMMC)、磁盘空间 、掉电不丢失数据
RAM: 内存、掉电丢失数据
+ -->求和指令
32为操作系统,寻址为32位。寻址空间位2^32,4G。
4.运算器:CPU对信息处理和运算的部件,常进行算术运算和逻辑运算,其核心是算术逻辑单元ALUCPU中用各种各样的数字电路搭配成各种各样的运算电路,如:加法、减法等。
如加法运算器:
5.控制器:整个计算机的指挥中心
重点想学习的是:程序运行的原理
思考:
1.运算器不同,处理指令不同,及对应指令集不同。不同的处理器上如何运行同一个c语言程序?
3. 指令的解析
一条指令(机器码)的执行通常分为三个阶段:
1)取指:控制器将PC寄存器中的值发送给内存,内存将对应地址中的指令(机器码)传送回CPU的指令寄存器IR中
2)译码:指令译码器对IR中的指令进行识别,即将指令(机器码)翻译成具体的运算操作(+/-/*...)
3)执行:运算器执行对应的指令并将结果写入寄存器
执行完一条指令后CPU内对应的硬件会将PC的值自动增加使PC指向内存中的下一条指令
- 指令的执行是按照流水线
- 取指--》取指器 根据PC值取指令
- 译码--》译码器
- 执行--》执行器
以上三个器件,都是单周期的器件,三个器件的工作是独立,
指令1 指令2 指令3 指令4 指令5
1 取指
2 译码 取指
3 执行 译码 取指
4 执行 译码 取指
5 执行 译码 取指
6 执行 译码
7 执行
- PC永远指向当前正在取指指令的地址,一旦取到指令,pc后移4byte,保存下一条指令地址。
4. 编译原理
CPU能够识别的唯一的语言是机器码,一个CPU能够识别哪些机器码是由处理器的硬件(运算器种类)决定的不同的机器码代表不同的运算,同样不同的CPU的机器码是不通用的即不可以移植,汇编是用一个标识符来代表一个机器码,所以不同的CPU汇编也不一样,即汇编语言不可以移植。
C语言编译的时候我们可以使用不同的编译器将C编译成不同的汇编和机器码,所以C可以不依赖CPU架构
总结面试题:
1.指令解析的过程
2.为什么不同处理器,要用不同的编译器编译程序代码?编译原理
5. 认识ARM
- ARM含义?
1. ARM代表一个公司
2. ARM表示一种技术
3. ARM可以表示一些处理器的统称
- 架构:
arm-v4,arm-v5,arm-v6,arm-v7(32Bits),arm-v8(64Bits)
架构指支持的汇编指令集
- 内核:
cortex-a9,a53,a73,a77
ARM公司授权芯片的公司,芯片产家在内核的基础上,增加了一些外设,发布一款芯片,这些芯片可以统称为SOC
- SOC:
System Of Chip:片上系统),S5P6818,骁龙855(高通),麒麟990(海思)
s5p6818:
- ARM的发展历史
1. 1978年,CPU公司
Cambridge processing Unit(剑桥仓库)
2. 1979年 Acorn(组装计算机)
3. 1985年, 32位,8MHz,
使用的精简指令集RISC
芯片的名字ARM ----》 Acorn RISC machine(爱康精简指令集计算机)
4.1990年,
iphone 150万英镑 VLSI: 25万英镑
12工程师+技术专利:150万英镑
ARM公司-》 Advanced RISC Machine(高级精简指令集计算机)
ARM公司不生产芯片,做技术的授权,提供解决方案。
例如:小米手机(买了高通芯片+UI+摄像头优化等)
5. 2016年,日本软银收购
- 指令集
- 精简指令集(RISC)-->微处理器(reduced instruction set computer)
在复杂指令集里边选取了一些比较简单,使用频率较高的指令
指令的宽度固定,多为单周期指令。
举例:如有加法运算器 ,没有乘法运算器 3*3 ---》3+3+3
- 复杂指令集(CISC)-->电脑CPU(complex instruction set computer)
注重的指令的功能性,指令的周期,指令的宽度不固定
eg精简:可以编译后,用反汇编查看代码指令。
使用交叉编译工具编译程序,生成arm的可执行程序
arm-linux-gnueabinf-gcc 1.c
file a.out -->查看可执行文件属性
使用反汇编的命令将elf文件转换为反汇编文件.dis
arm-linux-gnueabinf-objdump -D a.out > a.dis
查看ubuntu复杂指令集的指令:
gcc 1.c --->编译生成a.out可执行文件
file a.out 查看文件属性
objdump -D a.out > a.dis ---->反汇编
- ARM公司产品分布
- Cortex-A:
高通,联发科,海思,三星,飞思卡尔、面向尖端的基于虚拟内存的操作系统和用户应用。
- Cortex-R:
实时处理器为要求可靠性、容错功能和实时响应的嵌入式系统,提供高性能解决方案。汽车电子,照相机摄像机。
- Cortex-M:单片机
针对成本和功耗敏感的MCU和终端应用,一般不跑操作系统,可以运行实时操作系统:FreeRTOS,uCosII,LiteOS(华为),意法半导体(ST)STM32系列
- ARM体系结构: ARM-v8(A)--->Cortex-A53(8核)-->S5P6818 主频:1.4GHZ
- ARM数据类型的约定
ARM-v7架构:32bit处理器
- char:8位
- halfword:16位
- word:32位
- doubleword:64位(cortex-a)
ARM-v8架构:64bit处理器 ,向下兼容32位(我们学习32位)
- char:8位
- halfword:16位
- word:32位
- doubleword:64位(cortex-a)
- quadword:128位(ARM-v8)
- 处理器的32位和64位什么含义?
- 32位:一条指令可以进行32位数据的运算
- 64位:一条指令可以进行64位数据的运算
- 大部分ARM core 提供:
ARM-v7架构:
- ARM 指令集(32-bit)
一条指令占32位内存空间
- Thumb 指令集(16-bit )
一条指令占16位内存空间
ARM-V8:向下兼容ARM-v7架构
- ARM指令集:A64,A32
- Thumb指令集:T32,T16
- 不管是A64还是A32,每条指令都占32位空间
- 不管是T32还是T16,每条指令都占16位空间
ARM-v7:
ARM指令集:A32
Thumb指令集:T16
ARM指令集功能更全,性能更高
thumb指令集比ARM指令集指令密度要大
- ARM处理器的工作模式
ARM内核的命名规格历史
ARM7 ARM9 ARM10 ARM11 ,ARM11之后,命名规格改变
Cortex-A9 A53 A75
ARM7-11 有7种基本工作模式:
- User : 非特权模式,大部分任务执行在这种模式
- FIQ (Fast Interrupt Request) : 当一个高优先级(fast) 中断产生时将会进入这种模式
- IRQ (Interrupt Request): 当一个低优先级(normal) 中断产生时将会进入这种模式
FIQ和IRQ打断当前正在做的事去做其他的事情,做了再回来继续做自己的事情。鼠标键盘等都是这样实现的。(Linux内核会有中断,驱动写中断驱动代码)
中断的概念:
- Supervisor(SVC) : 当复位或软中断指令执行时将会进入这种模式
(任务的切换会切入这个模式,权限最高的模式,刚启动的时候在这个模式下,权限高,可以做一些核心的操作。进行系统调用的时候会切换这个模式。)
- Abort : 当指令存取异常时将会进入这种模式
- Undef : 当执行未定义指令时会进入这种模式
- System : 使用和User模式相同寄存器集的特权模式
保证不同任务每次调用同一个函数都是从头开始。
Cortex-A特有模式:
- Monitor : 是为了安全而扩展出的用于执行安全监控代码的模式;
也是一种特权模式
特定的模式拥有特定的权限,执行特定的代码,完成特定的功能
- CPU(内核)组成:
- 运算器
加法运算 --》加法器 --》加法指令
- 控制器
- 存储器---》REG Register:此寄存器由ARM公司集成到CPU的内部来存放机器码
- A32:每个寄存器可以存储一个32位数据
- A64:每个寄存器可以存储一个64位数据
总结:
- ARM7,9,11 有37个32-Bits长的寄存器
- 1 个用作PC( program counter)
- 1个用作CPSR(current program status register)
- 5个用作SPSR(saved program status registers)
- 30 个通用寄存器
- Cortex-A多出3个寄存器及有40个32-Bits长的寄存器
- Monitor 模式 r13_mon , r14_mon, spsr_mon
- R0-r15, CPSR,SPSR 这些寄存器是由ARM公司提供,每个寄存器都是32位的。
这些寄存器没有地址,只有一个唯一的编号,通过这些编号,就可以访问对应的地址空间。R0- R15,cpsr,spsr就是对应的编号,每个编号都对应的几位二进制?32
- R13:栈指针寄存器
the stack pointer, sp
存放栈顶的地址
- R14:链接寄存器
the link register, lr
- 函数调用时,保存返回地址
(保存调用函对应指令的下一条指令地址,返回的时候把lr值给pc,可以继续执行接下来的代码)
- R15:程序计数寄存器
the program counter, pc
- 存放当前取指指令的地址
cpu从内存中一条一条的拿指令(取值)
- CPSR:当前程序状态寄存器
- 存储当前程序运行状态
current program status register, cpsr
NZCV这几位叫条件位,后边八位叫控制位(C表示)
- SPSR:保存程序状态的寄存器
saved program status register
- 用于保存cpsr
- 时钟:负责发出CPU开始计时的时钟信号。
二、arm汇编指令学习
1. 基础概念
- c语言中可以那些代码可以生成汇编指令
1》带’;‘ 号的 语句 ,可以编译生成指令
2》带’#‘ 号 预处理 ,辅助编译器怎么编译,编译什么内容
- 汇编整体分类
1》指令: 编译完生成一条机器码存储在内存单元当中,CPU执行时能完成对应的操作(类似于C中的语句)
2》伪操作 (相当于c中的’#‘的内容)告诉编译器怎么编译):不会生成机器码也不会占用内存,其作用是告诉编译器怎样编译(类似于C中的预处理指令)
3》伪指令 (如:cpu中没有乘法器,对应没有乘法指令,3*3 ---》用加法器实现3+3+3,替换实现):不是指令,编译器在编译时将其替换成等效的指令
汇编中注释代码用'@'注释一行 ,注释一段代码 /**/
- 指令分类
1.数据处理指令: 对数据进行逻辑、算数运算
2.跳转指令: 实现程序的跳转,实质是修改PC
3.Load/Store指令: 对内存的读写操作//如 a++ 读a的值,将运算结果从cpu写道内存
4.状态寄存器传送指: 对CPSR进行读写操作//其他都不能动CPSR
5.异常中断产生指令: 触发软中断,常用于内核的系统调用 //SWI:软中断
6.协处理器指令: 操作协处理器的指令
//如3*3 ---》用加法器实现3+3+3,比较慢。我们可以外接一个协处理器(乘法器)(每个协处理器的功能比较单一),协处理器指令就是操作这个协处理器的,用的比较多的cp15协处理器。
- 汇编指令代码框架
.text @声明一段代码
.global _start @将_start 声明为一个全局的符号,其他.s文件也可以引用
@如调用函数 func : .global func
_start: @汇编的入口
@汇编代码段
.end @汇编的结束
2. 汇编指令
- 指令的语法格式:
- <opcode><code>{s} Rd,Rn,oprand2
opcode:指令的名字
code:条件码(if else),可以省略不写,默认指令是无条件执行
s:状态标志
加s,指令的执行结果影响cpsr的NZCV位,
不加s,不影响
Rd:目标寄存器
Rn:第一个操作寄存器
oprand2:第二个操作数,可以是普通寄存器,可以是立即数
注:指令的名字,条件码,s连到一起写,指令名和目标寄存器之间使用空格,寄存器和数据之间使用逗号隔开,指令中的字符不区分大小写
2.1 数据处理指令
1》数据搬移指令 mov
格式:
<opcode><code>{s} Rd,oprand2如果是立即数,前边必须加#
PC寄存器详细讲解:
指令的执行三步:取地,译码,执行(PC永远指向当前正在取指指令的地址)
2》立即数:立即数是保存在指令中的数,取指令的同时将值取过去,和普通变量的区别是,变量保存在内存中的数据,需要单独取值运算。
立即数的本质:立即数是包含在指令当中的数据(即属于指令的一部分)
立即数的优点:读取指令的同时也将立即数读取到了内存中,速度快
立即数的缺点:数量有限
如:MOV ,#0x12345678 @报错,不合法
注:使用mov 给寄存器里面存放值的时候,#号后面需是有效数(1:立即数,2:取反之后是立即数),如果不是立即数需要用ldr指令进行存放。
如果不是立即数,用伪指令ldr 赋值
3》算数运算指令
算数运算指令 add adc sub sbc mul
数据运算指令格式s
<操作码><目标寄存器><第一操作寄存器><第二操作数>
ADD R3,R1,R2 ;R2可以是立即数 只有乘法这不能为立即数
操作码 指定当前指令是哪种运算
目标寄存器 存放运算结果
第一操作寄存器 存放参与运算的一个数据(只能是寄存器)
第二操作数 存放参与运算的另一个数据(可以是寄存器/立即数)
add 普通的加法指令
adc 带进位的加法指令
假设2个64位的数相加
第一个64位的数,R0存放低32位,R1存放高32位,
第二个64位的数,R2存放低32位,R3存放高32位
结果R4存放低32位,R5存放高32位
注意:mul r2, r0, #0x4 @ 错误
乘法指令的第二个操作数只能是一个寄存器
mul r2, r1,r0
2.2 跳转指令
2.2 跳转指令
1》修改PC,不建议使用,因为需要查询指令的地址
2》 b bl :指令跳转
格式:b/bl Label
Label: 指令
相当C语言的函数调用
B指令(不带返回的跳转)
不保存返回地址的跳转(返回地址不保存到lr中)
BL指令(带返回的跳转指令),将LR的值修改成跳转指令下一条指令的地址,再将PC的值修改成跳转标识符下指令的地址
补充了解:
RM指令条件码表:可跟的判断条件成立跳转(NZCV在用于判断两者之间关系使用比较多)
如:c代码如下:
练习:
实现以下逻辑
unsigned int r1 = 9;
unsigned int r2 = 15;
while(1)
{
if(r1 == r2)
goto stop;
if(r1 > r2)
r1 = r1 - r2;
if(r1 < r2)
r2 = r2 - r1;
}
stop:
while(1);
汇编指令练习答案如下:
mov r1,#9
mov r2,#15
loop:
cmp r1,r2 @cmp 比较指令
beq stop
subhi r1,r1,r2
subcc r2,r2,r1
b loop
stop:
b stop
2.3 Load/Store指令
2.3 Load/Store指令
对内存的读写操作//如 a++ 读a的值,将运算结果从cpu写到内存
可用地址查找:(我们不用查找,脚本文件中配置了内存空间的分配)
查看内存中内容:
1>单寄存器操作指令 ldr/str
格式:ldr/str Rm, [Rn]
Rm: 存储是数据
Rn:存储的数据,地址
将CPU中r1寄存器中的数据存储到内存中r0地址的空间中
将r0指向的地址空间中的内容,读到r2寄存器中
ldr r2, [r0]
将r1中的值存储到r0+4指向的地址空间中,R0中的值不变
str r1, [r0, #4];
将r2中的值存储到r0指向的地址空间中,r0 = r0 + 4
str r2, [r0], #4
将R3中的值存储到R0+4指向的地址空间中,并且r0 = r0 + 4
str r3, [r0, #4]!
2>多寄存器操作指令 stm ldm
将r1到r4中的值存储到r0指向地址空间中,连续16个字节的地址空间
stm r0, {r1-r4}
将r0指向的地址空间中,连续的16个字节的数据,读到r5-r8寄存器中
ldm r0, {r5-r8}
如果寄存器列表中的寄存器编号既有连续又有不连续,连续的使用“-”隔开,不连续的使用“,”
stm r0, {r1-r3,r4}
2. 不管寄存器列表中的寄存器编号顺序如何变化,都是小地址对应小编号的寄存器高地址对应大编号的寄存器
stm r0, {r4,r3,r2,r1}
ldm r0, {r8,r7,r6,r5}
3>栈的操作指令 stmfd ldmfd
栈的种类
空栈(Empty)
栈指针指向的地址是空的,在栈中存储数据时,可以直接存储,存储完成之后需要将栈指针再次指向空的位置。
-
满栈(Full)
栈指针指向的地址有数据,在栈中存储数据时,需要先将栈指针,指向一个空的位置,然后在存储数据。
-
增栈(Ascending)
栈指针向高地址方向移动
-
减栈(Descending)
栈指针向低地址方向移动
操作栈的方式有四种
满增栈 满减栈 空增栈 空减栈
FA:Full Ascending 满增(FA)
FD:Full Descending 满减(FD)
EA:Empty Ascending 空增(EA)
ED:空减
ARM默认采用的是满减栈
stmfd/ldmfd
sp!, {寄存器列表}
stmfd sp!, {r1-r5}(写) (压栈)
更新栈指针指向的地址空间
ldmfd sp!, {r6-r10}(读) (出栈)
特殊:
stmfd sp!, {r1-r5,lr}(写) (压栈)
ldmfd sp!, {r6-r10,pc}(读) (出栈) //r1-r5出栈给r6-r10, 将lr的值出栈给pc
.text
.globl _start
_start:
/*
@1.数据处理指令
@1》数据搬移指令 mov
mov r0,#0x1
mov r1,#2
@mov r2,#0x00103000 不是立即数
mov r3,#0x00108000 @是立即数据
@mov r3,#0x12345678
@error: invalid constant (12345678) after fixup
@#0x1是立即数,携带在指令的数据,取指令的时候
@可以同时将数据取过来使用,读取数据快
@判断那些是立即数:将一个数据转化为二进制,所有的1组合起来可以
@形成一个0-255之间的数据,且将这个数据循环右移偶数位可以得到
@这个数据本身得数叫立即数。
@2》指令
ldr r4,=0x12345678
mov r5,#0xfffffff
@mvn r5,#0xf0000000 @按位取反指令
@3》算数运算
mov r0,#1
ldr r1,=0xffffffff
adds r2,r1,#0x2
adds r3,r1,r0
@举例:两个64位数据得加减运算
@0x00000030 fffffffe
@0x00000041 00000005
ldr r0,=0x00000030
ldr r1,=0xfffffffe
ldr r2,=0x00000041
ldr r3,=0x00000005
mov r8,#6
mov r9,#5
mul r10,r8,r9
adds r5,r1,r3
adcs r4,r0,r2
adc r11,r8,r9
subs r7,r3,r1
sbc r6,r2,r0
@2.跳转指令 本质修改pc的值
@mov pc,#0x00000005
@b(不携带返回的跳转) bl(携带返回的跳转)-会自动更新lr的值
ldr r0,=0x00000030
ldr r1,=0xfffffffe
bl loop
ldr r2,=0x00000041
ldr r3,=0x00000005
b stop
loop:
mov r8,#6
mov r9,#5
mov pc,lr
@3.Load/Store指令
@1》单寄存器操作指令 ldr/str
ldr r0,=0x40000100
ldr r1,=0x12345678
@ str r1,[r0] @将r1的值写道r0保存的内存地址中
@ ldr r2,[r0] @将r0保存的内存地址中的值读到r2中
@str r1,[r0,#8] @将r1的值写道r0+8内存地址中
@str r1,[r0],#8 @将r1的值写道r0保存的内存地址中,r0=r0+8
str r1,[r0,#4]!@将r1的值写道r0+4内存地址中,且r0=r0+4
@2》多寄存器操作指令 ldm/stm
ldr r0,=0x40000100
ldr r1,=0x11234567
ldr r2,=0x22234567
ldr r3,=0x33234567
bl add
ldr r4,=0x44234567
ldr r7,=0x77234567
ldr r8,=0x88234567
@ stm r0!,{r1-r4,r7,r8}
stm r0,{r1-r4,r7,r8}
ldr r1,=0xffffffff
ldm r0,{r3,r1,r4,r6,r2,r5}
*/
@3》栈的操作指令 stmfd ldmfd sp
ldr sp,=0x40000200
ldr r1,=0x11234567
ldr r2,=0x22234567
ldr r3,=0x33234567
ldr r4,=0x44234567
ldr r7,=0x77234567
ldr r8,=0x88234567
stmfd sp!,{r1-r4,r7-r8}
ldr r1,=0x1
ldr r2,=0x2
ldr r3,=0x3
ldr r4,=0x4
ldr r7,=0x7
ldr r8,=0x8
ldmfd sp!,{r1-r4,r7-r8}
stop:
b stop @while(1)
.end
2.4 状态寄存器指令
对CPSR进行读写操作//其他都不能动CPSR (SWI 指令是linux内核有,所以arm为了匹配才有的指令)(CPSR保存cpu的状态、模式、中断中断开关、运算状态,非常重要,不能任意更改,只有一类指令能操作这个寄存器)
1》读cpsr 指令mrs
2》写cpsr 指令 msr :一般情况不能修改cpsr,只能用msr命令修改,user模式下不能切换到其他模式。
注:修改CPSR的控制域(bit[7:0]),修改CPSR时必须指定修改哪个区域
USER模式下不能修改CPSR的值,防止应用程序修改CPU状态,保护操作系统
CPSR_C修改的是CPSR的低八位ctrl(控制)域,一般都只修改C域
2.5 异常中断产生指令(异常处理时讲)
触发软中断,常用于内核的系统调用 //SWI:软中断
正真触发软中断的处理过程代码验证:
3. 异常源及处理过程
- 模式回顾:
ARM7-11 有7种基本工作模式:(不同的模式下干不同的事,效率高)
User : 非特权模式,大部分任务执行在这种模式
FIQ : 当一个高优先级(fast) 中断产生时将会进入这种模式
IRQ : 当一个低优先级(normal) 中断产生时将会进入这种模式
Supervisor : 当复位或软中断指令执行时将会进入这种模式
Abort : 当存取异常时将会进入这种模式
Undef : 当执行未定义指令时会进入这种模式
System : 使用和User模式相同寄存器集的特权模式
Cortex-A特有模式:
Monitor : 是为了安全而扩展出的用于执行安全监控代码的模式;也是一种特权模式
3.1 异常
异常是理解CPU运转最重要的一个知识点,几乎每种处理器都支持特定异常处理,中断是异常中的一种。 有时候我们衡量一个操作系统的实时性就是看os最短响应中断时间以及单位时间内响应中断次数。
注:处理器遇到异常后会暂停当前的程序转而去处理异常(执行异常处理程序), 处理完成后返回到被异常打断的代码处继续执行
3.2 异常源
导致异常产生的事件有7个:FIQ、IRQ(接触最多)、Reset(复位)、软中断、DataAbort、PrefetchAbort、Undef。
1》reset复位异常 (svc)
当CPU刚上电时或按下reset重启键之后进入该异常,该异常在管理模式下处理。
2》irq/fiq一般/快速中断请求 (irq、fiq)
CPU和外部设备是分别独立的硬件执行单元,CPU对全部设备进行管理和资源调度处理,CPU要想知道外部设备的运行状态,要么CPU定时的去查看外部设备特定寄存器,要么让外部设备在出现需要CPU干涉处理时“打断”CPU,让它来处理外部设备的请求,毫无疑问第二种方式更合理,可以让CPU“专心”去工作,这里的“打断”操作就叫做中断请求,根据请求的紧急情况,中断请求分一般中断和快速中断,快速中断具有最高中断优先级和最小的中断延迟,通常用于处理高速数据传输及通道的中数据恢复处理,如DMA等,绝大部分外设使用一般中断请求。
3》预取指令中止异常 PrefetchAbort(abort)
该异常发生在CPU流水线取指阶段,如果目标指令地址是非法地址进入该异常,该异常在中止异常模式下处理。
4》未定义指令异常(Undef)
该异常发生在流水线技术里的译码阶段,如果当前指令不能被识别为有效指令,产生未定义指令异常,该异常在未定义异常模式下处理。
5》软件中断指令(swi)异常 (svc)
该异常是应用程序自己调用时产生的,用于用户程序申请访问硬件资源时,例如:printf()打印函数,要将用户数据打印到显示器上,用户程序要想实现打印必须申请使用显示器,而用户程序又没有外设硬件的使用权,只能通过使用软件中断指令切换到内核态,通过操作系统内核代码来访问外设硬件,内核态是工作在特权模式下,操作系统在特权模式下完成将用户数据打印到显示器上。这样做的目的无非是为了保护操作系统的安全和硬件资源的合理使用,该异常在管理模式下处理。
6》数据中止访问异常 DataAbort (Abort)
该异常发生在要访问数据地址不存在或者为非法地址时,该异常在中止异常模式下处理。
3.3 异常处理(重要)
步骤(自动完成):
1.拷贝CPSR到SPSR_
将正在运行的模式的cpsr保存到对应异常模式下的spsr中。
2.修改CPSR:(修改cpsr让它切换到对应异常模式下)
a.进入ARM状态 (强制)
b.进入相应的异常模式 (切换模式)
c.禁止相应中断 (再来异常,不会打断当前的异常处理)
3.保存返回地址到LR_
跳转到异常处理(修改的是pc)之前,将pc下一条指令地址保存lr中,再修改pc跳转对应异常处理位置。
4.设置PC为相应的异常向量地址(跳转到异常向量表中对应的位置)
修改pc切换到异常处理位置
步骤详解:
1》保存执行状态
当前程序的执行状态是保存在CPSR里面的,异常发生时,要保存当前的CPSR里的执行状态到异常模式里的SPSR里,将来异常返回时,恢复回CPSR,恢复执行状态。
2》模式切换
硬件自动根据当前的异常类型,将异常码写入CPSR里的M[4:0]模式位,这样CPU就进入了对应异常模式下。不管是在ARM状态下还是在THUMB状态下发生异常,都会强制切换到ARM状态下进行异常的处理,这是由硬件自动完成的,将CPSR[5] 设置为 0。同时,CPU会关闭中断IRQ(设置CPSR 寄存器I位),防止中断进入,如果当前是快速中断FIQ异常,关闭快速中断(设置CPSR寄存器F位)。
3》保存返回地址
当前程序被异常打断,切换到异常处理程序里,异常处理完之后,返回当前被打断模式继续执行,因此必须要保存当前执行指令的下一条指令的地址到LR_excep(异常模式下LR,并不存在LR_excep寄存器,为方便读者理解加上_excep,以下道理相同),由于异常模式不同以及ARM内核采用流水线技术,异常处理程序里要根据异常模式计算返回地址。
4》跳入异常向量表
该操作是CPU硬件自动完成的,当异常发生时,CPU强制将PC的值修改为一个固定内存地址,这个固定地址叫做异常向量。
3.4 异常向量表
整个异常处理过程是自动完成的,ARM内部会有对应硬件自动完成,即在设计ARM时就被固定了,所以每个异常处理会有固定的地址。异常向量表地址不能更改,后期一些新的处理器可以通过协处理器更改。
linu若想修改异常向量表的起始地址,需要借助协处理器完成,一般会将异常向量表的地址设置在0xFFFF0000地址处(若产生IRQ,异常向量地址在0xFFFF0018)
总结:
异常向量表是处于内存中的一段空间
在异常向量表中为每个异常源分配了四个字节的存储空间
当产生了异常后PC的值会自动变成该异常源在异常向量表中的地址
我们在异常向量表对应的位置写一条跳转指令使其跳转到异常处理程序入口
对于Cortex-A系列的处理器的异常向量表的基地址可以通过协处理器CP15来设置
详解:(帮助理解)
1》跳入异常向量表操作是异常发生时,硬件自动完成的,剩下的异常处理任务完全交给了程序员。由上表可知,异常向量是一个固定的内存地址,我们可以通过向该地址处写一条跳转指令,让它跳向我们自己定义的异常处理程序的入口,就可以完成异常处理了。
2》正是由于异常向量表的存在,才让硬件异常处理和程序员自定义处理程序有机联系起来。异常向量表里0x00000000地址处是reset复位异常,之所以它为0地址,是因为CPU在上电时自动从0地址处加载指令,由此可见将复位异常安装在此地址处也是前后接合起来设计的,其后面分别是其余7种异常向量,每种异常向量都占有四个字节,正好是一条指令的大小,最后一个异常是快速中断异常,将其安装在此也有它的意义,在0x0000001C地址处可以直接存放快速中断的处理程序,不用设置跳转指令,这样可以节省一个时钟周期,加快快速中断处理时间。
3》存储器映射地址0x00000000是为向量表保留的。在有些处理器中,向量表可以选择定位在高地址0xFFFF0000处【可以通过协处理器指令配置】,当今操作系统为了控制内存访问权限,通常会开启虚拟内存,开启了虚拟内存之后,内存的开始空间通常为内核进程空间,和页表空间,异常向量表不能再安装在0地址处了。
3.5 安装设置异常向量表及保存现场指令
2》 安装异常向量表
我们可以通过简单的使用下面的指令来安装异常向量表:
b reset ; 跳入reset处理程序
b undef_handler ;跳入未定义处理程序
b swi_handler ;跳入软中断处理程序
b pref_handler ;跳入预取指令处理程序
b data_handler ;跳入数据访问中止处理程序
b res ; 跳入未使用程序
b irq_handler ;跳入中断处理程序
b fiq_handler ;跳入快速中断处理程序
通常安装完异常向量表,跳到我们自己定义的处理程序入口,这时我们还没有保存被打断程序的现场,因此在异常处理程序的入口里先要保存打断程序现场。
2》 保存执行现场
异常处理程序最开始,要保存被打断程序的执行现场,程序的执行现场无非就是保存当前操作寄存器里的数据,可以通过下面的栈操作指令实现保存现场:
stmfd sp!, {r0-r1,lr}
需要注意的是,在跳转到异常处理程序入口时,已经切换到对应异常模式下了,因此这里的SP是异常模式下的SP了,所以被打断程序现场(寄存器数据)是保存在异常模式下的栈里,上述指令将R0~R1全部都保存到了异常模式栈,最后将修改完的被打断程序返回地址入栈保存,之所以保存该返回地址就是将来可以通过类似:MOV PC, LR的指令,返回用户程序继续执行。
异常发生后,要针对异常类型进行处理,因此,每种异常都有自己的异常处理程序,中断异常处理过程通过系统中断处理来进行分析。
3.6 异常处理的返回(用户自己完成)
异常处理代码自己写。
1)从SPSR_恢复CPSR,使处理器恢复到异常前的状态
2)从LR_恢复PC,使程序返回到被异常打断的位置继续执行
注:CPSR中保存的永远是当前程序运行状态,SPSR只是异常时对CPSR进行备份
异常处理完成之后,返回被打断程序继续执行,具体操作如下:
1-》恢复被打断程序运行时寄存器数据(从栈中恢复)
2-》恢复程序运行时状态CPSR(恢复spsr_<mode>到cpsr)
3-》通过进入异常时保存的返回地址,返回到被打断程序继续执行(恢复lr<mode>到pc)
3.7 异常源与异常模式对应关系
异常源: FIQ IRQ Reset/软中断 DataAbort/PrefetchAbort Undef
异常模式:FIQ IRQ SVC Abort Undef
3.8 异常响应优先级
Reset、Data Abort、FIQ、IRQ、Prefetch Abort、SWI、Undefined instruction
高 - 低
4. 协处理器指令+伪指令+伪操作(了解)
- 协处理器指令
操作协处理器的指令(用不到) (协助cpu处理数据)
1.数据运算
2.内存访问
3.与主处理器通信
MRC 将协处理器中寄存器的内容读取到ARM处理器的寄存器中
MCR 将ARM理器中寄存器的内容读取到协处理器的寄存器中
- 伪指令
本质:本身不是指令,但是cpu替换成等效的操作。(可能会编程成)
举例1:延时一个指令周期(耗时一条指令的时间) (cpu没有这个指令)
NOP ;执行NOP和MOV R0,R0一个效果,执行NOP,cpu替换成MOV R0,R0。
MOV R0,R0
LDR的两种形式
;->指令
LDR R1,[R2]
;->伪指令
LDR R1,=0x12345678 ;R1 = 0x12345678
;可以将任何一个32bit的数据放入寄存器
- 伪操作
指令是arm公司规定的,而伪操作是编译器规定的,不同的编译器伪操作指令不同。
(我们学的linux,用linux的编译器)
5. 汇编补充内容
位运算指令 and orr eor bic
格式:
<opcode><code>{s} Rd, Rn, oprand2
mov r0, #0xFF
[7:4]bits 清零
and r1, r0, #0xFFFFFF0F
and r1, r0, #(~(0xF << 4))
[11:8]bits 置1
orr r2, r0, #(0xF << 8)
[9:6]bits 取反
eor r3, r0, #(0xF << 6)
bic 位清除指令,把哪位清0,就给哪位写1
[4:0]bits 清零
bic r4, r0, #0x1F
比较指令 cmp 比较两个数大小
格式:
cmp Rn, oprand2
没有目标寄存器,指令的结果影响cpsr的nzcv位
并且不需要加s
本质:做减法运算
例子:比较r0和r1的大小,
如果r0 > r1,r0 = r0 - r1
如果r0 < r1,r1 = r1 - r0
mov r0, #5
mov r1, #9
cmp r0, r1
subhi r0, r0, r1
subcc r1, r1, r0
6. cpu控制硬件原理
我们学习的所有指令,六大指令里边,只有内存访问指令能访问cpu之外的内容。那cpu如何控制硬件?*****load/store指令--》操作4G内存
任何一个芯片都有一个地址映射表。告诉你地址空间是如何映射的,便于我们找到对应的硬件地址。
我们的SOC型号是S5P6818,对应的芯片用户手册为:(推荐:急速PDF阅读器)
S5P6818X用户手册V0.00
其中一章是:Memory map或Memory Controller 中的一张表中可以看地址隐射关系。
硬件控制原理:*****
CPU不能直接控制硬件,硬件是由其对应的控制器(寄存器)来控制的
每个控制器(寄存器)都会映射到CPU寻址范围内的一段空间
CPU通过对控制器(寄存器)的读和写实现对硬件间接的控制
硬件板子介绍:
提供的工程模板里有一个map.lds,是指导程序如何排布。运行裸机程序首先需要看这个文件。
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
/*OUTPUT_FORMAT("elf32-arm", "elf32-arm", "elf32-arm")*/
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
. = 0x43c00000; //起始地址,异常向量表的起始地址
. = ALIGN(4);
.text : //代码段
{
./Start/start.o(.text)
//运行的第一段代码是start.s编译生成的.o
*(.text)
}
. = ALIGN(4);
.rodata : //只读
{ *(.rodata) }
. = ALIGN(4);
.data : //.data段
{ *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss ://.bss
{ *(.bss) }
__bss_end__ = .;
}
7. start启动代码
start.S文件:
.text @ .:伪操作
.arm
.global _start
_start: @第一段代码
@ 第一条指令,内存地址0x43c00000
@ 异常向量表
b reset @直接通过跳转指令跳转到reset
b .
b .
b .
b .
b .
b irq @ IRQ异常对应的位置
b .
/* The actual reset code */
reset:
/* 将异常向量表的基地址重定向到0x43c00000,即这段代码执行后遇到IRQ后PC的值会变成0x43c00000 + 0x18 */
ldr r0,=0x43c00000 @伪指令,将0x43c00000 放到r0中
mcr p15,0,r0,c12,c0,0 @ Vector Base Address Register
@p15是一个协处理器,管内存的,c12,c0都是p15里边的寄存器。c12管异常向量表的位置的。
mrc p15, 0, r0, c1, c0, 0 @mrc是协处理器指令
bic r0, #(1<<13)
mcr p15, 0, r0, c1, c0, 0
/* 设置CPU为SVC模式 32位数据处理 ARM状态 */
mrs r0, cpsr
bic r0, r0, #0x1f
orr r0, r0, #0xd3
msr cpsr, r0
/* Enable NEON/VFP unit */ 设置协处理器,浮点运算的协处理器
mrc p15, #0, r1, c1, c0, #2
orr r1, r1, #(0xf << 20)
mcr p15, #0, r1, c1, c0, #2
mov r1, #0
mcr p15, #0, r1, c7, c5, #4
mov r0, #0x40000000
fmxr fpexc, r0
/* Cache init */协处理器设置高速缓存,比内存读取更快
mrc p15, 0, r0, c0, c0, 0
and r1, r0, #0x00f00000
and r2, r0, #0x0000000f
orr r2, r2, r1, lsr #20-4
cmp r2, #0x30
mrceq p15, 0, r0, c1, c0, 1
orreq r0, r0, #0x6
mcreq p15, 0, r0, c1, c0, 1
/* Invalidate L1 I/D */
mov r0, #0
mcr p15, 0, r0, c8, c7, 0
mcr p15, 0, r0, c7, c5, 0
/* 关闭MMU,所以该代码运行在实际的物理地址上 */mmc负责虚拟地址和虚拟地址转换的,关闭,操作的是实际的物理地址。
/*mmu:内存管理单元,基于操作系统才有*/
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #0x00002000
bic r0, r0, #0x00000007
orr r0, r0, #0x00001000
orr r0, r0, #0x00000002
orr r0, r0, #0x00000800
mcr p15, 0, r0, c1, c0, 0
/* 初始化各个模式下的栈 */每个模式下都有自己栈指针,即每个模式都有自己的栈地址。每个模式下的栈初始化都需要切换到这模式下。
/*********svc mode stack************/
mrs r0, cpsr
bic r0, r0, #0xdf
orr r1, r0, #0xd3
msr cpsr, r1
ldr sp, _stack_svc_end
/**********undef mode stack**********/
orr r1, r0, #0xdb
msr cpsr, r1
ldr sp, _stack_und_end
/*********abort mode stack*********/
orr r1, r0, #0xd7
msr cpsr, r1
ldr sp, _stack_abt_end
/*********irq mode stack************/
orr r1, r0, #0xd2
msr cpsr, r1
ldr sp, _stack_irq_end
/*********fiq mode stack************/
orr r1, r0, #0xd1
msr cpsr, r1
ldr sp, _stack_fiq_end
/*********user mode stack************/
orr r1, r0, #0x10
msr cpsr, r1
ldr sp, _stack_usr_end
思考:为什么最后初始化user模式下的栈?因为user模式下不能再切换到其他模式了。
/*Close Watch Dog Timer*/
ldr r0, =0xC0012004
ldr r1, [r0]
orr r1, r1, #0x04000000
str r1, [r0]
ldr r0, =0xC0019000
ldr r1, [r0]
and r1, r1, #0xFFFFFFDF
str r1, [r0]
/* USER模式下的栈放在最后初始化 */
/*
1.因为USER模式不能切换成其他模式
2.main执行时CPU处于USER模式
*/
/* Call _main */
b main @跳转到main函数运行代码。
/* IRQ的异常处理程序 */
irq:
@ 遇到IRQ异常后CPU自动保存的返回地址是遇到异常时指令下下条指令的地址,所以我们需要人为修正
sub lr,lr,#4
@ 因为IRQ模式下使用的寄存器与USER模式相同(R0-R12),所以在处理异常前要先将之前的寄存器压栈保护
stmfd sp!,{r0-r12,lr}
@ 异常处理,跳转到C处理(也可以用汇编在这写。)
bl do_irq
@ 异常返回
@ 1.将R0-R12寄存器的值出栈恢复
@ 2.将SPSR的值给CPSR,回到了USER模式
@ 3.将LR的值给PC,实现程序的返回
ldmfd sp!,{r0-r12,pc}^
_stack_svc_end: .long stack_svc + 512
_stack_und_end: .long stack_und + 512
_stack_abt_end: .long stack_abt + 512
_stack_irq_end: .long stack_irq + 512
_stack_fiq_end: .long stack_fiq + 512
_stack_usr_end: .long stack_usr + 512
.data
stack_svc: .space 512
stack_und: .space 512
stack_abt: .space 512
stack_irq: .space 512
stack_fiq: .space 512
stack_usr: .space 512
8. LED实验
步骤:
1.通过底板原理图,找到对应电路分析灯怎么亮。给一个高电平灯亮。
2.通过核心板原理,找到对应引脚。
3.通过手册确定引脚功能--》GPIO,输出一个高电平灯亮
GPIO共160个,分为5类:ABCDE 每一类32个,标号0-31
控制寄存器-->设置
1)选GPIOA28的功能 --》对应寄存器是GPIOAALTFN1
地址:0xC001A024 第24位和25位置为0就先择GPIO功能
2)设置GPIOA28为输出功能--》GPIOAOUTENB
地址:0xC001A004 第28位置为1是输出模式
3)设置GPIOA28为输出高电平--》GPIOAOUT
地址:0xC001A000 第28位置为1红灯亮
本地开发和交叉开发
本地开发:PC端编写代码,编译代码,运行代码
交叉开发:PC端编写代码,编译代码,Target(目标板)运行代码
PC Target
X86架构 arm架构
- 使用交叉编译工具链将源码编译成支持ARM架构的可执行程序
- 再将可执行程序拷贝到目标板上运行。
PC:gcc
交叉编译工具链:arm-none-linux-gnueabi-gcc
注:arm-none-linux-gnueabi-:交叉编译工具链的名字,名字就是一个代号,
在工作中用的不一定是这个,不同的公司做的交叉编译工具链的名字不同
安装交叉编译工具链
(1)获取交叉编译工具链
一般交叉编译工具链和uboot和linux内核源码,都具有配套的关系。
- 自己去gnu官网获取交叉编译工具链的源码,自己进行编译生成对应的交叉编译工具链。不推荐:编译过程很繁琐
- 直接从芯片厂家获取交叉编译工具链
- 直接跟开发板的生成厂家获取交叉编译工具链
- 直接找主管获取交叉编译工具链(单位)
**********************************************
(2)安装交叉编译工具链:
安装步骤:
1、 在ubuntu的家目录下创建arm-gcc目录
$ cd ~
$ mkdir arm-gcc
$ cd arm-gcc
将学生资料中“1.工具软件\2.汇编环境搭建\3.编译工具”中的gcc-4.9.4.tar.xz拷贝到arm-gcc目录下并解压
$ tar -xvf gcc-4.9.4.tar.xz
2、 将交叉编译工具链添加到全局环境变量使其全局可用
打开配置文件/etc/bash.bashrc
$ sudo vi /etc/bash.bashrc
在其最后一行添加如下内容
export PATH=$PATH:/home/hq/arm-gcc/gcc-4.9.4/bin
系统中环境变量的查看:printenv/env
重启配置文件使配置生效
$ source /etc/bash.bashrc
3. 测试交叉编译工具链是否安装成功
arm-none-linux-gnueabi-gcc -v
打印以下内容,表示成功
gcc version 4.9.4 (Sourcery G++ Lite 2010.09-50)
LED实验
- 分析电路图
- 分析电路图的思路:从外设(地板)---》SOC(核心板)分析
分析LED
- 在电路板上找到led灯的位置
- LED灯旁边会有白色的字,此白字为丝印,LED灯旁边的字,是led灯的编号
- 打开底板的原理图,在原理图上搜索led灯编号(RGB)
分析led的电路图
共阳三色二极管:三个二极管的,正极接到一起
- RGB_R/RGB_G/RGB_B 表示网络标号
网络标号名字相同表示具有相同的,电气连接属性,反应到电路板上,,他们通过导线连接到一起
- 根据网络标号到核心板原理图,找到soc哪个引脚驱动着LED灯
- 2. 读懂芯片手册(黑盒测试,白盒测试)
- 工作寄存器:R0-R15,cpsr,spsr,由ARM公司提供,没有地址
- 控制寄存器:就是内存的一块空间,具有地址,由芯片厂家提供。寄存器是在GPIO章节被使用,所以看芯片手册的时候看GPIO章节,里面一定有相关寄存器的使用和功能实现。
- 我们只需要向控制寄存器中写值或者读值,就可以让我们处理器完成一定的功能。这也就是我们软件编程控制硬件的思想。
1》GPIOxOUT:控制引脚输出高低电平
RED_LED--->GPIOA28
GPIOAOUT ---> 0xC001A000
GPIOA28输出高电平:
GPIOAOUT[28] <--写-- 1
GPIOA28输出低电平:
GPIOAOUT[28] <--写-- 0
2》GPIOxOUTENB:控制引脚的输入输出模式
GPIOAOUTENB ---> 0xC001A004
设置GPIOA28引脚为输出模式:
GPIOAOUTENB[28] <--写-- 1
3》GPIOxALTFN:控制引脚功能的选择
GPIOAALTFN1 ---> 0xC001A024
设置GPIOA28引脚为GPIO功能:
GPIOAALTFN1[25:24] <--写-- 0b00
00 = ALT Function0
01 = ALT Function1
10 = ALT Function2
11 = ALT Function3
GPIO引脚功能的选择:每两位控制一个GPIO引脚,
对应的功能可以在芯片手册的
2.3章节进行查看。
3. 编写代码
1. 设置为GPIOA28为GPIO功能
2. 设置GPIOA28为输出功能
while(1)
{
设置GPIOA28输出高电平
延时
设置GPIOA28输出低电平
延时
}
#define GPIOAALTFN1 ((unsigned int *)0xc001a024)
#define GPIOAOUTENB ((unsigned int *)0xc001a004)
#define GPIOAOUT ((unsigned int *)0xc001a000)
#define GPIOEALTFN0 ((unsigned int *)0xc001e020)
#define GPIOEOUTENB ((unsigned int *)0xc001e004)
#define GPIOEOUT ((unsigned int *)0xc001e000)
#define GPIOBALTFN0 ((unsigned int *)0xc001b020)
#define GPIOBOUTENB ((unsigned int *)0xc001b004)
#define GPIOBOUT ((unsigned int *)0xc001b000)
void delay_ms(unsigned int ms)
{
unsigned int i,j;
for(i = 0; i < ms; i++)
for(j = 0; j < 1800; j++);
}
int main()
{
*GPIOAALTFN1 &= (~(3<<24));
*GPIOAOUTENB |= (1<<28);
*GPIOEALTFN0 &= (~(3<<26));
*GPIOEOUTENB |= (1<<13);
*GPIOBALTFN0 |= (1<<25);
*GPIOBALTFN0 &= (~(1<<24));
*GPIOBOUTENB |= (1<<12);
while(1)
{
*GPIOAOUT |= (1<<28);
delay_ms(2000);
*GPIOAOUT &=(~(1<<28));
delay_ms(2000);
*GPIOEOUT |= (1<<13);
delay_ms(2000);
*GPIOEOUT &=(~(1<<13));
delay_ms(2000);
*GPIOBOUT |= (1<<12);
delay_ms(2000);
*GPIOBOUT &=(~(1<<12));
delay_ms(2000);
}
return 0;
}
4. 下载调试修改bug
1》拷贝.bin文件到windows中
2》开发板和电脑进行硬件连接
串口线的USB端插到电脑的USB口
串口线的串口端插到开发板的UART0端口上
开发板插上电源
3》配置windows超级终端
如果串口线第一次使用需要安装串口驱动
串口驱动文件在资料中
配置超级终端:
可以查看配置超级终端的使用说明文档
资料中有
在设备管理器中,查看串口线使用的那个端口号
配置端口属性:
波特率:115200
数据位:8
停止位:1
校验位:无
流控:无
4》开发板上电,超级终端会有打印信息
在倒计时减到0之前按任意键,进入到FS6818#界面
执行命令 loadb 0x43c00000 --》下载二进制文件到内存的0x43c00000
传送--》发送文件--》选择要下载.bin文件,选择Kermit协议 --》 确定下载
执行命令:go 0x43c00000 --》到0x43c00000位置运行代码
如果需要重新下载代码,重复步骤4
修改倒计时的时间:
setenv bootdelay 60
saveenv
三、系统移植
1.基础概念
1.1 什么是系统移植?
就是将操作系统移植到对应的硬件平台
linux系统移植到开发板
(电脑-Windows系统;手机-Android系统有了,应用程序跑起来,
系统跑起来才能做驱动开发)
1.2 为什么学习系统移植?
软硬件可裁剪
公司新的硬件平台---》
移植linux系统到硬件平台
1.3 学习系统移植的目的?
1》工作的需要
2》为后边的驱动开发搭建一个系统环境
3》嵌入式(软+硬)应用层的开发离不开操作系统
1.4 如何学习系统移植?
学习移植的流程即可
1.5 移植的流程
1. 环境搭建
2. uboot移植(BIOS)
uboot最主要的功能有以下几点
1)初始化一些硬件为后续做准备
2)引导和加载内核
3)给内核传参
4)执行用户命令
BIOS--》引导系统启动(SD卡启动,硬盘启动,U盘启动)
初始化了部分硬件
3. linux内核移植
windows中内核类似
windows=内核+库+图形化界面+文件系统
4. 根文件系统的移植
(windows C盘,d之类的,Linux是一颗倒置的树)
2. 本地开发和交叉开发
本地开发:PC端编写代码,编译代码,运行代码
交叉开发:PC端编写代码,编译代码,Target(目标板)运行代码
(疑问:不是已经将系统移植到目标板上了吗?为什么要使用交叉开发,不能直接在目标板上面编写和编译代码嘛?)
PC Target
X86架构 arm架构
使用交叉编译工具链将源码编译成支持ARM架构的可执行程序,再将可执行程序拷贝到目标板上运行。
PC:gcc
交叉编译工具链:arm-none-linux-gnueabi-gcc
arm-none-linux-gnueabi-:交叉编译工具链的名字,名字就是一个代号
(在工作中用的不一定是这个,不同的公司做的交叉编译工具链的名字不同)
3. 安装交叉编译工具链
1. 获取交叉编译工具链
一般交叉编译工具链和uboot和linux内核源码,都具有配套的关系。
1》自己去gnu官网获取交叉编译工具链的源码,自己进行编译生成对应的交叉编译工具链。不推荐:编译过程很繁琐
2》直接从芯片厂家获取交叉编译工具链
3》直接跟开发板的生成厂家获取交叉编译工具链
4》直接找主管获取交叉编译工具链(单位)
**********************************************
2.安装交叉编译工具链:
将代码编译成ARM架构的可执行程序
安装步骤:
1. 在ubuntu的家目录(~)下,创建toolchain
mkdir toolchain
2. 拷贝gcc-4.6.4.tar.xz到toolchain目录下
cp 目录/gcc-4.6.4.tar.xz ~/toolchain
3. 解压缩交叉编译工具链
tar -vxf gcc-4.6.4.tar.xz
4. 配置环境变量
打开 sudo vi /etc/bash.bashrc
在最后一行添加以下内容:
export PATH=$PATH:/home/linux/toolchain/gcc-4.6.4/bin/
修改为自己的路径
5. 使环境变量立即生效
source /etc/bash.bashrc
6. 测试交叉编译工具链是否安装成功
arm-none-linux-gnueabi-gcc -v
打印以下内容,表示成功
gcc version 4.6.4 (Sourcery G++ Lite 2010.09-50)
4. PC和Target如何进行硬件连接
1.串口线:打印各种调试信息到串口工具上
2.网线:用于下载uboot、内核、根文件系统的镜像(可执行程序)
通过网络去挂载根文件系统
需要在ubuntu中安装对应的服务器
通过网线下载文件---》tftp服务
通过网线挂载根文件系统(板子(路径))---》nfs服务
tftp服务和nfs服务的客户端uboot和内核源码默认已经安装
(服务器与客户端)
5. 安装tftp服务
Tftp是一个简单的文本文件传输协议,基于udp协议
1. 检查ubuntu是否安装了tftp服务
dpkg -s tftpd-hpa
打印以下内容表示安装了tftp服务:
Architecture: i386
Source: tftp-hpa
Version: 5.2-7ubuntu3.1
2. 安装tftp服务
(前提:ubuntu必须能连接外网)
sudo apt-get update 更新源
sudo apt-get install -f 更新依赖
sudo apt-get install tftpd-hpa tftp-hpa
3. 配置tftp服务
1. 在家目录下创建一个tftpboot文件夹
mkdir tftpboot
目的:tftpboot目录下存放的是你要下载到
开发板上的可执行文件
2. 修改tftpboot的权限
sudo chmod 777 tftpboot
3. 配置tftp服务的环境变量
打开sudo vi /etc/default/tftpd-hpa
修改以下内容:
1 # /etc/default/tftpd-hpa
2
3 TFTP_USERNAME="tftp"
tftp用户名,不需要修改
4 TFTP_DIRECTORY="/home/hq/tftpboot"
tftp服务下载文件的存放的路径,需要修改
改成自己的对应的tftpboot的路径
5 TFTP_ADDRESS="0.0.0.0:69"
tftp服务默认使用的69端口号
6 TFTP_OPTIONS="-c -s -l"
tftp服务的参数,这个需要修改
4. 重启tftp服务
1. sudo service tftpd-hpa restart 启动TFTP服务
2. sudo service tftpd-hpa restart 重启TFTP服务
5. 本地测试tftp服务是否安装成功
(ping 其它机器过程:本机网卡驱动,到内核,内核处理这个驱动,发送ping的包到另一个系统的网卡,处理这个包,把包的内容放到协议站(在内核里面),识别到ping的包,然后通过网卡驱动,到网卡,返回一个包(都会到真正物理硬件))
$ tftp 127.0.0.1
tftp> get 1.txt # 从tftpboot目录下下载
# 1.txt文件到当前目录 (本例子就是,
在tftpboot下面touch 1.TXT,然后在家目录下面运行)
tftp 127.0.0.1;然后get 1.txt,就把tftpboot下面的1.TXT下载到家目录下面了)
tftp> put 2.txt # 把当前目录中的2.txt文件
# 上传到tftpboot文件夹中
tftp> q <回车> 退出
6. 可能出现的问题
下载或上传是,一直卡,
原因:
- tftp服务安装成功,需要重启tftp服务
- tftp服务安装不成功
- 关闭windows和ubuntu的防火墙(防火墙会阻碍数据传输)
6. nfs服务的安装
Network File System
1. 检查nfs服务是否安装
dpkg -s nfs-kernel-server
2. 安装nfs服务(前提:可以上网)
sudo apt-get install nfs-kernel-server
3. 配置nfs服务
1》在家目录下创建nfs文件夹
mkdir nfs
2》设置文件夹的权限最大
sudo chmod 777 nfs
3》拷贝根文件系统到nfs目录下
根文件系统一会发给你们(rootfs-A53-ok.tar.xz)
cp /mnt/hgfs/share/rootfs-A53-ok.tar.xz ~/nfs
4》对根文件系统的压缩包进行解压缩
cd ~/nfs
tar -vxf rootfs-A53-ok.tar.xz
5》配置nfs服务的环境变量
sudo vi /etc/exports
在文件的最后一行添加以下内容:
/home/hq/nfs/rootfs/ *(rw,sync,no_subtree_check,no_root_squash)
解析:
/home/hq/nfs/rootfs/:自己的根文件系统的路径
*:所有的用户,注:*和后边的左括号"("之间不可以出现空格.
rw:可读可写的权限
sync:同步文件
no_subtree_check:不对子目录检查文件的权限
no_root_squash:如果客户端为root用户,那么他对整个文件具有root的权限
注意:这段话前边不要加#,#号是这个文件中的注释符号
4.重启nfs服务
1. sudo service nfs-kernel-server start 启动nfs服务
2. sudo service nfs-kernel-server restart 重启nfs服务
5. 本地测试nfs服务是否安装成功
1》回到家目录下
cd ~
2》sudo mount -t nfs 本机IP地址:/home/hq/nfs/rootfs/ /mnt
(本机IP地址:ubuntu 的IP)
sudo mount -t nfs 172.20.10.100:/home/hq/nfs/rootfs /mnt
sudo mount -t nfs 10.60.138.66:/home/hq/nfs/rootfs/ /mnt
nfs:使用nfs服务,将本机IP地址:/home/hq/nfs/rootfs/
文件挂载到/mnt目录下
3》检查/mnt目录下是否挂载成功
cd /mnt
ls (此时mnt下面的东西就和rootfs下面一样)
4》卸载挂载的文件
sudo umount /mnt
注意:不可以在/mnt目录下执行卸载的命令
7. 给开发板部署操作系统
7.1 部署uboot
1》新的开发板,一切都是空白,制作一个SD卡启动盘
1> 将sd卡启动盘的制作工具拷贝到ubuntu的toolchain目录下
cp /mnt/hgfs/share/sdtool.tar.xz ~/toolchain -raf
tar -xvf sdtool.tar.xz
2> 进入到sdtool目录下
cd ~/toolchain/sdtool
3> 将sd卡插到电脑上,让你的ubuntu识别sd卡
注意:必须使用读卡器,不可以使用电脑自带的SD卡卡槽
将SD卡插到读卡器上,读卡器插到电脑上,
在windows下将SD卡进行格式化。
虚拟机--》可移动设备--》realtek USB3.0-CRW---》连接
sdtool文件夹中的文件是什么?
s5p6818-sdmmc.sh :烧写uboot到sd卡的脚本
ubootpak.bin:uboot的可执行文件
/dev/sdb: sd卡的设备文件
4> 烧写ubootpak.bin到sd卡中
在ubuntu中执行命令
sudo ./s5p6818-sdmmc.sh /dev/sdb ubootpak.bin
如果打印一下信息表示,部署成功:
688+1 records in
689+0 records out
352768 bytes (353 kB) copied, 0.00914641 s, 38.6 MB/s
^_^ The image is fused successfully
总结: 报SD卡只读的错误,
将sd卡中开关拨到lock的位置,
lock靠近sd卡的触点位置
5> 测试sd卡是否制作成功
将sd卡插到开发板板上,
设置开发板上的拨码开关,为sd卡启动,
拨码开关用于设置开发板上uboot的启动方式的
OM1 OM2 OM3 Device
ON ON X nand flash
OFF ON X USB
ON OFF ON EMMC 本开发板flash为EMMC
OFF OFF OFF SD/TF
6> 开发板重新上电,启动成功。
7.2 开发阶段系统部署
uboot镜像------》Flash(EMMC) SD
linux内核镜像--》
通过网络方式(TFTP)直接下载到开发板内存中并且启动内核
根文件系统镜像--》
通过网络的方式(NFS)直接挂载根文件系统,(开发)
好处:高效,Flash的读写次数有限制。
7.3 产品发布系统部署
uboot镜像---------->Flash
linux内核镜像------>Flash
根文件系统镜像----->Flash
先将镜像使用tftp下载到内存,
再从内存中搬移到flash中,
再从flash中搬移到内存,
再从内存中启动
7.4 开发阶段系统部署
1. 将内核镜像uImage放到tftpboot目录下
cp /mnt/hgfs/share/uImage ~/tftpboot
2. 使用tftpboot命令将uImage下载到0x480000000
tftp 0x48000000 uImage
3. 设置uboot的自动参数bootargs,
bootargs:启动linux内核是,uboot会将
bootargs后边的参数传递给内核,内核(wind系统)根据这些参数,去设置IP等东西内容为:
bootargs=root=/dev/nfs nfsroot=192.168.1.99:/home/hq/nfs/rootfs rw console=ttySAC0,115200 init=/linuxrc ip=192.168.1.222
root=/dev/nfs :根文件系统的类型
nfsroot=192.168.0.99:/home/hq/nfs/rootfs 根文件系统的服务器的IP和路径
rw :文件系统的可读可写的权限
console=ttySAC0,115200:
使用串口0实现内核和PC的数据的交互,波特率是115200
init=/linuxrc:启动内核之后,运行的1号进程
ip=192.168.1.250 :开发板的IP地址
本次bootargs环境变量设置为:
setenv bootargs root=/dev/nfs nfsroot=192.168.1.99:/home/hq/nfs/rootfs rw console=ttySAC0,115200 init=/linuxrc ip=192.168.1.222
saveenv
4. 使用bootm命令启动内核
bootm 内核地址 根文件系统地址 设备树地址
bootm 0x48000000
5. 设置uboot为自启动模式,设置bootcmd环境变量
格式:
bootcmd=uboot名令1\;uboot命令2\;uboot名令3\;.....
以上三个命令会依次进行执行,知道结束。
setenv bootcmd uboot名令1\;uboot命令2\;uboot名令3\;.....
eg:
setenv bootcmd tftp 0x48000000 uImage\;bootm 0x48000000
saveenv
6. 重新给开发板上电。
(按任意键)
7.5 产品发布系统部署
1、uboot放在EMMC中
如何将uboot放到flash(EMMC)中
(uboot放在SD中,但是手机出厂的时候,不能将系统装在SD卡中吧?出厂的时候系统被放到flash中了)
前提:SD卡必须提前烧录一个uboot
1> 通过SD卡的方式启动uboot,进入到FS6818#界面
2> 拷贝ubootpak.bin到tftpboot目录下
cp /mnt/hgfs/share/ubootpak.bin /home/hq/tftpboot
3> 将ubootpak.bin使用tftp命令烧写到内存中
tftp 0x41000000 ubootpak.bin(板子上运行,注意链接网线)
(下载是下载到内存中,掉电不存在)
(0x41000000这个地址,0x42000000也可以)
4> update_mmc (将内存中数据搬移大emmc,emmc磁盘空间)
update_mmc 2 2ndboot 0x41000000 0x200 0x78000
update_mmc
- type : 2ndboot | boot | raw | part
:flash设备编号 EMMC:2
:类型 2ndboot
:uboot在内存中的起始地址,以字节为单位
:flash的起始地址:以块为单位
:往flash中下载多少块空间(字节长度)
Bytes transferred = 352296 (56028 hex)
一块的大小是512字节
length的大小 > 352296 / 512 = 688.07
fastboot=flash=mmc,
2:ubootpak:2nd:0x200,0x78000;
flash=mmc,2:2ndboot:2nd:0x200,0x4000;
执行命令:
update_mmc 2 2ndboot 0x41000000 0x200 0x78000
(在板子上面运行)
打印以下内容表示成功
head boot dev = 2
update mmc.2 type 2ndboot = 0x200(0x1) ~ 0x78000(0x3c0): Done
(告诉CPU你的UBOOT的地址以及启动参数,类型等,从而设备上电的时候引导uboot启动,当移植完kernel后,uboot引导kernel启动)
5> 测试是否更新ubootpak.bin到EMMC中
设置拨码开关,切换到EMMC启动
开发板重新上电。
8. uboot命令(1):
mmc命令
mmc info - display info of the current MMC device
显示当前MMC设置的详细信息
mmc read addr blk# cnt
addr:对应着内存的地址
blk#:mmc设备的块号
cnt:mmc设备块的个数
含义:将mmc以blk#为起始块,cnt块大小的数据,
读取到内存的addr地址处
(mmc为硬盘,以块为单位,Mem为内存)
mmc write addr blk# cnt
addr:对应着内存的地址
blk#:mmc设备的块号
cnt:mmc设备块的个数
含义:将内存起始地址为addr处的内容
写到mmc以blk#为起始块,cnt块大小的数据,
mmc erase blk# cnt
blk#:mmc设备的块号
cnt:mmc设备块的个数
含义:将mmc的起始块号为blk#,cnt块大小的数据,进行擦除。
擦除的时间,受cnt块的个数有影响,
cnt越大,试讲越长。
(不擦除可以,因为在写的时候,把之前的就覆盖掉了)
1. 将内核镜像uImage和ramdisk.img放到tftpboot目录下
cp /mnt/hgfs/share/uImage ~/tftpboot
cp /mnt/hgfs/share/ramdisk.img ~/tftpboot
2. 使用tftpboot命令将uImage下载到0x410000000
tftp 0x41000000 uImage
(有0x2681块)
3. 将内存中的内核镜像搬移到mmc中
mmc write 0x41000000 0x800 0x4000
4. 使用tftpboot命令将ramdisk.img下载到0x41000000
tftp 0x41000000 ramdisk.img
(有0x1369块)
ramdisk.img:根文件系统的镜像
5. 将内存中的根文件系统镜像搬移到mmc中
mmc write 0x41000000 0x20800 0x20800
6. 设置bootcmd命令,从flash中启动系统
setenv bootcmd mmc read 0x48000000 0x800 0x4000\;mmc read 0x49000000 0x20800 0x20800\;bootm 0x48000000 0x49000000
saveenv
7. 设置自启动的参数
setenv bootargs root=/dev/ram rw initrd=0x49000040,0x1000000 rootfstype=ext4 init=/linuxrc console=ttySAC0,115200
saveenv
(setenv bootargs root=/dev/ram(从ram里面挂载) rw(文件系统可读可写) initrd=0x49000040(根文件系统的起始地址),0x1000000(大小) rootfstype=ext4(文件系统) init=/linuxrc(启动之后是一号进程) console=ttySAC0(串口调试),115200(波特率))
注意:
0x49000040和0x1000000之间不允许出现空格,
使用英文逗号隔开
root=/dev/ram:从ram(内存)中挂载根文件系统
initrd=0x49000040 0x1000000 :
根文件系统的入口地址,省略前边64字节头
根文件系统的大小0x1000000
rootfstype=ext4:根文件系统的类型
8. 重启开发板,测试是否部署成功(可以关掉虚拟机)
boot:执行boot命令,自动的执行bootcmd环境
变量后边的命令。就不需要重启
9. uboot中的命令(2)
1.help命令
help 查看当前uboot支持的所有的命令命令就是一个字符串,uboot收到字符串之后,会解析字符串,完成对应的功能
FS6818# help
0 - do nothing, unsuccessfully
1 - do nothing, successfully
? - alias for 'help'
base - print or set address offset
bdinfo - print Board Info structure
boot - boot default, i.e., run 'bootcmd'
bootd - boot default, i.e., run 'bootcmd'
bootm - boot application image from memory
bootp - boot image via network using BOOTP/TFTP protocol
cmd - cmd [command] options...
cmp - memory compare
cp - memory copy
crc32 - checksum calculation
dhcp - boot image via network using DHCP/TFTP protocol
drawbmp - darw bmpfile on address 'addr' to framebuffer
env - environment handling commands
exit - exit script
ext4load- load binary file from a Ext4 filesystem
ext4ls - list files in a directory (default /)
ext4write- create a file in the root directory
fastboot- fastboot- use USB Fastboot protocol
fatinfo - print information about filesystem
fatload - load binary file from a dos filesystem
fatls - list files in a directory (default /)
fatwrite- write file into a dos filesystem
fdisk - mmc list or create ms-dos partition tables (MAX TABLE 7)
go - start application at address 'addr'
goimage - start Image at address 'addr'
help - print command description/usage
i2c - I2C sub-system
i2cmod - set I2C mode
iminfo - print header information for application image
loadb - load binary file over serial line (kermit mode)
loadbmp - load bmpfile with command or 'bootlog' environment
loads - load S-Record file over serial line
loadx - load binary file over serial line (xmodem mode)
loady - load binary file over serial line (ymodem mode)
loop - infinite loop on address range
md - memory display
mdio - MDIO utility commands
mii - MII utility commands
mm - memory modify (auto-incrementing address)
mmc - MMC sub system
mmcinfo - display MMC info
mtest - simple RAM read/write test
mw - memory write (fill)
nm - memory modify (constant address)
ping - send ICMP ECHO_REQUEST to network host
pmic - PMIC
printenv- print environment variables
reset - Perform RESET of the CPU
run - run commands in an environment variable
saveenv - save environment variables to persistent storage
saves - save S-Record file over serial line
sdfuse - sdfuse - read images from FAT partition of SD card and write them to booting device.
setenv - set environment variables
showvar - print local hushshell variables
source - run script from memory
test - minimal test like /bin/sh
tftpboot- boot image via network using TFTP protocol
udown - Download USB
update_mmc- update mmc data
version - print monitor, compiler and linker version
2. help 命令
查看命令的帮助手册
3. loadb (help loadb:查看loadb的作用和用法)
loadb 下载到内存的那个地址
下载二进制文件到内存的某个地址,使用kermit协议(文件运输协议)
flash memery
掉电不丢失 掉电丢失
速度慢 速度快
价格便宜 价格贵
flash访问通过 内存以字节为单位进行访问
块去访问,
每块大小位512字节
4. printenv 打印uboot的环境变量
FS6818# printenv
baudrate=115200
bootargs=root=/dev/nfs nfsroot=192.168.0.222:/home/hqyj/nfs/rootfs,v4,tcp rw console=/dev/ttySAC0,115200 init=/linuxrc ip=192.168.0.250
bootcmd=loadb 43c00000;go 43c00000
bootdelay=3
bootfile=uImage
ethact=dwmac.c0060000
ethaddr=00:e2:1c:ba:e8:60
ethprime=RTL8211
fastboot=flash=mmc,2:ubootpak:2nd:0x200,0x78000;flash=mmc,2:2ndboot:2nd:0x200,0x4000;flash=mmc,2:bootloader:boot:0x8000,0x70000;flash=mmc,2:boot:ext4:0x00100000,0x04000000;flash=mmc,2:system:ext4:0x04100000,0x2F200000;flash=mmc,2:cache:ext4:0x33300000,0x1AC00000;flash=mmc,2:misc:emmc:0x4E000000,0x00800000;flash=mmc,2:recovery:emmc:0x4E900000,0x01600000;flash=mmc,2:userdata:ext4:0x50000000,0x0;
gatewayip=192.168.0.1
ipaddr=192.168.0.250
loadb=0x43c00000;go 0x43c00000
netmask=255.255.255.0
serverip=192.168.0.222
stderr=serial
stdin=serial
stdout=serial
Environment size: 872/32764 bytes
注意:uboot对命令进行匹配时,是部分匹配
pri/print/printenv 常用
5. bootm (启动内核是在用)(go 命令后面只能跟一个地址)
bootm 内核的运行地址 根文件系统的运行地址 设备树的运行地址
引导linux内核启动的
6. ping命令
ping ip地址
用于开发板和电脑是否可以ping通
7. setenv 设置环境变量
1》给uboot添加环境变量
setenv 环境变量名 环境变量对应的值
eg:setenv xiaoming nozuonodie
注:等号会自动添加
2》删除环境变量
setenv 要删除的环境变量名
eg:setenv xiaoming
3》修改环境变量
setenv 旧的环境变量名 新的环境变量值
eg:setenv xiaoming hahaha
设置完环境变量之后,环境变量存在于内存中,
重新上电数据会丢失
8. saveenv 保存环境变量到flash(MMC)中
10. uboot中环境变量的作用
baudrate=115200 波特率
bootdelay=3 uboot启动后倒计时时间
gatewayip=192.168.0.1 网关
ipaddr=192.168.0.100 开发板的ip地址(FS6818)
netmask=255.255.255.0 子网掩码
serverip=192.168.0.99 服务器的ip地址(PC:Ubuntu)
11. 测试ping命令和tftp命令的使用
0》开发板和PC电脑如何连接
1》设置ubuntu的IP地址
1>修改ubuntu的ip地址
(1) 设置ubuntu系统使用有线网卡
(2) 设置ubuntu系统为固定的IP地址
(3)查看IP地址是否设置成功
ifconfig
2》设置开发板的IP地址
setenv serverip 192.168.0.99
setenv ipaddr 192.168.0.100
setenv gatewayip 192.168.0.1
setenv netmask 255.255.255.0
saveenv
3》测试开发板能够平通Ubuntu
在超级终端上执行以下命令
FS6818# ping 192.168.0.106
如果ping失败打印以下信息:
dwmac.c0060000 Waiting for PHY auto negotiation to complete......... TIMEOUT !
Waiting for PHY realtime link...... TIMEOUT !
done
dwmac.c0060000: No link.
ping failed; host 192.168.0.106 is not alive
如果ping成功打印以下信息:
Speed: 100, full duplex
Using dwmac.c0060000 device
host 192.168.0.106 is alive
总结:
ping 失败的原因
1》windows的防火墙可能没关
2》检查开发板和电脑之间的网线
3》重启tftp服务
4》检查tftp服务的环境配置是否正确
5》百兆全双工是否设置
总结:tftp失败的原因
1》包含上边ping失败的原因
2》检查uboot的环境变量设置是否正确
serverip ipaddr gatewayip netmask
3》检查ubuntu的网络设置
切记,开发板和ubuntu的ip地址在同一个网段
12. u-boot移植
【1】bootloader 概念
boot:引导
loader:加载
bootloader:用来引导和加载内核,并且启动内核,然后给内核传递参数的
bootloader属于内核引导程序的统称。
比如:u-boot Bios vivi redboot 等等
在嵌入式开发是使用最广泛的bootloader是u-boot。
uboot是一个开源软件
【2】u-boot的特点
1. u-boot是一个开源的软件
2. u-boot支持多种架构的平台
arm powerPC mips x86
3. u-boot的源码短小精悍
4. u-boot就是一个裸机代码
5. u-boot引导加载内核,启动内核,并给内核传递参数
6. u-boot可以完成部分硬件的初始化:uart,内存,emmc,网卡
7. u-boot是一个短命鬼,启动完内核,
给内核传递完参数(告诉内核从什么地方去挂载根文件系统),u-boot的生命周期结束。
【3】u-boot源码的获取
1. uboot官网获取
ftp://ftp.denx.de/pub/u-boot/
对于s5p6818芯片,不可以从官方获取,
三星并没有把s5p6818配到的代码,开源到uboot中
2. 芯片厂家获取
3. 开发板厂家 ---》目前市面上的6818的开发板,开发板厂家只提供u-boot.bin
4. 上级主管 ---》推荐
本次移植课程使用:u-boot-2014.07-netok.tar.bz2
【4】对于uboot版本的选择
1. 不要选择太新的版本
不稳定,资料较少
2. 不要选择太旧的版本
有可能不支持自己的硬件平台
3. 不要选择测试版本u-boot,
选择稳定版本的u-boot
后缀有rcx表示测试版本
4. 选择支持自己硬件平台版本的u-boot
本次移植课程使用:u-boot-2014.07-netok.tar.bz2
此版本的uboot支持s5p6818
【5】移植前的准备工作
获取基本的硬件信息
cpu(内核):cortex-a53
arch:armV8
vendor:samsung
SOC:S5P6818
board(公板): S5P6818
公板:芯片厂家根据芯片设计的一套参考电路板
NEXELL:韩国一个芯片生成厂家,
三星将S5P6818芯片授权给NEXELL
【6】开始准备移植uboot
1. 在ubuntu的家目录下创建bootloader
mkdir bootloader
2. 拷贝u-boot-2014.07-netok.tar.bz2到bootloader中
cp /共享文件夹路径/u-boot-2014.07-netok.tar.bz2 ~/bootloader
cp /共享文件夹路径/u-boot-2014.07.tar.bz2 ~/bootloader
3. 解压缩uboot源码
tar -vxf u-boot-2014.07-netok.tar.bz2
mv u-boot-2014.07 u-boot-2014.07-6818
tar -vxf u-boot-2014.07.tar.bz2(官网下载的)
【7】u-boot目录的介绍
平台相关代码:和硬件挂钩
arch(架构)
而官网上面下载的支持多个平台:
board(板子,可以支持的板子)
上面两个和硬件有关系。
平台无关代码:和硬件无关代码可以共用
fs
drivers
include
tools
....
华清提供的uboot源码中,已经将平台相关的源码,
没有使用到的全部删除。
【8】移植uboot
1. 读README
2. 配置交叉编译工具链
打开u-boot源码顶层目录的Makefile
vi Makefile (*****)
“可读性”(模块化;见名知意;)
下内容:
198 ifeq ($(HOSTARCH),$(ARCH))
199 CROSS_COMPILE ?=
200 endif
修改为
198 ifeq (arm,arm)(里面的两个相等即可,写ARM的原因是我们用的是arm)
199 CROSS_COMPILE ?= arm-none-linux-gnueabi- (后面不要写上空格)
200 endif
3. 删除u-boot源码的中间文件
make distclean/clean
4. 配置u-boot源码支持fs6818开发板
make _config
make fs6818_config
如果打印一下信息,表示成功:
hqyj@hqyj:u-boot-2014.07-fs6818$ make fs6818_config
Configuring for fs6818 board...
如果打印以下信息,表示u-boot不支持此开发板:
hqyj@hqyj:u-boot-2014.07-fs6818$ make maxiaoli_config
make: *** No rule to make target `maxiaoli_config'. Stop.
make: *** [maxiaoli_config] Error 1
5. 编译u-boot源码,生成ubootpak.bin
make / make all(编译的时间比较长)
Make的时候可能出现上面问题,出现上面问题,如下方法解决:
6. 将生成的ubootpak.bin文件下载到开发板,测试是否可以使用。
如何将uboot放到flash(EMMC)中
前提:SD卡必须提前烧录一个uboot
1> 通过SD卡的方式启动uboot,
进入到FS6818#界面
2> 拷贝ubootpak.bin到tftpboot目录下
cd bootloader/u-boot-2014.07-fs6818
cp ubootpak.bin ~/tftpboot
3> 将ubootpak.bin使用tftp命令烧写到内存中
tftp 0x41000000 ubootpak.bin
4> update_mmc
update_mmc
- type : 2ndboot | boot | raw | part
:flash设备编号 EMMC:2
:类型 2ndboot
:uboot在内存中的起始地址,以字节为单位
:flash的起始地址:以块为单位
:往flash中下载多少块空间
Bytes transferred = 352296 (56028 hex)
一块的大小是512字节
length的大小 > 352296 / 512
fastboot=flash=mmc,
2:ubootpak:2nd:0x200,0x78000; flash=mmc,2:2ndboot:2nd:0x200,0x4000;
执行命令:
update_mmc 2 2ndboot 0x41000000 0x200 0x78000
打印以下内容表示成功
head boot dev = 2
update mmc.2 type 2ndboot = 0x200(0x1) ~ 0x78000(0x3c0): Done
5> 测试是否更新ubootpak.bin到EMMC中
设置拨码开关,切换到EMMC启动
开发板重新上电。
13. u_boot 分析
一:makefile文件分析
由README可知,u_boot需要先配置后make
配置命令是make fs6818_config
由顶层makefile可找到如下命令:
1. 打开u-boot源码顶层目录的Makefile
vi Makefile
2. 搜索fs6818_config目标
通多部分匹配的方式搜索_config,
得到以下信息:
467 %_config:: outputmakefile
468 @$(MKCONFIG) -A $(@:_config=)
解析:
%:模式通配符
第一个@:后边的命令不回显到ubuntu终端
@:_config=:将fs6818_config中的_config干掉,
保留fs6818
方法1:
去掉第一个@符,重新执行make fs6818_config
/home/linux/bootloader/u-boot-2014.07-fs6818/mkconfig -A fs6818
上面当回显到终端上面的时候就可以看出,config前面是fs6818;
然后分析mkconfig文件,
下面是makefile中关于ARM的部分
vi Makefile
将一下内容:
198 ifeq ($(HOSTARCH),$(ARCH))
199 CROSS_COMPILE ?=
200 endif
修改为
198 ifeq (arm,arm)(里面的两个相等即可,写ARM的原因是我们用的是arm)
199 CROSS_COMPILE ?= arm-none-linux-gnueabi- (后面不要写上空格)
200 endif
要看这些文件怎么组织成u_boot。
看链接脚本u-boot.lds (此处参考实际文件注释,u-boot.lds:链接脚本(指导程序如何排布的))
由u-boot.lds文件可知,
第一个运行的文件是start.o,然后开始分析arch/arm/cpu/slsiap/s5p6818/start.S文件,到此makefile分析完毕。
所得的结论:1.第一个文件是:arch/arm/cpu/slsiap/s5p6818/start.S
二:start.s分析
功能描述:第一阶段:设置为SVC模式、关看门狗、屏蔽中断、初始化SDRAM、设置栈和时钟、代码从flash到SDRAM、清BSS段、调用start_armboot(C函数)
1>构建异常向量表
设置为SVC模式
设置为SVC模式、关看门狗
鼠标放到lowlevel_init,进行ctrl+]进行跳转
清BSS段
鼠标放在board_init_r,按ctrl+]进行跳转
跳转到arch/arm/lib/board.c 文件中
void board_init_f(ulong bootflag){}
板子的的结构体gd(global data)的初始化,gb结构体用于存储全局信息的结构体。
第二阶段:调用board.c\start_armboot开始
首先搞清楚u_boot的目标:从flash读出内核然后启动
分析代码board.c:
要想读出内核,必须支持flash,代码在561行和588行,分别调用了
flash_init()和nand_init()对nor和nand初始化。
接下来是617行环境变量函数env_relocate (),在u_boot里面输入print命令会出现一大堆环境变量,用来设置波特率等,环境变量来源于:
1.默认的 2.flash上保存的,启动时先看flash上有没有,如果没有,用默认的。
继续往下分析,中间不用管,是一些网卡和调试器的设置,一直到927行run_main_loop (),函数中死循环调用main_loop ()函数,转到文件main.c看此函数,
main_loop()函数中。若在bootdelay倒计时为0之前,U-Boot控制台有输入,则进入命令解析-执行的循环;若控制台无输入,U-Boot将启动内核。
autoboot_command()判断键盘是否有按下,也就是是否打断了倒计时,如果键盘按下的话就执行相应的
分支。run_command_list,此函数会执行参数 s 指定的一系列命令,也就是环境变量 bootcmd 的命令,
bootcmd 里面保存着默认的启动命令,因此 linux 内核启动!这个就是 uboot 中倒计时结束以后
自动启动 linux 内核的原理。如果倒计时结束之前按下了键盘上的按键,那么 run_command_list
函数就不会执行,相当于 autoboot_command 是个空函数。
回到 main_loop 函数中,如果倒计时结束之前按下按键,那么就会执行 cli_loop 函数,这个就是命令处理函数,负责接收好处理输入的命令。
run_command_list (s, -1,0)执行bootcmd命令启动内核; 所以读出和启动内核取决于s命令,即bootcmd,
从环境变量可以看出bootcmd(/uboot/include/configs/fs6818.h)
#define CONFIG_BOOTCOMMAND "ext4load mmc 2:1 0x48000000 uImage;ext4load mmc 2:1 0x49000000 root.img.gz;bootm 0x48000000"
内核格式有两类:zImage和uImage。
并不是所有U-Boot都支持zImage,是否支持就看其配置文件(fs6818.h没定义CONFIG_ZIMAGE_BOOT)中是否定义CONFIG_ZIMAGE_BOOT这个宏。所以有些uboot是支持zImage启动的,有些则不支持。但是所有的uboot肯定都支持uImage启动
ulmage格式的内核头部信息在/uboot/include/Image.h中定义。
ih_load是加载地址,即内核在DDR中的地址(运行地址);ih_ep是内核入口地址。
复制代码
typedef struct image_header {
uint32_t ih_magic; /* Image Header Magic Number */
uint32_t ih_hcrc; /* Image Header CRC Checksum */
uint32_t ih_time; /* Image Creation Timestamp */
uint32_t ih_size; /* Image Data Size */
uint32_t ih_load; /* Data Load Address */
uint32_t ih_ep; /* Entry Point Address */
uint32_t ih_dcrc; /* Image Data CRC Checksum */
uint8_t ih_os; /* Operating System */
uint8_t ih_arch; /* CPU architecture */
uint8_t ih_type; /* Image Type */
uint8_t ih_comp; /* Compression Type */
uint8_t ih_name[IH_NMLEN]; /* Image Name */
} image_header_t;
总结:
1)将内核搬移至DDR中;
2)校验内核格式、CRC;
3)准备传参;
4)跳转执行内核
三:run_command分析
可以想象,命令是一个结构体,{name,fun()},开始分析run_command,在main.c 1280行处为此函数实现。
直接跳到1325行看,1355行是解析命令,比如输入命令是md.w 0,则解析为argv[0]=”md.w”,
argv[1]=”0”。argv[0]放的是命令,argv[1]放的是参数。分析1361行如下代码:
if ((cmdtp = find_cmd(argv[0])) == NULL) {
printf ("Unknown command '%s' - try 'help'\n", argv[0]);
rc = -1; /* give up after bad command */
continue;}
如何查找命令呢?cmdtp是一个结构体,看具体代码(位于include\command.h第39行)。
find_cmd(argv[0])函数在common\command.c中346行,首先看360行__u_boot_cmd_start和__u_boot_cmd_end,这两个东西搜遍代码是搜不到的,它在链接脚本里,C语言中,链接脚本也可以传入值。下面分析*(.u_boot_cmd)段。在include\command.h中93行:
#define Struct_Section __attribute__ ((unused,section (".u_boot_cmd")))
输入bootm 后调用哪个函数不知道,在项目里搜bootm,发现它在common\cmd_bootm.c中
U_BOOT_CMD(
bootm, CFG_MAXARGS, 1, do_bootm,
"bootm - boot application image from memory\n",
"[addr [arg ...]]\n - boot application image stored in memory\n"
"\tpassing arguments 'arg ...'; when booting a Linux kernel,\n"
"\t'arg' can be the address of an initrd image\n"
#ifdef CONFIG_OF_FLAT_TREE
"\tWhen booting a Linux kernel which requires a flat device-tree\n"
"\ta third argument is required which is the address of the of the\n"
"\tdevice-tree blob. To boot that kernel without an initrd image,\n"
"\tuse a '-' for the second argument. If you do not pass a third\n"
"\ta bd_info struct will be passed instead\n"
#endif);
去搜索宏U_BOOT_CMD发现在include\command.h 97行:
#define U_BOOT_CMD(name,maxargs,rep,cmd,usage,help) \
cmd_tbl_t __u_boot_cmd_##name Struct_Section = {#name, maxargs, rep, cmd, usage, help}
把上面的宏展开得到:
cmd_tbl_t __u_boot_cmd_bootm
__attribute__ ((unused,section (".u_boot_cmd")))= {#name, maxargs, rep, cmd, usage, help}
从这里可以看出定义了__u_boot_cmd_bootm这样一个结构体,类型是cmd_tbl_t,
(代码是typedef struct cmd_tbl_s cmd_tbl_t; include\command.h中93行:)
这个结构体有个属性:__attribute__,强制把段属性section设置为.u_boot_cmd(u-boot.lds中)
里面的内容是:{#name, maxargs, rep, cmd, usage, help}
替换得:{bootm, CFG_MAXARGS, 1, do_bootm, "bootm - boot application image from memory\n", "bootm - boot application image from memory\n","[addr [arg ...]]\n - boot application image stored in memory\n" "\tpassing arguments 'arg ...'; when booting a Linux kernel,\n" "\t'arg' can be the address of an initrd image\n"}
实验内容:增加一个hello命令。
在common目录下新建一个名为cmd_hello.c的文件,里面代码根据cmd_bootm.c来修改,代码如下:
#include
#include
#include
int do_hello(cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
int i;
printf(“hello world!\n,参数的个数是%d”,argc);
for(i=0;i<=argc;i++)
printf(“参数是:%s ”,argv[i]);
return 0;
}
另外里面还需要定义一个宏:
U_BOOT_CMD(
hello, CFG_MAXARGS, 1, do_hello,
"hello short help.....",
"hello long help.............................................."\n
);
最后把此文件放到common目录下,修改此目录下的makefile第54行:
加上文件cmd_hello.c,然后重新make一下即可。
四:内核启动分析
一:
根据bootm 命令执行do_bootm_states函数,658行非常重要,通过bootm_os_get_boot_func查找系统启动函数,参数images->os.os为系统类型,函数返回值是查找到的系统启动函数do_bootm_linux。处理615行BOOTM_STATE_OS_PREP状态,调用do_bootm_linux->boot_prep_linux处理环境变量bootargs,bootargs保存着传递给linux kernel的参数。
638行调用boot_selected_os,启动linux内核,第4个参数为linux镜像头,boot_os数组如下:
static boot_os_fn *boot_os[] = {
[IH_OS_U_BOOT] = do_bootm_standalone,
#ifdef CONFIG_BOOTM_LINUX
[IH_OS_LINUX] = do_bootm_linux,
#endif
#ifdef CONFIG_BOOTM_NETBSD
[IH_OS_NETBSD] = do_bootm_netbsd,
#endif
#ifdef CONFIG_LYNXKDI
[IH_OS_LYNXOS] = do_bootm_lynxkdi,
#endif
#ifdef CONFIG_BOOTM_RTEMS
[IH_OS_RTEMS] = do_bootm_rtems,
#endif
#if defined(CONFIG_BOOTM_OSE)
[IH_OS_OSE] = do_bootm_ose,
#endif
#if defined(CONFIG_BOOTM_PLAN9)
[IH_OS_PLAN9] = do_bootm_plan9,
#endif
#if defined(CONFIG_BOOTM_VXWORKS) && \
(defined(CONFIG_PPC) || defined(CONFIG_ARM))
[IH_OS_VXWORKS] = do_bootm_vxworks,
#endif
#if defined(CONFIG_CMD_ELF)
[IH_OS_QNX] = do_bootm_qnxelf,
#endif
#ifdef CONFIG_INTEGRITY
[IH_OS_INTEGRITY] = do_bootm_integrity,
#endif
};
红色函数do_bootm_linux为对应的系统启动函数。
第 295 行,函数 kernel_entry,看名字“内核_进入”,说明此函数是进入 Linux 内核的,也
就是最终的大 boos!!此函数有三个参数:zero,arch,params,第一个参数 zero 同样为 0;第
二个参数为机器 ID;第三个参数 ATAGS 或者设备树(DTB)首地址,ATAGS 是传统的方法,用
于传递一些命令行信息啥的,如果使用设备树的话就要传递设备树(DTB)。
第 299 行,获取 kernel_entry 函数,函数 kernel_entry 并不是 uboot 定义的,而是 Linux 内
核定义的,Linux 内核镜像文件的第一行代码就是函数 kernel_entry,而 images->ep 保存着 Linux
内核镜像的起始地址,起始地址保存的正是 Linux 内核第一行代码!
boot_jump_linux,第 315~318 行是设置寄存器 r2 的值?
为什么要设置 r2 的值呢?Linux 内核一开始是汇编代码,因此函数 kernel_entry 就是个汇编函
数。向汇编函数传递参数要使用 r0、r1 和 r2(参数数量不超过 3 个的时候),所以 r2 寄存器就是
函数 kernel_entry 的第三个参数。
如果使用设备树的话,r2 应该是设备树的起始地址,而设备树地址保存在 images
的 ftd_addr 成员变量中。
如果不使用设备树的话,r2 应该是 uboot 传递给 Linux 的参数起始地址,也就
是环境变量 bootargs 的值,调用 kernel_entry 函数进入 Linux 内核,此行将一去不复返,uboot 的使命也就
完成了。
14. linux内核的移植
【1】linux内核的特点
1》linux内核源码开源
2》linux支持多种架构平台 arm x86(手机和Ubuntu都使用的是Linux内核)
3》linux内核代码采用模块化的方式()
4》linux内核代码采用分层的思想
5》linux内核源码具有良好的移植特性和裁剪特性
6》linux内核源码都是使用汇编和c语言实现的
【2】linux内核源码的选择
1》可以从linux内核官方获取
https://mirrors.edge.kernel.org/pub/linux/kernel/
2》从芯片厂家获取
3》从开发板厂家获取
4》从技术主管获取 --》推荐
【3】linux内核的命名规则
linux-主版本号.次版本号.修订版本号.tar.xz
主版本号:内核源码有较大跟新,才会升级主版本号
次版本号:
次版本号为偶数:表示稳定版本
次版本号为奇数:表示测试版本
修订版本号:内核源码中只要有代码更新就会,
升级修订版本号
【4】如何选择linux内核的版本
1》不可以太新
2》不可以太旧
3》选择稳定版本
三星公司没有对S5P6818的源码,开源到Linux内核中,所以不能从官网下载linux内核源码,使用samsung提供的内核源码。
本课程使用的内核源码为:
kernel-3.4.39-ok.tar.bz2
【5】linux内核源码的配置和编译
1. 在ubuntu的家目录下创建kernel文件夹
将内核源码压缩包拷贝到kernel文件夹中
mkdir kernel
cd kernel
cp /mnt/hgfs/share/kernel-3.4.39-ok.tar.bz2 ./
对内核源码进行解压缩
tar -vxf kernel-3.4.39-ok.tar.bz2
2. 进入到内核源码目录中,分析内核源码的目录结构
cd kernel-3.4.39
ls
平台相关:跟硬件有关的,代码不可以共享
arch
平台无关:代码可以共享
fs
include
driver
net
tools
usr
ipc
....
3. 配置交叉编译工具链
打开内核源码顶层目录的Makefile,
搜索CROSS_COMPILE
195 ARCH ?=
196 CROSS_COMPILE ?=
修改为:
195 ARCH ?= arm
196 CROSS_COMPILE ?= arm-none-linux-gnueabi-
4. 读README
1》拿到内核源码之后,应该先清除
内核源码中的中间文件
make clean
make distclean
make mrproper --》清除更干净
2》 配置内核源码支持当前的硬件平台
183 "make ${PLATFORM}_defconfig"
184 Create a ./.config file by using the default
symbol values from
186 arch/$ARCH/configs/${PLATFORM}_defconfig.
187 Use "make help" to get a list of all available
188 platforms of your architecture.
(创建一个.config文件,使用默认的符号值,从arch/arm/configs/目录下的这个文件${PLATFORM}_defconfig.生成一个.config文件。)
方法1:
make help
得到以下信息:
fs6818_defconfig - Build for fs6818
方法2:m
进入arch/arm/configs/目录
发现以下文件fs6818_defconfig
所以PLATFORM=fs6818. :q!
让当前的内核支持自己的硬件平台,
应该执行make fs6818_defconfig
执行结果,打印以下信息:
hqyj@hqyj:kernel-3.4.39$ make fs6818_defconfig
HOSTCC scripts/basic/fixdep
HOSTCC scripts/kconfig/conf.o
SHIPPED scripts/kconfig/zconf.tab.c
SHIPPED scripts/kconfig/zconf.lex.c
SHIPPED scripts/kconfig/zconf.hash.c
HOSTCC scripts/kconfig/zconf.tab.o
HOSTLD scripts/kconfig/conf
#
# configuration written to .config
#
执行完命令将板子的配置信息写到内核源码顶层目录下的.config文件中
//可以支持硬件板子fs6818之后,需要安装板子上的硬件设备驱动
3》基于菜单选项的方式对内核进行配置
执行命令:make menuconfig
实际开发中随内核进行菜单选项的配置,都是使用make menuconfig命令
问题1:
第一次使用make menuconfig 需要安装图形化界面的工具
配置之前需要安装图形图(make meuconfig):
sudo apt-get install libncurses5-dev
问题2:
出现以下错误:
cripts/kconfig/mconf Kconfig
Your display is too small to run Menuconfig!
It must be at least 19 lines by 80 columns.
make[1]: *** [menuconfig] Error 1
make: *** [menuconfig] Error 2
原因:终端的字体太大,缩小一点
4》编译内核生成uImage
make uImage
//编译得时候可能会相对于比较长,可以不用编译,用老师发得,有些插件可能不全,可以去用一下。
time make uImage -jx
time:回显编译的时间
-jx:使用多线程的方式进行编译
x可以是2,4,6,8
在编译的过程中可能出现如下错误:
"mkimage" command not found - U-Boot images will not be built
make[1]: *** [arch/arm/boot/uImage] Error 1
make: *** [uImage] Error 2
错误的原因:找不到mkimage命令,
根据提示分析出来mkimage应该存在uboot源码目录中
uboot源码必须进行编译之后才会有mkimage可执行程序
解决问题的方法:
将uboot源码的tools目录下的mkimage,
sudo cp mkimage /usr/bin
拷贝到到ubuntu的/use/bin目录下:
sudo cp ./tools/mkimage /usr/bin
uboot目录 ubuntu目录
再次对内核源码进行编译即可通过。
5》将arch/arm/boot/目录下的uImage拷贝到tftpboot目录下,
测试uImage是否可以正常启动,并且挂载根文件系统(重启开发板,如下图,自动方式:先将uImage下载到内存里面,然后bootcmd启动内核,然后给内核传递参数)
通过tftp的方式下载uImage到内存中,
通过nfs的方式挂载根文件系统。
注意检查bootcmd和bootargs设置是否正确。
面试题:Makefile .config Kconfig文件之间的关系
Makefile:指导内核进行编译
.config:存放的是内核的配置的信息+硬件
Kconfig:存放菜单选项(menuconfig的源代码)
执行make fs6818_defconfig命令,根据fs6818_config文件和Kconfig文件中的配置信息生成.config文件
make menuconfig 执行时根据Kconfig生成菜单的图形化界面,
如果根据菜单图形化界面进行配置之后,会更新.config文件
Makefile文件根据.config文件中的信息,决定将那些文件
编译到uIamge中,那些不编译到uImage中。
例子:
驱动移植
1.需要有一个驱动对应的.c的代码
2. Kconfig .config Makefile
第一步:修改Kconfig:(Kconfig是产生菜单的文件)
53 config CHEN_JINGJING //在.config中生成选项
54 bool "chengjingjing said welcome to hqyj!" //选项菜单中的名字(OBJ目标文件名:obj-$(CONFIG_CHEN_JINGJING))
(tristate:有三种选项,三态(Y(把驱动编译到内核中),M(模块),N(不编译驱动)),bool:两态)
添加完选项以后,回到最顶层目录执行make menuconfig
在这个目录下面执行:make menuconfigCO
//通过选项选择要安装的驱动,图形界面能让我们快熟的找到想安装的驱动。
找到上图的device drivers,然后按回车,出现下面界面,找到character devices,
按回车
看到上面的自己的添加的内容,根据自己的需求按键盘上面的Y,M,N三选一;选中以后到exit退出即可(选择yes)
vi .config
CONFIG_MA_XIAOLI=y
vi Makefile (打开Makefile的路径)
obj-$(CONFIG_FARSIGHT_DEMO) +=demo.o
编译:
make uImage-->uImage(包含了新的驱动的内核)
在板子上对应路径(/dev)就有了chenjingjing的驱动文件。
make modules -->demo.ko(M(编译生成模块))
--------------------
Makefile
modules:
编译模块的规则
Y(要编译到内核中) M(编译生成模块) N(不编译驱动)
sudo insmod demo.ko 安装驱动
sudo rmmod demo 卸载驱动
在实际开发中,都是使用模块化的方式进行驱动的开发,
需要使用驱动时,通过insmod命令加载到内核中,
不需要使用驱动时,通过rmmod命令卸载驱动。
好处: 1. 开发更加的方便便捷
2. 节约开发的时间
3. 容易发现错误
如果编译到内核中,内核一旦崩溃,不确定是内核的问题换是驱动的问题。
如果使用模块化的方式进行编译,启动内核可以成功,说明内核没有问题,
如果加载驱动是,内核崩溃,说明驱动有问题。
15. 案例1:将led灯的驱动编译到内核中
驱动就是给应用层提供接口,驱动提供了亮灯和灭灯的接口,
具体你是实现流水灯,还是呼吸灯,由应用层的代码决定
1. 拷贝fs6818_led.c和fs6818_led.h文件到
drivers/char目录下
cp /mnt/hgfs/share/led-driver/fs6818_led.c
~/kernel/kernel-3.4.39/drivers/char
cp /mnt/hgfs/share/led-driver/fs6818_led.h
~/kernel/kernel-3.4.39/drivers/char
2. 打开drivers/char目录下的Makefile,添加对应的驱动的编译信息
obj-$(CONFIG_FS6818_RGB) += fs6818_led.o
3. 打开drivers/char目录下的Kconfig,添加对应的菜单选项 ,添加以下信息
12 config FS6818_RGB
13 bool "FS6818_LED_DREVERS"
14 default y
15 help
16 This is FS6818 LED Driver!~
4. 执行make menuconfig 进行菜单选项的配置
Pressing <Y> includes, (包含)
<N> excludes, (不包含)
<M> modularizes features. (模块化)
Press <Esc><Esc> to exit, (退出)
<?> for Help, (查看帮助手册)
</> for Search.(搜索)
Legend: [*] built-in (*编译)
[ ] excluded (空不编译)
<M> module
< > module capable
如何查找FS6818_LED_DREVERS菜单选项在哪个子菜单中?
按“/”键,出现搜索的对话框,输入config后边的内容:
“FS6818_RGB”,回车,得到以下信息。
│ Symbol: FS6818_RGB [=y] 配置信息
│ Type : boolean 菜单选项类型
│ Prompt: FS6818_LED_DREVERS 菜单选项
│ Defined at drivers/char/Kconfig:12
│ Location:
│ -> Device Drivers 所在的子菜单
│ -> Character devices
(Makefile得到的信息是y,及将后面的XXX.o编译到uImage)
5. 编译内核生成uImage
make uImage
CC drivers/char/fs6818_led.o
6. 拷贝uImage到tftpboot目录下
cp arch/arm/boot/uImage ~/tftpboot
重启开发板,通过网络的方式,下载内核,挂载根文件系统
7.测试驱动是否可以正常工作
编译应用层的代码,生成可执行文件
cp /mnt/hgfs/share/led-driver ~/ -raf
arm-none-linux-gnueabi-gcc fs6818led_test.c
最终生成a.out可执行文件
拷贝a.out可执行文件到根文件系统中,
cp led-driver/a.out ~/nfs/rootfs
进入超级终端,执行./a.out
案例2:将led灯的驱动使用模块化的方式进行编译《模块》
问题1. make uImage 编译时间问题
如果执行make clean 所有的.o文件都需要重新生成,所以编译时间会很长
如果之前编译过,如果对应的.c文件没有修改过,编译时不在重新生成.o文件,只重新编译生成修改过的.c文件,重新生成.o文件。
Makefile可以根据文件的时间戳,进行分析
1. 拷贝fs6818_led.c和fs6818_led.h文件到
drivers/char目录下
cp /mnt/hgfs/share/led-driver/fs6818_led.c
~/kernel/kernel-3.4.39/drivers/char
cp /mnt/hgfs/share/led-driver/fs6818_led.h
~/kernel/kernel-3.4.39/drivers/char
2. 打开drivers/char目录下的Makefile,添加对应的
驱动的编译信息
obj-$(CONFIG_FS6818_RGB) += fs6818_led.o
3. 打开drivers/char目录下的Kconfig,添加对应的
菜单选项 ,添加以下信息
12 config FS6818_RGB
13 tristate "FS6818_LED_DREVERS"
14 default y
15 help
16 This is FS6818 LED Driver!~
bool:
[*] : 对应的驱动编译到内核uImage镜像中
[ ] : 对应的驱动不编译到内核uImage镜像中
stistate:三态
<*> : 对应的驱动编译到内核uImage镜像中
< > : 对应的驱动不编译到内核uImage镜像中
<M> :对应的驱动不编译到内核uImage镜像中,
而是使用模块化的方式编译驱动,生成****.ko
需要使用驱动是,将驱动通过命令加载到内核中,
不需要使用驱动是,通过命令将驱动从内核中卸载掉
4. 执行make menuconfig 进行菜单选项的配置
如何查找FS6818_LED_DREVERS菜单选项在哪个子菜单中?
按“/”键,出现搜索的对话框,输入config后边的内容:
“FS6818_RGB”,回车,得到以下信息。
│ Symbol: FS6818_RGB [=y] 配置信息
│ Type : tristate 菜单选项类型:三态
│ Prompt: FS6818_LED_DREVERS 菜单选项
│ Defined at drivers/char/Kconfig:12
│ Location:
│ -> Device Drivers 所在的子菜单
│ -> Character devices
将以下菜单选项修改为M
<M> FS6818_LED_DREVERS
5. 编译内核,重新生成uImage,将之前uImage中的led灯的驱动代码给删除
make uImage
重新拷贝uImage文件到家目录的tftpboot
cp arch/arm/boot/uImage ~/tftpboot
6. 对内核源码进行模块化的编译生成.ko文件
make modules
编译信息为以下内容,表示成功:
LD [M] drivers/char/fs6818_led.ko
拷贝fs6818_led.ko文件到根文件系统的home目录下
cp drivers/char/fs6818_led.ko ~/nfs/rootfs/home
7.测试驱动是否可以正常工作
编译应用层的代码,生成可执行文件
cp /mnt/hgfs/share/led-driver ~/ -raf
arm-none-linux-gnueabi-gcc fs6818led_test.c
最终生成a.out可执行文件
拷贝a.out可执行文件到根文件系统的home目录下,
cp led-driver/a.out ~/nfs/rootfs/home
重启开发板,通过tftp下载内核,通过nfs挂载根文件系统,
启动成功之后,
在超级终端上,输入cd home 进入根文件系统的home目录下。
使用模块化命令将驱动加载到内核中,
insmod fs6818_led.ko 安装模块化驱动到内核中
lsmod 查看模块化加载的驱动
rmmod fs6818_led 卸载驱动,不需要加.ko
mknod 在/dev目录下创建设备节点mo
格式:mknod /dev/led c 500 0
设备节点的名字 字符设备 主设备号 次设备号
进入超级终端,执行./a.out
# rmmod fs6818_led
rmmod: can't change directory to '/lib/modules': No such file or directory
解决办法:
在lib目录下创建modules文件夹
cd lib
mkdir modules
再次执行rmmod fs6818_led
rmmod: can't change directory to '3.4.39-farsight': No such file or directory
解决办法:
在lib/modules目录下创建3.4.39-farsight文件夹
cd lib/modules
mkdir 3.4.39-farsight
16. 根文件系统(方式一)
【1】概念
根文件系统:系统运行所必须依赖的一些文件
(比如脚本、库、配置文件...),本质就是目录和文件。根文件系统镜像:将根文件系统按照某种格式进行打包压缩后生成的单个文件 rootfs-----> ramdisk.img
文件系统:一种管理和访问磁盘的软件机制,
不同文件系统管理和访问磁盘的机制不同
【2】 移植根文件系统的工具 busybox
1. 短小精悍
2. 版本更新较快,版本之间差异不大
【3】 如何获取busybox
https://busybox.net/downloads/
【4】根文件系统中目录的介绍
注释:个文件功能解析
bin: 命令文件(通过busybox工具制作)
dev: 设备文件(被操作系统识别的设备才有对应的文件,即设备运行时)
etc: 配置文件(配置内核相关的一些信息)
lib: 库文件、比如C的标准库(从交叉编译工具链中复制的)
linuxrc:根文件系统被挂载后运行的第一个程序(通过busybox工具制作)
mnt: 共享目录(非必要)比如挂载SD卡等时将SD卡挂载在该目录
proc: 与进程相关的文件(当有进程运行时才会有文件)
root: 用户权限(板子本身就是以root用户运行)
sbin: 超级用户命令、一般用户不可用(板子本身是超级用户 通过busybox工具制作)
sys: 系统文件(系统运行时,系统加载后才会有文件)
tmp: 临时文件(比如插入新的设备时会产生临时文件)
usr: 用户文件(通过busybox工具制作)
var: 存放下载的文件和软件 (可有可无)
mkdir lib mnt proc root sys tmp var
【5】使用busyBox工具制作根文件系统需要使用gcc-4.9.4版本的交叉编译工具链,需要重新配置交叉编译工具链
步骤:
1. 在ubuntu的家目录(~)下,创建toolchain
mkdir toolchain
2. 拷贝gcc-4.9.4.tar.xz到toolchain目录下
cp 共享目录/gcc-4.9.4.tar.xz ~/toolchain
3. 解压缩交叉编译工具链
tar -vxf gcc-4.9.4.tar.xz
4. 配置环境变量
打开 sudo vi /etc/bash.bashrc
在最后一行添加以下内容:
export PATH=$PATH:/home/hqyj/toolchain/gcc-4.9.4/bin/
修改为自己的路径
注意:需要将gcc-4.9.1的注释掉
5. 使环境变量立即生效
source /etc/bash.bashrc
6. 测试交叉编译工具链是否安装成功
切换完交叉编译工具链,ubuntu的终端需要重新开
arm-none-linux-gnueabi-gcc -v
打印以下内容,表示成功
gcc version 4.9.4 (Sourcery G++ Lite 2010.09-50)
【6】使用busybox工具制作rootfs根文件系统
1. 拷贝busybox-1.31.1.tar.bz2到ubuntu的家目录下
cp /mnt/hgfs/share/busybox-1.31.1.tar.bz2 ~/
2. 对根文件系统进行解压缩,并切换到busybox-1.31.1目录下
tar -vxf busybox-1.31.1.tar.bz2
cd busybox-1.31.1
3. 分析README
没有获取太多重要的信息
4. 通过make help 获取编译的帮助信息
清除中间文件
make clean - delete temporary files created by build
make distclean - delete all non-source files (including .config)
编译
make all - Executable and documentation
通过图形化界面的配置
make menuconfig - interactive curses-based configurator
安装和卸载
make install - install busybox into CONFIG_PREFIX
make uninstall
5. 修改Makefile,配置为交叉编译工具链
打开Makefile
将一下内容:
164 CROSS_COMPILE ?=
190 ARCH ?= $(SUBARCH)
修改为
164 CROSS_COMPILE ?= arm-none-linux-gnueabi-
190 ARCH ?= arm
6. 执行make menuconfig 命令,对busybox进行配置
1》 使用静态库,不使用共享库,共享库的话当移植到板子上面的时候,就不能使用了
Settings --->
[*] Build static binary (no shared libs) (NEW)
2》 配置为vi风格的行编辑命令
Settings --->
[*] vi-style line editing commands (NEW)
3》 配置根文件系统的安装路径
Settings --->
(./_install) Destination path for 'make install'
修改./_install 为 /home/hq/rootfs
进来以后先按一下tab键,然后再去修改就可以
4》配置支持驱动模块化的命令
Linux Module Utilities --->
此前前边默认是*,将*修改为空
[ ] Simplified modutils
[*] depmod (27 kb) (NEW)
[*] insmod (22 kb) (NEW)
[*] lsmod (1.9 kb) (NEW)
[*] Pretty output (NEW)
[*] modinfo (24 kb) (NEW)
[*] modprobe (28 kb) (NEW)
[*] Blacklist support (NEW)
[*] rmmod (3.3 kb) (NEW)
7. 对源码进行编译
make / make all
8. 安装根文件系统到/home/hqyj/rootfs
make install
9. 测试根文件系统,对根文件系统进行部署
1》首先将nfs目录的的rootfs跟换一个其他的名字
cd ~/nfs
mv rootfs rootfs-ok
2》将自己制作的rootfs文件拷贝到~/nfs
cp ~/rootfs ~/nfs -raf
10. 重启开发板,测试rootfs是否可以正常使用
前提,使用tftp下载uImage
使用nfs挂载根文件系统
1》挂载成功,但是打印一下错误信息:
can't run '/etc/init.d/rcS': No such file or directory
can't open /dev/tty2: No such file or directory
can't open /dev/tty3: No such file or directory
can't open /dev/tty4: No such file or directory
解决办法:
Cd ~/nfs/rootfs
创建dev文件夹,创建etc/init.d文件夹,
在etc/init.d目录下创建rcS文件
mkdir -p etc/init.d
mkdir dev
cd etc/init.d
touch rcS
2》继续重启开发板,进行测试
出现以下问题:
can't run '/etc/init.d/rcS': Permission denied
此问题解决办法:修改rcS的权限最大
chmod 777 rcS
在rcS文件中必须添加以下内容:
#!/bin/sh
/bin/mount -a
echo /sbin/mdev > /proc/sys/kernel/hotplug
/sbin/mdev -s
解释:
mount -a:系统会自动解析fstab配置文件,系统根据
此配置文件进行一系列的挂接动作
echo /sbin/mdev > /proc/sys/kernel/hotplug:
向文件/proc/sys/kernel/hotplug写入字符串"/sbin/mdev" 其实就是告诉内核驱动将来创建设备文件的程序是/sbin/mdev
mdev -s:系统启动,将内核驱动对应的设备文件进行自动创建
can't open /dev/tty2: No such file or directory
can't open /dev/tty3: No such file or directory
can't open /dev/tty4: No such file or directory
此问题,后边会解决
再次重启开发板:
3》在根文件系统的etc目录下创建文件fstab inittab
cd etc
touch fstab
touch inittab
打开inittab文件添加以下内容:
::sysinit:/etc/init.d/rcS
::askfirst:-/bin/sh
::restart:/sbin/init
::ctrlaltdel:/sbin/reboot
解释:系统启动的流程
打开fstab文件添加以下内容
#device mount-point type options dump fsck orde
proc /proc proc defaults 0 0
tpfs /tmp tmpfs defaults 0 0
sysfs /sys sysfs defaults 0 0
tmpfs /dev tmpfs defaults 0 0
解析:
第一列:设备类型
第二列:挂载点
第三列:类型
第4,5,6列:访问权限
再次重启开发板,基本上成功,
4》创建其他文件:
mkdir lib mnt proc root sys tmp var
再次重启开发板,根文件系统制作成功。
5》添加用户名
在根文件系统的etc目录下创建profile文件,
打开profile文件,添加以下内容:
export HOSTNAME=hq
export USER=root
export HOME=ot
#export PS1="\[\u@\h \W\]\$ "
#cd root
export PS1="[$USER@$HOSTNAME \W\]\# "
PATH=/bin:/sbin:/usr/bin:/usr/sbin
LD_LIBRARY_PATH=/lib:/usr/lib:$LD_LIBRARY_PATH
export PATH LD_LIBRARY_PATH
重启开发板
6》移植共享库到根文件系统中,
在ubuntu中创建test.c 编写以下代码
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("hello world!\n");
return 0;
}
使用交叉编译工具链编译test.c生成test可执行程序
arm-none-linux-gnueabi-gcc test.c -o test 拷贝test可执行程序到新制作的文件系统中
cp test ~/nfs/rootfs
重启开发板,在超级终端终端上,运行应用程序test
./test
出现以下错误提示:
-/bin/sh: ./test: not found
问题原因:缺少应用程序运行必要的动态库。
解决办法:将交叉编译工具链中的库,拷贝到根文件系统的lib目录
cp /home/hq/toolchain/gcc-4.9.4/arm-none-linux-gnueabi/sysroot/lib/* ~/nfs/rootfs/lib
cp /home/hq/toolchain/gcc-4.6.4/arm-arm1176jzfssf-linux-gnueabi/sysroot/lib/* ~/nfs/rootfs/lib
到此课程结束:
具体的ramdisk.img根文件系统的制作,可以根据实验手册按照步骤要求去做即可
交叉编译工具链,132M,太大,不往板子里面装交叉编译工具链
17. 根文件系统的制作
【实验目的】
掌握根文件系统的制作及移植方法
说明:在实验中命令行提示符为“$”表示在主机上运行,“#”表示在目标板上运行
【实验环境】
- ubuntu 12.04发行版
- FS6818平台
【实验步骤】
1. 将学生资料中“工具与源码\第三天\BusyBox-1.22.1”目录下的“busybox-1.22.1.tar.bz2”文件拷贝到ubuntu中的一个目录并解压
$ tar -xvf busybox-1.22.1.tar.bz2
2. 进入到busybox源码的顶层目录下对源码进行配置
$ cd busybox-1.22.1/
执行如下命令进入配置界面
$ make menuconfig
在图形化配置界面中进入“Busybox Settings --->”菜单,再进入“Build Options --->”菜单,在“Build Options”界面下选中“Build BusyBox as a static binary (no shared libs)”选项,删除“Build with Large File Support (for accessing files > 2 GB)”选项,然后在 “Cross Compiler prefix”处填写使用的编译器的前缀“arm-none-linux-gnueabi-”。
3. 退出配置界面后执行如下命令编译busybox源码
$ make
4. 执行如下命令安装busybox
$ make install
安装完成后在源码的顶层目录下生成_install目录,在_install目录下生成了根文件系统所需要的相关文件,如图所示:
5. 完善根文件系统的其他目录
进入到_ install目录
$ cd _install/
创建相关的目录
$ mkdir dev etc home lib mnt proc root sys tmp var
将交叉编译工具链中的库复制到lib目录中
$ cp /home/linux/toolchain/toolchain-4.5.1-farsight/arm-none-linux-gnueabi/libc/lib/ . -a
删除其中的静态库
$ sudo rm lib/*.a
删除库文件中的符号表减小库文件体积:
$ arm-none-linux-gnueabi-strip lib/*
拷贝原有根文件系统中etc中的内容到自己制作的根文件系统
$ cp -rf /home/linux/rootfs/etc/* etc/
删除之前的根文件系统
$ rm -rf /home/linux/rootfs/*
将自己制作的根文件系统拷贝到nfs的挂载目录
$ cp -rf * /home/linux/rootfs/
6. 按照实验四中的第3、4步测试自己制作生成的根文件系统是否能够被挂载使用,如果不能正常被挂载使用,检查上述步骤是否正确
根文件系统是一个散列的文件,我们要想将其烧写到EMMC中就必须将这些文件打包压缩成一个某种格式的镜像文件
7. 将根文件系统制作成根文件系统镜像
进入到ubuntu的家目录
$ cd ~
执行如下命令制作一个大小为8M的镜像文件
$ dd if=/dev/zero of=ramdisk bs=1k count=8192
将该镜像格式化为ext2格式
$ mkfs.ext2 -F ramdisk
将该镜像文件挂载到ubuntu下的/mnt目录下
$ sudo mount -t ext2 ramdisk /mnt
将我们自己制作的根文件系统中所有的文件拷贝到该镜像中
$ sudo cp -a /home/linux/nfs/rootfs/* /mnt/
解除挂载
$ sudo umount /mnt
压缩镜像文件
$ gzip --best -c ramdisk > ramdisk.gz
使用mkimage工具为镜像文件添加校验头然后生成可用的镜像ramdisk.img
$ mkimage -n "ramdisk" -A arm -O linux -T ramdisk -C gzip -d ramdisk.gz ramdisk.img
将自己制作生成的根文件系统镜像拷贝到tftp的下载目录下并修改其权限
$ cp ramdisk.img /home/linux/tftpboot/
$ chmod 777 /home/linux/tftpboot/ramdisk.img
8. 重新配置linux内核使其支持ramdisk文件系统
进入到实验七使用的linux源码的顶层目录下
$ cd kernel-3.4.39/
执行以下命令进入内核配置界面
$ make menuconfig
在图形化界面中进入到“Device Drivers --->”菜单,再进入“[*] Block devices --->”菜单,将 “RAM block device support” 选为“Y”,将“Default RAM disk size (kbytes)”修改为“8192”,如图所示:
配置完成后保存退出。
回到内核源码的顶层目录下重新编译内核源码:
$ make uImage
将生成的uImage文件拷贝到tftp服务器的下载目录中:
$ cp arch/arm/boot/uImage /home/linux/tftpboot/
$ chmod 777 /home/linux/tftpboot/uImage
9. 按照试验四的第6、7步骤,新生成的内核镜像uImage和根文件系统镜像ramdisk.img烧写到EMMC中并从EMMC中启动测试
四. 驱动
1.驱动课程大纲
- 内核模块
- 字符设备驱动
- 中断
2.ARM裸机代码和驱动有什么区别?
- 共同点:都能够操作硬件
- 不同点:
- 裸机就是用C语言给对应的寄存器里面写值,驱动是按照一定的套路往寄存器里面写值
- arm裸机单独编译单独执行,驱动依赖内核编译,依赖内核执行(根据内核指定好的架构和配置去实现)
- arm裸机同时只能执行一份代码,驱动可以同时执行多分代码(且当要操作串口的时候,内核写的一部分代码咱们程序员就不用去写了,比较方便)
- arm裸机只需要一个main就可以了,在main函数中写相应的逻辑代码即可驱动是依赖内核的框架和操作硬件的过程。
(驱动里面操作LED灯的寄存器)(驱动模块是依赖内核框架执行代码)
3.linux系统组成
- 0-3G的用户空间是每个进程单独拥有0-3G的空间
- 系统调用(软中断swi)----(应用层通过系统调用与底层交互,swi,将应用层切换到内核层。
注:1G的物理内存映射成0~4G的虚拟内存,每个进程都可以访问内核,0~3G是每个进程单独拥有的,3G~4G是所有的共有的。代码运行在物理内存上,向虚拟内存上面写值,其实是写在物理内存上面的
- kernel : 【3-4G】
内核5大功能:
- 进程管理:进程的创建,销毁,调度等功能
注:可中断,不可中断,就是是否被信号打断。从运行状态怎样改到可中断等待态,和不可中断等待态操作系统开始会对每个进程分配一个时间片,当进程里面写了sleep函数,进程由运行到休眠态,但是此时CPU不可能等着。有两种方法,1:根据时间片,CPU自动跳转,2:程序里面自己写能引起CPU调度的代码就可以
- 文件管理:通过文件系统ext2/ext3/ext4 yaff jiffs等来组织管理文件
- 网络管理:通过网络协议栈(OSI,TCP)对数据进程封装和拆解过程(数据发送和接收是通过网卡驱动完成的,网卡驱动不会产生文件(在Linux系统dev下面没有相应的文件),所以不能用open等函数,而是使用的socket)。
- 内存管理:通过内存管理器对用户空间和内核空间内存的申请和释放
- 设备管理: 设备驱动的管理(驱动工程师所对应的)
- 字符设备驱动: (led 鼠标 键盘 lcd touchscreen(触摸屏))
1.按照字节为单位进行访问,顺序访问(有先后顺序去访问)
2.会创建设备文件,open read write close来访问
- 块设备驱动 :(camera u盘 emmc)
1.按照块(512字节)(扇区)来访问,可以顺序访问,可以无序访问
2.会创建设备文件,open read write close来访问
- 网卡设备驱动:(猫)
1.按照网络数据包来收发的。
4.宏内核、微内核
- 宏内核:将进程,网络,文件,设备,内存等功能集成到一个内核中
特点:代码运行效率高。
缺点:如果有一个部分出错整个内核就崩溃了。
eg:ubuntu Android
- 微内核:只将进程,内存机制集成到这个内核中,文件,设备,驱动在操作系统之外。通过API接口让整个系统运行起来。
缺点:效率低 优点:稳定性强(华为手机)
eg:鸿蒙
5.驱动模块(三要素:入口;出口;许可证)
- 入口:资源的申请
- 出口:资源的释放
- 许可证:GPL(写一个模块需要开源,因为Linux系统是开源的,所以需要写许可协议)
#include <linux/init.h>
#include
static int __init hello_init(void)
(__init可以不指定,及可以不写,但是正常是写的)
//__init将hello_init放到.init.text段中
{
return 0;
}
static void __exit hello_exit(void)
//__exit将hello_exit放到.exit.text段中
{
}
module_init(hello_init);
//告诉内核驱动的入口地址(函数名为函数首地址)
module_exit(hello_exit);
//告诉内核驱动的出口地址
MODULE_LICENSE("GPL");
//许可证
- Makefile:
KERNELDIR:= /lib/modules/$(shell uname -r)/build/ //Ubuntu内核的路径
#KERNELDIR:= /home/linux/kernel/kernel-3.4.39/
(板子内核路径)
PWD:=$(shell pwd)//驱动文件的路径
(打开一个终端看终端的路径)
all: //目标
make -C $(KERNELDIR) M=$(PWD) modules
(-C:进入顶层目录)
注:进入内核目录下执行make modules这条命令
如果不指定 M=$(PWD) 会把内核目录下的.c文件编译生成.ko
M=$(PWD) 想编译模块的路径
clean:
make -C $(KERNELDIR) M=$(PWD) clean
obj-m:=hello.o //指定编译模块的名字
追代码
创建索引文件
ctags -R
在终端上
vi -t xxx
在代码中跳转
ctrl + ]
ctrl + t
Ubuntu内核所对应的内核路径
hello.c代码部分:
#include <linux/init.h>
#include <linux/module.h>
//入口函数:申请资源
//存储类型 数据类型 制定存放区域 函数名(形参)
static int __init hello_init(void)
{
return 0;
}
//出口函数:释放资源
static void __exit hello_exit(void)
{
}
//入口
module_init(hello_init);
//出口
module_exit(hello_exit);
//许可证
MODULE_LICENSE("GPL");
Makefile代码部分:
#KERNEL_PATH=/home/hq/kernel/kernel-3.4.39 #开发板内核路径
KERNEL_PATH=/lib/modules/$(shell uname -r)/build
#pc电脑上的路径 gcc
PWD=$(shell pwd)
all:
make -C $(KERNEL_PATH) M=$(PWD) modules
#在内核顶层目录执行make modules才可以将hello.c生成驱动
#-C 路径:找到这个路径执行这个命令
.PHONY:clean
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
obj-m = hello.o
6.命令:
sudo insmod hello.ko 安装驱动模块
sudo rmmod hello 卸载驱动模块
lsmod 查看模块
dmesg 查看消息
sudo dmesg -C 直接清空消息不回显
sudo dmesg -c 回显后清空
7.内核中的打印函数
搜索函数,搜到以后,在里面任意找到一个,看函数原形就OK
printk(打印级别 "内容")
printk(KERN_ERR "Fail%d",a);
printk(KERN_ERR "%s:%s:%d\n",__FILE__,__func__,__LINE__);
(驱动在哪一个文件,哪一个函数,哪一行)
printk("%s:%s:%d\n",__FILE__,__func__,__LINE__);
vi -t KERN_ERR(查看内核打印级别)
include/linux/printk.h
#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 */ (调试级别)
0 ------ 7
最高的 最低的
linux@ubuntu:~$ cat /proc/sys/kernel/printk 4 4 1 7
终端的级别 消息的默认级别 终端的最大级别 终端的最小级别
#define console_loglevel (console_printk[0])
#define default_message_loglevel (console_printk[1])
#define minimum_console_loglevel (console_printk[2])
#define default_console_loglevel (console_printk[3])
只有当消息的级别大于终端级别,消息才会被显示
但对与咱们的这个Ubuntu被开发者修改过来,所有消息不会主动回显。
修改系统默认的级别
su root
echo 4 3 1 7 > /proc/sys/kernel/printk
虚拟机的默认情况:
板子的默认情况:
如果想修改开发板对应的打印级别
vi rootfs/etc/init.d/rcS
echo 4 3 1 7 > /proc/sys/kernel/printk
在rootfs/etc/init.d/rcS里面添加上以后再起板子,板子的级别就为如下:
安装驱动和卸载驱动时,消息会打印。
printk.c代码部分:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/printk.h>
//入口函数-申请资源
static int __init printk_init(void)
{
printk("hello wrold.\n");
printk(KERN_INFO "%s %s %d\n",__FILE__,__func__,__LINE__);//6
printk(KERN_ALERT "---%s %s %d\n",__FILE__,__func__,__LINE__);//1
return 0;
}
//出口函数-释放资源
static void __exit printk_exit(void)
{
printk("welcome to hqyj.\n");
printk(KERN_INFO "%s %s %d\n",__FILE__,__func__,__LINE__);//6
printk(KERN_ALERT "***%s %s %d\n",__FILE__,__func__,__LINE__);//1
}
module_init(printk_init);
module_exit(printk_exit);
MODULE_LICENSE("GPL");
Makefile代码部分:
KERNEL_PATH=/home/hq/kernel/kernel-3.4.39 #开发板内核路径
#KERNEL_PATH=/lib/modules/$(shell uname -r)/build
#pc电脑上的路径 gcc
PWD=$(shell pwd)
all:
make -C $(KERNEL_PATH) M=$(PWD) modules
#在内核顶层目录执行make modules才可以将hello.c生成驱动
#-C 路径:找到这个路径执行这个命令
.PHONY:clean
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
obj-m = 1-printk.o
8.驱动多文件编译
hello.c add.c
Makefile
obj-m:=demo.o
demo-y+=hello.o add.o
(-y作用:将hello.o add.o放到demo.o中)
最终生成demo.ko文件
9.模块传递参数
- 命令传递的方式
sudo insmod demo.ko hello world
---------------------------------------------------------
* Standard types are:
* byte, short, ushort, int, uint, long, ulong (没有找到char!!!!!!!!)
* charp: a character pointer
* bool: a bool, values 0/1, y/n, Y/N.
* invbool: the above, only sense-reversed (N = true).
- module_param(name, type, perm)
功能:接收命令行传递的参数
参数:
@name :变量的名字
@type :变量的类型
@perm :权限 0664 0775(其它用户对我的只有读和执行权限,没有写的权限)
modinfo hello.ko(查看变量情况)
- MODULE_PARM_DESC(_parm, desc)
功能:对变量的功能进行描述
参数:
@_parm:变量
@desc :描述字段
只能传十进制,不可以写十六进制
练习:
1.byte类型如何使用 (传递参数用ascii)
2.如何给一个指针传递一个字符串
sudo insmod hello.ko a=20 b=30 c=65 p="hello_world"
注意:传字符的时候写ASCII码值;传递字符串的时候,不能有空格
- module_param_array(name, type, nump, perm)
功能:接收命令行传递的数组
参数:
@name :数组名
@type :数组的类型
@nump :参数的个数,变量的地址
@perm :权限
sudo insmod hello.ko a=121 b=10 c=65 p="hello" ww=1,2,3,4,5
param.c代码部分:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/moduleparam.h>
char ch = 'A';
module_param(ch, byte, 0770);
MODULE_PARM_DESC(ch, "this is char value.");
short a = 10;
module_param(a, short, 0775);
MODULE_PARM_DESC(a, "this is short value.");
char *p = NULL;
module_param(p, charp, 0774);
MODULE_PARM_DESC(p, "this is char pointer.");
int st[8];
int num;
module_param_array(st,int,&num,0770);
MODULE_PARM_DESC(st,"this is 8 int number.");
//入口函数-申请资源
static int __init printk_init(void)
{
int i;
printk("hello wrold. num=%d\n",num);
printk("ch=%c a=%d p=%s\n", ch, a, p);
printk(KERN_INFO "%s %s %d\n", __FILE__, __func__, __LINE__); //6
printk(KERN_ALERT "---%s %s %d\n", __FILE__, __func__, __LINE__); //1
for(i=0;i<num;i++)
{
printk("%d\n",st[i]);
}
return 0;
}
//出口函数-释放资源
static void __exit printk_exit(void)
{
printk("welcome to hqyj.\n");
printk("ch=%c a=%d p=%s-----\n", ch, a, p);
printk(KERN_INFO "%s %s %d\n", __FILE__, __func__, __LINE__); //6
printk(KERN_ALERT "***%s %s %d\n", __FILE__, __func__, __LINE__); //1
}
module_init(printk_init);
module_exit(printk_exit);
MODULE_LICENSE("GPL");
Makefile代码部分:
#KERNEL_PATH=/home/hq/kernel/kernel-3.4.39 #开发板内核路径
KERNEL_PATH=/lib/modules/$(shell uname -r)/build
#pc电脑上的路径 gcc
PWD=$(shell pwd)
all:
make -C $(KERNEL_PATH) M=$(PWD) modules
#在内核顶层目录执行make modules才可以将hello.c生成驱动
#-C 路径:找到这个路径执行这个命令
.PHONY:clean
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
obj-m = param.o
10. 复习
1.模块
三要素
- 入口
static int __init hello_init(void)
{
return 0;
}
module_init(hello_init);
- 出口
static void __exit hello_exit(void)
{
}
module_exit(hello_exit)
- 许可证
MODULE_LICENSE("GPL");
- 多文件编译
obj-m:=demo.o
demo-y+=hello.o add.o
- 内核中的打印:
printk(打印级别 “打印的内容”);
printk(“打印的内容”);
/proc/sys/kernel/printk
4 4 1 7
出现这个错误,提示,说明scripts下没有生成相应的文件,cd到kernel所在目录,执行: make scripts
然后 make 就可以编译了
11. 字符设备驱动
linux系统中一切皆文件
- 应用层: APP1 APP2 ...
fd = open("led驱动的文件",O_RDWR);
read(fd);
write();
close();
- 内核层:
对灯写一个驱动
led_driver.c
driver_open();
driver_read();
driver_write();
driver_close();
struct file_operations {
int (*open) (struct inode *, struct file *);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*release) (struct inode *, struct file *);(close)
}
cdev:
设备号1 设备号2 设备号n
设备驱动1 设备驱动2 .... 设备驱动n
设备号:32位,无符号数字
高12位 :主设备号 :区分哪一类设备
低20位 :次设备号 :区分同类中哪一个设备
- 硬件层: LED uart ADC PWM
每个驱动里面都有对应的file_operations
- open的过程:
open打开文件,这个文件与底层的驱动的设备号有关系,
通过设备号访问设备驱动中的struct file_operations里面的open函数。
- read的过程:
open函数会有一个返回值,文件描述符fd,read函数通过fd
找到驱动中的struct file_operations里面的read函数。
Led驱动:字符设备 步骤:
- 注册字符设备驱动 - 得到一个字符设备驱动的框架,并且得到设备号
- 确定操作的硬件设备 - led灯(初始化灯)
- 初始化灯(先建立灯实际物理地址和虚拟地址之间的映射)-
基于操作系统开发,操作虚拟内存,
- 用户空间数据拷贝到内核空间数据的交互(用户使用的时候,驱动才会被真正运行,涉及数据交互)
- 在应用层创建一个设备文件(设备节点)
12. 字符设备驱动的注册
- int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
功能:注册一个字符设备驱动
参数:
@major:主设备号
:如果你填写的值大于0,它认为这个就是主设备号
:如果你填写的值为0,操作系统给你分配一个主设备号
@name :名字 cat /proc/devices
(
当注册一个字符设备驱动的时候。
如果成功的话,当你使用cat /proc/devices 命令查看的时候可以看到系统自动分配的主设备号和这个名字
@fops :操作方法结构体
返回值:major>0 ,成功返回0,失败返回错误码(负数) vi -t EIO
major=0,成功主设备号,失败返回错误码(负数)
- void unregister_chrdev(unsigned int major, const char *name)
功能:注销一个字符设备驱动
参数:
@major:主设备号
@name:名字
返回值:无
chrdev.c代码部分:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/fs.h>
#define NAME "chrdev_led"
unsigned int major = 0;
int chrdev_open(struct inode *node_t, struct file *file_t)
{
printk("%s %s %d\n", __FILE__, __func__, __LINE__);
return 0;
}
ssize_t chrdev_read(struct file *file_t, char __user *ubuf, size_t n, loff_t *off_t)
{
printk("%s %s %d\n", __FILE__, __func__, __LINE__);
return 0;
}
ssize_t chrdev_write(struct file *file_t, const char __user *ubuf, size_t n, loff_t *off_t)
{
printk("%s %s %d\n", __FILE__, __func__, __LINE__);
return 0;
}
int chrdev_close(struct inode *node_t, struct file *file_t)
{
printk("%s %s %d\n", __FILE__, __func__, __LINE__);
return 0;
}
struct file_operations fops = {
.open = chrdev_open,
.read = chrdev_read,
.write =chrdev_write,
.release =chrdev_close,
};
//入口函数-申请资源
static int __init printk_init(void)
{
//注册字符设备驱动
major=register_chrdev(major, NAME, &fops);
if(major < 0)
{
printk("register_chrdev err.");
return -EINVAL;
}
return 0;
}
//出口函数-释放资源
static void __exit printk_exit(void)
{
//注销设备驱动
unregister_chrdev(major,NAME);
}
module_init(printk_init);
module_exit(printk_exit);
MODULE_LICENSE("GPL");
user_app.c代码部分:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, const char *argv[])
{
int fd=open("./led",O_RDWR);
char buf[32];
read(fd,buf,sizeof(buf));
write(fd,buf,sizeof(buf));
close(fd);
return 0;
}
Makefile.c代码部分:
#KERNEL_PATH=/home/hq/kernel/kernel-3.4.39 #开发板内核路径
KERNEL_PATH=/lib/modules/$(shell uname -r)/build
#pc电脑上的路径 gcc
PWD=$(shell pwd)
all:
make -C $(KERNEL_PATH) M=$(PWD) modules
#在内核顶层目录执行make modules才可以将hello.c生成驱动
#-C 路径:找到这个路径执行这个命令
.PHONY:clean
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
obj-m = chrdev.o
13. 手动创建设备文件
sudo mknod led (路径是任意) c/b 主设备号 次设备号
sudo –rf led 删除的时候记得加-rf
14. 通过字符设备驱动点亮板子上的led灯
app: test.c char buf[3]
1 0 0
0 1 0
0 0 1
------------------|------------------------
kernel: led_driver.c
-------------------|------------------------
hardware: RGB_led
- 应用程序如何将数据传递给驱动(读写的方向是站在用户的角度来说的)
#include
- int copy_from_user(void *to, const void __user *from, int n)
功能:从用户空间拷贝数据到内核空间(用户需要写数据的时候)
参数:
@to :内核中内存的首地址
@from:用户空间的首地址
@n :拷贝数据的长度(字节)
返回值:成功返回0,失败返回未拷贝的字节的个数
- int copy_to_user(void __user *to, const void *from, int n)
功能:从内核空间拷贝数据到用户空间(用户开始读数据)
参数:
@to :用户空间内存的首地址
@from:内核空间的首地址
@n :拷贝数据的长度(字节)
返回值:成功返回0,失败返回未拷贝的字节的个数
chrdev.c代码:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#define NAME "chrdev_led"
unsigned int major = 0;
char kbuf[32]="welcome to hqyj";
int chrdev_open(struct inode *node_t, struct file *file_t)
{
printk("%s %s %d\n", __FILE__, __func__, __LINE__);
return 0;
}
ssize_t chrdev_read(struct file *file_t, char __user *ubuf, size_t n, loff_t *off_t)
{
printk("%s %s %d\n", __FILE__, __func__, __LINE__);
//将内核空间的数据拷贝到用户空间
if (sizeof(kbuf) < n)
n = sizeof(kbuf);
if (copy_to_user(ubuf, kbuf, n) != 0)
{
printk("copy_to_user err.");
return -EINVAL;
}
return 0;
}
ssize_t chrdev_write(struct file *file_t, const char __user *ubuf, size_t n, loff_t *off_t)
{
printk("%s %s %d\n", __FILE__, __func__, __LINE__);
if (sizeof(kbuf) < n)
n = sizeof(kbuf);
//将用户空间的数据拷贝到内核空间
if (copy_from_user(kbuf, ubuf, n) != 0)
{
printk("copy_from_user err.");
return -EINVAL;
}
printk("kbuf=%s\n", kbuf);
return 0;
}
int chrdev_close(struct inode *node_t, struct file *file_t)
{
printk("%s %s %d\n", __FILE__, __func__, __LINE__);
return 0;
}
struct file_operations fops = {
.open = chrdev_open,
.read = chrdev_read,
.write = chrdev_write,
.release = chrdev_close,
};
//入口函数-申请资源
static int __init printk_init(void)
{
//注册字符设备驱动
major = register_chrdev(major, NAME, &fops);
if (major < 0)
{
printk("register_chrdev err.");
return -EINVAL;
}
return 0;
}
//出口函数-释放资源
static void __exit printk_exit(void)
{
//注销设备驱动
unregister_chrdev(major, NAME);
}
module_init(printk_init);
module_exit(printk_exit);
MODULE_LICENSE("GPL");
user_app.c:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, const char *argv[])
{
int fd = open("./led", O_RDWR);
char buf[32];
char buf_r[32];
fgets(buf, sizeof(buf), stdin); //hello world
read(fd, buf_r, sizeof(buf_r));
printf("buf_r=%s\n", buf_r);
write(fd, buf, sizeof(buf));
read(fd, buf_r, sizeof(buf_r));
printf("buf_r=%s\n", buf_r);
close(fd);
return 0;
}
Makefile.c代码:
#KERNEL_PATH=/home/hq/kernel/kernel-3.4.39 #开发板内核路径
KERNEL_PATH=/lib/modules/$(shell uname -r)/build
#pc电脑上的路径 gcc
PWD=$(shell pwd)
all:
make -C $(KERNEL_PATH) M=$(PWD) modules
#在内核顶层目录执行make modules才可以将hello.c生成驱动
#-C 路径:找到这个路径执行这个命令
.PHONY:clean
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
obj-m = chrdev.o
- 驱动如何操作寄存器
rgb_led灯的寄存器是物理地址,在linux内核启动之后,在使用地址的时候,操作的全是虚拟地址。需要将物理地址转化为虚拟地址。在驱动代码中操作的虚拟地址就相当于操作实际的物理地址。
物理地址<------>虚拟地址
- void * ioremap(phys_addr_t offset, unsigned long size)
(当__iomen告诉编译器,取的时候是一个字节大小)
功能:将物理地址映射成虚拟地址
参数:
@offset :要映射的物理的首地址
@size :大小(字节)(映射是以业为单位,一页为4K,就是当你小于4k的时候映射的区域都为4k)
返回值:成功返回虚拟地址,失败返回NULL((void *)0);
- void iounmap(void *addr)
功能:取消映射
参数:
@addr :虚拟地址
返回值:无
#define ENOMEM 12 /* Out of memory */
15. 点灯
- 软件编程控制硬件的思想:
只需要向控制寄存器中写值或者读值,就可以让我们处理器完成一定的功能。
RGB_led
1》GPIOxOUT:控制引脚输出高低电平
RED_LED--->GPIOA28
GPIOAOUT ---> 0xC001A000
GPIOA28输出高电平:
GPIOAOUT[28] <--写-- 1
GPIOA28输出低电平:
GPIOAOUT[28] <--写-- 0
2》GPIOxOUTENB:控制引脚的输入输出模式
GPIOAOUTENB ---> 0xC001A004
设置GPIOA28引脚为输出模式:
GPIOAOUTENB[28] <--写-- 1
3》GPIOxALTFN:控制引脚功能的选择
GPIOAALTFN1 ---> 0xC001A024
设置GPIOA28引脚为GPIO功能:
GPIOAALTFN1[25:24] <--写-- 0b00
00 = ALT Function0
01 = ALT Function1
10 = ALT Function2
11 = ALT Function3
GPIO引脚功能的选择:每两位控制一个GPIO引脚
red :gpioa28
GPIOXOUT :控制高低电平的 0xC001A000
GPIOxOUTENB:输入输出模式 0xC001A004
GPIOxALTFN1:function寄存器 0xC001A024
(一个寄存器36个字节)
green:gpioe13
0xC001e000
blue :gpiob12
0xC001b000
练习:
1.字符设备驱动实现流水灯(30分钟)
//读,改,写
writel(v,c)
功能:向地址中写一个值
参数:
@ v :写的值
@ c :地址
readl(c)
功能:读一个地址,将地址中的值给返回
参数:
@c :地址
head.h代码:
#ifndef __HEAD_H__
#define __HEAD_H__
#include <asm-generic/ioctl.h>
#define RED_ON _IO('A',1)
#define RED_OFF _IO('A',0)
#endif
chrdev.c代码:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <asm/io.h>
#include <linux/device.h>
#include "head.h"
#define NAME "chrdev_led"
//定义宏表示实际物理首地址
#define RED_BASE 0xc001a000 //#GPIOA28
#define GREE_BASE 0xc001e000 // #GPIOE13
#define BLUE_BASE 0xc001b000 //#GPIOB12
unsigned int major = 0;
char kbuf[32] = "welcome to hqyj";
//定义指针保存映射后的虚拟地址的首地址
unsigned int *red_addr = NULL;
unsigned int *gree_addr = NULL;
unsigned int *blue_addr = NULL;
struct class *cls = NULL;
struct device *dev = NULL;
int chrdev_open(struct inode *node_t, struct file *file_t)
{
printk("%s %s %d\n", __FILE__, __func__, __LINE__);
return 0;
}
/*ssize_t chrdev_read(struct file *file_t, char __user *ubuf, size_t n, loff_t *off_t)
{
printk("%s %s %d\n", __FILE__, __func__, __LINE__);
//将内核空间的数据拷贝到用户空间
if (sizeof(kbuf) < n)
n = sizeof(kbuf);
if (copy_to_user(ubuf, kbuf, n) != 0)
{
printk("copy_to_user err.");
return -EINVAL;
}
return 0;
}
ssize_t chrdev_write(struct file *file_t, const char __user *ubuf, size_t n, loff_t *off_t)
{
printk("%s %s %d\n", __FILE__, __func__, __LINE__);
if (sizeof(kbuf) < n)
n = sizeof(kbuf);
//将用户空间的数据拷贝到内核空间
if (copy_from_user(kbuf, ubuf, n) != 0)
{
printk("copy_from_user err.");
return -EINVAL;
}
printk("kbuf=%s\n", kbuf);
if (kbuf[0] == 1)
{
//红灯亮
*red_addr |= (1 << 28);
}
else if (kbuf[0] == 0)
{
//红灯灭
*red_addr &= (~(1 << 28));
}
if (kbuf[1] == 1)
{
*gree_addr |= (1 << 13);
}
else if (kbuf[1] == 0)
{
*gree_addr &= (~(1 << 13));
}
if (kbuf[2] == 1)
{
*blue_addr |= (1 << 12);
}
else if (kbuf[2] == 0)
{
*blue_addr &= (~(1 << 12));
}
return 0;
}*/
long chrdev_ioctl(struct file *file_t, unsigned int request, unsigned long n)
{
switch(request)
{
case RED_ON:
*red_addr |= (1 << 28);
break;
case RED_OFF:
*red_addr &= (~(1 << 28));
break;
}
return 0;
}
int chrdev_close(struct inode *node_t, struct file *file_t)
{
printk("%s %s %d\n", __FILE__, __func__, __LINE__);
return 0;
}
struct file_operations fops = {
.open = chrdev_open,
// .read = chrdev_read,
// .write = chrdev_write,
.unlocked_ioctl= chrdev_ioctl,
.release = chrdev_close,
};
//入口函数-申请资源
static int __init printk_init(void)
{
//注册字符设备驱动
major = register_chrdev(major, NAME, &fops);
if (major < 0)
{
printk("register_chrdev err.");
return -EINVAL;
}
//初始化灯-引脚功能(GPIO) 输出功能 灭
//1.基于操作系统开发。建立灯物理地址和虚拟地址之间映射
//红灯
red_addr = (unsigned int *)ioremap(RED_BASE, 40);
if (red_addr == NULL)
{
printk("ioremap err.");
return -EINVAL;
}
//gree
gree_addr = (unsigned int *)ioremap(GREE_BASE, 40);
if (gree_addr == NULL)
{
printk("ioremap gree err.");
return -EINVAL;
}
blue_addr = (unsigned int *)ioremap(BLUE_BASE, 40);
if (blue_addr == NULL)
{
printk("ioremap blue err.");
return -EINVAL;
}
//通过虚拟地址操作实际物理地址向对应寄存器写值
//配置引脚GPIO
*(red_addr + 9) &= (~(3 << 24));
*(red_addr + 1) |= (1 << 28); //输出模式
*red_addr &= (~(1 << 28));
*(gree_addr + 8) &= (~(3 << 26));
*(gree_addr + 1) |= (1 << 13);
*gree_addr &= (~(1 << 13));
*(blue_addr + 8) |= (1 << 25);
*(blue_addr + 8) &= (~(1 << 24));
*(blue_addr + 1) |= (1 << 12);
*blue_addr &= (~(1 << 12));
//设置自动创建设备节点
//1.提交目录信息
cls = class_create(THIS_MODULE, NAME);
if (IS_ERR(cls))
{
printk("class_create err.");
return -EINVAL;
}
//2.提交文件信息
dev = device_create(cls, NULL, MKDEV(major, 0), NULL, "led");
if (IS_ERR(dev))
{
printk("class_create err.");
return -EINVAL;
}
return 0;
}
//出口函数-释放资源(先申请的后释放,后申请先释放)
static void __exit printk_exit(void)
{
//销毁创建的设备节点
device_destroy(cls,MKDEV(major,0));
class_destroy(cls);
//取消映射
iounmap(blue_addr);
iounmap(gree_addr);
iounmap(red_addr);
//注销设备驱动
unregister_chrdev(major, NAME);
}
module_init(printk_init);
module_exit(printk_exit);
MODULE_LICENSE("GPL");
user_app.c代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include "head.h"
int main(int argc, const char *argv[])
{
int fd = open("/dev/led", O_RDWR);
while(1)
{
ioctl(fd,RED_ON);
sleep(1);
ioctl(fd,RED_OFF);
sleep(1);
}
close(fd);
return 0;
}
Makefile.c:
KERNEL_PATH=/home/hq/kernel/kernel-3.4.39 #开发板内核路径
#KERNEL_PATH=/lib/modules/$(shell uname -r)/build
#pc电脑上的路径 gcc
PWD=$(shell pwd)
all:
make -C $(KERNEL_PATH) M=$(PWD) modules
#在内核顶层目录执行make modules才可以将hello.c生成驱动
#-C 路径:找到这个路径执行这个命令
.PHONY:clean
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
obj-m = chrdev.o
16. 设备节点创建问题(udev/mdev)
(mknod hello c 243 0,手动创建设备节点hello)
(宏有返回值:为最后一句话执行的结果)
#include
自动创建设备节点:
struct class *cls;
- cls = class_create(owner, name) /void class_destroy(struct class *cls)//销毁
功能:向用户空间提交目录信息(内核目录的创建)
参数:
@owner :THIS_MODULE(看到owner就添THIS_MODULE)
@name :目录名字
返回值:成功返回struct class *指针
失败返回错误码指针 int (-5)
if(IS_ERR(cls)){
return PTR_ERR(cls);(PTR_ERR:把错误码指针转换成错误码)
}
IS_ERR() :返回值为0,不在错误码地址范围,非0,在错误码地址范围
内核从0xffffffff 地址开始往地址减少的方向,预留了4K空间用来作为错误码的地址。
- struct device *device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...)(内核文件的创建),每个文件对应一个外设(硬件设备)
/void device_destroy(struct class *class, dev_t devt)//销毁
功能:向用户空间提交文件信息
参数:
@class :目录名字
@parent:NULL
@devt :设备号 (major<<12 |0 < = > MKDEV(major,0))
@drvdata :NULL
@fmt :文件的名字
返回值:成功返回struct device *指针
失败返回错误码指针 int (-5)
----------------------------------------------------------------
17. ioctl函数
注:用户程序所作的只是通过命令码告诉驱动程序它想做什么,至于怎么解释这些命令和怎么实现这些命令,这都是驱动程序要做的事情。驱动程序提供了对ioctl的支持,用户就可以在用户程序中使用ioctl函数控制设备的I/O通道 // GRERED_ONE_ON BLUE_ON
(功能:input output 的控制)
- user:
#include
- int ioctl(int fd, int request, ...);(RED_ON)
(让点灯的代码变得简洁)
参数:
@fd : 打开文件产生的文件描述符
@request: 请求码(读写|第三个参数传递的字节的个数),
:在sys/ioctl.h中有这个请求码的定义方式。
@... :可写、可不写,如果要写,写一个内存的地址
--------------------------------------------------------
- Kernel:
(在驱动程序中实现的ioctl函数体内,实际上是有一个switch{case}结构,每一个case对应一个命令码,做出一些相应的操作。怎么实现这些操作,这是每一个程序员自己的事情;)
fops:
- long (*unlocked_ioctl) (struct file *file,
unsigned int request, unsigned long args);
对于使用ioctl函数时,主要的就是请求码的设计,请求码主要在sys/ioctl.h文件里面进行了设计。
表示我本次是读还是写的字节的大小;再往下看当调用_IOC的时候怎样把四个域组合在一起的。
一个一个看,鼠标放在_IOC_DIRSHIFT,进行跳转,出现下面的同学
#define _IO(type,nr)
_IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size)
_IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define RDE_LED _IO(type,nr)
这些宏是帮助你完成请求码的封装的。
#define _IOC(dir,type,nr,size) \
(((dir) << _IOC_DIRSHIFT) | \
((type) << _IOC_TYPESHIFT) | \
((nr) << _IOC_NRSHIFT) | \
((size) << _IOC_SIZESHIFT))
dir << 30 | size<<16 | type << 8 | nr << 0
2 14 8 8
方向 大小 类型 序号
(方向:00 01 10 11读写相关,)
(大小:sizeof(变量名))
(类型:组合成一个唯一的不重合的整数,一般传一个字符)
(序号:表示同类型中的第几个,当开灯的时候写0,那关的时候就不写0)。
#define RLED_ON _IOWR('a',0,int)//亮灯
#define RLED_OFF _IOWR('a',1,int) //灭灯
内核中已经使用的命令码的域在如下文档中已经声明了。
vi kernel-3.4.39/Documentation/ioctl$ vi ioctl-number.txt
(2^32次方 = 4G的数字,所以可以使用,内核的想法是:每一个数字代表一个,功能和数字一一对应,但是不一样的驱动使用的时候相同也是可以的)
练习:
- ioctl函数的使用
-
request.h代码: #ifndef __REQUEST_H__ #define __REQUEST_H__ #include <asm-generic/ioctl.h> //命令码的约定,相当于获取唯一的一个key #define RED_type 'A' #define RED_ON _IO(RED_type,1) #define RED_OFF _IO(RED_type,0) #define GREE_type 'B' #define GREE_ON _IO(GREE_type,1) #define GREE_OFF _IO(GREE_type,0) #define BLUE_type 'C' #define BLUE_ON _IO(BLUE_type,1) #define BLUE_OFF _IO(BLUE_type,0) #endif chrdev.c代码: #include <linux/init.h> #include <linux/module.h> #include <linux/printk.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <asm/io.h> #include <linux/device.h> #include "request.h" #define NAME "chrdev_led" //用宏保存物理地址,物理地址不可更改 #define RED_BASE 0xc001a000 //GPIOA28 #define GREE_BASE 0xc001e000 //GPIOE13 #define BLUE_BASE 0xc001b000 //GPIOB12 //定义指针保存映射后的虚拟地址 unsigned int *red_addr = NULL; unsigned int *gree_addr = NULL; unsigned int *blue_addr = NULL; struct class *cls = NULL; struct device *dev = NULL; int major = 0; char kbuf[32]; int ret; int myopen(struct inode *node_t, struct file *file_t) { printk("%s %s %d\n", __FILE__, __func__, __LINE__); return 0; } int myclose(struct inode *inode_t, struct file *file_t) { printk("%s %s %d\n", __FILE__, __func__, __LINE__); return 0; } long myioctl(struct file *file_t, unsigned int request, unsigned long args) { //判断是那个请求完成对应的硬件控制 switch (request) { case RED_ON: *red_addr |= (1 << 28); break; case RED_OFF: *red_addr &= (~(1 << 28)); break; case GREE_ON: *gree_addr |= (1 << 13); break; case GREE_OFF: *gree_addr &= (~(1 << 13)); break; case BLUE_ON: *blue_addr |= (1 << 12); break; case BLUE_OFF: *blue_addr &= (~(1 << 12)); break; } return 0; } //点等法赋值 struct file_operations fop = { .open = myopen, .unlocked_ioctl = myioctl, .release = myclose, }; static int __init hello_init(void) { //注册字符设备驱动 major = register_chrdev(major, NAME, &fop); if (major < 0) { printk("register chrdev error.\n"); return -EINVAL; } //灯 - 操作硬件需要建立虚拟地址和物理地址的映射关系 //红灯 red_addr = (unsigned int *)ioremap(RED_BASE, 40); if (red_addr == NULL) { printk("ioremap red led err.\n"); return -EINVAL; } //绿灯 gree_addr = (unsigned int *)ioremap(GREE_BASE, 40); if (gree_addr == NULL) { printk("ioremap gree led err.\n"); return -EINVAL; } //蓝灯 blue_addr = (unsigned int *)ioremap(BLUE_BASE, 40); if (blue_addr == NULL) { printk("ioremap blue led err.\n"); return -EINVAL; } //初始化灯,全部初始化为关闭状态 *(red_addr + 9) &= (~(3 << 24)); *(red_addr + 1) |= (1 << 28); *red_addr &= (~(1 << 28)); //灭 *(gree_addr + 8) &= (~(3 << 26)); *(gree_addr + 1) |= (1 << 13); *gree_addr &= (~(1 << 13)); *(blue_addr + 8) &= (~(1 << 24)); *(blue_addr + 8) |= (1 << 25); *(blue_addr + 1) |= (1 << 12); *blue_addr &= (~(1 << 12)); //设置自动创建设备节点 //提交目录信息 cls = class_create(THIS_MODULE, NAME); if (IS_ERR(cls)) { printk("class create err.\n"); return -EINVAL; } //提交文件信息 dev = device_create(cls, NULL, MKDEV(major, 0), NULL, NAME); if (IS_ERR(dev)) { printk("device create err.\n"); return -EINVAL; } return 0; } static void __exit hello_exit(void) { device_destroy(cls, MKDEV(major, 0)); class_destroy(cls); //取消映射 iounmap(blue_addr); iounmap(gree_addr); iounmap(red_addr); //注销字符设备驱动 unregister_chrdev(major, NAME); } module_init(hello_init); //入口:申请资源 本质-回调一个自己写的函数 module_exit(hello_exit); //出口:释放资源 MODULE_LICENSE("GPL"); //许可证 :公共许可协议(开源协议) use_app.c代码: #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/ioctl.h> #include "request.h" int main(int argc, const char *argv[]) { int fd=open("/dev/chrdev_led",O_RDWR); if(fd < 0) { perror("open led err."); return -1; } while(1) { ioctl(fd,RED_ON,NULL); sleep(1); ioctl(fd,RED_OFF,NULL); sleep(1); ioctl(fd,GREE_ON,NULL); sleep(1); ioctl(fd,GREE_OFF,NULL); sleep(1); ioctl(fd,BLUE_ON,NULL); sleep(1); ioctl(fd,BLUE_OFF,NULL); sleep(1); } close(fd); return 0; } Makefile代码; KERNELDIR = /home/hq/kernel/kernel-3.4.39 #开发板路径 #KERNELDIR=/lib/modules/$(shell uname -r)/build #pc机 PWD=$(shell pwd) all: make -C $(KERNELDIR) M=$(PWD) modules #基于内核框架将驱动代码编译生成驱动模块 #需要在内核的顶层目录下执行make modules. #-C:指定到那个路径下执行这个命令 #M赋值:要将那个路径下的驱动文件编译生成驱动模块 .PHONY:clean clean: make -C $(KERNELDIR) M=$(PWD) clean obj-m += chrdev.o
18. Linux内核中断
Eg:
ARM里当按下按键的时候,他首先会执行汇编文件start.s里面的异常向量表里面的irq,在irq里面进行一些操作。
再跳转到C的do_irq();
进行操作:1)判断中断的序号;2)处理中断;3)清除中断;
Linux内核实现和ARM逻辑实现中断的原理是一样的。
内核:当按键按下后依然到异常向量表,再到handler_irq函数(写死的),在handler_irq里面定义了一个数组,数组中每个成员里面存放的是结构体,在结构体里面有个函数指针,这个函数指针就指向了咱们自己提交函数的名字;(数组的下标是Linux内核的软中断号,它和硬件中断号之间有个映射关系)。内核实现中断时,在handler_irq函数里面把中断的寄存器都初始化好了,咱们只需要拿到软中断号,绑定我的中断处理函数就可以
- int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
功能:注册中断
参数:
@irq : 软中断号
gpio的软中断号
软中断号 = gpio_to_irq(gpino号);//160--》 0-159
gpiono = m*32+n(n:组内的序号)
m:那一组 A B C D E(5组)
0 1 2 3 4
gpioa28 = 0*32+28
gpiob8 =1*32+8 gpiob16 = 1*32+16
控制器中断号(ADC):
find -name irqs.h(在内核源码中找)
find -name irqs.h ./arch/arm/mach-s5p6818/include/mach/irqs.h
find -name s5p6818_irq.h ./arch/arm/mach-s5p6818/include/mach/s5p6818_irq.h
#define IRQ_PHY_ADC (41 + 32) //IRQ_PHY_ADC软中断号
@handler: 中断的处理函数
irqreturn_t (*irq_handler_t)(int irqno, void *dev);
IRQ_NONE //中断没有处理完成
IRQ_HANDLED //中断正常处理完成
@flags :中断的触发方式
#define IRQF_DISABLED 0x00000020 //快速中断(在处理函数里面写了他,就先处理这个中断)
#define IRQF_SHARED 0x00000080 //共享中断(中断的接口较少,但是器件都想要中断,那管脚需要外接两个,寄存器里面有中断状态标志位,看中断状态标志位有没有置位。一个口不可以链接两个按键,按键没办法区分)
#define IRQF_TRIGGER_RISING 0x00000001(上升沿触发)
#define IRQF_TRIGGER_FALLING 0x00000002(下降沿出发)
#define IRQF_TRIGGER_HIGH 0x00000004
#define IRQF_TRIGGER_LOW 0x00000008
@name :名字 cat /proc/interrupts
@dev :向中断处理函数中传递参数 ,不想传就写为NULL
返回值:成功0,失败返回错误码
- void free_irq(unsigned int irq, void *dev_id)
功能:注销中断
参数:
@irq :软中断号
@dev_id:向中断处理函数中传递的参数,不想传就写为NULL
- Eg:按键所对应的中断号是多少?及找所对应的GPIO;
- 第一步:找底板原理图,找到按键
- 第二步:拷贝网络标号,到核心板
及对应的软中断号为:gpio_to_irq (gpiob8 = 1*32+8);gpio_to_irq (gpiob16 = 1*32+16)
ARRAY_SIZE计算数组里面元素的个数;
- 问题解决方法
[root@farsight]#insmod farsight_irq.ko
[ 21.262000] request irq146 error
insmod: can't insert 'farsight_irq.ko': Device or resource busy
通过 cat /proc/interrupts
146: GPIO nxp-keypad
154: GPIO nxp-keypad
说明中断号已经被占用了
- 解决办法:在内核中将这个驱动删掉
如何确定驱动文件的名字是谁?
grep "nxp-keypad" * -nR
arch/arm/mach-s5p6818/include/mach/devices.h:48:
#define DEV_NAME_KEYPAD "nxp-keypad"
grep "DEV_NAME_KEYPAD" * -nR
drivers/input/keyboard/nxp_io_key.c:324: .name = DEV_NAME_KEYPAD,
驱动文件的名字是nxp_io_key.c
找宏的名字,在Makefine里面知道;
如何从内核中将他去掉?
选项菜单的名字?Kconfig
config KEYBOARD_NXP_KEY
tristate "SLsiAP push Keypad support"
make menuconfig
<>SLsiAP push Keypad support
去掉图形化界面里面的*号后,可以把nxp_io_key.o删除掉,这样再次编译内核的时候就可以看出来nxp_io_key.c是否备编译,如果被编译就有对应的.o生成,如果不被编译,就不会生成nxp_io_key.o文件。
make uImage 重新编译内核
cp arch/arm/boot/uImage ~/tftpboot
重新启动板子;
安装驱动:
然后按键,进行测试;
interrupt.c代码:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#define GPIO_NO(n,m) (n*32+m)
#define GPIONO_B8 GPIO_NO(1,8)
#define GPIONO_B16 GPIO_NO(1,16)
int i;
int gpiono[]={GPIONO_B8,GPIONO_B16};
char *name[]={"interrupt_b8","interrupt_b16"};
//中断处理函数
irqreturn_t handler_irq(int irqno, void *dev)
{
if(irqno==gpio_to_irq(GPIONO_B8))
{
printk(KERN_ERR "++++++++++++++++++++++++++++++++++\n");
}
if(irqno==gpio_to_irq(GPIONO_B16))
{
printk(KERN_ERR "-----------------------------------\n");
}
return IRQ_HANDLED;
}
static int __init interrupt_init(void)
{
//注册中断
for(i=0;i<sizeof(gpiono)/sizeof(int);i++)
{
if(request_irq(gpio_to_irq(gpiono[i]),handler_irq,IRQF_TRIGGER_FALLING,name[i],NULL)!=0)
{
printk("request irq err.");
return -EINVAL;
}
}
return 0;
}
static void __exit interrupt_exit(void)
{
//注册中断
for(i=0;i<sizeof(gpiono)/sizeof(int);i++)
{
free_irq(gpio_to_irq(gpiono[i]),NULL);
}
}
module_init(interrupt_init);
module_exit(interrupt_exit);
MODULE_LICENSE("GPL");
Makefile.c 代码:
KERNEL_PATH=/home/hq/kernel/kernel-3.4.39 #开发板内核路径
#KERNEL_PATH=/lib/modules/$(shell uname -r)/build
#pc电脑上的路径 gcc
PWD=$(shell pwd)
all:
make -C $(KERNEL_PATH) M=$(PWD) modules
#在内核顶层目录执行make modules才可以将hello.c生成驱动
#-C 路径:找到这个路径执行这个命令
.PHONY:clean
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
obj-m = interrupt.o
19. Linux内核定时器
- 定时器的当前时间如何获取?
jiffies:内核时钟节拍数
jiffies是在板子上电这一刻开始计数,只要
板子不断电,这个值一直在增加(64位)。在
驱动代码中直接使用即可。
- 定时器加1代表走了多长时间?
在内核顶层目录下有.config
CONFIG_HZ=1000
周期 = 1/CONFIG_HZ
周期是1ms;
- 分配的对象
struct timer_list mytimer;
- 对象的初始化
struct timer_list {
unsigned long expires; //定时的时间
void (*function)(unsigned long); //定时器的处理函数
unsigned long data; //向定时器处理函数中填写的值
};
void timer_function(unsigned long data) //定时器的处理函数
{
}
mytimer.expries = jiffies + 1000; //1s
mytimer.function = timer_function;
mytimer.data = 0;
init_timer(&mytimer); //内核帮你填充你未填充的对象
- 对象的添加定时器
void add_timer(struct timer_list *timer);
//同一个定时器只能被添加一次,
//在你添加定时器的时候定时器就启动了,只会执行一次
int mod_timer(struct timer_list *timer, unsigned long expires)
//再次启动定时器 jiffies+1000
- 4.对象的删除
int del_timer(struct timer_list *timer)
//删除定时器
Int gpio_get_value(int gpiono);//通过gpiono获取当权gpio的所处状态
返回0,低电平 非0:高电平
interrupt_time.c代码:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/timer.h>
#define GPIO_NO(n,m) (n*32+m)
#define GPIONO_B8 GPIO_NO(1,8)
#define GPIONO_B16 GPIO_NO(1,16)
int i;
int gpiono[]={GPIONO_B8,GPIONO_B16};
char *name[]={"interrupt_b8","interrupt_b16"};
//分配一个定时器对象
struct timer_list mytimer;
//中断处理函数
irqreturn_t handler_irq(int irqno, void *dev)
{
//再次启动定时器
mod_timer(&mytimer,jiffies+120);
return IRQ_HANDLED;
}
//定时处理函数,时间到就调用这个函数
void time_fun(unsigned long data)
{
//1.获取引脚状态 0 - 处理中断
if(gpio_get_value(GPIONO_B8)==0)
{
printk(KERN_ERR "++++++++++++++++++++++++++++++++++\n");
}
if(gpio_get_value(GPIONO_B16)==0)
{
printk(KERN_ERR "----------------------------------\n");
}
}
static int __init interrupt_init(void)
{
//初始化定时器
mytimer.expires=jiffies+120;
mytimer.function=time_fun;
mytimer.data=0;
init_timer(&mytimer);
//添加定时器
add_timer(&mytimer);
//注册中断
for(i=0;i<sizeof(gpiono)/sizeof(int);i++)
{
if(request_irq(gpio_to_irq(gpiono[i]),handler_irq,IRQF_TRIGGER_FALLING,name[i],NULL)!=0)
{
printk("request irq err.");
return -EINVAL;
}
}
return 0;
}
static void __exit interrupt_exit(void)
{
//注册中断
for(i=0;i<sizeof(gpiono)/sizeof(int);i++)
{
free_irq(gpio_to_irq(gpiono[i]),NULL);
}
}
module_init(interrupt_init);
module_exit(interrupt_exit);
MODULE_LICENSE("GPL");
Makefile。c代码:
KERNEL_PATH=/home/hq/kernel/kernel-3.4.39 #开发板内核路径
#KERNEL_PATH=/lib/modules/$(shell uname -r)/build
#pc电脑上的路径 gcc
PWD=$(shell pwd)
all:
make -C $(KERNEL_PATH) M=$(PWD) modules
#在内核顶层目录执行make modules才可以将hello.c生成驱动
#-C 路径:找到这个路径执行这个命令
.PHONY:clean
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
obj-m = interrupt.o
20. 模块导出符号表
思考1:应用层两个app程序,app1中拥有一个add函数,app1运行时app2是否可以调用app1中的add函数? 不行,因为应用层app运行的空间是私有的(0-3G)没有共享。
思考2:两个驱动模块,module1中的函数,module2是否可以调用?可以,他们公用(3-4G)内核空间,只是需要找到函数的地址就可以。好处:减少代码冗余性,代码不会再内存上被重复加载。代码更精简,一些代码可以不用写,直接调用别人写好的函数就可以。
编写驱动代码找到其他驱动中的函数,需要用模块导出符号表将函数导出,被人才可以使用这个函数。他是一个宏函数。
在驱动的一个模块中,向使用另外一个模块中的函数/变量,只需要使用EXPORT_SYMBOL_GPL将变量或者函数的地址给导出。使用者就可以用这个地址来调用它了。
EXPORT_SYMBOL_GPL(sym)
sym:变量名或函数名
代码举例1:两个独立的代码驱动模块
代码举例2:提供者为内核已经安装使用的驱动
moudule1.c代码:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/printk.h>
int number=1000;
int add(int a,int b)
{
return a+b;
}
int sub(int a,int b)
{
return a-b;
}
EXPORT_SYMBOL_GPL(number);
EXPORT_SYMBOL_GPL(add);
EXPORT_SYMBOL_GPL(sub);
static int __init module1_init(void)
{
printk("%s add=%d sub=%d\n",__func__,add(2,3),sub(2,3));
return 0;
}
static void __exit module1_exit(void)
{
printk("module1 exit.\n");
}
module_init(module1_init);
module_exit(module1_exit);
MODULE_LICENSE("GPL");
Makefile.c代码:
#KERNEL_PATH=/home/hq/kernel/kernel-3.4.39 #开发板内核路径
KERNEL_PATH=/lib/modules/$(shell uname -r)/build
#pc电脑上的路径 gcc
PWD=$(shell pwd)
all:
make -C $(KERNEL_PATH) M=$(PWD) modules
#在内核顶层目录执行make modules才可以将hello.c生成驱动
#-C 路径:找到这个路径执行这个命令
.PHONY:clean
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
obj-m = module1.o
Moudle2.c代码:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/printk.h>
extern int number;
extern int add(int a,int b);
extern int sub(int a,int b);
static int __init module2_init(void)
{
printk("%s add=%d sub=%d\n",__func__,add(5,2),sub(5,2));
printk("number=%d\n",number);
return 0;
}
static void __exit module2_exit(void)
{
printk("bye~\n");
}
module_init(module2_init);
module_exit(module2_exit);
MODULE_LICENSE("GPL");
Makefile.c代码:
#KERNEL_PATH=/home/hq/kernel/kernel-3.4.39 #开发板内核路径
KERNEL_PATH=/lib/modules/$(shell uname -r)/build
#pc电脑上的路径 gcc
PWD=$(shell pwd)
all:
make -C $(KERNEL_PATH) M=$(PWD) modules
#在内核顶层目录执行make modules才可以将hello.c生成驱动
#-C 路径:找到这个路径执行这个命令
.PHONY:clean
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
obj-m = module2.o
总结:
编译:
1.先编译提供者,编译完成之后会产生一个Module.symvers
2.将Module.symvers拷贝到调用者的目录下
3.编译调用者即可
安装:
先安装提供者
再安装调用者
卸载:
先卸载调用者
再卸载提供者
如果调用者和提供者时两个独立(xx.ko)驱动模块,他们间传递地址的时候,是通过Module.symvers传递的。
如果提供者是内核的模块(uImage),此时调用者和提供者间就不需要Module.symvers文件传递信息。
总结:
linux底层:arm、linux系统移植、驱动开发
arm-程序运行原理、硬件控制原理
c语言、电路的基础
框架思想、解决问题的办法。
实操-问题多-耐心
理论偏多-枯燥
led灯控:
通过分析电路图,对应引脚输出一个高电平灯亮
引脚-多功能引脚(GPIO)
输入或输出-输出功能
输出高或低电平-输出高电平灯亮 、 低电平灯灭
mrs msr - 修改cpsr
swi 软中断
异常处理:
异常源:reset、swi 、fiq、irq、undef、dataabort、prefetchabort
异常处理过程:(自动完成步骤)
1.复制cpsr到对应模式下spsr保存
2.修改cpsr
1》arm状态
2》切换到对应模式
3》禁止相应中断
3.保存返回地址给lr
4.pc跳转到异常向量表对应异常处理位置。
异常向量表:0x00000000 每个异常预留4byte处理对应异常
是一段固定地址空间,规定0x00000000-0x0000001c
自己写:异常处理函数(恢复过程)
压栈保存现场 stmfd sp!,{r0-r12,lr}
处理过程
出栈恢复现场-cpsr->spsr pc->lr ldmfd sp!,{r0-r12,pc}^
控制硬件的原理:
led -
1.引脚功能选择
2.输入或输出-输出功能
3.输出高或低电平-输出高电平灯亮
GPIOA28-red
GPIOE13-gree
GPIOB12-blue
配置开发板的ip地址:
setenv ipaddr 192.168.1.66
setenv netmask 255.255.255.0
setenv gatewayip 192.168.1.1
setenv serverip 192.168.1.99
saveenv
开发阶段系统部署:
举例:手动验证
uImage放到tftpboot文件中
uboot命令终端:tftp 0x48000000 uImage 下载内核到开发板的内存中
bootm 0x48000000 从开大坂内存中启动程序
设置自启动:
bootcmd - 设置自动下载内核镜像
bootargs - 设置自动挂载跟文件系统
setenv bootcmd tftp 0x48000000 uImage\;bootm 0x48000000
setenv bootargs root=/dev/nfs nfsroot=192.168.1.99:/home/hq/nfs/rootfs rw console=ttySAC0,115200 init=/linuxrc ip=192.168.1.66
saveenv
移植的代码:
1.uboot - 第一段代码(裸机代码)
作用:初始化部分硬件、引导加载内核、给内核传递参数、uboot命令
uboot命令-setenv saveent 设置更改、保存(flash)环境变量的值
ping ip ->检测是否能对应ip通信
loadb ->串口下载代码
go ->启动程序
boot ->自启动bootcmd环境变量的指令
reset ->重启上电
bootm ->启动运行对应内存地址的程序
tftp ->tftp文件服务器下载指令
2.内核-第二段代码
3.跟文件系统-文件操作
移植-分为两种情况:
开发阶段系统部署:
uboot-sd
内核-tftp服务器实时下载到开发板内存中启动运行
跟文件系统-nfs服务器实时挂载到开发板
产品阶段系统部署:
uboot、内核、跟文件系统都需要移植放到开发板的flash(EMMC)
启动-讲三段代码从开发板的flash中读到开大坂的内存中启动运行
1.将三段代码通过tftp服务器传送到开发板内存中,再从内存搬移到
开发板的flash中。
uboot:
tftp 0x42000000 ubootpak.bin
update_mmc 2 2ndboot 0x42000000 0x200 0x78000
uImage:
tftp 0x42000000 uImage
mmc write 0x42000000 0x800 0x4000
根文件:
tftp 0x42000000 ramdisk.img
mmc write 0x42000000 0x20800 0x20800
/*可以不用网络直接启动:
将代码从flash中读到内存中启动
mmc read 0x48000000 0x800 0x4000
mmc read 0x49000000 0x20800 0x20800
bootm 0x48000000 0x49000000
读出来的过程。直接运行跟文件系统启动失败。*/
设置自启动参数:bootcmd bootargs
setenv bootcmd mmc read 0x48000000 0x800 0x4000\;mmc read 0x49000000 0x20800 0x20800\;bootm 0x48000000 0x49000000
setenv bootargs root=/dev/ram rw initrd=0x49000040,0x1000000 rootfstype=ext4 init=/linuxrc console=ttySAC0,115200
saveenv
uboot编译的过程:
准备-配置代码、配置编译工具、修改Makefile
执行编译指令:
1.make clean/distclean 清除
2.make fs6818_config 支持公板
3.make 编译
内核的编译过程:
准备-配置代码、配置编译工具、修改Makefile
指令:
1.make clean/distclean 清除
2.make fs6818_defconfig 支持公板
3.make menuconfig
菜单选项(选择是否需要编译到内核的驱动,是否编译生成模块)
// make
4.make uImage 直接编译生成uImage镜像
三个文件:Makefile Kconfig .config
举例:将led灯的驱动
1》将驱动编译到内核中 - 内核移植成功,直接使用
1.将驱动放到对应文件
2.vi Makefile
obj-$(CONFIG_RGB_LED) += fs6818_led.o
3.vi Kconfig
config RGB_LED
tristate "rgb_led char driver"
help
this is led driver!!!
bool/tristate 两态/三态
bool - y(编译到内核) n(不编译到内核)
tristate - y M(编译生成模块) N
4.回到内核顶层目录执行:
make menuconfig 选择将驱动添加编译到内核
5.make uImage
生成的uImage就有驱动
2》将驱动单独编译生成驱动模块
- 先移植内核启动成功,然后安装驱动,驱动才可以使用
不同步骤:
make menuconfig 选择将驱动单独编译生成模块
make modules -得到驱动模块
fs6818_led.ko
安装驱动的步骤:先移植一个不包括led驱动的内核
将fs6818_led.ko放到挂载的跟文件系统中
移植完成:开发板执行
insmod fs6818_led.ko 安装驱动
lsmod 查看驱动
rmmod fs6818_led 卸载驱动
将驱动编译到内核或编译生成驱动模块各自的优势?
make 目标
Makefile:
目标:依赖
<tab>命令
.PHONY:目标1
目标1:
<tab>命令
= -》递归赋值
:= -》立即赋值
+= -》追加递归赋值
?= -》询问赋值
make工具的特点->根据文件时间戳编译文件
Makefile ->工程文本文件,make唯一读入配置文件
内容是工程的编译过程。
命令的三要素:
命令名称 [选项 ...] [参数] ....
ls -l 文件名
特殊makefile变量:一般情况下程序默认命令规则
CC=编译器
CFLAGS=选项
OBJS=目标
RM=rm -f
-C 路径 ->到指定路径执行对应指令
M=路径 ->到某个路径编译路径下的c程序
变量=$(shell ls) -》将命令执行的结果赋值给一个变量
驱动:
裸机开发和驱动开发都是操作硬件
区别:
内核五大功能:
进程、网络、设备、内存、文件
设备驱动
字符设备驱动:
块设备驱动:
网卡设备驱动:
宏内核、微内核
驱动三要素:
入口:申请资源
出口:释放资源
许可证:GPL
printk(等级 “format”,...);
1-7 高-低
cat /proc/sys/kernel/printk
输出内容:终端等级 消息的默认输出等级 取值范围1-7
printk 输出消息的等级大于终端等级可以在终端回显
多文件编译驱动:
Makefile
obj-m=demo.o
demo-y +=add.o
....
安装驱动通过命令行传参:
insmod xxx.ko 参数列表
char -byte
int - int uint
short -short ushort
char * - charp
module_param(变量名,类型(byte...),权限)//other用户无写权限
MODULE_PARM_DESC(变量名,描述信息)
数组 - module_param_array(数组名,类型,保存个数变量的地址,权限)
***字符设备驱动搭建的过程
字符设备驱动会给应用层提供一个字符设备文件。open read write close
创建驱动的时候会对应分配一个唯一的设备号,通过设备号
创建的设备节点(设备文件)。他们之间是一一对应的关系。
设备号=主设备号+次设备号
搭建字符设备驱动的框架:
注册字符设备驱动-register_chrdev
主设备号 = register_chrdev(主设备号(0-系统自动分配),驱动名称,&fops)
copy_from_user
copy_to_user
主设备号=register_chrdev
初始化硬件-基于操作系统开发,操作的是虚拟内存
物理内存和虚拟内存映射
虚拟地址首地址=ioremap(物理地址,size)
iounmap(虚拟地址)
自动创建设备节点:
cls=class_create(THIS_MODULE,NAME);
dev=device_create(cls,NULL,MKDEV(主设备号,0-次设备号),NULL,"led")