内嵌汇编 ==================================================== 每条汇编指令对应一条机器指令 int main() { int a,b; __asm__ ("mov %0, #5\n mov %1, #6\n" : "=r" (a),"=r" (b): :); 这段语句编译好之后其实不止两条语句,还有一些对变量a,b的取值和赋值。 printf("a = %d\n",a); return 0; } <例子:> __asm__ __volatile__("mov %0, #5\n mov %1, #6\n" : "=r" (a),"=r" (b): :); <格式:> 汇编语句:输出:输入:破坏 __asm__ __volatile__ __volatile__是指不让编译器重新排列和优化我们写的汇编和汇编前后语句的,这个__volatile__和c里面的volatile不一样 <说明:> 汇编语句: 汇编语句用"\n"来分隔 输出部分: 输出,输入都是相对于汇编语句的,比如 "mov %0 %1\n" %1就是输入,%0是输出,所以可以写成: ("mov %0, %1\n" : "=r" (a): "r" (b):); "=r" (a) 输出给变量a,=代表只写, "+r" (a) +代表可读可写 什么都没有就算只读, r代表寄存器,就是把一个寄存器和a绑定 一般情况下在输出部分的东西中"=&r",r前面加个&表示不要把输入部分和输出部分分成同一个寄存器。具体见后面的一个错误。 在输出部分的"=r" (a)和"+r" (a)的区别在于在执行我们的汇编语句之前,会多一条指令,就是把对应变量的值先放到被选中的寄存器中这样相当于对寄存器的初始化 输出部分: 输出部分可以设成可读可写;输入部分只能设成只读。所以如果一个变量又是可读又是可写就要放在输出部分。 破坏部分: __asm__ ("mov r8,#99\n" "mov %0,r8\n" : "=r" (a) : : "r8","memory" ); 只在汇编中指定使用某个寄存器,编译器是没有办法知道这个插入的汇编使用了那些寄存器,所以这是很危险的,因为编译器有可能在加入的汇编前面或后面使用了r8,所以这个寄存器就被神不知鬼不觉地被修改了。这时候就可以在破坏部分加寄存器告诉编译器我们使用了那些寄存器。这样编译器就知道怎么做了。 这个地方还可以用"memory",它的意思就是,在这段内嵌汇编的代码中会修改内存,在汇编后的c的代码在访问内存时就必须从真实内存中拿,防止优化从cache中拿。 __asm__ __volatile__ ("\n":::"memory"); 这条指令就可以形成一个屏障的作用,防止优化,让后面的代码在访问内存时必须从真实内存中取,而不是用cache的。 <一个错误> "add %0, %1, #5\n" "mov %1,#98\n" : "=r" (a) : "r" (5) 结果出服意料,为什么? 因为%0是只写,%1是只读,那么编译器看到这样有可能就把%0和%1都分成了r3,这是没有问题的 r3 <= 5 add r3,r3,#5 但是第二条指令就有问题了,因为前面你没有说%1用在写,所以编译器把98赋给了r3,编译器没有错 mov r3,#98 r3 => a 寄存器 & 内存寻址 ===================================================== S3C2410 RAM (外部存储) +-------------------+ +-------+ |+--r0--+ +-------+|<----->| | |+------+ | ARM || . . |+--r1--+ | || . . |+------+ +-------+| . . |+--r15-+ | +-------+ |+------+ | +-------------------+ <寄存器> 寄存器用来暂时存放内存取来的数据或结果 它不是cache缓存,它是cpu电路的一部分,频率和cpu一致, cache也不一致。 S3C2410就相当于主板加cpu <内存寻址> x86 可以直接mov这种指令来存取ram中的东西 mov eax,[0x50],但是arm上面不行,只能通过特有的取址指令来完成ldr,str,ldm,stm。这是由risc结构决定的.这里的寻址都是指虚拟地址,因为你的程序运行在linux上面,mmu开着的。 int main() { int a[100],i; __asm__ __volatile__ ( "mov r0,#0\n" "mov r1,#0\n" "loop:\n" "str r0,[%0,r1]\n" "add r1,r1,#4\n" "add r0,r0,#1\n" "cmp r0,#99\n" "ble loop\n" : : "r" (a) --->在访问内存的指令ldr,str中,它们需要的是一个地址而不是一个变量,所以我们只需要输入部分就可以了。所以我们在访问内存的时候,要想办法把地址告诉内嵌汇编。 : "r0","r1","memory" ); for(i = 0; i < 100; i++) { printf("%d ",a[i]); if(!(i % 10)) printf("\n"); } printf("\n"); return 0; } 1.[base,offset] 没有offset就是[base],base一定要是寄存器,offset可以是立即数,offset可以是负数#-8 2.[base,offset]! 和上面的差不多,就是base = base + offset.前加 3.[base],offset 和上面的也差不多,但是先取base的地址内容,然后 base = base + offset,后加 还可以在上面3中情况后面加移位。 [base,offset,lsl #2] [base,offset,lsl #2]! [base],offset,lsl #2 int main() { int i; char *src = " hell!"; char des[50] = "Go to"; 情况2一般这样使用 __asm__ __volatile__ ( "loop:\n" "ldrb r0,[%1,#1]!\n" "cmp r0,#0\n" "strb r0,[%0,#1]!\n" "bne loop\n" : : "r" (des+4), "r" (src-1) ---->进去的时候地址减一 : "r0","memory" ); 情况3就比较好用一点 __asm__ __volatile__ ( "loop:\n" "ldrb r0,[%1],#1\n" "cmp r0,#0\n" "strb r0,[%0],#1\n" "bne loop\n" : : "r" (des+5), "r" (src) --->是怎样就怎样 : "r0","memory" ); 在arm中 字节,8bit 半字,16bit 字,32bit(与x86不同16bit) strb -> 8bit strh -> 16bit str -> 32bit arm汇编 ===================================================== add des = op1 + op2 只有op2才可以是立即数 cmp也是,起码有一个寄存器,而cmp实际上就是subs 如果是movs,一般只用在ne或eq的情况。看那个值是否为0 立即数不能大于255!! <移位>,有符号数采用算术右移,无符号数采用逻辑右移!! MOV r0, r1, LSL#2 r0 = r1 << 2,R1的值不变。 MOV R0, #8, LSL#2 --> R1的这个位置一定要是一个寄存器 add r0, r0, r1, asr lsl #2 r0 = r0 + (r1 >> 2) 先移位,再操作 而用c语言写出来的程序有符号数和无符号数的移位,区别就在使用asr还是lsr <除法>, 没有除法,汇编后是调用arm-gcc库里面的__divsi3 <测试> TEQ --> status = op1 EOR op2 只有等于和不等于两个结果,所以后面不能用 bgt这种东西 TST --> status = op1 AND op2 当and结果!=0则清除Z位,当==0则设置Z位. arm模式 & cpsr ===================================================== 7中模式, ==模式的区别: 能否响应中断,访问内存的权限 ==为什么要有模式: 就是分隔权限,为操作系统服务 linux中没有用到fiq和sys。 之所以fiq是快速因为它有自己的从r8~r14的寄存器,比irq保存更少的上下文。 pc始终指向下两条指令。 ==5级流水线,效率高 取址,译码,执行,移位,回写 问题在分支指令,有两条指令被废弃了,而mips则跳转语句的后两条指令没有被废弃, 而是执行,这样编程就比较麻烦,但是运行速度快。 ==cpsr N Z C V I F mode 31 30 29 28 7 6 4 ~ 0 负 零 进 溢 可以通过修改cpsr来切换模式,但是user/sys模式不允许改模式的那几位,但是可以改前面的N,Z,C,V。其他模式可以改模式位 ble感觉上是两个步骤,比较和修改pc,但是实际上是1条机器指令。因为大部分arm汇编都可以加比较条件,这个比较是在取址,译码,执行的时候完成。所以arm指令中的条件和位移是不用白不用,都要占时间 栈 =================================================== 栈,一种存取方式的约定,先进后出 对于存操作来说,栈可以分成: 减空栈 减满栈 增空栈 增满栈 ED FD EA FA --> 按照栈的性质 后减 先减 后加 先加 DA DB IA IB --> 按照操作,和栈没有什么关系 高地址 高地址 高地址 高地址 +-----+ +-----+ +-----+ +-----+ ||||||| ||||||| | | | | +-----+ +-----+ +-----+ +-----+ ||||||| t->||||||| | | | | +-----+ +-----+ +-----+ +-----+ t->| | | | t->| | | | +-----+ +-----+ +-----+ +-----+ | | | | ||||||| t->||||||| +-----+ +-----+ +-----+ +-----+ | | | | ||||||| ||||||| +-----+ +-----+ +-----+ +-----+ 低地址 低地址 低地址 低地址 stm,ldm的低地址对应小编号的寄存器!!!!这是由stm,ldm指令格式决定的。 比如: "stmia %0, {r0,r2,r1}\n" %0对应a[] a[0] = r0; a[1] = r1; a[2] = r2; ed,fd,ea,fa这一套比较难理解,在取值的时候,要想清楚在图形上是属于那种栈的取值情况(记得栈是按存方式命名的) 栈 其他 LDMED LDMIB 预先增加装载 LDMFD LDMIA 过后增加装载 LDMEA LDMDB 预先减少装载 LDMFA LDMDA 过后减少装载 STMFA STMIB 预先增加存储 STMEA STMIA 过后增加存储 STMFD STMDB 预先减少存储 STMED STMDA 过后减少存储 符号,标签 ================================================ 全局变量,lable,函数名都是标签(标签) 在链接过后就没有标签了,只有地址。 汇编可以写在函数外面,函数调用过程(只限于一次调用): __asm__ ( __asm__ ( "abc:\n" "abc:\n" "b test\n" "b test\n" "mov pc,lr\n" ); ); void test() void test() { { printf("aaaaa\n"); printf("aaaaa\n"); } } int main() int main() { ==========> { abc(); ==========> abc(); return 0; return 0; } } 汇编后: 汇编后: abc就相当于一个函数 abc就相当于一个函数 +-> abc: +-> abc: | .......---+ | ....... | ....... | | mov pc,lr + | test: | | test: | | ....... | | ....... | | ....... v | ....... | | mov pc,lr --->飞了,其实没有飞,lr没有改 | mov pc,lr | | main: | main: | | ....... | ....... | | ....... | ....... | | mov lr,pc 0x40 -->pc=0x48,以后 | mov lr,pc | +---- b abc 0x44 就可以返回 +---- b abc | ....... 0x48 .......<--+ 所以有些语句编译器帮我们写好的: mov pc,lr mov lr,pc 还有参数传递,返回值赋值。 ...... 前4个参数放在r0,r1,r2,r3中,返回之在r0里面 后面从第五个参数开始就是放到栈中传递,小号参数放到低位: 比如A()调用了B(p1,p2,p3,p4,p5,p6,p7) B有7个参数,那么 前4个参数放在r0~r3中,函数A需要在自己栈中分配后3个变量空间,跳到B后,B就可以根据sp的当前值找到p4~p7 +-------+ 高地址 | p7 | +-------+ | p6 | +-------+ | p5 | +-------+ 低地址 <-- sp bl == mov lr,pc + b APCS =============================================== 无论你用不用apcs,r15(pc)和cpsr是特殊寄存器,这是硬件规定的, 但是r14(lr),r13(sp),r11(fp),r10(sl)是apcs规定的特殊寄存器,如果 我们的汇编不遵循apcs,我们可以自由使用这些寄存器。 但是因为我们要使用arm-linux-gcc的东西,所以要遵循它,而apcs是arm公司提出来的。 apcs里面的栈是满的栈,所以是先减再用 在栈上一点和x86不同: int test(int a) x86上面a是test中的一个局部变量,它将被放到栈上面,而arm上面是r0 在函数调用过程中我们起码要保护sp,lr,fp 为什么要fp,因为在函数中修改sp,偏移量就不好控制,所以有个fp,永远指向一个地方,这就不会乱了。。 编译器不用r4~r9,所以我们可以随便使用,不用怕调用一个库的函数后被修改!!但是这样自己调用自己的函数还是有问题,而且你用别人的函数也不知道有没有被用,所以最好还是用之前在栈中保存。 r0~r3就是不安全的。这里有一个例子: int main(int argc, char **argv) { char *s = "hello\n"; int a; __asm__ __volatile__ ( "mov r0, %1\n" "mov %0, #5\n" "sub sp, sp, #4\n" "str %0, [sp]\n" "bl printf\n" "ldr %0, [sp]\n" : "=&r" (a) : "r" (s) ); printf("a = %d\n", a); return 0; } 这里a输出的竟然不是5!! 这是因为%0在编译后使用了r3,但是r3是不安全的,在printf中被修改了。 所以这里不单是r0~r3,还要注意%x,因为他们也有可能使用了r0~r3 !!!sp是向下增长的满栈!!!! !!!sp在启动代码里面设置!!! 编译器对寄存器的保存: void test() { int a; a = 1; } test: mov ip, sp -->之所以要先把sp放到ip是因为后面一条指令stmfd 要修改sp的值sp! stmfd sp!, {fp, ip, lr, pc} --->注意编号小的寄存器放低地址 sub fp, ip, #4 -->把fp移到我们自己的空间中,之所以减4因为 我们要32位对齐,如果减1后面就有问题 sub sp, sp, #4 -->一个局部变量 ===>中间怎样修改sp都可以,但不要该fp mov r3, #1 -->a = 1;赋值 str r3, [fp, #-16] sub sp, fp, #12 -->sp现在指向fp了,准备恢复 ldmfd sp, {fp, sp, pc} -->恢复,也是编号小的寄存器放低地址 最后,把lr赋值给pc完成跳转 +-------+ ||||||||| <-原来的sp +-------+ | pc | +-------+ | lr | +-------+ | ip(sp)| +-------+ | fp | +-------+ | a | <-sp (运行时) +-------+ -------------------------- 在main里面如果定义一个int a[0],从汇编的角度看是分了一个4字节空间的 但是如果a[0]定义在一个结构体里面就不会分配空间 可以用这种方式分析c的代码 void test() { char d[0][0][0][0][0][0]; int b; b = *****d; } test: mov ip, sp stmfd sp!, {fp, ip, lr, pc} sub fp, ip, #4 sub sp, sp, #8 sub r3, fp, #16 -->可以看到编译器在看到*的时候并没有真的去取地址 因为小于6颗星,地址都是一样的。这只不过是偏移器在干活。 str r3, [fp, #-20] sub sp, fp, #12 ldmfd sp, {fp, sp, pc} 有几个地方要很注意: 1。是ldr,ldm 还是str,stm要很清楚 2。问清楚自己,你要的是一个地址还是值,如果是地址就直接是fp+xx,如果是值那就要取值[fp+xx] 3. 对sp基本上都是sub,而对于fp究竟是加还是减就要想清楚 arm指令格式 ================================================= 控制 输入1 输入2 | | | | | | | | +------------------+ +-----| CPU | | | +------------------+ | | | | 输出 加/减法器 一个最简单的结构,控制==1做减法,控制==0做加法 所以一个5-4指令为 1 101 100 csic 变长指令 risc 定长指令 arm 32位指令,也就是说对于arm cpu有32根输入线用作指令处理 ~~~~~~~~~~~~~~~~~~~~~~~~~ ====数据处理指令 高 <-----------------------> 低 xxxx000a aaaSnnnn ddddcccc ctttmmmm 寄存器形式 xxxx001a aaaSnnnn ddddrrrr bbbbbbbb 立即数形式 在文件中是从低地址开始(以byte为单位)保存的所以在文件中看到的是: 寄存器形式 xxxx 000a aaaS nnnn dddd cccc cttt mmmm mmmm cttt cccc dddd nnnn aaaS 000a xxxx <立即数表示>: 12位, bbbbbbbb循环右移rrrr*2位得到一个32位立即数,所以不是所有的数字都符合这个规则, 比如bbbbbbbb = 7 rrrr = 2 ==> 0x70000000 这就是为什么+/-255以内的数是安全的. 因为rrrr=0时只看bbbbbbbb 这样就有+/-128了,再通过移位就可以得到+/-255 7 = 0111 循环右移 2*2 = 4 位 伪指令可以帮我们完成这个动作: __asm__ ( "ldr r9, =9999\n" "add r0, r9, #3\n" ); 汇编器实际上做的是把 ldr r9, =9999 这条指令其实是这样做的: ldr r9, NUM -->把num所在地址的内容拿上来 .... NUM: .word 9999 -->把9999放到内存的一个地方 ====分支指令 xxxx101L oooooooo oooooooo oooooooo 24位来表示偏移量。 目的地址 = 当前地址 + 8 + (4 * 偏移量) 目标地址是那条b指令 + 8是因为pc总在当前指令的后两条 4*偏移量,每条指令32位, 它是有符号数,所以实际上是23位,就是8M,而每条指令有4bytes,所以可以上下跳32M。 GAS ================================================= 每个.o都有一导出符号表 as 只能把一个.S编译成一个.o A.c B.c main() { test() { test(); } } | | | | | | | | v v A.o B.o 导出符号表 导出符号表 main test 导入符号表 | test | | | `------------+-------------' | 和crtxx.o链接在一起 | 而crtxx.o的导入符号有main | 没有导出符号,所以编译好 | 后就没有导出符号和导入符号了。。 v /usr/local/arm/3.4.1/arm-linux/lib/ crt1.o,crti.o,crtn.o 可执行文件 c中的函数名,全局变量等总是被导出,但是汇编就不是. 在汇编里面用.global添加导出符号。 .global main main: @ save context mov ip, sp stmfd sp!,{fp,ip,lr,pc} sub fp, ip, #4 @ one value sub sp, sp, #4 @ fp - 16 ldr r0, =str -->伪指令 bl printf str r0, [fp, #-16] ldr r0, [fp, #-16] @ restore context sub sp, fp, #12 ldmfd sp, {fp,sp,pc} @1110 1000 1001 1101 1010 1000 0000 0000 .word 0xe89da800 str: .byte 'G' .byte 'o' .byte '\n' .byte 0 =======地址对齐 如果要取一个字节,那么你的地址要能被1整除 如果要取两个字节,那么你的地址要能被2整除 不存在取3个字节 如果要取4个字节,那么你的地址要能被4整除 linux在操作系统层中可以处理: | | | | | +-+-+-+-+ |5|6|7|8| +-+-+-+-+ |1|2|3|4| <---sp +-+-+-+-+ 低 高 两个byte虽然"23"没有按2对齐,但是linux可以处理, 4个byte虽然"3456"没有按4对齐,但是linux可以处理, struct { int a; char b; int c; } __attribute__((packed)) d; 在紧缩后,c的地址就是不对齐的,但是,程序可以正常跑,为什么? 因为编译器把4个字节分开了,用ldrb来取,取完后用移位把4个东西组合成一个整形。 在汇编中为了防止不对齐而造成的问题可以用 .align 2 --> 2^2按照4对齐 .byte .short .word .ascii "hello world\n\0" -->不带尾0 .asciz "hello world\n" -->带尾0 全局变量: .section .data .global str --->记得要声明到导出符号表中 str --->但是要千万记住,这相当于一个数组名,因为这是个地址 .asciz "hello" .align 2 || vv -------case 1--------正确 ------- -------- extern char str[10] int main() { printf("%s\n",str); } main: mov ip, sp stmfd sp!, {fp, ip, lr, pc} sub fp, ip, #4 ldr r0, .L2 ----------->拿到str直接传给printf bl printf mov r3, #0 mov r0, r3 ldmfd sp, {fp, sp, pc} .L3: .align 2 .L2: .word str ---------->把str这个地址方在内存中,对于大立即数只能 .size main, .-main 够这样做了。 .ident "GCC: (GNU) 3.4.1" -------case 2--------错误, ------- -------- extern char *str; int main() { printf("%s\n",str); } main: mov ip, sp stmfd sp!, {fp, ip, lr, pc} sub fp, ip, #4 ldr r3, .L2 ---------->把str的地址取出来。 ldr r0, [r3, #0] ---->问题在于,str被声明为指针,因为指针 bl printf 内容才是地址,所以先去取str的内容作为地址。 mov r3, #0 把hell作为一个地址,那肯定错了。。 mov r0, r3 ldmfd sp, {fp, sp, pc} .L3: .align 2 .L2: .word str -------------->一样是把str放在内存中 .size main, .-main .ident "GCC: (GNU) 3.4.1" 一般的c语言中,int b; b = 5; 其实是 *(&b) = 5,在汇编中对于内存我们只能看到地址。 那就是说数组也是一个符号,那为什么sizeof()可以得到它的长度呢? 因为sizeof是在编译时得到的,编译器把它替换成一个常数。 为什么要有段? 因为有了段就可以把数据分类,控制一个程序的不同部分分别放在不同的地方。 比如,我们的代码段放在rom中,只读的;而数据段放在sdram中可以修改。 .setction .text execX()函数在装载是以段为单位,它会一次过把所有段都放到内存,而段在执行是有没有什么关系?是没有的,段只是在execX()装载时起到装载单位的效果.mmap的一个单位。 动态库,静态库 =============================================== /lib/ld-linux.so.2 系统的动态库加载器,这其实是一个可执行文件,它要在板子上,因为它是执行时用到的 在一个目录里面,如果既有动态库,又有静态库,优先链接动态库 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ |在运行时告诉可执行文件动态库在那里有两种方法:| +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ >1.指定LD_LIBRARY_PATH环境变量 LD_LIBRARY_PATH=. ./main 为main设置库的路径。一个正常的命令应该是: <环境变量列表> <命令> <参数> >2.修改配置文件 可以修改 /etc/ld.so.conf,把新库的路径加在这里面 +~~~~~~~~+ |手动链接| +~~~~~~~~+ arm-linux-ld -dynamic-linker /lib/ld-linux.so.2 abc.o test.o /usr/local/arm/3.4.1/arm-linux/lib/crt*.o -lc -o abc -dynamic-linker --> 告诉可执行文件,如果需要动态库,就用这个加载器来加载。这是在板子的角度的路径,在可执行文件里面有这么一句话:/lib/ld-linux.so.2可以用vi看到。 /usr/local/arm/3.4.1/arm-linux/lib/crt*.o 程序启动是从_start开始的,而_start在crt*.o里面,而且它们还会设置sp等环境,所以我们要链接它们。 -lc c库 +~~~~~~~~~~~~~~~~+ |动态库的两个地址| +~~~~~~~~~~~~~~~~+ 在编译时的-L只是告诉可执行文件在那里链接动态库。 而在执行文件前面的LD_LIBRARY_PATH=. 是执行时让动态库加载器在那里找。 +~~~~~~~~~~~~~~+ |自己的一个例子| +~~~~~~~~~~~~~~+ --main.c-- int main() { fun("go to hell\n"); return 0; } --fun.c-- #include int fun(char * str) { printf("got it:%s\n",str); return 0; } arm-linux-gcc -c main.c -o main.o arm-linux-gcc -shared -fPIC fun.c -o libfun.so arm-linux-ld -dynamic-linker /lib/ld-linux.so.2 main.o /usr/local/arm/3.4.1/arm-linux/lib/crt*.o -lc -lfun -L. -o mm 链接脚本 =============================================== 为什么要链接脚本? 它可以修改每个段加载到内存的地址!!想象一下uboot这种东西,它要在物理内存的高地址 可以安排中间文件那些内容放到目标文件中的那个段中,还可以自己定义段。 一般gcc编出来的东西不能在裸机上运行,原因: 1. 没有文件管理 2. elf,需要解析器 3. 地址不对,链接问题,需要链接脚本指定内存地址0x30000000 ~ 0x34000000 2410上面sdram只能在这个地址上面 个人认为连接器和连接脚本的作用是一定程度上决定程序运行时的物理地址,如果是ELF格式,这个地址就会让ELF的解析器去把程序load到这个地址。另外一方面程序里面利用符号的寻址不可能都用pc+偏移这种方式来完成,连接器+连接脚本就像定义了一个绝对的地址-连接地址,它假设程序运行的起始地址,最后把符号都变成连接地址。 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ |运行在uboot/裸机上的程序制作 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ ---------------链接脚本---------------- vim linker.lds -------------- OUTPUT_FORMAT("elf32-littlearm","elf32-littlearm","elf32-littlearm") >>指定输出文件格式,这里有3个格式,什么意思? >>arm-linux-ld -EL/-EB 连接器可以使用-EL/-BL指定是大头小头,-EL对应括号中第一个格式,-EB对应第二个,不带这个参数时对应第三个 OUTPUT_ARCH(arm) >>体系结构,可以忽略,因为本来连接器就是arm的 ENTRY(_start) >>ENTRY只是给elf格式用的 ,指定程序从那个标签开始这个程序, >>而在uboot上或在裸机上的二进制文件都是从代码段的开始位置开始执行。 SECTIONS { .text 0x30000000 : { --> 开始地址,要注意:号前后空格不能少 *(.text) --> 只要代码段,*是所有文件,也可以指定文件名 } . = ALIGN(4); --> 对齐,这个地方的参数是字节数4,等号前后的空格也不能少 修改当前位置,那样下面的代码段就可以从一个对齐的地址开始 .data : { *(.data) } } -------------编译,工具----------------- arm-linux-as 1st.S -o 1st.o arm-linux-ld -T linker.lds 1st.o -o 1st >>-T使用自己的链接脚本 arm-linux-objcopy -I elf32-littlearm -O binary 1st 1st.bin >>objcopy在这里的作用主要是去掉关于elf的包装,让目标只剩下二进制指令。 cp 1st.bin /tftpboot/ ---------------执行-------------------- u-boot(armzone)=> tftp 30000000 1st.bin u-boot(armzone)=> go 30000000 >>go命令把一个地址强制转换成一个函数,然后直接调用。common/cmd_boot.c >>但是要注意,我们提供的这个函数要符合apcs才能够正常返回 ## Starting application at 0x30000000 ... ## Application terminated, rc = 0x1 u-boot(armzone)=> +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ |在自己的程序中调用uboot的函数| +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ --------------找地址------------------ uboot目录下面在编译完后会有一个System.map,它会告诉我们uboot的每个函数地址 比如: 33f8c668 T printf --------------汇编------------------- .section .text .global _start _start: --->注意,把入口函数放在代码段的最上面,因为在 @save context 裸机里面只会从代码段的开始地址执行。 mov ip, sp stmfd sp!,{fp,ip,lr,pc} sub fp, ip, #4 @call printf ldr r0, =FMT ldr r1, printf ---->这里我们不能再使用bl命令了,因为如果现在printf这个地址的内容才是我们要的地址 mov lr, pc 所以我么要先ldr上来,再修改pc。这里另外一个问题是mov lr,pc要在mov pc,r1刚好上面一句 mov pc, r1 如果ldr r1, printf和mov lr, pc交换的话pc的值就会有问题,指向了mov pc,r1 @restore context sub sp, fp, #12 ldmfd sp, {fp,sp,pc} printf: ------->现在我们知道printf在内存里面的地址了, .word 0x33f8c668 通过这种方法存放地址 FMT: .asciz "as,ld,objcpy-go go go!!\n" 编译过程和前面的一样。 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ | 其他 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ 默认链接脚本可以通 arm-linux-ld --verbose 看到 链接脚本是控制可执行文件的,而不是中间文件. 汇编中的.section .align等等都是只对中间文件有效(本文件有效). 在操作系统里面和在uboot里面执行程序最大的区别在于对内存的访问方式。 uboot中是直接访问内存,而在linux中,即操作系统起来后cpu是通过mmu来访问内存的。其他的都差不多,pc也是4个字节那样前进,不过在uboot中就是物理地址,在linux中就是虚拟地址。因为有mmu所以才有虚拟地址,有mmu才会有缺页异常。 uboot中go命令bug:common/cmd_boot.c 105~107注释掉 // setup_linux_param(0x30000100); // call_linux(0,0x0c1,0x30008000); // printf("ok\n"); elf文件开头是elf的格式,由操作系统execX来解析,在裸机上不需要也不能有,因为裸机要的是直接的机器指令。 elf头部记录了链接脚本中指定的地址信息,在加载时linux会看这个地址,然后加到指定位置 elf,coft,a.out,pe(windows) .o有的段一定要在链接脚本中着落,如果一个.o的段在链接脚本没有指定放那里,不如然出错。 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ |定址,链接脚本的地址,下到板上的地址| +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ 在uboot中tftp把一个二进制文件放在某个地址再用go命令跳到那里,二进制文件里面的都是机器指令,没有像elf中那种地址信息为什么我们还要去写编译脚本?哪这个地址和链接脚本中指定的地址有什么关系呢? 链接脚本的地址有什么作用呢?看下面一个例子: 1 .section .text 2 .global _start 3 _start: 4 @save context 5 mov ip, sp 6 stmfd sp!,{fp,ip,lr,pc} 7 sub fp, ip, #4 8 9 @call printf 10 ldr r0, LL --> ldr r0, =FMT 11 ldr r1, printf 12 mov lr, pc 13 mov pc, r1 14 15 @restore context 16 sub sp, fp, #12 17 ldmfd sp, {fp,sp,pc} 18 printf: 19 .word 0x33f8c668 20 LL: 21 .word FMT 22 FMT: 23 .asciz "as\n" 这段程序所做的事情是利用uboot中的printf函数打印一句话。 这是一般伪指令ldr的做法,先把FMT的地址放到一个4字节中,然后把它ldr上来。这里有个问题FMT的地址是多少呢?不知道,知道的只是当程序运行到第10行时它的地址是pc+#20,因为LL是汇编器自己搞出来的,所以肯定可以通过pc寻址.那这个地址是在那里决定的呢?是在链接时决定的(定址),链接器通过链接脚本得知代码段的起始地址,然后加上一些偏移得到一个绝对的地址 现在是3000002c,这样在二进制文件里面就会有这么四字节带了一个地址信息。这是链接脚本地址对目标二进制文件的一个影响。 如果这时候你把程序放到0x3200 0000上面,在运行到ldr那句话是因为地址已经写死在二进制代码中,所以printf还是去0x3000 002c去找字符串,但是那个地址并不是一个字符串,就会出错。符号都是在连接时才决定的. 30000000 <_start>: 30000000: e1a0c00d mov ip, sp 30000004: e92dd800 stmdb sp!, {fp, ip, lr, pc} 30000008: e24cb004 sub fp, ip, #4 ; 0x4 3000000c: e59f0014 ldr r0, [pc, #20] ; 30000028 30000010: e59f100c ldr r1, [pc, #12] ; 30000024 30000014: e1a0e00f mov lr, pc 30000018: e1a0f001 mov pc, r1 3000001c: e24bd00c sub sp, fp, #12 ; 0xc 30000020: e89da800 ldmia sp, {fp, sp, pc} 30000024 : 30000024: 33f8c668 mvnccs ip, #109051904 ; 0x6800000 30000028 : 30000028: 3000002c andcc r0, r0, ip, lsr #32 3000002c : 3000002c: 000a7361 andeq r7, sl, r1, ror #6 这有两种方式可以用来取得一个标签的地址,其一伪指令ldr,他实际上是转换成上面两条汇编: 另外一种方法就是伪指令adr,和伪指令ldr表面上差不多,但是adr是使用相对于pc地址的相对地址,所以二进制文件中就不会代绝对地址,这样基本上脱离链接脚本。 ldr,adr区别 -------------------- ldr r0,=str -->实际转成 ldr r0, num ldr r0, [pc, #20] ...... 实际上反汇编是--> ... num: num: .word str .word str num的地址是确定的可以通过pc的偏移来标识 adr r0,str -->实际转成 add r0,pc,#8 adr虽然好用,但是也有限制: 1.就是立即数的构造规律限制了不是所有的地址都可以表示。 2.不能跨越段前缀。而这也是我们需要伪指令ldr的地方。 b跳转指令是根据pc和偏移量来跳转,而不是根据连接地址来跳的,所以它和地址无关。 xxxx101L oooooooo oooooooo oooooooo 用后24位来标记这个偏移量。 对b指令方汇编后之能看到连接地址,不要以为它是根据连接地址寻址的。 30000020: ea000004 b 30000038 应该根据指令内容来计算偏移pc多少。 +~~~~~~~~~~~~+ | 跨越段前缀 | +~~~~~~~~~~~~+ 跨越段前缀的意思是 .section .text ldr r0, =str mov r1, r0 .setction .data str: asciz "haha hoho\n" 在一个段里面使用另外一个段的内容。 有一个问题:是不是不管链接脚本怎样写(代码段在0x30000000,数据段在0x32000000),在二进制文件中段都是一个紧接着一个的呢? 不是的,objcopy是严格按照ELF里面记录的地址信息(链接脚本里面的位置)去填充中间的空位,所以如果把代码段定在3000 0000,数据段放3200 0000.这样objcopy出来的bin就很大,elf很小,因为elf可以记录段的开始地址。但是bin文件不行,因为它里面不会保存地址信息,所以objcopy只能填充东西来保证data段在32000000。 所以如果跨越段来引用另外一个东西,是不可能根据pc加偏移来找到所有标签的,因为你根本不知道另外一个段会在那里。adr就有问题了。 二进制文件在这种情况下可以通过,分段处理的方法来处理: 可以分开把段造出来,分段拷贝到一个指定的地址 arm-linux-objcopy -j .text -I elf32-littlearm -O binary 3rd text.bin -j 可以指定复制某个指定的段 在编译好text.bin,data.bin后分别把他们放在对应的地址就可以了。 arm-linux-objdump -d(只反汇编出代码段) -D(把所有段都反汇编出来) +~~~~~~~~~~~~~~~+ | 链接脚本 其他 | +~~~~~~~~~~~~~~~+ >>>链接脚本中定义标签,这些标签都是常量,代码中可以: ldr r0,=data_start ldr r1,=data_end c里面也可以使用,就当它是一个常量就可以了。 链接脚本中: SECTIONS { .text 0x30000000 : { *(.text) } . = ALIGN(4); data_start = .; .data : { *(.data) } data_end = .; } 在命令行中指定每个段的地址: arm-linux-ld -T3rd.lds -Ttext=0x40000000 -Tdata=0x... 3rd.o -o 3rd >>>多个文件a.o,b.o,在链接脚本中 SECTIONS { .text 0x30000000 : { *(.text) } 那究竟那个文件的机器代码放在前面呢? 只有指定某个文件 SECTIONS { .text 0x30000000 : { a.o(.text) *(.text) } uboot 的连接: 很多编译相关的选项都在config.mk里面定义。 从config.mk中可以看到如果用户没有定义,它是会从板子那个目录去找一个叫u-boot.lds的连接脚本。 LDSCRIPT := $(TOPDIR)/board/$(BOARDDIR)/u-boot.lds 但是看到这个u-boot.lds里面,uboot是从0开始连接的 board/smdk2410/u-boot.lds SECTIONS { . = 0x00000000; . = ALIGN(4); .text : { 但是uboot明明是运行在0x33f80000这个地址的,为什么从0开始连接呢? 再看config.mk中的连接选项: LDFLAGS += -Bstatic -T $(LDSCRIPT) -Ttext $(TEXT_BASE) $(PLATFORM_LDFLAGS) uboot的text段是手动指定连接地址的。而这个TEXT_BASE是在板子目录下面的config.mk中定义的 board/smdk2410/config.mk TEXT_BASE = 0x33F80000 最后在Makefile中使用了这个连接选项 +~~~~~~~~~~~~~~~~~~~~~~+ | lma,vma | +~~~~~~~~~~~~~~~~~~~~~~+ ------------------------------------ [root@localhost printf]# arm-linux-objdump -h printf -h选项可以看到二进制文件中有什么段 printf: file format elf32-littlearm Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000030 30000000 30000000 00008000 2**0 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 .glue_7 00000000 30000030 30000030 00008030 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 2 .glue_7t 00000000 30000030 30000030 00008030 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 3 .data 00000000 32000000 32000000 00010000 2**0 CONTENTS, ALLOC, LOAD, DATA 4 .bss 00000000 32000000 32000000 00010000 2**0 ALLOC vma- virtual address 在那里运行代码 lma- local address 代码是从那里来的 lma的应用: +----+ | | rom/nor flash | | | | | | +----+ | | ram | | | | | | | | | | | | | | | | | | +----+ 二进制文件放到一个rom中不让别人去改,但是代码段就有问题了,代码段是要可以修改的,所以代码段要搬到另外一个地方。 现在是我们怎样修改链接脚本,让vma和lma不一样? AT来改lma .text 0x30000000 : AT (0x50000000) { } 我们以前一直指定的是vma 原来情况是把30000000的内容放到30000000上面去运行。 如果修改了AT后,linux会把0x50000000地址上面的内容搬到0x30000000上面来运行,但这是elf用到的,二进制文件与这没有关系 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ | 汇编 其他 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ >>>>>>汇编中的宏 1. 常数宏 .equ A, 4 不占地方 .equ A, _start标签, .equ A, 4 - 5 2. 带代码/带参数宏 .macro function_start mov ip,sp stmfd sp!, {fp, ip, lr, pc} sub fp,ip,#4 .endm 只是用到的地方才占空间,所以尽管它定义在开头也不是程序的开端。 .macro swap, reg1, reg2 mov r0, \reg1 ------------->转义一下 mov \reg1, \reg2 mov \reg2, r0 .endm 调用: swap r0, r1 宏里面的标签: .macro build, arch printf_\arch: ------->直接就粘在一起了,不像c语言要用## mov r0,r0 mov r1,r1 .endm 调用: build arm >>>>>>汇编中的.bss段和.data段 int a[1000] = {1,2,3}; 这样a就会在.data段中.data段会在磁盘文件中保存,因为需要空间放他们的值 int b[1000]; 这样b就放在.bss段中.bss不会存放在文件中,在程序load进内存时,在内存中开辟空间 ram +------+ | | disk | | +------+ | | | | +------+ | | |.bss | | | +------+ +------+ |.data | <-----------> |.data | +------+ +------+ |.text | <-----------> |.text | +------+ +------+ 在汇编中定义bss中的数据: .comm str, 1000, 4 --->空间放在.bss段中 1000是总长度,4是对齐方式 用法和标签一样, 另外两个关键字:.space/.fill .space 和.word一样,不过它可以指定空间大小,内容填0 ll: .space 1000 --> 空间就在这里开始打后1000个字节 .fill 和 .space 一样 >>>>>内嵌汇编的变量可以起别名: 这样比较方便,在新添加一个输入,输出是不用修改原来的编号 __asm__ __volatile ( "mov r0, %[a]\n" : [a] "=&r" (a), [c] "=&r" (c) : [b] "r" (b) ); >>>>>数字标签 1: stmfd sp!, {fp,ip,lr,pc} 2: sub fp,ip,#4 b 1b --> 对应数字标签我们可以用1b/1f,分别表示前面那个1号标签还是后面那个 add r0,r0,1f b = backward前面, f = foreward后面 2: sub sp,fp,#12 1: ldmfd sp, {fp,sp,pc} 1: ...... 1: ..... b 1b --->如果前面或后面有多个1号,那1b指的是最靠近的那个。 1: .... 1: b 1b --->在自己身上是最靠近的 uboot ============================================== 编译过程 1. 清除中间文件 对应当初的make make clean 2. 清除configure是产生的makefile 对应当初的configure(这里对应的是make smdk2410_config) make distclean 3. Makefile 里面有个错误,找linux_yu 4. examples/Makefile 中找srec 把62行注释到这两句 #SREC = hello_world.bin #BIN = hello_world.bin hello_world 5. 安装2.95.3的编译器,这是uboot需要的编译器, 这个版本的u-boot使用这个版本的编译器 可以在顶层Makefile中指定。 tar -xvf cross-2.95.3.tar.bz2 -C /usr/local/arm/ 这是uboot的makefile中决定的。 6. make smdk2410_config -->相当于configure 7. 如果是需要从nand flash中启动,那还需要打一个patch -p1 < ../u-boot-lfc.patch lfc的补丁,这个补丁还会自动修改Makefile的中编译器的版本为2.95.3 8. make wince(bsp) VS linux(bootloader) --------------------------------------------- win mobile --> 包含很多应用,包含核 win ce --> 核 wince | linux | | | ------|-|----+ v v |------- bsp |bootloader 可以说bsp的功能比bootloader还要强一些,和在pc上一样, window在启动之后还会去条用bios中的功能,而linux在启动之后 就不在依赖bios了。 board/ 关于板的 common/ 公共命令 cpu/ cpu代码 disk/doc disk on chip,把一个nand flash通过ide来访问 rtc/ 时钟 tool/ 是在pc上使用的工具如mkimage 制作uImage +~~~~~~~~~~~~~~~~~+ | u-boot 移植概况 | +~~~~~~~~~~~~~~~~~+ 1. 更改u-boot顶层目录下的Makefile, 复制 smdk2400_config : unconfig @./mkconfig $(@:_config=) arm arm920t smdk2400 NULL s3c24x0 为: myboard_config : unconfig @./mkconfig $(@:_config=) arm arm920t myboard NULL mycpu   这样,就创建了一个新的配置选项,cpu为mycpu, 板子为myboard, 体系架构为arm, IP Core 为arm920t 2. 进入到 cpu/arm920t下面, 复制s3c24x0 为 mycpu; 这样,就创建了mycpu的代码目录,以后对cpu相关的更改就在这里面; 3. 进入到 board/下面, 复制smdk2410目录为myboard目录; 这样,就创建了myboard的代码目录,以后对板子代码的更改就在这里面; 4. 进入到include/configs下面,复制smdk2410.h 为myboard.h; 这样,就创建了这块板子的配置文件(如内存多大,直始地址多少,开不开mmu,命令提示符是什么样子的);以后对板子配置的更改就在这里; 5. 先执行make clean, 清除以前编译产生的中间文件; 再执行make distclean, 清除以前的编译配置; 再执行 make myboard_config, 这样,就会创建相应的Makefile和编译配置文件; 再执行make,就能够产生最终的u-boot.bin. 如果在make的过程中,发现有关于hello_word.srec的错误,请更改 examples/Makefile中的hello_world.srec为hello_world.o; hello_world.bin 为hello_world.o 对于uboot使用的编译器可以在根下面的Makefile看到 +--------------------------------------------+ | +----------------+ | | | +----------+ | | | | | ARM920t | | | | | | | | | | | | | | | | | +----------+ | | | | | | | | s3c2410 | | | +----------------+ | | smdk2410 | +--------------------------------------------+ ARM920t 对应目录cpu/arm920t/ s3c2410 对应目录cpu/arm920t/s3c24x0/ smdk2410对应目录board/smdk2410/ +~~~~~~~~~~~~~~~~~+ | 补丁制作和加载 | +~~~~~~~~~~~~~~~~~+ 1. 单文件补丁 a. 制作补丁 diff -uN old.txt new.txt > my.patch -u产生标准patch命令认识格式 -N记录增删文件情况 b. 升级old.txt, 请确保当前目录下只有old.txt: patch -p0 < my.patch c. 降级new.txt, 请确保当前目录下只有new.txt: patch -p0 -R < my.patch -R反操作 -p0在但前目录寻找 2. 目录补丁 a. 制作补丁 diff -ruN old/ new/ > my.patch b. 升级old目录,先进入到old目录下,然后执行: patch -p1 < ../my.patch -p1忽略第一级目录 c. 降级new目录, 先进入到new目录下, 然后执行: patch -p1 -R < ../my.patch 电路图可以分为: 原理图和pcb布线图 n reset 低电频有效 不一定电压就是1,是相对的,人定义的 -12 ---> 1 | 12 ---> 0 总线: =========================================================== 总线按照传输的东西可以分成: 数据 bus 指令 bus 地址 bus 总线就好比各种交通 公路,航空,地铁. 有快有慢,但是不是快就好,就像广州,深圳,之间座飞机一样,很多时间花到别的地方了. 现实社会中有城市若干规定,在多少米以上才能修高速公路. arm公司对自己的核也有类似的规定,就是AMBA. AHB-快 ASB-中 APB-慢 2410 只用了AHB,APB +----------+ | RAM | +----------+ ^ | +-----+ AHB v +-----------------+ | ARM |===============| bridge(AHB-APB) | +-----+ +-----------------+ || UART <-------->|| || APB || IIC <-------->|| || 所有的器件的数据最终都是通过AHB来到达ARM的. 频率代表工作速度,uboot对2410默认设置为1:2:4(cpu:AHB:APB),即ARM频率为200M,那么AHB频率为100M,APB的为50M。 像UART这种器件,频率十分低,达不到50M,这样当它传一个数据到APB中时,这个数据就会被APB上面的数据往上推。 _ +----------+ R/W -->--| | | | Data--<--| | | | CLK -->--| | +----------+ _ R/W 表示低电频表示读,高电频表示写 CLK 就是时钟 .----. .----. .----. .----. .----. . CLK | | | | | | | | | | | ----' '----' '----' '----' '----' '----' _ ----. . .---------- R/W |< setup time >.< holding time >| '---------------------------------------' .---------. DATA ------------------------| |-------------------- '---------' 因为数据并不知道是高还是低,所以一般用框来代表,不用知道里面的内容 setup time 建立时间 holding time保持时间,即数据在这个时间里面是稳定的,可以在这个时间内读取, 有些时候,用到了斜线图,因为电压的变化是需要时间的。 那一个周期从那里开始呢?这没关系,只要在同一个图中都用同一个点作为周期开始就可以了。 .---. .---. .---. .---. / \ / \ / \ / \ ---' '---' '---' '---' '--- ---. .---. \ / \ '---------------------------' '--- GPIO: ===================================================== | | | | | +-------------------+ | | | | | GPIO(通用IO) arm核中有多个针脚,但从s3c2410出来的只有一个 | +---------+ 选择| 就是arm中多个脚对应2410中一个脚,里面会有些选择电路,外面 | -| ARM920t | .-. | 的叫对应那个arm脚是由一些GPIO的寄存器控制,arm出来的叫 | -| |-| |----- io port, s3c2410出来的叫GPIO,引脚复用。 | -| |-| | | | +---------+ '-' | | | | | | | s3c2410 | +-------------------+ | | | | | 统一编址,独立编址 在x86上面,可以插4G的内存,那要访问外部寄存器来控制器件怎么办?外部控制器是在另外一个 地址空间的,需要通过in,out等特殊指令才能访问。这就是独立编址 在arm上面,采用统一编址,2410上面sdram只能有256,而其外部寄存器的地址被安排在 0x4800 0000 ~ 0x6000 0000直接通过访问内存的指令来访问,如ldr,str等 如何找到一个器件对应的寄存器: 比如:需要找到大板上面的LED3对应那些寄存器 1. 在板上看器件的名字,LED3就叫LED3, LED3 2. 在主板电路图中寻找接在核心板座子上面的名字 KEYBOARD 3. 通过这个名字,找到s3c2410接的是那一只脚 GPB2 GPIO除了GPA之外其他的都有GPXCON(控制寄存器) GPXDAT(数据寄存器) GPXUP(启用上拉电阻) GPXCON 选择对应针脚的功能,根据一个针脚有多少个功能,有GPXCON有n位对应一只脚 GPXDAT 控制对应针脚的电频高低 GPXUP 上拉电阻的意思是,当这只脚没有接东西的时候,这只脚的电频是不定的, 如果启用了上拉电阻就是让这只脚处在高电频状态。 而在GPIO中输入和输出的意思是,对于一只脚(gpio都是对一只脚来说的),如果把它设成输入,就是说我们可以通过GPXDAT来读取这只脚的电频高低,如果设成输出,就是说我们可以通过GPXDAT来设置这只脚的电频高低。 控制硬件和语言 ================================================================== C语言 ---------------------------- 用c语言来写裸机的程序要注意,main一定要放在文件开头,他前面不能有别的代码 #define GPBCON (*((volatile unsigned long *)0x56000010)) 注意要加volatile防止优化 GPBCON = 5; GPBCON = 7; 编译器可能会优化掉第一个赋值直接赋值7,但对应寄存器来说是不对的。。 汇编 ---------------------------- mvn 相当于取反操作 mov ... lsl #4 相当于移位 bic R0, R0, #%1011 可以直接控制某些位 4k sram ================================= nand flash 只有控制总线,没有数据和地址总线,数据和地址都跑在控制总线上,不能通过访问内存的方法访问*() +------------------+ nand controller | +-------+ +-+ | +---------+ | |ARM920t| | | |======================| | | | | | | | | | | +-------+ +-+ | | | | +-+ | ram controller | | | | | | |---------| | +--------+ | | |=========+ |111111111| | |4k SRAM | +-+ |========+| +---------+ | +--------+ | ||3400 0000 | +------------------+ +--------+ | \ \ | | | \ \ | | | \ \ | | | \ \ | | | \ \ | | | \ \ | | | \ \ | | | \ \ | | | \ \ +--------+ | \ \ 3000 0000 | \ \ | \ \ 0000 1000(4k) | \ +--------+ | \ |11111111|<-----------+ -------- +--------+ 0000 0000 第一阶段把nand flash的前4k放到sram中--地址00000000~00001000. 第二阶段也把uboot的前4k复制到sdram中了,为什么?有可能有一个函数正好包含了1000这个地址,那就会有问题,所以干脆都放进去。 7种模式:(usr,sys,irq,fiq,und,abt,svc) ================================================ uboot运行在svc模式下. 用户模式: -------------- usr sys 用户,如果要主动跳,只能通过软中断跳到svc模式 特权模式,异常(包括中断) -------------- irq,fiq--就是x86里面的中断 irq 中断模式 fiq 快速中断模式 und,abt,svc--就是x86的trap,cpu自己产生的,svc就是系统调用128中断 und 未定义模式----机器指令没有定义 abt 忽略模式------在一个奇数地址取4字节(不对齐的情况),分成指令忽略(mov pc一个未对齐的地址),数据忽略(ldr, str访问未对齐内存) svc 超级用户模式--系统调用 他们之间的跳转关系: 1. irq不能嵌套,irq状态下不能直接被irq中断 2. fiq可以直接中断irq 3. irq,fiq中执行指令时可以跳到und,abt中 4. und,abt可以被irq,fiq中断 +------------+ | 异常向量表 | +------------+ 异常向量表:在0x00000000 ~ 0x0000001c,就是前面8个4字.这是有硬件决定的 这个表也是uboot的开头。 向量表的地址不能改,但每个表项只有4个字节,所以只能放一个跳转指令.00000000处理开机reset的情况,跳到初始化的地方。 对于这个表要分清模式和异常,模式是模式,异常是异常,一个异常向量表中同一个模式下可以有多个异常,比如svc对应两个异常(reset,software interrput),当有一个特定事情发生时,arm就会去中断向量表去找东西,这和模式没有绝对的关系。 Address Exception Mode in Entry 0x00000000 Reset Supervisor 0x00000004 Undefined instruction Undefined 0x00000008 Software Interrupt Supervisor 0x0000000C Abort (prefetch) Abort 0x00000010 Abort (data) Abort 0x00000014 Reserved Reserved 0x00000018 IRQ IRQ 0x0000001C FIQ FIQ 模式切换时自动保存lr,svc -> abt 先把svc的pc放到abt 中的lr中,返回时从 下面是从svc模式下面触发一个数据abort的例子: abt svc | | xxxx | | <--------- ldr jjj | | aaaa | | | bbbb | | | cccc | | | ---------> | | | yyyy | | | zzzz | 这里有几个问题: 从svc跳到abt是中断向里表的跳转语句做的,但是模式是怎样返回的呢? a. 硬件自动保存svc_pc到abt_lr当中,自动保存svc_cpsr, 到abt_spsr中。之所以有要每个模式下面有自己的sp,lr,spsr(r13,r14,spsr是独立的)就是这个原因,如果没有了它们,模式就不能正常返回。 b. 在aaaa,bbbb,cccc这个过程中调用其他函数还是要遵循apcs规则的,不然lr等就会被污染了. c. 在从abt返回到svc时,把abt_spsr->svc_cpsr,abt_lr->pc这些动作是要手动完成的.请看后面的例子。 下面是一个设置向量表的程序,也是一个bootloader的开始: .section .text .global _start _start: b reset ---> 下面8条指令就是中断向量表,刚启动 b und 之后就会执行b reset,跳到后面做 b swi 初始化工作。后面遇到异常时cpu自动 b p_abt 会找这个表。 b d_abt mov r0, r0 b irq b fiq reset: und: swi: p_abt: d_abt: -->这个例子中我们只关心数据abort的情况:(异常处理函数) ldr sp, =0x32000000 因为每个模式下面都有自己的sp,所以在进入异常后要设置自己的栈. sub lr, lr, #4 后退一条指令,因为在abt时pc是指向后两条指令。 stmfd sp!, {r0-r14} 把所有寄存器,除了pc都保存在栈中。这也是x86中在进入异常时经常看到的。 adr r0, abt_str mov r1, lr ldr r2, printf mov lr, pc mov pc, r2 ldr r0, show_mode mov lr, pc mov pc, r0 ldmfd sp, {r0-r13, pc}^ 这里要注意:我们不可以使用 mov pc,lr;msr spsr,cpsr来替换上面那条指令, 因为如果先mov lr到pc,程序就返回了,没有执行下面那条指令,而如果先msr 模式又回到svc了,lr已经是svc的lr,也是不对。应该用 ldmfd sp, {}^,加了 ^(恢复cpsr)这条指令就可以帮我们一方面从栈中恢复pc,另外一方面cpsr也恢复。 保存r0~r14的原因是显而易见的,那保存cpsr是要保存各种标记位,以防返回 后的cmp指令出问题。 还有另外一种方法可以恢复篇 movs 如果操作的是pc,同时也恢复cpsr 现在又回到svc模式了 movs pc, lr irq: fiq: printf: .word 0x33f8c58c und_str: .asciz "undefined: svc pc ==> und lr ==> 0x%x\n" .align 2 abt_str: .asciz "data abort: svc pc ==> abt lr ==> 0x%x\n" .align 2 show_mode: .word 0x30000018 每个模式的情况还不一样的,上面的例子可以看到,在d_abt中,修正pc是往回退4个字节。 但是在und模式下,pc就不用修正了,因为未定义指令是在译码的时候已经发现了,所以pc 正好指向下一条指令。 这和流水线有关。 +------+------+------+ +------+------+ | 取址 | 译码 | 执行 | | 移位 | 写回 | +------+------+------+ +------+------+ +------+------+ +------+------+------+ | 取址 | 译码 | | 执行 | 移位 | 写回 | +------+------+ +------+------+------+ +------+ +------+------+------+------+ | 取址 | | 译码 | 执行 | 移位 | 写回 | +------+ +------+------+------+------+ 为什么usr模式没有spsr呢?因为没有一个模式需要从usr模式返回,usr模式自然不用保存程序是从那里跳过来的,usr模式是最顶层的。 linux起来后会重写sram中的异常向量表。 swi 软中断 ------------------ pc >= 3G svc模式 内核空间 pc < 3G usr模式 用户空间 一个程序在用户态的标记是pc指针在0~3G 用户态切换到实际上就是: 1. pc指向3G-4G的地址 2. 模式切换到svc了 软中断号是怎样传递的? swi: @ldr sp, =0x32010000 sub lr, lr, #4 -----> 因为swi是在译码的时候跳转的,所以pc是 下一条指令,这样在减4就是swi指令本身了 stmfd sp!, {r0-r14} ldr r3, [lr] -----> 通过分析指令本身来得到异常号,swi后面 mov r1, #0xff 异常号占24位,足够了。 mvn r1, r1, lsl #24 and r3, r3, r1 adr r0, swi_str mov r1, r3 ldr r2, printf mov lr, pc mov pc, r2 ldmfd sp, {r0-r13, pc}^ 在系统调用中用r0~r6 7个寄存器来传递参数,r0为返回值 arm中的系统调用号可以在这个头文件中看到: /usr/local/arm/3.4.1/arm-linux/include/asm/unistd.h linux只是利用了软中断来实现系统调用而已. 一个模拟器也就是通过解析每一条xxx.bin里面的指令然后在pc上面执行相应动作 内部中断:(在s3c2410中的控制器发生的) ===================================================== +---------+ | ARM920t | ARM核 | | +-+-----+-+ irq| \ fiq ^ \ 主中断控制器 .-----------------------. | .--------. | | | | INTPND | | |- | '--------' ^ |- | | | |- | .--------. | |- | |PRIORITY| | |- | '--------' | |- | ^ | | | .--------. | | | |INTMASK | | | | '--------' | | | | | | | .-------------------. | | | INTMOD | |- 级联中断控制器 | '-------------------' |------------.----------------. | ^ |------------| .------------. | | .-------------------. |------------| | INTSUBMASK | | | | SRCPND | |------------| '------------' | | '-------------------' | | .------------. | '-----------------------' | | SUBSRCPND | | | | | | '------------' | | | | '----------------' / | \ | / | \ | LCD watchdog IIC UART 概况: 为什么我们需要中断控制器: 因为arm出来只有两根和中断有关的脚,irq/fiq. 但是外围器件众多,如果都连在这两根线上就根本分不清是谁来的中断。 中断控制器就像arm核的秘书,可以告诉arm是谁来的中断(通过访问寄存器). arm和x86一样采用级联的方式连接两个中断控制器来工作,不过在arm上面每个 中断控制器有32只脚,两个控制器之间通过5根线连接。 要忽略中断有两个方法: 1. 禁止中断 把cpsr的 irq/fiq 位置1,就可以禁止中断,这两位叫禁止中断位。 2. 屏蔽中断 编辑中断控制器的INTMASK寄存器就可以屏蔽某个中断。 ====寄存器: SRCPND: 32位,每位分别代表主控制器的32只脚,当某一位被设置成1时,表示这个位对应的东西,比如是UART中断来了,如果同时来两个不同的中断,就有两位被设成1.当时如果某一位,比如UART正在处理中它那位可能还是1(arm还没有清除这位),这时又来了一个UART的中断,那么这个中断就丢失了。 INTMOD: 32位,每位代表的中断源和SRCPND一样。0代表接到irq,1代表接到fiq. PRIORITY: 表明中断线的优先级,中断控制器根据这个优先级在SRCPND中选取一个中断,放到INTPND中 INTPND: 32位,也是每位代表SRCPND的中断源,不一样的是一个时刻只能有一位被设置成1,这是由优先级选择器来保证的。 当中断发生时,SRCPND,INTPND是硬件设置的。但是当在中断处理函数完成后是要自己把相应的位清除的,如果不清除中断控制器就不断地给arm产生中断。在清除时要先清除SRCPND相应位,然后在清除INTPND,这是因为,INTPND相应位的设置是由SRCPND来产生的。先根除SRCPND。 因为这两个寄存器是硬件设置的,所有我们的清除方法不一样,要清那一位,就往那一位上设一,硬件自动对这一位异或,有个很好的清除方法: SRCPND = INTPND --> SRCPND相应的位就被清了 INTPND = INTPND --> 再来清自己 ===级联中断控制器: 级联的中断控制器只有两个寄存器: INTSUBMASK: 相当于INTMASK, SUBSRCPND: 相当于SRCPND 级联中断控制器没有priority,也不能选择irq/fiq,整个子中断控制器的priority和irq/fiq选择都由连到主控制器的那个脚来决定。 子控制器只接了 3个uart,1个触摸屏,1个A/D转换 ===中断处理函数: 和别的异常处理函数没有太大区别,主要是硬中断号,也就是那个外设来的中断,在INTPND中可以看到。 无论我们在刚刚进入中断处理函数时清除INTPND,还是中断处理函数退出时清除INTPND是一样的,这个本质原因是arm中irq是不可以嵌套的,irq模式下不能被另外一个irq抢占。 如果有几个中断到了SRCPND中但是被INTMASK屏蔽了,有一天打开了这个MASK,SRCPND 是会往INTPND中传递中断的。所以如果不希望这样,我们要在启用这个中断之前把SRCPND也清除了。linux内核在启动中断前也是要清除这些位的。 会话,进程组。 一个会话就是一堆进程,而这些进程的控制终端都是同一个。一个会话可以有多个进程组,但是只有一个进程组是处于活动状态的。 一个进程不能是多个会话的领导。会话领导是一个进程组的组长。 看门狗 ====================================== 看门狗主要有两种用途,产生一个中断或者是产生一个reset信号(arm上面有专门的一只脚来reset),看门狗的应用主要在:比如在linux里面可能有一个后台进程不断地喂狗,当系统死掉之后,调度也就停止了,当然喂狗进程也就没有被调度,然后系统就可以重启了。 3个寄存器: WTCON: 控制寄存器,可以控制产生中断还是产生reset信号,还可以控制看门狗多长时间计数跳一下. WTDAT, WTCNT: 看门狗每隔一个时间单位,就会对WTCNT减一,直到WTCNT为0,中断或reset信号就会产生. 如果是中断模式,在WTCNT为0时WTCNT会重新从WTDAT装载计数。但是对WTDAT的第一次设置不会马上生效,要等中断产生后才会装载。如果很急就应该手动把WTCNT也设置了。所有对于reset模式来说WTDAT这个寄存器是没用的。注意:WTCNT虽然是一个32位寄存器,但是只有低16位有用,所以最大为0xffff 计算看门狗的时间间隔方法: t_watchdog = 1/(PCLK / (Prescaler value + 1) / Division_factor ) 假如我们要设置5秒产生一次中断: PCLK = 50M 我们可以先定了WTCNT的次数为0xffff,在把Division_factor也定了,比如16,那样就可以求出Prescaler value x = 1/(50000000/(P+1)/16 x = 5 * 0xffff 外部中断 ======================================= 外部中断是相对于内部中断来说的,是具体有一根线到s3c2410外面来,供外围器件使用,有24根 +----------------------------------------------------+ | +---------+ | | | ARM920t | .--UART2 | | | | | .--UART1 | | +---------+ | | .--UART0 | | | | | | | | | +----------------+ --- +------------------+ | | | INT controllor | --- |usb INT controllor| | | | | --- | | | | +----------------+ --- +------------------+ | | |||| | | | | | | |||| +------------------+ |A/D converter | | |||| |EXT INT controllor| | | | |||| +------------------+ touch screen | | |||| ||| ... |||| | | |||| ||| ... |||| | | |||| ||| ... |||| | | |||| ||| ... |||| | +---||||--------||| ... ||||-------------------------+ |||| ||| ... |||| 0123 456 ... 23 EXT INT controllor不是一个独立的东西,只是s3c2410电路的一部分,但是有专门 的寄存器对这部分电流进行控制。 从图中可以看到24根外部中断线中前4根是直接连在主中断控制器上面的,而其他20根是先连到外部中断控制器(EXT INT)中然后再到主中断控制器。外部中断控制器的功能基本上就相当于一个子中断控制器。 寄存器: EINTMASK 这个寄存器就相当于INTMASK,这个东西只能控制连在它上面的20根线 EINTPEND 就相当于SRCPND寄存器,有中断就会在这里面记录,中断处理完之后记得清楚这里面的相应位,清除方法和SRCPND一样 内部中断使用那种中断触发方式对我们不可见,我们也不用去了解,只要它能触发就可以了。但是外部的中断就要很清楚接在它上面的器件是用那种方式触发的了。 触发方式: | 上升沿 rising edge | 边沿触发----+ 下降沿 falling edge | | 双沿 both edge + | | 高电频 high level | 电频触发----+ | 低电频 low level 边沿触发就是一直是高或低电频都不会触发,在电频发生变化的瞬间触发。用途比较广泛。 电频触发是处于高或低电频时,中断一直被触发,知道有人把它设置成另外一个电频为止,比如电话,就是电频触发。网卡也是。 我们可以通过设置EINT0~2来改变这些外部中断的触发方式。 例子:比如我们利用外部中断2: void _start(void) { unsigned long flag; /* open interrupt */ __asm__ __volatile__( ---->打开cpsr中的irq中断 "mrs r0, cpsr\n" "mov r1, #1\n" "mvn r1, r1, lsl #7\n" "and r0, r1, r0\n" "msr cpsr, r0\n" : : : "r0","r1" ); /* clear INTMASK and set INTMOD */ -->因为2号外部中断是接在 *INTMSK &= ~(1 << 2); 主中断控制器上面的,所以 *INTMOD &= ~(1 << 2); 直接设置主控制器上的mask /* set GPIO F to EINT2*/ *GPFCON &= ~(3 << 4); -->选择gpio让gpf2用来做外部中断线 *GPFCON |= (2 << 4); /* set EXTINT */ *EXTINT0 &= ~(7 << 8); -->选择2号中断的触发方式,这里是下降沿触发 *EXTINT0 |= (2 << 8); //falling edge trigger } 寄存器再总结一下: 中断源 屏蔽 其他 主控制器 SRCPND INTMSK INTMOD,PRIORITY,INTPND 子控制器 SUBSRCPND SUBCMDMASK 外部控制器 EINTPEND EINTMASK EINT0,1,2 时钟,频率 ===================================== PLL 锁相环,这个东西是用来倍频的,如果比例是n,那增大后的频率是原来频率的2^n倍,PLL一般通过叠加两个波来得到更大的频率。 +----------------------------------------------------------------------+ |s3c2410 .--------------. | | |ram controllor| | | '--------------' | | ^ ^ EXTCLK | | Data| | | | | | | | +-------+ v AHB| +-----------------+ | | +-------->|ARM920t|=========| bridge(AHB-APB) | | | .-------. | [FCLK]+-------+ [HCLK] +-----------------+ | | +------+ | |-+ ^ || | EXTCLK --+->| MPLL |--|CLKCNTL|-----------------------+ UART <--->|| | 12M || +------+ | |--------+ || | || '-------' | || | || +------+ | (PCLK) || | |+>| UPLL |<--> USB +------------------------------>||APB | | +------+ IIC <--->|| | | || | | | +----------------------------------------------------------------------+ 在smdk2410上面只提供了一个12M的外部时钟源,这个时钟进来s3c2410之后有主要分两条路,一边通过UPLL给USB提供时钟。另外一方面通过MPLL出来一个FCLK给ARM920t提供时钟,所以ARM920t用的是FCLK,这个FCLK通过另外一个东西CLKCNTL分成HCLK和PCLK供AHB,APB使用。FCLK/HCLK/PCLK是按一定的比例来分频的。可以通过后面提到的寄存器设置。如果有一个UART接在APB总线上面,假如PCLK是50M,UART自己没有时钟源,它就会从APB上面接时钟到UART上面,如果UART只工作在10M频率下,这样就要给UART接一个分频器。如果外围有自己的时钟源就要更小心了,防止和总线上面的时钟不匹配。 时钟的设置要在系统启动的前期,不如会影响正在工作的器件(特别是UART),linux提供修改的接口. 寄存器: 主要的寄存器是: MPLLCON 用来配置MPLL从而控制FCLK的值,见P237用户手册 UPLLCON 同理UPLLCON设置UPLL CLKDIVN 用来处理FCLK,HCLK,PCLK的之间的比例关系,见P240 nand flash ================================== nand VS nor ---------- nand flash: 控制总线,容量大,读速度比较慢,察除速度快,寿命长,成本低 nor flash: 地址总线(在读的情况下和ram一样,写的时候是另外一种时序),容量小,读速度快,察除速度慢,写也慢,寿命短,成本高,所以nor flash支持xip(片内执行,只读方式) nand 的一些特性: 擦除,写入: 在写nand flash的时候,只能把一位从1变成0。而只有擦除操作的时候才能把一位从0变成1。 位反转: 这是说nand flash中的某些位可能受到外界的干扰出现反转的,即原来1变成了0,原来0变成了1.这是在硬件上面反转了。 坏块: 坏块是指一个block里面有一个或多个bit不能在擦除操作,写操作中把0变成1或把1变成0,那这整个块就不能用了,有坏块不怕,怕的是读写的方法不一致。处理坏块就是在读写的时候都是往后一块。 判断坏块: 写操作:在写结束后查看返回结果,如果失败,有两个原因: 1.电压不稳。 2.坏块。 读操作:在读取之后,通过对比ECC,看校验: 1.位反转。2.坏块。 擦除操作: 查看返回结果,如果失败,有两个原因: 1.电压不稳。2.坏块。 写操作,擦除操作的电压不稳的可能性十分低,基本上可以认为是坏块。而读操作在ECC校验失败后应该进行一次擦除操作看是否是坏块。 处理坏块: 在发现一个坏块后应该在坏块中的第一个页中的OOB的最后一个字写成非0xff(0xff表示不是坏块),只能写在后面,因为OOB的前面几个字用来记录ECC。 关于ECC。nand flash控制器生成的硬件ECC只占3个字节。一般硬件对256个字生成3个字节的ECC,512就需要6个字。如果ECC校验中发现有一位错了,它可以自动帮我们找到是那一位并修正。 flash 读以页为单位512B+16B,擦写要以块为单位32页,16KB. 地址:nand的地址是 块号(4096个块 12bit)(64M) + 页号(块内32页 5bit) + 字节(页内512B 9bit) 而有些地方用列和行的概念:第几列就是第几个块,第几行就是第几个页。 nand flash在读的时候是只能读到一个页的尽头,528B包括OOB。到头之后不能自动转到下一个页中. +------.------+----+ |256B .256B |16B | | . | | | . | | | . | | | . | | | . | | +------.------+----+ 上半页 下半页 OOB A区 B区 C区 OOB部分的空间没有编排在flash的正常地址中,所以地址513是在第二个页中 但是如果你从一页开始读和写如果顺序读写528,是会直接到oob区的,所以在到一页结束时要设置地址.这是我们为什么要换页地址。所以要访问OOB区有两种方法,1.直接使用0x50命令从OOB区开始直接访问。2.从上半页或下半页开始读,一直往后操作,直到OOB区. +---------+----.----------. | ARM920t |----|memory | | |----|controllor| +---------+----| | | | '----------' ||||| ||||'-----.----------. .-------. |||'------|+------+ |----| nand | ||'-------||regis | |----| flash | |'--------|+------+ |----| | '---------|nand |----| | |flash |----| | |controllor| | | | | | | '----------' '-------' nand flash controllor 一方面它接在内存控制器上(其实是接在内存控制器前面),这样arm才能通过地址直接访问nand flash控制器的寄存器,外围的寄存器都是这样的原理. 另外一方面通过控制总线连接具体的nand flash。 nand flash控制器和nand flash的物理连接: -------------------------------------- +---------------+ +----------+ | 2410 +------+| |k9f1208 | | |D[7:0]||---------| I/O port | | |CLE ||---------| CLE | | |ALE ||---------| ALE _ | | |nFCE ||---------| _ CE | | |nFRE ||---------| RE _ | | |nFWE ||---------| _ WE | | |R/nB ||---------| R/B | | +------+| | | +---------------+ +----------+ D[7:0]: 命令,地址,数据都在这里传,有8条线. CLE : 命令锁存,表示我马上要传命令 ALE : 地址锁存,表示我马上要传地址 nFCE : 控制打开还是关闭这个flash nFRE : 告诉对面我们要在D[7:0]上面进行数据读取 nFWE : 在D[7:0]上面传命令,地址,数据的时候都是要拉低这位 R/nB : 用来读取flash现在的状态是ready还是busy 时序 ----------------------- 具体时序要看k9f1208的手册,在大概P20 很多情况nand flash控制器已经帮我们控制好这些时序了,只有3个时间需要我们根据k9f1208的手册来配置nand flash控制器的寄存器: . . . . .----. .----. .----. .----. .----. .----. HCLK | | | | | | | | | | | | ' '----' '----' '----' '----' '----' '---- . .-------.-------------------.---------. CLE/ALE ./ . . .\ ----------. . . . '------- ----------.---------. . .-------.--------- nWE . .\ ./ . . . '-----------------. . .< TACLS >.< TWRPH0 >.< TWRPH1>. setup hold0(data) hold 为了找到对应的值,要在k9f1208器件手册中P20左右在看时序图,找到在具体器件中的对应值,这里还要注意,因为控制器关心的一个一般情况,而我们在k9f1208手册要考虑写地址,写命令两种情况,比如在写地址的情况下TACLS = [5us:1us],而在写命令的情况下TACLS = [3us:0us],那么我们的TACLS只能[3us:1us],取交集。 在计算时序时,考虑一条指令的所占用的时序,1/200M = 10ns. 寄存器: ------------------------- NFCONF: 它可以控制nand flash控制器是否开启,flash是否开启,注意这是两个概念。控制上面提到的3个时序.是否启用硬件计算ECC. 还要注意,这里的保留位一定要设成1,好像是一个bug NFCMD: 命令,这是指k9f1208支持的命令,可以查k9f1208的手册来查看。P8 NFADDR: 地址。 NFDATA: 数据 _ NFSTAT: 查看flash现在的状态,忙还是ready。在读这个寄存器的时候,其实就是看R/B这条线的电频高低 NFECC: 如果在NFCONF中启用了ECC,那么如果你的命令是读,那么在读完后nand flash控制器会自动帮我们计算好ECC,我们可以用它来和flash中的ECC进行比较,看是否正确。如果我们是写操作,在写完后我们也可以得到一个ECC的值,我们把这个值写到OOB区。 操作注意: ------------------------- 1. 读过程: 读操作的起始地址可以是一个页的任意地址,只要清楚命令用的是00还是01就可以了。 a.判断起始地址在上半页还是下半页,从而选取命令是00还是01,然后发送命令。 b.发地址,这里要注意,无论是读还是写,不传第9位,分别传[0~7],[9~16],[17~24],[25],因为第9位确定这个地址是上半也还是下半页,这已经由命令决定了。 c.等待状态变成ready。 d.真实读数据。 2. 写过程: 写操作的起始地址只能是上半页。 a.发命令0x80作为开始。 b.发地址,和读操作是一样的。 c.写数据. d.发命令0x10表示结束。 e.等待状态变成ready。 f.发送0x70命令查看返回结果,判断是否成功。 3. 擦除过程: a.发命令0x60 b.发送地址 9~25位 c.发命令0xd0 d.等待状态变成ready. e.发送0x70命令查看返回结果,判断是否成功。 在开启nand flash,就是第一次用它的时候要复位一下nand flash。在写nand flash的c程序时要注意,nand很多寄存器都是8位或16位的,相应我们也要用的相应的强制转换: (unsigned long *) --> 触发 ldr (unsigned char *) --> 触发 ldrb 在做外围器件的驱动的时候,问自己: 1。外围器件是个什么东西 2。外围器件和板上的什么东西连在一起 3。两者之间是通过怎样的方式通信 内存控制器: ====================================== s3c2410 的前面1G可以接东西让我们访问。 对于内存控制器有32根data数据线,27根地址线,但是27根地址线只能够访问128M的空间,而2410还有另外8根片选信号来达到8*128M = 1G,8个bank可以使用,片选信号是根据arm给出的地址的高3位来决定的。 13个寄存器: 内存控制器控制了8个blank。 BWSCON 这个寄存器主要是控制了每个blank在访问时的位宽,是否使用wait信号来引人延时. BANKCON0~5 控制0~5个blank,在2410上面接的是一些外围器件如:IDE接口,扩展串口,cs8900a等。这6个寄存器用来控制这些器件的访问时序。 BANKCON6~7 控制6~7两个blank,因为这两个blank使用来控制sdram的,比上面6个blank要复杂一些,不过基本上一样,就是多了两位,用来决定某个blank的内容,是sdram,rom/sram. REFRESH 这个选项是说对于sdram,是否开启刷新功能,如果开了,用那种模式来刷新。还有关于刷新的时序。 BANKSIZE 主要是说blank 6/7放的是多大的一个sdram MRSRB6~7 对blank 6/7 sdram的进一步配置。 mmu ====================================== 协处理器:协处理器都在arm920t里面,它的存在是为了进行专业运算。 比如: DSP 用于音视频,积加计算 FPU 浮点运算 VPU 用于矢量运算,2D图形 GPU 用于图形处理,3D图形,显卡上面 +-------------------+ |ARM920t | | +--------+ | | |ARM core| | | | | | | +--------+ | | ^ | | | mrc | v mcr | | +--------+ | | | MMU |P15 | | | ==== | | | | ==== | | | | ==== | | | | ==== | | | | ==== | | | +--------+ | | | +-------------------+ MMU 也是协处理器,在arm中它的编号是P15,它里面有自己的寄存器来对它进行配置。它也是arm7和arm9的主要区别,arm7没有mmu,arm9有,正因为arm7没有所以它是冯络依曼结构,而arm9是哈佛结构。而arm7有内存保护单元,方便操作系统使用。 为什么需要mmu呢?想象这样一种情况,我们用连接脚本来指定某个程序运行在某个地址上,几个程序还可以安排好地址,但是成千上万的程序地址就没办法很好地安排了。而这个问题的本质是进程之间的地址空间有可能冲突。有了mmu就可以一方面增大地址空间(当然它不会增大物理空间),另外一方面它可以为每个进程分配一个独立的地址空间,操作系统负责页表的切换。 物理内存 linux vim qq +--------+ +-------+ +-----+3G +-----+3G | | mmu | | | | | | | | +------+ | | | | | | | | +-----| c2 |<------| | | | | | | | | +------+ | | <----------|=====|0x11111111|=====| | | | | | | | +-----+0 +-----+0 | | | | | | | <----------------------------/ | | | +------+ | | | | | | | | | | +-------+ | | | | | | +--------+ | |qq 页表 | <---+ +--------+ |vim页表 | +--------+ mmu本质就是根据页表的mapping找到虚拟地址所对应的物理地址进行访问,页表的开始地址由mmu中的c2寄存器,这个寄存器由linux在进程切换时修改,要注意,c2的内容是物理地址。这是可以理解的,如果这是一个虚拟地址就是一个悖论。 在开了mmu之后整个arm在访问内存时都是用虚拟地址的,无论是在linux里面还是uboot里面,都会使用虚拟地址,如果没有设好页表是会导致访问失败。在mmu起来后ldr,中断向量表都是在虚拟地址上面的。缺页在arm中是产生取数据异常。就是当这个页表是空的时候触发缺页异常,而如果是权限不能访问产生的断错误,他们同是取数据异常,是那种错误可以通过c5寄存器得到,而是那个地址出错可以在c6得到。 mrc/mcr --------------------- mrc,mcr命令是处理器和协处理器之间通信用的。 一般格式为: mrc p15,op1,Rd,Cx1,Cx2,op2 p15是协处理器的号,而具体操作是通过op1,Cx2,op2共同决定的。具体需要在手册中找每一个命令的意义。不过这条指令就是把Cx1(mmu上的第几个寄存器)放到Rd(arm上的). 寄存器: --------------------- c0: 可以通过这个寄存器得到,arm的版本和cache type,都是一些信息。 c1: bit 用途 0 是否开启mmu 1 是否开启数据对齐访问检查。注意 这个功能是在arm核中完成的,却在 这里设置,所以mmu中的很多寄存器 都不是关于mmu的。而是关于arm核的。 2 data cache 3-6 reserved 1111 7 决定系统使用大小头 8 S bit 权限相关,下面会看到 9 R bit 权限相关 10-11 reserved 00 12 instruction cache 13 V bit 这一位决定中断向量表的位置,0在0x00000000 1就在0xffff0000,考虑linux肯定要把中断向量表 设置在0xffff0000不然用户态也就可以修改它了。 14 选择TLB在满了的时候抛弃旧条目的算法。round robin/随即选择 15-29 reserved 30 otFastBus select 31 Asynchronous clock select后面两位和FCLK:HCLK时钟的比例有关。 c2: 用来读取和设置页表的基地址,再次强调这里要设物理地址。 c3: domain寄存器,用在权限上面。后面会看到 页表的结构: --------------------- arm920t的mmu只能对1M,64K,4K,1K块进行映射。考虑如果对每个byte都进行映射的话,每个表项用4byte,这样也要4G*4byte那是不可能。所以我们要映射一块连续的空间。这样做的坏处就是导致映射不灵活,因为块内的物理地址和虚拟地址一定要一样,比如你将0x80100000映射到物理上的0x30100000,映射单位是1M,那么0x80100003在物理上面只能是0x30100003了,块内地址只能是一样,记住!!0xfff00000 有12位决定映射块的号,这也体现了有多少个虚拟地址可以映射到同一个物理地址。0xfff就只有4095个地址可以映射到一个物理地址。映射块越小,可以映射到同一个物理地址的虚拟地址就越多,越灵活。 另外一方面页表项规定是4个字节。如果我们的映射单位是1M,那么我们在表项中只用到了12位,因为1M为单位只用12位就够了。后面20位有别的用途,而且对于64K,4K,1K用的位数都不一样。 +----------+ | 4byte | 0x03 +----------+ | 4byte | 0x02 +----------+ | 4byte | 0x01 +----------+ | 4byte | 0x00 +----------+ mmu每个表项4byte,他只能通过下标来找到对应的物理地址,它的缺点是尽管有些页表项没有用到但是它还需要占空间。 section模式下的一个映射例子: --------------------- 31 20 19 0 +-----------------+----------------------------------+ 1. | table index | section index |------+ +-----------------+----------------------------------+ | | | +-------------------12 bit--------------------+ | | | 31 14 13 0 | | +--------------------------+-------------------------+ | | 2. | Translation base | | | | +--------------------------+-------------------------+ | | | | | |18 bit +----------------+ | | | |20 bit 31 v 14 13 v 0 | +--------------------------+---------------------+-+-+ | 3. | Translation base | Table index |0|0| | +--------------------------+---------------------+-+-+ | . | . | . | 31 v 20 19 12 11 9 | +-------------------+---------+---+-+------+-+-+-+-+-+ | 4. | section base addr | |AP | |Domain|1|C|B|1|0| | +-------------------+---------+---+-+------+-+-+-+-+-+ | | | |12 bit | v | +-------------------+--------------------------------+ | 5. | section base addr | section index |<-----+ +-------------------+--------------------------------+ 第一个是一个地址,用前12位作为下标在页表中查询。 第二个是我们的c2,页表的己地址。c2一定要16K对齐, 第三个是 先把table index乘4就是左移两位,因为每个表项是4个byte。在加上高位的组成页表的具体地址。 第四个是那个对应的页表项的内容可以看到映射到的物理上这1M的物理地址。 第五是1M的高12位的物理地址,在加上M内地址就可以组成一个具体的物理地址。 mmu对权限的控制 --------------------- mmu起到保护内存的作用,这种保护的存在是为不同模式服务的,而保护的对象是虚拟地址。 和内存保护相关的东西是: c3 寄存器,页表项中的AP,domain域,c1寄存器的S bit和R bit 权限控制有两个层次: 要不要检查权限: Domain寄存器(c3) 32位分成16组,每组2 bit,对应这两位 00 直接不能访问。 01 可以访问,但是要进行下面的权限检查 11 直接可以访问,不用进行权限检查。 而页表项中的domain(4 bit)域就是用来决定是用domain寄存器里面的那个组 之所以需要这个Domain寄存器,是方便分组,同时修改一组的权限只修改一 处地方就够了。 在检查时是否符合权限规定: 当doumain相应组表明01需要进行权限检查。这个权限检查是通过AP两位, c1的R位和S位。 AP S R Supervisor User Notes Permissions Permissions 00 0 0 No access No access Any access generates a permission fault 00 1 0 Read only No access Supervisor read only permitted 00 0 1 Read only Read only Any write generates a permission fault 00 1 1 Reserved 01 x x Read/write No access Access allowed only in supervisor mode 10 x x Read/write Read only Writes in user mode cause permission fault 11 x x Read/write Read/write All access types permitted in both modes. xx 1 1 Reserved supervisor permissions就是指授权模式,就是fiq,riq,abt,svc,und模式 user permissions 就是usr,sys模式。 uboot下面使用mmu --------------------- 要让uboot可以在开mmu的状态下运行,必须考虑下面一些地址,要在开mmu之前做好这部分页表。 1. uboot本身代码,数据。 2. 异常向量表 3. 硬件寄存器,如uart等。 所以干脆: 0x30000000 - 0x34000000 sdram 0x00000000 - 0x00001000 sram 0x48000000 - 0x60000000 寄存器 做好映射。 TLB --------------------- TLb称为快表,一共128个条目,分成数据TLB和指令TLB各64条。 数据TLB(DTLB):放一般的ldr,str指令; 指令TLB(ITLB):放pc指针寻过的址,pc取指令(每一次取址都会为ITLB). 下面是一个例子: 0x10 mov r0, #5 0x14 mov r1, #6 0x16 ldr r2,[0x20] 0x18 b 0x24 0x20 .word 2222 DTLB ITLB 0x20 0x60 0x10 0x50 0x14 0x54 0x16 0x56 0x18 0x58 0x20 0x60 当TLB满了之后,c1寄存器的14 bit (RR bit) 决定了淘汰算法,0=随机淘汰,1=round robin. TLB是不能单独关的,当MMU开启之后就一定要使用. 3种无效方式(不是清空TLB,而是装入) c8寄存器来控制: Function | Data | Instruction ------------------------------------------+--------------+---------------------- Invalidate TLB(s) | SBZ | MCR p15,0,Rd,c8,c7,0 Invalidate I TLB | SBZ | MCR p15,0,Rd,c8,c5,0 Invalidate I TLB single entry (using MVA) | MVA format | MCR p15,0,Rd,c8,c5,1 Invalidate D TLB | SBZ | MCR p15,0,Rd,c8,c6,0 Invalidate D TLB single entry (using MVA) | MVA format | MCR p15,0,Rd,c8,c6,1 1. 无效所有TLB 2. 无效单个TLB 3. 无效一个TLB条目 注释:Data 指的就是SBZ, MVA就是虚拟地址, SBZ是说 should be zero值一定要为0 arm中访问一个无效地址是不会出现取数据异常的,只会取错. 每个TLB表项的内容是虚拟地址和物理地址的mapping +-----------------+-----------------+ | 虚拟地址 | 物理地址 | +-----------------+-----------------+ 所以如果我们修改了页表就肯定小心地无效TLB D cache/ I cache ------------------------ D cache: 数据缓存 I cache: 指令缓存 D cache和I cache的区别和 D TLB和I TLB的区别一样因为pc指针寻址的地址放到I cache,其他放在D cache中. 每个cache有265个表项. 和TLB 功能上不同,cache是虚拟地址和物理地址所在内容的mapping +-----------------+-----------------+ | 虚拟地址 | 内容 | +-----------------+-----------------+ cache的使用有两个方向,对主存的读和写,I cache不涉及对主存的写,代码段是只读的。对D cache,当对主存进行写操作时就有两个选择,写D cache时马上写回主存(write through),写D cache时,过程是先把内容写到write buffer,随后才写到主存(write back)。注意这里所说的write buffer其实并不是实际存在一个buffer来保存修改的数据。D cache是包含write buffer的,空间只有一个就是D cache,write buffer就相当于一些bit,标记D cache中每个条目是不是脏的,所以write buffer纯粹是为write back模式服务的,如果是write through就直接写不用记录了。 | write buffer | | .---. | | .------->|...|-------. | | | '---' | | +-------+ | +-------+ .---------. v | | | .-|--->| D TLB |---->| D cache |---. | | RAM | | | +-------+ '---------' | | | | +------+ | | ^ | | | | | |---' | '--->+-------+ | | +------------+ | | | core | | | mmu | +-->| 内存控制器 |<===AHB===>| | | |---. | .--->+-------+ | | +------------+ | | +------+ | | v | | | | | | +-------+ .---------. | | | | '-|--->| I TLB |---->| I cache |---' | | | | +-------+ '---------' | +-------+ | | | | | CP15协处理器 | 所有这些cache, tlb, buffer都在cp15中。流水线结果告诉我们假如第一条指令ldr,下面是mov,和mov那么第一条在执行时的第三条在取址,所以他们需要同时访问内存,这时就需要总线上面的总线仲裁器来为他们排队。所以arm9不是真正意义上的哈佛结构。 启用和操作。 -------------------------- I cache 通过设置c1寄存器的I bit来启动。 D cache 通过设置c1寄存器的C bit来启动。而且还要在mmu开起来的前提下工作。 对于D cache的情况它的工作模式可以通过C bit(Ccr),页表中的C bit(Ctt)和B bit(Btt)来共同决定。 Ctt & Ccr Btt Data cache, write buffer and memory access behavior 0 0 D cache不开,读写都是直接访问主存的。 0 1 write buffer开启,没有D cache,对于读取的时候直接 访问主存。在写的时候先写到write buffer,随后再写 到主存。 1 0 有cache,就是write through模式。 1 1 有cache,就是write back模式。 对cache的操作,c7寄存器 Function Data Instruction ------------------------------------------------+---------------+----------------------- Invalidate ICache & DCache | SBZ | MCR p15,0,Rd,c7,c7,0 Invalidate ICache | SBZ | MCR p15,0,Rd,c7,c5,0 Invalidate ICache single entry (using MVA) | MVA format | MCR p15,0,Rd,c7,c5,1 Prefetch ICache line (using MVA) | MVA format | MCR p15,0,Rd,c7,c13,1 Invalidate DCache | SBZ | MCR p15,0,Rd,c7,c6,0 Invalidate DCache single entry (using MVA) | MVA format | MCR p15,0,Rd,c7,c6,1 Clean DCache single entry (using MVA) | MVA format | MCR p15,0,Rd,c7,c10,1 Clean and Invalidate DCache entry (using MVA) | MVA format | MCR p15,0,Rd,c7,c14,1 Clean DCache single entry (using index) | Index format | MCR p15,0,Rd,c7,c10,2 Clean and Invalidate DCache entry (using index) | Index format | MCR p15,0,Rd,c7,c14,2 Drain write buffer (1) | SBZ | MCR p15,0,Rd,c7,c10,4 Wait for interrupt (2) | SBZ | MCR p15,0,Rd,c7,c0,4 对于cache,主要是使无效和清除两种操作: 是无效:意思是表中的项是无效的,让cpu不要命中cache,直接到主存中取内容。 清除:意思是更新D cache中的脏数据到主存中。 我们不能直接干预cache中的内容。 在实际编程时要注意: 1.开启MMU前,使无效I cache/D cache和write buffer, 无效I/D TLB 2.关闭MMU前,清空I cache/D cache,即把脏数据写到主存 3.如果代码被修改,要是无效I cache 4.使用DMA操作可以被cache的内存是,将内存的数据发送出去时,要清空cache,将内存的数据读入是,要使无效cache 5.在修改页表的内容时,要注意TLB和cache,内核要很小心地完成这个工作。 6.开启I cache或 D cache 时,要考虑i cache或 d cache的内容是否和主存一致。最好在开启cache后先无效cache 7.I/O地址不能使用cache和write buffer,因为这种寄存器是有副作用的,我们需要保证硬件时序,不能优化 mmu 切换须知: ------------- 切换时要按照下面的循序来进行设置,不然会有问题: 1. 使无效I cache 2. 使无效D cache 3. 无效write buffer 4. 更新c2 5. 无效I/D TLB __asm__ ( "switch_mmu:\n" "mov r0, #0\n" "ldr r1, =0x31010000\n" /* invlidate I & D cache */ "mcr p15, 0, r0, c7, c6, 0\n" "mcr p15, 0, r0, c7, c5, 0\n" "mcr p15, 0, r0, c7, c10, 4\n" /* update c2 */ "mcr p15, 0, r1, c2, c0, 0\n" /* invalidate I & D TLBs*/ "mcr p15, 0, r0, c8, c7, 0\n" "mov pc, lr\n" ); 而且切换页表这段代码在前一个页表和当前页表的虚拟地址要一样。 要注意在让I/D cache和write buffer无效后不能马上(就是几条指令后)使用ldm指令,这样会出一些莫名奇妙的问题.所以只能用汇编来写这段代码,不能用c或内嵌汇编。 二级页表: --------------------- 单级页表的问题再也不用的地址也要占空间,而2级页表的话要好一些,一级页表中没有使用的相就不需要有对应的二级页表了。 一级页表总是16K大小的。(对应4G空间)所以地址也是16K对齐的。 粗页vs细页,他们每个表示1M的空间,他们决定2级页表的大小,即表项个数,使用的位数就决定了个数了。粗页的页表中有256个表项,细页的页表中有1K个表项,粗页可以用在64k,4k页,细页可以用在64k,4k,1k。一般64k,4k时用粗页,1k时使用细页。 当使用64k页时用粗页表是有浪费的,因为每个页表项有64K,用不着256个项就可以表示1M了(每个一级页表表示1M),4k的情况是最好的刚好要256个表项才能表示1M。同样道理,在细页中(有1024个表项)用64k页和4k的页都是有浪费的。 大页和细页有4个ap值可以分别给4个子页设置权限。很灵活。 ....补充。。。 冯络依曼vs哈佛 --------------------- 冯络依曼结构就是数据和指令在同一条总线上传送。 哈佛结构是数据和指令分别在两条总线上面传送,可以看到上那个大图中,取数据和取指令时是有可能冲突的。arm9通过I cache 和 D cache来模拟了哈佛结构。 编译内核和产生的东西 ========================================== make clean ------->清除中间文件 make mrproper ------->清除配置文件 make menuconfig/gconfig/config ------->创建配置文件,主要是建立.config,不过还有做一些小动作,比如创建一些连接文件等。所以最好还是做一下。 make dep(检查选项的依赖关系) 在2.6中已经不用这个选项了,因为make已经包含了这部分功能,不过这个命令可以起作用。它就是在include/linux/下面创建autoconfig.h,这个.h就是从.config转换而来的,就是.config的内容转换成一些宏,用来给内核源码使用。 内核也提供了一些简便的方法配置。make s3c2410_defconfig来配置smdk2410这块板,在每个体系结构目录下面有一些default的配置文件供我们使用比如s3c2410_defconfig就是在arch/arm/configs下面。有的就在./s390/defconfig. vmlinux / Image / zImage / --------------------------- vmlinux: 源代码编译后会出来一个ELF文件,就在内核目录下面的vmlinux. 对应make vmlinux Image : ELF需要解析器,不能在裸机上跑,在objcopy后的文件是 arch/arm/boot/Image 对应make Image zImage : arch/arm/boot/Image + arch/arm/boot/compressed/*.c 编译成为 arch/arm/boot/compressed/vmlinux 然后再把这个ELF objcopy成 arch/arm/boot/compressed/zImage. arch/arm/boot/compressed/*.c其实 就是一个解压程序,arch/arm/boot/Image压缩后成arch/arm/boot/compressed/piggy.gz作为这个程序的 数据段,当运行这个程序时,解压程序把内核解压到内存中,然后跳过去。 对应make zImage Image,zImage 都是二进制文件,都可以之间使用. make = make zImage + make modules uboot 到内核 ========================================== uboot: common/cmd_bootm.c: do_bootm() | | v lib_arm/armlinux.c: do_bootm_linux() | | thekernel() 把0x30008000强制转换成一个函数指针,然后调用. v ------------------------------------------------------------- kernel: | | | | | v | arch/arm/boot/compressed/head.S 压缩内核 v arch/arm/kernel/head.S 非压缩内核 uboot在调用0x30008000时需要调用3个参数 Documentation/arm/Booting - CPU register settings r0 = 0, r1 = machine type number discovered in (3) above. machine类型smdk2410 r2 = physical address of tagged list in system RAM. 内核参数列表 taglist taglist: +---------+ | size1 | 这个大小包括自己本身 +---------+ | type1 | 32位整形 +---------+ | ...... | | ...... | +---------+ | size2 | +---------+ | type2 | +---------+ | ...... | | ...... | +---------+ | size3 | +---------+ | type3 | +---------+ | ...... | 各种类型都在内核中定义 include/asm-arm/setup.h | ...... | +---------+ uImage ------------------------------ uboot在使用bootm命令时boot一个东西时并不知道那个是什么东西,它可以是一个应用,也可以是一个操作系统.从上面的linux的例子可以看到操作系统在启动时需要一些参数,所以在跳到具体代码时前是需要做一些别的东西的.所以使用bootm命令的对象需要在要运行的东西前面加一个头,标记这个程序的一些信息. 这样组合在一起就是一个uImage +-----------+ . |-----------| \ |---header--| 0x40B 固定长度. |-----------| / +-----------+ ' | | | | | zImage / | | Image | | | | | | | | | +-----------+ uboot提供了mkimage工具让我们方便制作uImage, 代码在tools/mkimage.c mkimage -A 体系结构 arm -O 操作系统 linux -T 类型,是内核还是应用 kernel -C 是压缩文件还是一般文件.注意我们的zImage不是一个压缩程序,它只不过是包含了一个压缩了的内核,它本身是一个自解压程序.如果传进去的是piggy.gz就应该指定压缩类型. none/gzip/bz2 -a 把这个东西放到内存的那个地方. -e zImage的首地址,如果-a指定的是0x30008000,那么-e就应该是0x30008040 -n 随便一个名字 -d 输入文件 例子: mkimage -A arm -O linux -T kernel -C none -a 0x30008000 -e 0x30008040 -n "kernel" -d zImage myImage(输出文件名) 移植文件系统 ========================================== 1. 制作文件系统镜像(看前面的笔记) 2. 编译移植busybox(基本命令) 3. 拷贝相应的库(命令需要的库) 嵌入式系统 1. bootloader, uboot 2. linux kernel 3. file system (镜像,busybox,库) 为什么要有文件系统? 判断系统是否起来,标准是是否能和用户交换。而用户和内核的交换只能通过系统调用,而系统调用又是通过应用程序来实现,但应用程序是在文件系统里面的,所以文件系统是必须的。 busybox配置编译 ------------------------------------------------ make clean make distclean make menuconfig make make install 选项中主要包含两个选项: 1. Busybox Settings 对busybox编译的选项,需不需要debug的选项,静态编译,动态库等等,其中Installation option可以选择编译完成后放在PC的那个地方。 2. Applets 应用选择 必选: 1. Busybox Settings ---> General Configuration ---> [*] Support for devfs (NEW) 设备文件系统 [*] Use the devpts filesystem for Unix98 PTYs (NEW) Build Options ---> [*] Do you want to build BusyBox with a Cross Compiler? (/usr/local/arm/3.4.1/bin/arm-linux-) Cross Compiler prefix 指定交叉编译器 2. Applets Init Utilities ---> [*] init Shells ---> [*] ash 先选中了ash,然后再修改上一个选项,选择默认使用ash Choose your default shell (ash) ---> ash make TARGET_ARCH=arm 给TARGET_ARCH这个Makefile变量赋值 make install 默认安装在./_install/ make install PREFIX=../ 或者在配置中设置Busybox Setting -->installation options 都可以更换安装目录. busybox bug: ------------- bug: 在没有选择loopback设备时选择mount会出错 mount.c: ENABLE_FEATURE_MOUNT_LOOP if (rc && loopFile) { | v if (rc && loopFile && ENABLE_FEATURE_MOUNT_LOOP) { -------------------------------------------------- 注意: 1. 制作镜像,见前面笔记。 在把busybox拷贝到目标镜像后,一定要建一个dev文件夹,不然内核启动不了。 2. 在修改了config后,要make clean后重新编,不然中间文件不会重新编译。 3. 注意自己要编程静态的还是动态的Busybox Sections ---> Build Options --->静态库 一般我们是使用动态编译,因为这样会小很多,但是这样的话,我们就要把编译器目录下面的 /usr/local/arm/3.4.1/arm-linux/lib/库复制到板子下面的/lib 4. 在添加insmod的时候这两个选项是互斥的,所以只能选一个 [ ] Support version 2.2.x to 2.4.x Linux kernels [*] Support version 2.6.x Linux kernels (NEW) 其他: 1. 要想让机器起来干某些事情: /etc/init.d/rcS 修改权限为可以执行 #!/bin/sh echo "_______ ^_^ ________" mount -t proc none /proc 2. 挂载proc文件系统 mount -t proc none /porc 没有proc ps/mount都不能使用,所以要先mount proc 不要选择Linux System Utilities --->Support for the old /etc/mtab file现在已经不用mtab了 3. mount /dev/mtdblock/2 /temp -o rw 读写挂载。 调试环境: 去除可执行文件中的调试信息 strip 这样就要注意了,busybox和动态库的版本要匹配 3.4.1 动态库和动态加载器 cp /usr/local/arm/3.4.1/arm-linux/lib/* /armroot2/lib -a uboot的编译器 busybox的编译器 动态库 内核的编译器 gdb ================================================== 1. 调试信息的多少,g3最多调试信息 gcc -g1/g2/g3 test.c -o test 2. 一些有用的命令 list/l 看某行代码 info breakpoints 看当前有那些断点 del 删除某个断点 ptype <变量> 看某个变量的类型 where 看gdb现在在那一行 3. 要看调用当前函数的函数某个局部变量 up ---> 去上一级栈,然后就可以p出想要的变量值了。 down ---> 在看完后记得回到当前这层栈。 4. finish运行完当前函数. 加-g后,调试信息不会改变代码的内容,只是另外开了一个段来放这些信息,而且这些信息也只不过是一些符号和地址的对应关系。 远程调试,gdbserver ------------------- 联机调试环境: ------------------------------------- host | tagert ------------------------------------+------------------------------ 程序主人 | 使用者 test (未经strip的文件) | test (可以是已经strip后的文件) test.c(源代码) | gdbserver | | (1) gdbserver : test (2) gdb test (不用加参数) | (3) target remote : | (4) continue | host 是指程序运行的平台; target是指程序解释指令是什么平台的; gdbserver运行在target上面, gdb运行在host上面; A. 所以,编译gdb就是: | host x86 运行在x86上 gdb + | target arm gdb能够认识arm的指令 ./configure --host=i386 --target=arm-linux gdb编译完成之后,会产生很多文件,通常情况下,configure用--prefix指定安装目录,在编译完成之后,用 make install ,就会把所产生的文件拷贝到 --prefix指定的目录下; 所以,通过: ./configure --host=i386 --target=arm-linux --prefix=/usr/local/arm/3.4.1/ 来产生配置文件, 然后: make 再然后: make install 就会将arm-linux-gdb安装到 /usr/local/arm/3.4.1/bin下面; 在x86上,就可以用arm-linux-gdb来运行; B. 编译gdbserver就是: |host arm gdbserver + |target arm ./configure --host=arm-linux --target=arm-linux 但是, 光指定target还不够,因为同一个平台有多种编译器,所以你还得通过CC环境变量指定编译器是谁: CC=arm-linux-gcc ./configure --host=arm-linux --target=arm-linux 但是,仅仅指定编译器是谁也是不够的,因为还有一些编译用到的头文件和链接所用的库没有指定,在通常情况下,configure都是用--exec-prefix指定编译器所在的目录,所以头文件和链接所用的库文件就是在--exec-prefix/include 和 --exec-prefix/lib下面去找, 因些用: CC=arm-linux-gcc ./configure --host=arm-linux --target=arm-linux --exec-prefix=/usr/local/arm/3.4.1/ make 去编译gdbserver. 编译完成之后,将gdbserver copy到板子上,就可以运行了。 gdbserver和gdb之间是用tcp来进行通信的。 gdb 要能够解析target机的指令才能正常工作,这也是可以理解的,体系结构,寄存器都不知道怎么调呢。 而在x86上,无论那个版本的gcc编译出来的程序都可以运行,这是因为在pc上把所有版本的库和动态库加载器都放在/lib中了。 内核编译方法: ======================================================== 第一件事是改Makefile中的体系结果,这关系到内核在编译时采用的很多默认值 ARCH ?= arm CROSS_COMPILE ?= /usr/local/arm/3.4.1/bin/arm-linux- Kconfig语法: ------------------------------------------------- 可以在这个文件中看Kconfig的语法。 kernel-2.6/Documentation/kbuild/kconfig-language.txt 下面是一个自己定义的Kconfig文件: make menuconfig是通过script目录中的工具提取Kconfig的内容显示出来的。 mainmenu "Linux Kernel Configuration" source "arch/arm/myconfig" ---> 用source来include别的Kconfig,注意这个路径要相对于内核源码的根目录 menu "sub menu 1" config SUB1 ---> 在.config中把SUB1中变成CONFIG_SUB1,如果选中了就是CONFIG_SUB=y。 bool "select?"---> 两中状态,y,n(只有选中还是不选)。 default y ---> 默认是选上还是没有选上。 menu "sub sub menu 1" config THREE ---> 三种状态y,m,n tristate "xxx" config CROSS_COMPILE --->这样.config中就是CONFIG_CROSS_COMPILE="/usr/arm/gcc" string "yyy" --->字符串类型 depends on SUB1 --->如果sub1没有被选上,这个选项不会显示出来。 default "/usr/arm/gcc" endmenu endmenu --->每个menu对应一个endmenu menu "sub menu 2" config SUB2 bool "select?" config SUB3 bool "select?" endmenu .config和Makefile是怎样联系起来的呢? Makefile中$(CONFIG_LEGACY_PTYS)是一个变量,而这个变量是在那里定义的呢?没错就在.config中,上层的Makefile会include .config文件,.config的内容是: ~~~~~~~~~.config~~~~~~~~~~~~ CONFIG_UNIX98_PTYS=y CONFIG_LEGACY_PTYS=y CONFIG_LEGACY_PTY_COUNT=256 ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~Makefile~~~~~~~~~~ obj-y += mem.o random.o tty_io.o n_tty.o tty_ioctl.o obj-$(CONFIG_LEGACY_PTYS) += pty.o obj-$(CONFIG_UNIX98_PTYS) += pty.o obj-y += misc.o obj-$(CONFIG_VT) += vt_ioctl.o vc_screen.o consolemap.o \ consolemap_deftbl.o selection.o keyboard.o ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .config刚好就是这些变量的定义,所以obj-$(CONFIG_LEGACY_PTYS)最终会根据有没有在.config中选上而变成 obj-y 或者是 obj-。前者是会被编进内核的,后者不会被编译。 arch/{Makefile第193行:ARCH}/Kconfig | | v .config | 用途1 | 用途2 +--------------+--------------+ | | v v Makefile包含.config .config会在make dep后被转换 来判断是否编译这个 成include/linux/autoconf.h 文件 这个.h里面就是把CONFIG_LEGACY_PTYS=y 转化成#define CONFIG_LEGACY_PTYS 1 这样就可以在代码中引用这种宏来更精密 地控制代码编译。 添加文件到内核: -------------------------------------------------- 1. 增加单个文件: 就在某个目录下面的Makefile中添加 obj-y = test.o,如果 要做成选项,就要在Kconfig中添加相应的config 2. 内核中增加一个目录: a. 先在父目录的Makefile中把整个目录加进来。 obj-$(CONFIG_MYCONFIG) += test1/ b. 再进到子目录加一个Makefile,里面内容是: obj-y += a.o b.o c.o obj-m 编译模块 编译模块: -------------------------------------------------- cat proc/kallsyms 显示所有当前符号,新插的模块符号也会在里面 1. make modules 是只编译模块,而不编译出zImage/Image.但是这样是一次编译完所有的模块 自己模块的Makefile 中要写好: obj-m += test.o 2. 在内核源码的根目录下面,可以给M赋值来指定要编译模块所在的目录 make modules M=drivers/char/temp M是Makefile中的一个变量. 但是我们的代码还是要放在内核源码的目录下面。 3. 我们模块代码放在任意目录下面,-C选项先让make在目录下进行编译模块,到这个目录下主要就是得到根Makefile和一些.h,.config make modules -C /home/arm/armzone/Linux/kernel/kernel-2.6 M=temp 在编译内核模块时,我们只需要.config和.h就足够了。 4. 放到一个Makefile中: 5. 一个模块包含多个文件 一个obj-m 就是一个模块. obj-m = temp.o temp-objs = test.o a.o 注意: 模块的名字temp.ko,模块名字这里不能是test因为已经有一个test.c了.当编译时,先有test.o,a.o如果模块又叫test,这样就是把test.o,a.o编译成test.o,这样就乱了。所以不能有一个源代码的名字和模块名字相同。 6. 多个目录编成一个模块,只能这样了: obj-m = temp.o temp-objs = test.o a.o aaa/b.o KERNELS = ../../kernel-2.6 default: make -C $(KERNELS) modules M=$(shell pwd) cp driver.ko /home/bzli/teaching/4th/arm/ .PHONY:clean clean: rm -f /home/bzli/teaching/4th/arm/driver.ko make -C $(KERNELS) modules M=$(shell pwd) clean -C $(KERNELS) 是在内核源码目录下面找最大的Makefile,再由Makefile找.config,所有内核起码要make menuconfig配置好之后才可以用来编译模块。 字符设备驱动基础: ------------------------------------------ 注意写模块时,内核代码时包含头文件的循序: 1. 2. module_init(init)这个宏接收的函数一定要有一个整形的返回值,不然会insmod可能失败.insmod调用系统调用sys_init_module而这个系统调用会执行init并取得返回值从而告诉insmod是否成功。而且是返回0代表正确,负数代表失败。如果没有返回值,也就是r0不确定。 EXPORT_SYMBOL(test)导出符号。 声明了也出warning是因为,在编译的时候编译器回到内核源码目录下面的Module.symvers看有没有这个符号,所有在这个机器上编译过的模块导出的符号都会在这里面看到。所有当前内核导出符号可以在/proc/kallsyms上面看到,在插入时检查符号,如果缺少符号就会失败。 模块被移除之后不会留下任何东西,包括全局变量等。 模块参数: int a = 999; char *s = "hello"; ----> 注意这是指针而不是数组 module_param(a,int,S_IRUGO); module_param(s,charp,S_IRUGO); insmod test.ko a=999 s="hahah" /sys/module 下面可以找到上面的参数,也可以通过上面第三个参数S_IRUGO/S__IWUSR权限控制这两个参数的权限,实时修改里面的内容。 这个目录下面还有别的信息. 如果把一个模块编译进内核了,在开机后加载,在关机时调用exit函数. 很多时候一个硬件对应多个linux中的设备。 struct cdev { struct kobject kobj; 父类,内核里面主要的数据结果都会有一个kobj struct module *owner; THIS_MODULE 监护人 struct file_operations *ops; struct list_head list; dev_t dev; unsigned int count; }; MKDEV register_chrdev_region cdev_init cdev_add cdev_del ------>顺序要注意,不然有问题 unregister_chrdev_region mknod 不要用fopen来打开设备,它设立了一层缓冲,会出问题。 MODULE_LICENSE("GPL"); 这些东西在编译后会放在一个段里面。 module_info 可以看license这些东西。 kernel-headers 头文件,配置等等。 /lib/modules/2.6.18/build kernel-devel 完整源代码 /lib/modules/2.6.18/source 通过lseek(fd,0,SET_CUR)来得到当前位置 #define __user 是空的,在程序中只是起到注释作用 ssize_t test_read(struct file *fp, char *buf, size_t size, toff_t *off) { memcpy(buf, s+fp->f_pos, size); off = fp->f_pos + size; --> 在read/write函数中不能直接修改fp->f_pos来该文件当前位置,它是只读的。我们应该修改off来反应这个位置的变化。 其他函数,如lseek是可以写的。 } 如果模块中没有实现某些函数,内核会使用一些默认的函数来相应系统调用。 copy_to_user -----> 1. 检查是否用户空间地址,防止黑客. 2. 防止缺页。 返回有多少个byte没有拷贝。 设备,数据的组织: 如果设备和数据分开定义比如: char buf[512]; struct cdev buf_dev; 一个设备可以接收,但是,如果要创建两个这样的设备,就麻烦了: char buf[512]; char buf2[512]; struct cdev buf_dev; struct cdev buf_dev2; 一方面不好看,另外一方面在read/write等函数中对不同的buf操作时就要判断了,这样的设备只能创建一次。 设备更应该用面向对象的方法来实现,所以应该: struct buf_dev_t { struct cdev dev; char buf[512]; }; 这样的话物理创建多少个对象都没有问题。而在write/read等方法就需要根据file->private_data来传递, 在open的时候从inode->cdev取得对应的buf_dev_t,再把这个结构体的地址放到file->private_data中。这样read/write操作时就可以根据file来得到buf_dev_t了。 对于设备文件,在磁盘上面inode只保存了这个设备的设备号。在open这个设备时就会建立inode->cdev printk: -------------------------- printk根据下面的级别会打到当前tty上面的,但是tty7被x用了,所以一般打不出来. firefox |-------->虚拟终端和tty7被x切断了关系 xclient | xserver | tty7 printk 8个级别,级别越小,priority越高 /proc/sys/kernel/printk的1-8对应printk里面的0-7 cat /proc/sys/kernel/printk 6 4 1 7 6: 当前日志级别 ------> 会打印到当前tty里面,同时记录在/sys/var/log 4: 默认 ------> printk没有指定的级别 1: 最小允许的级别 7: 启动日志级别 可以这样改写它: echo 4 6 > /proc/sys/kernel/printk result: 4 6 1 7 echo 8 > /proc/sys/kernel/printk result: 8 6 1 7 fcntl vs ioctl ---------------------------------- fcntl 更多是针对文件的,不过也有一些对设备的影响,它主要是影响struct file。修改打开方式,文件锁,等等。 ioctl 只对设备文件,而具体作用是不同驱动有不同实现,它实际上是做一些功能扩展,file_operations不能包含所有的功能。 /usr/include/sys/这是编译器的包含头文件,一般在完成驱动之后应该把这个驱动的头文件放在编译器的这个文件中。这个目录的头文件大部分都是从内核中复制过去的。 这样诸如ioctl这种函数的第二个参数,是命令,如果不做成头文件和放在编译器目录,应用在编译的时候就有问题。 ioctl的命令构成: 32位 magic 方向 size num 我们选用命令时应该使用linux/ioctl.h(/usr/include/asm-generic/ioctl.h)中的_IO宏: _IO(magic,num) 只控制了magic,num两个字段。 _IOR/_IOW(magic,num,size) 控制了所有4个字段. 还有更多类似的宏。 可以在Documentation/ioctl-number.txt下面记录了那些magic number被人用了。 对于各种返回值可以在include/asm-generic/errno-base.h定义,perror,errno都是根据这些值来做相应的动作。 /sbin/init 会解析配置文件/etc/inittab,这是init的配置文件,告诉init要干什么事情。 2.6 进程相关: ====================================================== 每个时钟中断都会调用do_timer,注意这个timer和cpu频率FCLK没有直接关系,它是另外一个时钟节拍,用于调度,调度依赖于时钟中断。 意思就是每个进程可以持续运行多长时间,就是多少个时钟中断。 10ms 最小时间片 100ms 一般时间片 200ms 最大时间片 task_struct->mm_struct->vm_area_struct就相当于程序的各个段,mm_struct里面的start_code,end_code,arg_start,arg_end,env_start,等等在execX之后就会根据ELF的地址(也就是连接脚本指定的内容)设置这些变量,并建立相应的vm_area_struct。 current当前进程 ---------------------------------- current就是相当于task_struct的指针: #define current (get_current()) | v static inline struct task_struct *get_current(void) { return current_thread_info()->task; } | v static inline struct thread_info *current_thread_info(void) { register unsigned long sp asm ("sp"); ---> 这是一个arm gcc的特性 register unsigned long sp 分配一个寄存器变量,sp和任意一个寄存器关联起来.然后后面的asm("sp")则指定了这个寄存器变量是和那个寄存器关联起来。 return (struct thread_info *)(sp & ~(THREAD_SIZE - 1)); --->8k对齐,取到thread_info的基地址 } .+--------+ kernel sp / | | / | | / | | | | 8k | | | | | | \ | | \ +--------+ \ | | '+--------+ thread_info thread_info这个地址8K对齐所以可以通过sp & ~(THREAD_SIZE -1)来得到这个地址。 struct thread_info { unsigned long flags; /* low level flags */ __s32 preempt_count; /* 0 => preemptable, <0 => bug */ mm_segment_t addr_limit; /* address limit */ struct task_struct *task; /* main task structure */ struct exec_domain *exec_domain; /* execution domain */ __u32 cpu; /* cpu */ __u32 cpu_domain; /* cpu domain */ struct cpu_context_save cpu_context; /* cpu context */ __u8 used_cp[16]; /* thread used copro */ unsigned long tp_value; union fp_state fpstate; union vfp_state vfpstate; struct restart_block restart_block; }; linux中的抢占是由被抢占进程决定的,如果被抢占的task_struct->preempt_count > 0别人是不能抢它的。 内核要配置好之后才能够编译,因为我们需要autoconfig.h等东西 内核中除非内核调用schedule()函数,不然调度只能够发生在返回到用户态的时候。 schedule while(1) { schedule(); } 在调用的时候是从schedule调用到下一个进程,在进程退出的时候也是返回到schedule里面。 s3c2410_timer_interrupt() --> 时钟中断 '-->timer_tick() |-->do_timer() '-->update_process_times() '-->scheduler_tick() 进行调度 1. timer中断引起时间的计算进而进程切换 2. 进程切换只能发生在用户态,除非内核主动调用schedule() 进程切换: ------------------------------------------ kernel/sched.c static inline task_t * context_switch(runqueue_t *rq, task_t *prev, task_t *next) { struct mm_struct *mm = next->mm; struct mm_struct *oldmm = prev->active_mm; if (unlikely(!mm)) { next->active_mm = oldmm; atomic_inc(&oldmm->mm_count); enter_lazy_tlb(oldmm, next); } else switch_mm(oldmm, mm, next); --> 换页表 if (unlikely(!prev->mm)) { prev->active_mm = NULL; WARN_ON(rq->prev_mm); rq->prev_mm = oldmm; } /* Here we just switch the register state and the stack. */ switch_to(prev, next, prev); --> 更换寄存器。当进程被换出之后寄存器保存在thread_info->cpu_context_save当中 return prev; } 1. 先找到要运行的下一个进程的task_struct。 2. context_switch被调用,切换页表task_struct->mm_struct->pgd,用switch_mm切换页表。 3. 用switch_to将当前进程的寄存器的值保存在thread_info->cpu_context_save中,然后把下一个要运行的thread_info->cpu_context_save换到cpu里面,也就是寄存器。 __switch_to最后定义在arch/arm/kernel/entry-armv.S penny 互斥,竞态 ============================= 在内核态中竞态发生在: 1. 普通的内核代码与抢占进程的内核代码之间。 (禁抢占, 信号量) --> 1.preempt_disable 2.spin_lock 3.semaphore 2. 普通的内核代码于中断处理的内核代码之间。 (禁中断, spin_lock_irq) --> 1.local_irq_disable 2.spin_lock_irq 3. 普通的内核代码与内核线程之间。 (信号量,互斥体) 4. SMP(对称:处理器的型号一样),中普通内核代码和普通内核代码之间。 (信号量,互斥体,自旋锁) 5. 中断嵌套 spin_lock_irq只能在中断里面禁止中断了 6. 软中断 spin_lock_bh 原子变量: ------------------------ a = a + b 这句c语言在汇编里面是分成好几句的。在这期间可能会被中断等竞态打断。 原子变量就可以处理这种简单的整形操作。 atomic_t a,b; atomic_set(&a,1); atomic_inc_and_test(&a) //if(!++a) atomic_dec_and_test(&a) //if(!--a) atomic_sub_and_test(5,&a) //if(!(a -= 5)) atomic_add_return(5,&a) //a += 5, return a atomic_sub_return(5,&a) //a -= 5, return a atomic_inc_return(&a) //return ++a atomic_dec_return(&a) //return --a 自旋锁: ------------------------- 用于SMP中,自旋锁内部是会禁用抢占的。 变种spin_lock_irq还可以禁中断。 local_irq_disable ---> 设置cpsr的i位。 local_bh_disable ---> arm中的swi指令,但是因为swi不像irq那样有东西禁用,内核只是在swi发生时不去相应而已。 禁用了irq就不可能发生bh(软中段),这是因为bh的触发条件是从中断返回,所有根本没有方法触发。 spin_lock_irq => preempt_disable() + local_irq_disable() + 相当于local_bh_disable() spin_lock_bh => preempt_disable() + local_bh_disable() local_bh_disable() local_irq_disable() spin_lock => preempt_disable() 编译内核的时候的抢占选项和preempt_disable中的thread_info->preempt_count没有任何关系,在没有选择抢占选项preempt_disable也是会起作用的,就是不要换出我这个内核进程。而抢占的内核选项只是影响调度部分的代码。 schedule 调度器是一定会重新开中断的。 irq_save版本 信号量 -------------------------- 在持有自旋锁的时候是不允许被调度出去的,也许可以被中断。 但是在持有信号量的情况下是可以被换出的。 sem_init(&sem,1); down/down_interruptible up 对于锁一定要注意在函数各个出口前up掉 spin_lock down 1. 降低cpu反应速度 适用于一般内核代码 2. 适用于短小紧急的代码 spin_lock_irq 适用于和中断之间的互斥。 注意,在中断之中不能够使用信号量,因为信号量有可能让中断处理例程挂起,而中断处理例程有不能参与调度,所以挂起之后就回不去中断处理函数了。 完成量 -------------------------- 完成量和信号量的区别: 基本上是一样的,两者都可以完成下面的工作: 进程A 中wait_for_completion / down_interruptible 进程B 中complete / up case 1: ./A 等待 ./B 唤醒A ./A 再运行A,在等待 ./B 唤醒A ...... 一次等待,一次唤醒 case 2: ./A 等待 ./A 运行第二个A,多一个进程等待 ./A 运行第三个A,多一个进程等待 ./B 把第一个A唤醒了,其他的都在等待 ./B 把第二个A唤醒了 ...... 在这个情况下一个complete可以用无数次 区别在于: case 3: ./A 等待 ./A 运行第二个A,多一个进程等待 ./A 运行第三个A,多一个进程等待 ./B 调用complete_all,而不是complete 这样所有的A都被唤醒了。但是从此以后这个完成量就没有用了,来多少个A都不会等待了。 信号量要实现这个东西比较麻烦,完成量就比较方便。 等待队列 ------------------------------ 1. 等待队列 (wait_queue_head_t/wait_queue_t) 2. select 阻塞型I/O (用户空间:select 内核空间:a. 定义两个等待队列,用于读和写 b. 实现file_operations中的poll函数,用poll_wait把队列加进去 c. 判断读写,适当返回标志。 3. 异步通知: 用户空间:fcntl(fd,F_SETFL,O_ASYNC); fcntl(fd,F_SETOWN,getpid()); sigaction 内核空间:fasync, 得到fasync struct kill_fasync发送信号 用fasync_helper清除 wait_event(head,condition)/wait_up 当有人调用了wait_up并且wait_event的条件满足了才会醒来,如果被wait_up了,但是条件没有满足的时候,进程会继续进入挂起。 +-----------------+ |wait_queue_head_t| +-----------------+ | task_struct v +------+ .-------. condition A| A |<-----+ | wait_queue_t +------+ '-------' | task_struct v +------+ .-------. condition B| B |<-----+ | wait_queue_t +------+ '-------' | v ...... #define wait_event_interruptible(wq, condition) ({ int __ret = 0; if (!(condition)) __wait_event_interruptible(wq, condition, __ret); __ret; }) #define __wait_event_interruptible(wq, condition, ret) do { EFINE_WAIT(__wait); for (;;) { prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE);-->prepare_to_wait 调用list_add,就是把新来的wait_queue_t放在链表头!! if (condition) break; if (!signal_pending(current)) {-->看是不是因为信号导致进程的醒来 schedule(); -->要让一个进程睡眠,不单要把它的状态改成TASK_INTERRUPTIBLE,而且还要调用schedule continue; } ret = -ERESTARTSYS; break; } finish_wait(&wq, &__wait); -->对应于prepare_to_wait,就是把状态改成TASK_RUNNING,然后把wait_queue_t从队列中删除。 } while (0) #define DEFINE_WAIT(name) wait_queue_t name = { .task = current, .func = autoremove_wake_function, .task_list = { .next = &(name).task_list, .prev = &(name).task_list, }, } wait_event 的可移植性要好一些。不过prepare_to_wait,finish应该也可以。 每进程可以有自己的condtion,这样就看wait_up这一边设置什么条件了。很方便。 惊群问题,如果实际上我们只想唤醒一个到几个,但是在wait_up时会把所有在这条queue上的进程都唤醒。但是最后我们只想有一个醒来,这样的做法就是让所有进程醒来其中一个把条件设回不成立,这样是可以做到,但是cpu消耗很大。 可以通过设置wait_queue_t->flag = WQ_FLAG_EXCLUSIEVE来让wait_up不一次过把所有的进程都叫醒. 而是在唤醒地一个具有WQ_FLAG_EXCLUSIEVE标志的进程之后停止唤醒其他进程。 +------------+ | WQ head | +------------+ | v .-------. | | A | flag = 0 依次叫醒,直到遇到第一个有WQ_FLAG_EXCLUSIEVE的进程,它后面的进程就不会被唤醒 | '-------' | | | v | .-------. | | B | flag = WQ_FLAG_EXCLUSIEVE v '-------' | v .-------. | C | flag = 0 '-------' | v ...... if(!signal_pending(current)) { ... } 这句是用来判断醒来的原因,是不是因为一个信号的到来。这是在状态设成TASK_UNINTERRUPTIBLE后,进程的醒来一般就是有人wait_up了,或者有人给这个进程发了个信号。(INTERRUPTIBLE就是指可不可以处理到来的信号,信号中断) 1. 持有信号量不能睡眠 2. 持有自旋锁不能睡眠 3. 等待队列不能在用在中断上下文中 我们的驱动中一般都需要实现read/write的O_NONBLOCK selet / poll ------------------------------- unsigned int test_poll(struct file *fp, struct poll_table_struct *table) { int mask = 0; struct test_cdev *dev = fp->private_data; poll_wait(fp, &dev->rq, table); --->把设备有的等待队列加到table中 poll_wait(fp, &dev->wq, table); 以便在do_poll中放弃cpu. 其实 已经在这里把当前进程添加到等待队列了。 见__pollwait(), 这个函数是不会阻塞的 if (dev->r != dev->w) { -->判断这个设备是否可以读IN=可读 mask = POLLIN | POLLRDNORM; // read } if (!((dev->w == (dev->r - 1)) || (( dev->w == (BUF_SIZE - 1)) && (dev->r == 0) ))) { -->判断这个设备是否可以写OUT=可读 mask |= POLLOUT | POLLWRNORM; // write -->!!!!注意这里一定要用 |= 而不是赋值,因为可能上面 } return mask; } 这个函数会进来多次,因为如果当前进程被唤醒了,会再次进来这里判断是否有东西可读或可写。 poll有两个作用: 1. 把当前进程添加到所有等待队列当中。 2. 判断这个设备是否有东西可读可写。 让内核可以向用户发送 ------------------------------- 想要在自己的驱动中向用户发送信号,在自己的应用中接收这些信号就需要下面这样做: 应用程序: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ void my_action(int sig, siginfo_t *info, void *data) { printf(" %d sent to us.\n", info->si_pid); } int main(int argc, char **argv) { int fd, ret; char buf[10]; struct sigaction new; memset(buf, 0x00, 10); if (argc < 2) { fprintf(stderr, "Usage: %s \n", argv[0]); return 0; } fd = open(argv[1], O_RDWR | O_NONBLOCK); fcntl(fd, F_SETFL, O_ASYNC); fcntl(fd, F_SETOWN, getpid()); new.sa_handler = my_action; sigemptyset(&new.sa_mask); new.sa_flags = SA_SIGINFO; sigaction(SIGIO, &new, NULL); ret = write(fd, "abcde", 5); printf(" 1st write: %d bytes \n", ret); while(1); close(fd); return 0; } 1. 调用fcntl(fd, F_SETFL, O_ASYNC);和fcntl(fd, F_SETOWN, getpid());来让应用程序可以接收驱动发来的信号。前面一句fcntl(fd, F_SETFL, O_ASYNC);实际上就是调用fd对应设备驱动。后面一句fcntl(fd, F_SETOWN, getpid());就是设置这个设备发出来的信号都送到那个进程。每个设备只能够关联到一个进程来接收它的信号。 2. new.sa_handler = my_action;sigemptyset(&new.sa_mask);new.sa_flags = SA_SIGINFO;sigaction(SIGIO, &new, NULL);设置好信号处理函数。 内核驱动: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ struct test_cdev { char buf[BUF_SIZE]; int w, r; dev_t no; struct cdev dev; struct semaphore sem; wait_queue_head_t rq; wait_queue_head_t wq; struct fasync_struct *fasync; }; struct file_operations test_ops = { .open = test_open, .release = test_release, .read = test_read, .write = test_write, .poll = test_poll, .fasync = test_fasync, }; int test_fasync(int fd, struct file *fp, int mode) { struct test_cdev *dev = fp->private_data; return fasync_helper(fd, fp, mode, &dev->fasync); } int test_release(struct inode *no, struct file *fp) { struct test_cdev *d; d = container_of(no->i_cdev, struct test_cdev, dev); fasync_helper(-1, fp, 0, &d->fasync); printk("test release.\n"); return 0; } ssize_t test_write(struct file *fp, char *buf, size_t size, loff_t *off) { ...... kill_fasync( &dev->fasync, SIGIO, POLLIN | POLLOUT ); return ret; } 驱动里面关于这个信号的函数涉及到fasync,release,在具体操作里面发送信号。 1. 实现file_operations里面的fasync函数。在这个函数里面就是通过fasync_helper内核函数来取得一个fasync_struct结构体,每个进程都有一个这样的结果,fasyn函数是在用户进程调用fcntl(fd, F_SETFL, O_ASYNC);是在内核中被调用的。这个结构体就是用来把要发什么信号,信号发给谁这所有一切都关联起来的。 2. 在file_operations里面的具体操作中比如:write/read等操作中调用kill_fasync来把那个信号发给fcntl(fd, F_SETOWN, getpid())所设置的用户进程。 3. 在file_operations里的release函数,我们要把用完的fasync_struct结构题归还给内核,也是调用fasync_helper,描述符-1代表归还操作 延时 -------------------- 长延时: 1. 忙等待 2. time_before() + schedule() 3. wait_event_timeout()/schedule_timeout() 短延时: udelay 是死循环,精度要看体系结构,arm就是FCLK(200M),最精只能到16ns mdelay 一般是用在中断中,因为他不会休眠。也用在控制硬件的时序上面。它是精确的。 ndelay ssleep 会休眠,不是很精确。 msleep time_before(to,jiffies) 判断to这个时钟滴答是否在当前jiffies之前。时钟滴答使用个n*HZ time_after framebuffer 一般是双buffer,一个用来写,另外一个正在被显示,保证不会一个正在写的buffer会被拿来显示。 时钟 -------------------------- 应用:只能够用alarm信号或多线程来模拟时钟。 内核:是通过timer定时器来实现的。 关于时钟,这里有几个概念: FCLK;timer4;jiffies;HZ;rtc 1. FCLK: arm920t的主频,因为5级流水线,所以每条指令会用5个时钟周期,只不过叠加起来就像一个时钟周期一样。所以这里是指令的时钟频率。一天arm在运行都有这个东西,uboot,linux当然也需要。 2. timer4: 是操作系统使用的,uboot这种东西不需要,因为这个时钟的本质作用是帮助调度。这个timer的频率就是HZ 3. jiffies: 这就是滴答数,开机以来timer4跳了多少下,所以 <<开机以来的秒数=jiffies/HZ>> 4. HZ: 就是timer4的频率,x86一般是1000。arm上面用200,就是一秒跳200下。 5. rtc: 强钟。当禁止中断的时候timer4中断不会被处理,那么jiffies在开中断后值是错的,这时候内核需要rtc这个时钟来修正jiffies。rtc是用电池供电,不受操作系统影响。在s3c2410里面。 FCLK=200M左右,也就是16纳秒跳一下。而HZ=200,也就是0.005秒,所以一个时钟滴答足够运行很多条代码了。 struct timer_list my_timer; my_timer.function = 在模块的exit函数的时候记得去掉所有timer,当timer到期,要执行相应的事,但是模块已经被移除了,系统将崩溃。 timer到期后timer会先从内核timer list中删除,然后执行timer->function(),所以不用手动删除timer. 一般用init_timer(),然后add_timer()就足够了。 init_timer(&my_timer); add_timer(&my_timer); del_timer(&my_timer); --> 在到期之前驱动想取消这个timer就要用这个东西 mod_timer(&my_timer); --> 在修改后timer重新 内核绝大部分都运行在svc模式 时钟是中断上下文。 如果没有宿主进程,也就是不是一个用户进程通过系统调用引发的一个内核函数。 休眠的本质就是把一个task_struct 放在一个wait queue中,把它的状态改成interruptible/uninterruptile 使用调度器schedule()来选另外一个进程执行。 tasklet 它的作用是在一个模块中初始化一个tasklet,在另外一个模块启动这个tasklet,比如一个光驱有两个驱动,一个是块设备读光盘,另外一个是马达 用处不大。 工作队列 可以休眠,一般是配合一个timer或一个硬中断来使用。 +----+ +----+ +----+ +----+ | |上半部 irq | | tasklet| | timer| | +----+ +----+ +----+ +----+ +----+ +----+ +----+ +----+ | |下半部 | | | | | | | | | | | | | | | | | | | | | | work queue | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +----+ +----+ +----+ +----+ 工作队列不能用等待队列 一个触摸屏的例子,设一个timer在背后定时采集触摸屏,而不是采用中断,当定时器起来之后用一个work queue来完成采集工作。 当内核栈有8k,timer,tasklet,work struct 等等都是利用当前进程thread_info的内核栈。 如果是4k,则用内核专门分配的一个per cpu的页来作为内核栈。 内存管理: ---------------------------------- 页表的创建和管理并不算是内存管理,它只是为mmu服务。 虚拟地址 mmu 物理地址 +---+ (2) 0x34000000 +---->| |struct page +-------+4G............+-+.............+---------+ | +---+ | | (1) | | | 4 k |-----+ | | 内核 | | | +---------+ v | | | | | 4 k |-----+ +---+ | | | | +---------+ +---->| |struct page | | | | | 4 k | +---+ | | --------| |------------ +---------+ | +-------+3G. / ------| |-------------| | v ./ / | | | ...... | / / | | | | +-------+3G/ / | | | 64 M | ...... | | / / . | | | | | 应用 |/ / . | | | | | | / . | | | | | | | / . | | | | v | |/ . | | +---------+ +---+ | | . | | | 4 k |---------->| |struct page +-------+0G . +-+.............+---------+ +---+ 0x30000000 (1) 内核在启动时会把全部的物理内存都映射了(假如物理内存小于896M),但是这个映射并没有计算在page结构体的count中。 (2) 也是在内核启动时对应每个4k的物理页会建立一个page结构体来与之关联,方便管理。这里没有896M的限制,有多少建多少。 (3) 内核在启动的这个映射的虚拟地址和物理地址之间只存在一个偏移,可以用宏__pa(kaddr)来把这部分内核的虚拟地址(3G~3.064G)转换成物理地址,当然也可以用宏__va(phys)来把物理地址转换成对应的内核虚拟地址。 __pa 实质上是 #define __virt_to_phys(x) ((x) - PAGE_OFFSET + PHYS_OFFSET) 而PAGE_OFFSET=0xc0000000 PHYS_OFFSET=0x30000000 __va 实际上是 #define __phys_to_virt(x) ((x) - PHYS_OFFSET + PAGE_OFFSET) 再次强调这两个宏只能用在内核虚拟地址和物理地址之间的转换。 (4) 内核动态分配空间的过程就是在page链表中找没有被使用的页,page结构体中的_count用来标识一个页有没有被使用,应该用宏page_count来读取这个值。内核就是寻找一个count为0的页再找到对应的物理地址来填写页表,而page到物理地址的转换是通过page_to_pfn来得到: #define page_to_pfn(page) (((page) - mem_map) + PHYS_PFN_OFFSET) mem_map是page链表的头,(page) - mem_map就是第几个page结构,也是对应物理上第几个页,而PHYS_PFN_OFFSET是物理地址开头的. #define virt_to_page(kaddr) (pfn_to_page(__pa(kaddr) >> PAGE_SHIFT)) #define pfn_to_page(pfn) ((mem_map + (pfn)) - PHYS_PFN_OFFSET) 可以把内核虚拟地址转换成对应的page结构。 (5) 用户空间分配内存其实也是一样,只不过是内核帮它寻找可用的页。还要帮它修改页表(当然修改页表是在缺页的时发生的). /proc/sys/vm/overcommit_memory=2 可以修改内存分配策略,让内核在分配用户空间时直接得到内存而不会发生缺页。 4G .+--------+ +--------+ / | 80M | ---------------------> | | 128M +--------+ \ | | \ |||||||||| \ | | '+--------+. \ . | | / | | \ \ . | | 896M | | \ \ . | | \ | | \ \ . | | 3G '+--------+. \ \ . | | | | \ \ \ . | | | | \ \ \ | | | | \ \ \ | | | | \ \ '-----------> | | | | \ \ | | | | \ '-------------- +--------+ | | \ | | | | \ | 896M | | | \ | | 0G +--------+ '-------------- +--------+ (1) 在物理内存大于896M的情况下内核虚拟地址的前896M是和物理内存的前896M一一映射起来.这就是在启动的时候建立的内核页表 (2) 而内核虚拟地址的后面128M有80M左右用在动态映射.因为前面只映射了896M,在mmu开着的情况下没有页表是不可能访问物理内存的,而虚拟地址现在又不够了,所以只能通过后80M来进行动态的映射,动态的修改页表。比如要访问160M的内存,就只能先映射前面用到的80M,再在用到后面80M的时候才修改映射。 在钱896M的内存中,因为内核已经建立好映射了,所以尽管没有分配内存也是可以直接访问的,不像用户程序那样会出段错误。有这样一个实验: 1. 在uboot中在一个物理地址0x33fd10000上面写入一些东西。 2. 在linux中用__va找到对应的内核虚拟地址,接访问,可以看到在uboot中写入的东西. 几对有用的分配内存的函数,而且下面函数分配的内存在物理上也是连续的: 1. kmalloc(bytes,GPF_) ------- kfree(addr) 最大只能分配128K的内存. 2. alloc_pages(GPF_,order) ------- __free_pages(page) 返回对应的page结构体. 3. __get_free_pages(GPF_,order) ----- free_pages(addr) 返回对应的内核虚拟地址. order是2^order那么多个byte 3是由2来实现,而1是由3来实现的。 这3组函数中都需要一个GFP flag,主要用到的两个是GFP_KERNEL 和 GFP_ATOMIC: GFP_KERNEL的意思就是在分配内存时运行分配函数的休眠(之所以要休眠是因为如果页不够了,需要释放或交换一些页来满足需求),这个适用于一般内核代码。 GFP_ATOMIC的意识是如果分配失败了,就不休眠,而是直接返回,这个适用在中断上下文里面。 -------------------------------------------- 中断上下文一般不要超过20ms,对应于arm中的16ns一条指令,也就是大概100000条指令。 kmalloc,alloc_pages,__get_free_pages有一个问题是它们不能分配很大的内存,这是因为它们会分配连续的物理内存,如果没有那么大的连续物理地址,它们也只能返回失败。所以有了vmalloc,vmalloc可以把零碎的页映射到连续的虚拟地址中,可以当连续的内存来使用。 ===========PC机的情况: +----------+ | 保留区 | +----------+ <--- FIXADDR_TOP 0xfffff000 | 专用页面 | 3M +----------+ <--- FIXADDR_START 0xffc56000 | 高端内存 | 4M +----------+ <--- PKMAP_BASE 0xff800000 | |||||||||| 8K +----------+ <--- VMALLOC_END 0xff7fe000 | | | vmalloc | 111M | | +----------+ <--- VMALLOC_START 0xf8800000 | |||||||||| +----------+ 3G + 896M | | | | ...... | | | | +----------+ 3G 0xc0000000 物理内存只是分为normal(一般内存),high(高端内存) vmalloc优先从物理内存的高端内存分配,当根本没有高端内存的时候,再在一般内存中分配。 pkmap(高端内存)就一定在高端内存中分配, ===========arm的情况: +----------+ 256M |||||||||||| 0xe0000000 +----------+ | |\ 440M | vmalloc | \ (2) | |\ \ 0xc4800000 +----------+ \ \ 8M |||||||||||| \ \ 0xc4000000 +----------+ \ \ +----------+ | | \ \ | | +----------+ \ '-----> +----------+ 64M | 4k | ----\------> | 4k | <-----. +----------+ \ +----------+ 64M | | | \ | | | +----------+ '---> +----------+ | | 4k | -----------> | 4k | <--. | +----------+ (1) +----------+ | | | | | | | | ....... ........ | | | | | | | | 0xc0000000 +----------+ +----------+ | | | | +----------+ | | | | | | +----------+ | | | 4k | --------------------------------' | +----------+ | | | | +----------+ | | 4k | -----------------------------------' +----------+ (3) | | ...... | | +----------+ x86 vs s3c2410 arm --------------------------- s3c2410最多只能配置256M的sdram,尽管再加上别的什么rom等等都没有896M,所以,s3c2410直接没有高端内存,专用页面,保留区等东西,只有vmalloc了,而且vmalloc也只能映射内核已经映射过的物理内存了。 有了vmalloc后一个物理内存有可能有3个映射: (1) 内核启动时做的全部内存的映射。 (2) 当用vmalloc分配一个空间时,建立了一个映射。 (3) 如果用户空间使用mmap映射了这部分vmalloc分配的内存,用户0~3G的地址也会有一个映射。 vmalloc会使用__GFP_HIGHMEM,这个标记告诉内核在分配的时候可以分配高端内存,但是还是优先分配一般的内存。如果没有这个参数是不会从高端内存中分配的。 I/O 的映射 -------------------------------- I/O 内存和一般内存的区别是在I/O内存的页表里面条目中,B C位不设,不能用cache,write buffer. I/O 端口,是x86这种分开编址的平台上才有的,因为0-4G都用在访问内存,没有更多的地址来映射物理内存。外围的寄存器,需要用另外一条总线,io总线来,用特殊指令inb,outb等来访问。I/O端口就是这条总线的地址。 /proc/iomem可以参考现在有那些io内存已经被系统映射了。 静态映射,系统已经映射好的: ----------------------------------------- 两个文件: arch/arm/mach-s3c2410/s3c2410.c arch/arm/mach-s3c2410/mach-smdk2410.c 在s3c2410芯片上面的器件,比如lcd,uart,gpio等就在s3c2410.c这个文件中. 在smdk2410板子上的器件的映射就在这里,现在只有显卡被映射了。 如果我们要添加一个rom或者添加一个nor flash,那肯定是添加到板子上,不可能放到s3c2410芯片里面,可以在smdk2410里面添加。 ======== arch/arm/mach-s3c2410/s3c2410.c ========== static struct map_desc s3c2410_iodesc[] __initdata = { IODESC_ENT(USBHOST), IODESC_ENT(CLKPWR), IODESC_ENT(LCD), IODESC_ENT(UART), IODESC_ENT(TIMER), IODESC_ENT(ADC), IODESC_ENT(RTC), IODESC_ENT(IIS), //for IIS sound driver IODESC_ENT(GPIO), //for IIS sound driver IODESC_ENT(WATCHDOG) }; struct map_desc { unsigned long virtual; ---> 映射到那个虚拟地址 unsigned long physical; ---> 物理地址 unsigned long length; ---> 映射多长 unsigned int type; ---> type决定页表项的 B C 位 }; #define IODESC_ENT(x) { S3C24XX_VA_##x, S3C2410_PA_##x, S3C24XX_SZ_##x, MT_DEVICE } ======== arch/arm/mach-s3c2410/mach-smdk2410.c ======== static struct map_desc smdk2410_iodesc[] __initdata = { /* nothing here yet */ {vSMDK2410_ETH_IO, pSMDK2410_ETH_IO, SZ_1M, MT_DEVICE}, }; 用ioremap完成动态io内存映射 -------------------------------- .--------------------. 1. | request_mem_region |--> 如果没有被人映射这段物理地址的话就可以使用,它会体现在/proc/iomem '--------------------' | v .--------------------. 2. | ioremap | '--------------------' | v .-----------------------. 3. | ioread8() iowrite8() | | ioread16() iowirte16()| | ioread32() iowrite32()| '-----------------------' | v .--------------------. 4. | iounmap | '--------------------' | v .--------------------. 5. | release_mem_region | '--------------------' 1. #define request_mem_region(start,n,name) __request_region(&iomem_resource, (start), (n), (name)) 申请一段从start开始的,大小长度为n那么大的物理地址,name会出现在/proc/iomem中 2. ioremap默认是不带缓存的,即清除页表的 B C位。ioremap == ioremap_nocache cookie = 物理地址 size = 物理地址的大小 #define ioremap(cookie,size) __ioremap(cookie,size,0,1) #define ioremap_nocache(cookie,size) __ioremap(cookie,size,0,1) #define ioremap_cached(cookie,size) __ioremap(cookie,size,L_PTE_CACHEABLE,1) #define iounmap(cookie) __iounmap(cookie) 从/proc/iomem可以看到默认情况下sram没有被映射,所以我们可以给这段快速的内存做一个驱动,用来做一个设备的缓存。这时候我们就应该选择ioremap_cached设置页表的 B C 位. 这里还要注意,ioremap的起始地址一定要4k对齐。 mmap每次调用都会创建一个新的vm_area_struct 1. 普通磁盘文件 ------------------- void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset); 把fd这个文件偏移offset这么多的地址,length那么长的一段内容,映射到start开头的虚拟地址上面去。 length,offset都必须是4096的倍数. 如果start为NULL,表示让内核帮我们选一段地址,从返回值得到。如果不为NULL,用户就指定了这个虚拟地址了。两种情况都要判断返回值。 prot: 这个参数影响这段分配的虚拟地址是否可读可写。 PROT_EXEC Pages may be executed. PROT_READ Pages may be read. PROT_WRITE Pages may be written. PROT_NONE Pages may not be accessed. flag: 这个参数是说我们对这段地址的内容的修改会不会反应到磁盘上面。 MAP_FIXED MAP_SHARED 会反应 MAP_PRIVATE 不会反应 2. 设备文件 ------------------ 利用mmap我们可以在用户空间实现启动所做的事情。 struct vm_area_struct { struct mm_struct * vm_mm; /* The address space we belong to. */ unsigned long vm_start; /* Our start address within vm_mm. */ unsigned long vm_end; /* The first byte after our end address within vm_mm. */ /* linked list of VM areas per task, sorted by address */ struct vm_area_struct *vm_next; pgprot_t vm_page_prot; /* Access permissions of this VMA. */ 这就是页表条目上面的后面一节:后12位包括ap位等 内核帮我们分配一个新的vma,也帮我们连到mm_struct上面,vm_start,vm_end都帮我们选好了(也可能用户选好了).然后在调用到file_operations->mmap,所以我们只要做适当的映射就可以了。 int sram_mmap(struct file *fp, struct vm_area_struct *vma) { vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot); return remap_pfn_range (vma, vma->vm_start, 0x00000, 4096*4, vma->vm_page_prot); } vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot); pgprot_noncached宏用来设置页表条目中不能使用B C位,即不使用cache。 int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr, unsigned long pfn, unsigned long size, pgprot_t prot) 可以使用这个函数帮助我们修改页表,虚拟地址已经在vma里面了。而物理地址用物理页框号来表示。 记得要unmap 一个很好的例子就是sram,映射整块sram用途很大。 nopage: ---------------------------- 24 struct page *my_nopage(struct vm_area_struct *area, unsigned long address, int *type) 25 { 26 struct page *my_page; 27 28 my_page = pfn_to_page(0x0); 29 30 if (type) 31 *type = VM_FAULT_MINOR; 32 33 return my_page; 34 } 35 36 struct vm_operations_struct vm_ops = { 37 .nopage = my_nopage, 38 }; 39 40 41 int sram_mmap(struct file *fp, struct vm_area_struct *vma) 42 { 43 // vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot); 44 // return remap_pfn_range(vma, vma->vm_start, 0x00000, 4096, vma->vm_page_prot); 45 vma->vm_ops = &vm_ops; 46 return 0; 47 } 在用户空间缺页时就会到缺页异常,而如果这个页是一个mmap出来的vma,就调用vma->nopage,而这个函数就是根据address(产生缺页的虚拟地址) 返回一个这个驱动想给它映射的一个物理页,在外面的do_no_page会帮我们建立页表的。 struct page *my_nopage(struct vm_area_struct *area, unsigned long address, int *type) ioremap/remap_pfn_range/nopage ioremap映射的是一段物理地址到内核虚拟地址上面去,这个得到的虚拟地址不能由驱动或用户决定。一般在模块初始化的时候使用。当内核想使用某些io物理地址前页表的建立。 mmap: remap_pfn_range映射一段物理地址到用户空间的虚拟地址上面去,这个虚拟地址可以在mmap的时候用户决定,但是不一定能成功。 nopage也是映射到用户空间,因为一般只有用户空间才会缺页。 中断的使用 ====================================== 申请中断: int request_irq(unsigned int irq, irqreturn_t (*handler)(int, void *, struct pt_regs *), unsigned long irq_flags, const char * devname, void *dev_id) irq 中断号: 不同平台有区别。 .-INTPND -. | | 中断向量表的 irq --> + SUBSRCPND+--> f(I,S,E) ---> 中断号 | | '-EINTPND -' f() 出来的结果都定义在文件include/asm-arm/arch-s3c2410/irqs.h中,方便我们使用 /proc/interrupts看那些中断被使用,触发方式,触发多少次 irq_flags: 这个标志位主要用来控制这个中断号被我注册后能不能再被申请。 SA_SHIRQ irq可以共享,别人还可以申请。 SA_INTERRUPT 在运行中断处理程序的时候禁中断 cpsr的I位。加了安全一点。 SA_SAMPLE_RANDOM 给熵池(随机数池作贡献) devname: 在/proc/interrupts 显示用。 dev_id: 作为一个参数,到时会传递给中断处理函数。如果多个共享irq用的同一个处理函数,我们就可以根据这个 值来判断是第几个共享中断被启动。不过更多是用来传递device数据结构。反正如果要用共享irq,这个值 就不能为空,而且还要不一样。 另外一个用途是free_irq的时候是根据这个dev_id来表示拆掉共享中断那个处理函数。所以共享中断dev_id 不能相同。 在request_irq之前使用,控制触发方式: int set_irq_type(unsigned int irq, unsigned int type) irq: 中断号 type: 触发类型 #define IRQT_NOEDGE (0) #define IRQT_RISING (__IRQT_RISEDGE) #define IRQT_FALLING (__IRQT_FALEDGE) #define IRQT_BOTHEDGE (__IRQT_RISEDGE|__IRQT_FALEDGE) #define IRQT_LOW (__IRQT_LOWLVL) #define IRQT_HIGH (__IRQT_HIGHLVL) #define IRQT_PROBE (1 << 4) 中断处理函数在中断上下文中,不能休眠,而且它的返回值是IRQ_NONE/IRQ_HANDLED,IRQ_HANDLED表示我这个函数响应了这个中断,但是要注意,在一个共享irq上面如果有多个中断处理函数,不管前面中断处理函数有没有响应,所有中断处理函数都会依次被调用。 typedef int irqreturn_t; #define IRQ_NONE (0) #define IRQ_HANDLED (1) 1. 初始化异常向量表。 2. 初始化linux中irq相关的数据结构。 3. 调用request_irq,初始化我们的中断处理函数。 4. 中断触发。 ================================================== .--------------. +------+ +------+ +------+ | request_irq | 初始化 |action| .-->|action| .-->|action|(my_handler) '--------------'---------> +------+ | +------+ | +------+ ^ 启动后 ^ |next |---' |next |---' |next | | | .-> +------+ +------+ +------+ | | | .----' | | | .--------------. | __do_irq | ...... | | struct irqdesc irq_desc[] | '--------------' | +------+ | ^ | irq 0 |handle|--------------> do_dege_IRQ/ | '---------|action| do_level_IRQ | +------+ ^ | irq 1 |handle| | .--------------. 初始化 |action| | .--| init_IRQ |---------> +------+ | | '--------------' irq 2 |handle| | v ^ |action| | 启动时 | ...... asm_do_IRQ ^ | ^ | | | | .--------------. | irq模式 | svc模式 | | '--| trap_init | | +--------+------------> get_irqnr_and_base | '--------------' \ | | | | | ^ \ | | stub |<-+ | | | \ | +--------+ | | | | \ 初始化 | | | | | '----> | | | | .--------------. | +--------+ | | | | start_kernel | | | vector |--+ | | '--------------' | +--------+ | | ^ | ^ | | | 内核启动 | | 中断到来 | | 1. 初始化异常向量表。 ---------------------------------- start_kernel '-->trap_init '-->__trap_init (arch/arm/kernel/entry-armv.S) 这个初始化也没有干太多的事情,设置c1寄存器的V位,把异常向量表的位置设成0xffff0000,__trap_init就是把linux预先最备好的异常向量表拷贝到0xffff0000中,同时也把部分相关的代码也拷贝了,具体操作见entry-armv.S的__trap_init. @ +-----------+ @ | | @ | | @ +-----------+ @ |\\\\\\\\\\\| __stubs_start <--. @ +-----------+. | 0xffff0200 @ | | \ 0x200 | @ | | / | @ +-----------+' | @ ||||||||||||| .LCvectors <--. | @ +-----------+ 0xffff0000 | | @ | | | | @ | | | | @ | | | | @ | | | | @ | | | | @ | | | | @ +-----------+ | | @ ||||||||||||| | | @ +-----------+ .LCvectors ----' | @ |\\\\\\\\\\\| | @ +-----------+ __stubs_start ---' @ | | @ 因为我们要移动vector到0xffff0000的那个地方,而.LCvectors的b指令又是要跳到 @ 它后面距离200的一段叫做stubs的代码当中,所以我们不单止要拷贝vector,还要 @ 拷贝stubs, stubs里面包含了对各种异常的一些处理的工作。 这段stub的作用主要是从irq模式切换到svc模式,然后保存所有当前进程的寄存器,正是因为这个快速离开irq的做法,让arm可以实现irq的处理嵌套(而不是irq本身的嵌套),所以irq的处理是运行在svc模式下的,而且对INTPND的清除也是在svc模式下的。 2. 初始化linux中irq相关的数据结构。 ---------------------------------- 数据结构: 一个全局的struct irqdesc的数组 irq_desc irq_desc .+--------------+ irq 0 / | .--------. | | | handle | | | +--------+ | struct irqdesc| | chip | | | +--------+ | | | action | | \ | '--------' | '+--------------+ irq 1 / | | struct irqdesc ...... \ | | '+--------------+ irq 2 | | ...... | | +--------------+ irq 3 | | ...... 每个中断号对应一个struct irqdesc结构体,其中有三个比较重要的成员: handle 当中断发生时,最终是会调用这个函数指针来处理这个中断 chip 中断控制器相关的,比如ack,enable,disable等操作. action 我们在request_irq的时候申请的中断处理函数 start_kernel '-->init_IRQ '-->init_arch_irq(smdk2410_init_irq) '-->s3c24xx_init_irq 初始化 irq_desc init_IRQ: 这个函数先把irq_desc中每个irqdesc结构体初始化成一个无效值 bad_irq_desc init_arch_irq(smdk2410_init_irq): 没干什么,直接调用s3c24xx_init_irq() s3c24xx_init_irq: 1. 清除INTPND(中断控制器),SUBSRCPND(级联中断控制器),EINTPND(外部中断控制器) 这3个寄存器的内容,因为有肯能已经有中断发生过了,但是我们只管后面来的中断。 2. 初始化每个中断号对应的struct irqdesc结构体中的handle(赋值成do_edge_IRQ/do_level_IRQ) chip(赋值成s3c_irq_adc/s3c_irq_uart2......不同的中断不一样,这是可以想象得到的,因为 你这个中断是直接接在主控制器上面,还是外部中断控制器上面是不一样的) 在初始化完struct irqdesc后,我们基本上就可以处理中断了。 void __init init_IRQ(void) { struct irqdesc *desc; extern void init_dma(void); int irq; #ifdef CONFIG_SMP bad_irq_desc.affinity = CPU_MASK_ALL; bad_irq_desc.cpu = smp_processor_id(); #endif //irq_desc是一个全局变量,是一个irqdesc数组。 for (irq = 0, desc = irq_desc; irq < NR_IRQS; irq++, desc++) { //初始化成无用值 *desc = bad_irq_desc; INIT_LIST_HEAD(&desc->pend); } //init_arch_irq = mdesc->init_irq;又是我们的 machine descriptor smdk2410_init_irq() init_arch_irq(); init_dma(); } 3. 调用request_irq,初始化我们的中断处理函数。 ------------------------------ 310 int request_irq(unsigned int irq, 311 irqreturn_t (*handler)(int, void *, struct pt_regs *), 312 unsigned long irqflags, const char * devname, void *dev_id) 313 { 314 struct irqaction * action; 315 int retval; 316 317 /* 318 * Sanity-check: shared interrupts must pass in a real dev-ID, 319 * otherwise we'll have trouble later trying to figure out 320 * which interrupt is which (messes up the interrupt freeing 321 * logic etc). 322 */ 323 if ((irqflags & SA_SHIRQ) && !dev_id) 324 return -EINVAL; 325 if (irq >= NR_IRQS) 326 return -EINVAL; 327 if (!handler) 328 return -EINVAL; 329 330 action = kmalloc(sizeof(struct irqaction), GFP_ATOMIC); 331 if (!action) 332 return -ENOMEM; 333 334 action->handler = handler; 335 action->flags = irqflags; 336 cpus_clear(action->mask); 337 action->name = devname; 338 action->next = NULL; 339 action->dev_id = dev_id; 340 341 retval = setup_irq(irq, action); 342 if (retval) 343 kfree(action); 344 345 return retval; 346 } 创建一个action结构体,把我们的handler,dev_id等都放到这个结构体中。连到一个action中组成一个链表。而且新加的节点是放到最后的。 do_edge_IRQ(handler) request_irq setup_irq __do_irq SA_SAMPLE_RANDOM IRQ_HANDLED 4. 中断触发。 ------------------------------- vector_irq(vector_stub 来构造) '--->__irq_svc '--->asm_do_IRQ 1). 保存被中断进程的上下文 cpsr,r0-r15,跳到svc模式 2). 用get_irqnr_and_base计算中断号 3). 将中断号和上下文作为参数传递并跳转到asm_do_IRQ 1). 保存被中断进程的上下文 cpsr,r0-r15 在中断被触发后进入irq模式,并且硬件会到异常向量表中找指令执行,所以就到了0xffff0000 就是我们刚才的 .LCvectors (entry-armv.S) 它又会跳到stub的代码里面,不过这个阶段实在 是没有干什么事情,只不过是保存当前进程的r0-r15和cpsr到内核栈里面,把模式切换到svc模式下 2). 用get_irqnr_and_base计算中断号 这里我们根据INTPND,EINTPND,SUBSRCPND寄存器的值求出中断号 3). 将中断号和上下文作为参数传递并跳转到asm_do_IRQ 这里我们跳到c代码了。 asm_do_IRQ '--->desc->handle(do_edge_IRQ/do_level_IRQ)(根据不同的中断号有不同的handle,看上面初始化) |---> desc->chip->ack(irq) '---> __do_irq '--->action->handler asm_do_IRQ,先根据中断号中找到这个中断号对应的struct irqdec,然后调用对应的desc->handle()函数. desc->chip->ack(irq)是清除相应的位,(INTPND,EINTPND,SUBSRCPND)来表示这个中断已经相应完了。 最后__do_irq中把连在这个irq上面的中断处理函数(我们在request_irq中申请的中断处理函数)都执行一遍。 do_edge_IRQ/do_level_IRQ的主要区别是什么时候回应去清除xxxPND寄存器 do_edge_IRQ 在中断处理程序前就要清除,因为我们尽量不让中断丢失。 do_level_IRQ 在中断处理完了之后在对方把电频拉高或拉低的时候才清除xxxPND,这种触发方式是为了保证这个中断不能缺失,所以要不断产生中断,在对方产生另外一个中断时才改变这个电频,而我们的do_level_IRQ就是在电频恢复中断前那种状态的时候才清除xxxPND,不然程序会不断被中断,一点都没有前进。 A B +----+ +----+ | |--------. | | | | '--------| | | | | | | |-----------------| | | | | | +----+ +----+ A B +----+ +----+ | |-----------------| | | | | | | | | | | | .--------| | | |--------' | | +----+ +----+ 电频触发 块设备 --------------------------------------------- 主要特点是:任意寻址 1.块设备是基于磁盘设备出来的,所以它很大一部分实现是优化I/O请求。 2.分区,类型:主分区,扩展分区 3.文件,文件系统(要放文件就必须有文件系统) .-----------------. | register_blkdev | '-----------------' | v .-----------------. | alloc_disk | (次设备号的数量)也就是分区数 '-----------------' | v .-----------------. | 初始化 | '-----------------' | v .-----------------. | add_disk | '-----------------' | v .-----------------. |unregister_blkdev| '-----------------' | v .-----------------. | del_gendisk | '-----------------' 初始化部分: ------------------------------------------------- test.gd->major = TEST_MAJOR; 主设备号 test.gd->first = 0; 次设备号的开始号 test.gd->fops = &test_ops; ops test.gd->queue = blk_init_queue(request_proc, &test.lock); 我们申请的请求队列,request_proc处理请求,为什么我们后面还要有个自旋锁呢?因为请求队列的竞态有可能发生在操作系统底层,和驱动上层,为了防止重复锁。 strcpy(test.gd->disk_name,"test"); 可以在/proc/devices中看到这个名字,我们在/dev下面不用自己建立设备文件,系统帮我们建立。 set_capacity(test.gd, TEST_NSECTORS); 设置这个磁盘有多大,这里的单位是扇区数,每个扇区内核规定是512B。如果设备的扇区不是512B,驱动要自己处理,做一个转换,欺骗内核。 test.gd->private_data = &test; test.data = vmalloc(SECTOR_SIZE*TEST_NSECTORS); memset(test.data, 0x00, SECTOR_SIZE*TEST_NSECTORS); ------------------------------------------------- 在旧内核中一般有一个ioctl的命令GETGEO来得到磁头数,磁道扇区数,柱面数,开始扇区号。 在新内核中在block_device_operations现在有一个成员.getgeo来代替ioctl这个命令。 int test_getgeo(struct block_device *dev, struct hd_geometry *geo) { /* heads : 磁头数 , 盘面数 */ /* sectors: 每个磁道的扇区数 */ /* cylinders; 柱面数, 每个盘面的磁道数 */ /* start: 开始扇区号 */ /* 一个盘面的扇区数 = 每盘磁道数 x 每磁道的扇区数 */ /* 整个硬盘柱面数 x 每磁道的扇区数*/ /* cylinders x sectors */ /* cylinders = 一个盘面的扇区数 / 每个磁道的扇区数 */ /* 整个硬盘的扇区数 / 每个柱面的扇区数 */ geo->heads = 4; geo->sectors = 128; geo->cylinders = TEST_NSECTORS / (geo->heads * geo->sectors); geo->start = 0; return 0; } 分区这些动作不是我们驱动做的是,而是fdisk这种东西用内核的函数来做的,我们只管建立整个磁盘就行了。 主要的函数是这两个: void test_trans(struct test_dev *dev, int offset, int sector_count, unsigned char *buf, int write) { if ((offset + sector_count) > TEST_NSECTORS) { return; } if (write) { memcpy(dev->data + offset * SECTOR_SIZE, buf, sector_count * SECTOR_SIZE); } else { memcpy(buf, dev->data + offset * SECTOR_SIZE, sector_count * SECTOR_SIZE); } } void request_proc(request_queue_t *rq) { struct request *re; struct test_dev *dev; while(( re = elv_next_request(rq)) != NULL) { if (blk_fs_request(re)) { dev = re->rq_disk->private_data; test_trans(dev ,re->sector, re->current_nr_sectors, re->buffer, rq_data_dir(re)); end_request(re, 1); } else { end_request(re, 0); } } } 设备模型 driver model ---------------------------------------- 我们需要设备模型去做下面的事情: 1. 设备插入和删除的时候,driver model也相应创建/删除 2. 自动加载设备对应的模块 devfs VS udev =============================================== >>>devfs: 用在2.6.9之前, 它的特性: a. devfs在驱动加载的时候创建节点。 b. 打开一个不存在设备节点,去尝试自动加载驱动。(不断地insmod,rmmod) c. 由内核创建节点 如果我们想利用devfs,我们的驱动中也要相应要加devfs_create等代码. 内核选项: File systems Pseudo filesystems /dev file system support 新内核不会看到这个选项,需要修改Kconfig devfs的缺点是: 开发者已经不做维护,而且这个创建节点的工作在内核中完成并不是十分合适 >>>>udev(原名hotplug): socket application : udevd守护进程 ^ | --------------------------------| uevent | 内核 : netlink 内核中有代码(设备驱动模型),用户空间也有代码(udev),而内核的代码只是在有设备接入的时候通过uevent通知用户进程(udevd),而这里使用的内核和用户通信的方式是netlink/socket.在内核空间中看到的是一个netlink,在用户空间看到的是一个socket,他们用uevent来传递信息。 设备模型的数据结构:(kobj,kset) ---------------------------------------- kobj: 很好地维护了树形结构,无论在添加还是删除(在删除的时候可以处理子设备的依赖问题,处理好节点被删除的顺序),还有维护使用计数. kset: kobj的集合. kobj_type: 里面包含了一些操作,主要是用在sysfs的显示方面 +------------------------------------+ | kset +---------------+ | | +-------------+ |kset_uevent_ops| |-->用来发送uevent消息给用户空间 .-----------------| | kboj | +---------------+ | | | | +---------+ | | | | | |kobj_type| | +---------+ | | | | +---------+ | |kobj_type| | | .---->| +-------------+ +---------+ |<------. | | +------------------------------------+ | | | ^ | | | | | | +-------------+ +-------------+ +-------------+ | | kboj | | kboj | | kboj | '---->| +---------+ |--------->| +---------+ |-------->| +---------+ |---> ...... | |kobj_type| | | |kobj_type| | | |kobj_type| | | +---------+ | | +---------+ | | +---------+ | +-------------+ +-------------+ +-------------+ 有些问题: 1. AMBA vs PCI等总线 AMBA并不是一种我们可以看到的总线,它是一种内部总线(更像pc上面的南北桥)。内部总线的意思就是它里面的数据传输是硬件实现的,软件不可能看得见也不可能干预。就像我们在2410上面访问中断控制器那样,都是直接访问寄存器就可以控制了,也看不见里面是怎样控制的,可以理解成直连,看上去就像这个控制器直接连到cpu那样。 而PCI/USB这些总线就不一样的,总线更大意义上是指这种。这些线往往都是有一个根设备,cpu不能直接访问总线,把信息放到总线上面,而是通过这个根设备(一个控制器)来给总线发信息。而这又和我们的nand flash控制器不相同,因为在这种情况下我们只需要看到nand flash控制器就可以了,我们只需要控制好控制器就可以了,并不用知道它对真实的nand flash memory做了什么事情。但是PCI/USB这些总线不同,他们都是有协议的,它们需要知道发给总线上面的消息,什么时候该发什么东西,所以一般总线都有对应的协议。 所以,需要通过控制器才可以访问,并且有相应协议的东西才是总线。 2. udev 的确不需要通过bus/device/driver这种结构来工作,只需要class_device就可以实现自动建立节点的能力了,但是这只相当于devfs,如果没有bus/device/driver那么他就没有那种设备插入可以找到对应驱动的能力。 3. 在插入设备的时候一方面总线会去match他上面有没有相应驱动,另外一方面是插入设备的时候uevent会到用户空间要求插入模块。那这两个工作的顺序是怎样?是先uevent到用户空间插入模块,再在总线上面match,如果模块已经在里面了,就insmod失败,不过也没有关系。 设备模型的数据结构:(device,device_driver,bus_type) ---------------------------------------------------- 所有设备模型的类型都在include/linux/device.h中 bus_type +----------------+ | subsystem | | +-------------+| | | kset || | | +----------+|| | | | kobj_type||| | | | +-------+||| | | | |sys_ops|||| | | | +-------+||| | | +----------+|| | +-------------+| +----------------+ | | .------' '--------. | | kset v v kset +-----------+ +-----------+ .---------------| kobj | .-------| kobj | | +-----------+ | +-----------+ | | kboj_type | | | kboj_type | | +-----------+ | +-----------+ | | | | v device device v device_driver device_driver +----------+ +----------+ +----------+ +----------+ | +------+ | | +------+ | | +------+ | | +------+ | | | kobj | |---->| | kobj | |---->... | | kobj | |----->| | kobj | |---->... | +------+ | | +------+ | | +------+ | | +------+ | +----------+ +----------+ +----------+ +----------+ bus_type: 用这个结构体来描述一条总线 device: 用来描述一个设备 device_driver: 用来描述一个驱动 注意这3个数据结构的3角关系: bus_type有两个kset(就相当于两个链表),分别连着这条总线上面的所有device和device_driver. device有成员bus指向它所在的总线,有成员driver指向它所对应的驱动device_driver device_driver有成员bus指向他所在的总线,有一个链表把正在使用这个驱动的所有设备连起来。 subsystem相当于一家,设备,驱动分别是这一家里面的猫和狗,而/sys/class是一类东西,它包含了很多家的猫和狗。udev主要是看/sys/class目录下面的内容来创建/dev节点。 关于bus一方的程序: ==================== 每一条总线都起码有一个设备,这个设备代表这个bus,在建立这个bus的时候就应该申请这个设备,这个设备一般不会有驱动和它对应,而且这个设备一般不会在这条总线上面,因为它就是这条总线,所以它不属于这条总线。 总线相当于婚姻介绍所,总线本身一定要是一个设备,就像申请这个介绍所老板本身一定是一个人,而这个总线并不一定要有驱动,就像老板不一定要有女朋友,这不妨碍他开这个介绍所。但是进来找女朋友的人(设备)就一定要找到女朋友才会走。所以插到这个总线上面的设备都要找到对应的驱动。 #include "./test_bus.h" MODULE_LICENSE("GPL"); MODULE_AUTHOR("bzli"); struct bus_type test_bus; struct device test_root; struct test_driver { struct device_driver driver; }; struct test_device { struct device device; }; int test_driver_register(struct test_driver *dri) { int ret = 0; dri->driver.bus = &test_bus; ret = driver_register(&dri->driver); if (ret < 0) { printk("<0> driver_register failed.\n"); } return ret; } EXPORT_SYMBOL(test_driver_register); void test_driver_unregister(struct test_driver *dri) { driver_unregister(&dri->driver); } EXPORT_SYMBOL(test_driver_unregister); int test_device_register(struct test_device *dev, char *name) { int ret = 0; strcpy(dev->device.bus_id, name); dev->device.parent = &test_root; dev->device.release = test_root.release; dev->device.bus = &test_bus; ret = device_register(&dev->device); if (ret < 0) { printk("<0>""device_register faile.d\n"); } return ret; } EXPORT_SYMBOL(test_device_register); void test_device_unregister(struct test_device *dev) { device_unregister(&dev->device); } EXPORT_SYMBOL(test_device_unregister); int test_bus_match(struct device *dev, struct device_driver *dri) { printk("<0> test_bus_match\n"); return !strcmp(dev->bus_id, dri->name); } void test_bus_release(struct device *dev) { } struct bus_type test_bus = { .name = "test_bus", .match = test_bus_match, }; struct device test_root = { .bus_id = "test_root", .release = test_bus_release, }; int test_init(void) { int ret; ret = bus_register(&test_bus); if (ret < 0) { printk("<0>" "bus_regsiter failed.\n"); return ret; } ret = device_register(&test_root); if (ret < 0) { printk("<0>" "device_regsiter failed.\n"); bus_unregister(&test_bus); return ret; } return 0; } void test_exit(void) { device_unregister(&test_root); bus_unregister(&test_bus); } module_init(test_init); module_exit(test_exit); 总结一下上面的代码,其实就是创建一条虚拟的bus,下面是实现一条总线所必须要做的事情: 1. 定义一个struct bus_type结构体,它就是我们的总线数据结构,在模块init,exit函数中使用bus_register()/bus_unregister()来在内核中注册我们这条总线。对于这个结构体我们要为它初始化两个成员:.name和.match,名字就是在sysfs中看到设备名字,.match是当有新设备或模块插到总线上的时候用来匹配设备和驱动。match函数的内容我们自己定义。 2. 就像上面所说的那样,每条总线都需要有一个设备这里是struct device test_root.同样也在模块的init,exit中注册和反注册这个设备,使用(device_register()/device_unregister())来完成这个任务。对于这个根设备,我们要给它初始化两个成员.bus_id和.release,.bus_id是这个设备的名字,也是反应在sysfs上面。.release是一个函数指针,当这个设备被卸载的时候,这个函数就会被调用。我们必须实现release函数, 如果device->release为空,在调用device_register()加到内核的时候会失败。 3. 每种总线上面都有自己的设备类型和驱动类型,比如usb,它的设备类型是struct usb_device,驱动是 struct usb_driver,所以我们也要定义自己的类型这里是test_device和test_driver. test_device起码要包含struct device,而test_driver起码要包含struct device_driver,因为我们要继承这两个基类。 4. 有了自己的设备和驱动,自然我们就需要有自己设备和驱动的注册和反注册函数:对于test_driver_register()我们最后只是调用driver_register,而test_device_register则调用device_register,二者都是要初始化数据结构。 关于device/device_driver一方的程序: ========================================= ssize_t test_read(struct file *fp, char *buf, size_t size, loff_t *off) { } int test_open(struct inode *no, struct file *fp) { } int test_release(struct inode *no, struct file *fp) { } struct cdev test_cdev; struct file_operations test_ops = { .open = test_open, .release = test_release, .read = test_read, }; dev_t test_no = MKDEV(27, 0); struct class *test_class; struct class_device *test_class_device; extern struct test_device test_device; extern struct test_driver test_driver; int test_probe(struct device *dev) { printk("<0> test_probe.\n"); if (register_chrdev_region(test_no, 1, "test cdev")) { printk("register chrdev error.\n"); return -1; } cdev_init(&test_cdev, &test_ops); cdev_add(&test_cdev, test_no, 1); test_class = class_create(THIS_MODULE, "test"); if (IS_ERR(test_class)) { printk("<0> class_create failed.\n"); return PTR_ERR(test_class); } test_class_device = class_device_create(test_class, NULL, test_no, &test_device.device, "test0"); if (IS_ERR(test_class_device)) { printk("<0> class_device_create failed.\n"); return PTR_ERR(test_class_device); } return 0; } int test_remove(struct device *dev) { printk("<0> test_remove\n"); cdev_del(&test_cdev); unregister_chrdev_region(test_no, 1); class_device_destroy(test_class, test_no); class_destroy(test_class); return 0; } struct test_device test_device; struct test_driver test_driver = { .driver = { .name = "test0", .probe = test_probe, .remove = test_remove, .owner = THIS_MODULE, }, }; int test_init(void) { int ret = 0; ret = test_device_register(&test_device, "test0"); if (ret < 0) { printk("<0>""test_driver_register failed.\n"); } ret = test_driver_register(&test_driver); if (ret < 0) { printk("<0>""test_device_register failed.\n"); test_device_unregister(&test_device); } return ret; } void test_exit(void) { test_device_unregister(&test_device); test_driver_unregister(&test_driver); } module_init(test_init); module_exit(test_exit); 1. 我们需要一个设备,一个驱动。不过其实驱动和设备更应该分开实现struct test_device test_device; struct test_driver test_driver;驱动和设备的类型就是我们在bus部分定义的那两个,在模块的init中我们应该调用我们之前准备好的test_device_register/test_driver_register为我们注册驱动和设备,注册到那里呢?在这个两个函数中把device->bus和driver->bus设置成我们的bus,所以在后面的device_register和driver_register就知道怎样做了。 2. 驱动的初始化: struct test_driver test_driver = { .driver = { .name = "test0", .probe = test_probe, .remove = test_remove, .owner = THIS_MODULE, }, }; 对于驱动,我们要初始化他上面4个成员。 3. 我们的cdev/blkdev就是在probe这个函数中实现的,为什么放在probe中而不是放在模块的init中实现呢?这是显而易见的,probe的意思是探测,就是说虽然总线觉得这个设备和我这个驱动合适,但是还是要驱动本身来决定是不是真的合适。如果probe发现还是不对,应该直接返回,只有真的合适才创建设备和拿到资源,所以driver->probe()和device->remove()就像当年我们的模块的init和exit. 4. class_create()和class_device_create()代表我们要在/sys/class建立目录,前者代表我们在/sys/class下面建立一类设备的目录,而后者是在某一类下面创建一个目录来表示属于这一类的一个设备。 s3c2410设备启动的初始化 ============================================== 所有设备都是需要有一个总线,如果不是总线结构的设备都放在platform bus上面,也就是那些焊死在cpu附近,不能够拔出来的设备都放在platform bus上. 不同的bus有不同的做法,platform_bus需要我们自己建立一个platform_device,而PCI则不需要。 [总线结构]: +---------+ | cpu |------. watchdog | |------' +--|||||--+ +---+ (1)| | +---+ | U | | S | | B | (2) | |--------. U disk | |--------' 对于一个u盘,其实已经有两个驱动 (1) usb host/hub总线的驱动,因为usb host不是热插拔的,所以在系统启动就会检测到它并为它加载usb host/hub的模块.它申请了一些中断给热插拔。 (2) 当一个U盘插入的时候,中断被触发,总线的驱动host/hub就会去向udevd发送uevent,让他去加载模块.这就是热插拔.所以对于udev是先有设备,再有驱动。 [平台结构]: 像watchdog它就是platform device, 它不可能进行热插拔。它的驱动用另外一种方式进行加载. [关于firmware]: 有些设备中需要一个rom来放一些程序,这就是firmware,有两个选择:1.直接放在设备的rom中。2.可以把firmware放到内核,在设备插入的时候再下载到设备,这样可以方便更新firmware. 在文件arch/arm/mach-s3c2410/devs.c定义了s3c2410的platform总线的设备,而它们组成了一个集合在arch/arm/mach-s3c2410/mach-smdk2410.c中。 static struct platform_device *smdk2410_devices[] __initdata = { &s3c_device_usb, &s3c_device_lcd, &s3c_device_wdt, &s3c_device_i2c, &s3c_device_iis, &s3c_device_nand, &s3c_device_rtc, &s3c_device_ts, &s3c_device_sdi, }; 而在arch/arm/mach-s3c2410/cpu.c中的s3c_arch_init()会遍历这个结构体数组对每个platform 设备调用platform_device_regiser把设备都注册到内核。 struct platform_device { char * name; u32 id; struct device dev; u32 num_resources; struct resource * resource; }; 这个结构体中最为突出的是struct resource,它主要是指明这个设备要申请那些设备,主要用到的是irq和io物理地址.见arch/arm/mach-s3c2410/devs.c struct resource { const char *name; unsigned long start, end; unsigned long flags; struct resource *parent, *sibling, *child; }; 比如watchdog的驱动可以在 drivers/char/watchdog/s3c2410_wdt.c 中看到. 匹配bus->match其实是发生在驱动加载的时候,因为: 设备在插入后引起uevent到了用户空间,用户空间insmod插入模块,这时候引发bus->match的匹配. 设备模型和我们以前的简单cdev的区别是,cdev只是实现了和用户交换的部分,没有实现受内核管理的部分。 移植udev ============================== 1. 修改makefile 指定安装目录,就是板子的根目录 prefix = /armroot2 指定交叉编译器 CROSS_COMPILE = /usr/local/arm/3.4.1/bin/arm-linux- 2. make & make install 在这之后就会在/sbin下面有一个udevd /etc下面会有一些配置文件 3. busybox & kernel 在busybox中禁掉devfs,现在已经不需要了。 kernel中相应也要去掉devfs的选项 4. 准备工作: 因为udevd只能在init启动之后才能起来,但是内核启动的过程需要使用两个设备console和null,所以需要我们手动创建两个设备节点,这是要在制作根文件系统的时候mknod mknod console c 5 1 mknod null c 1 3 5. 把我们的规则写到/etc/udev/rules.d/下面的一个文件里面。 格式: KERNEL=="watchdog", NAME="haha" KERNEL=="watchcat", NAME="hehe" KERNEL是指在/sys/下面的一个设备叫做"watchdog",在/dev下面建立一个"haha"的设备 6. 挂载sysfs: 因为udev很大程度上要依赖sysfs来创建节点,所以我们需要在启动kernel后马上挂载sysfs,所以最好放在/etc/init.d/rcS里面 当然最重要的当然是让udevd运行起来了。 mount -t proc none /proc mount -t sysfs none /sys udevd -d flash这种介质不是那么适合使用gendisk,request queue结构,比如ECC校验,在request queue拿下来一个request后如果ECC校验失败(坏块),我们也没有办法处理跳过坏块的情况。所以flash是基于mtd驱动的(Memory Technology Devices (MTD))。 网络驱动 ------------------------------------- 网络设备为什么不是文件? 假如我们要访问192.168.20.1但是有两张网卡,你也不知道用那个文件,所以这部分工作交给iptable来干,所以有设备文件也没什么意义,只要有socket接口就够了,所以没有设备文件也不需要有设备号了。 +---------------------------------+ | |协议栈 +---------------------------------+ | ^ | | +-----|--------------------|------+ | | netif_rx | | | ^ | | v | | | head_start_xmit irq_handler |--------->链路层的一部分,协议部分在上层 | | ^ | +------------v-------------------|------------+ | | +-------+ +-------+ +-------+ | | | | register1 register2 register3 | | | | +-------+ +-------+ +-------+ | | | +---------------------------------+ | | | ^ | | v | | | +---------------------------------+ | 网卡 | | 物理 | | | | |--------->物理层 | +---------------------------------+ | +---------------------------------------------+ 下面我们这个程序所做的事情是: | 虚拟网卡1(192.168.0.1) <--------> | (192.168.0.2) | | | 我们的驱动 | | 虚拟网卡2(192.168.1.1) <--------> | (192.168.1.2) | 我们的驱动就是注册两张虚拟网卡,让他们在两个不同的网段中也能够通信。 内核 ------------------------------ | | hard_header() netif_rx() | ^ v | test_tx() test_rx() | ^ v | test_hw_tx() test_interrupt() | ^ v | 硬件 <-----------> 硬件 #include #include #include #include #include #include #include #include /* 我们的私有数据 */ struct test_priv { unsigned char data[ETH_FRAME_LEN]; int len; spinlock_t lock; }; struct net_device *test_dev[2]; int test_rx(unsigned char *data, int len, struct net_device *dev) { struct sk_buff *skb = dev_alloc_skb(len); memcpy(skb_put(skb, len), data, len); skb->dev = dev; skb->protocol = eth_type_trans(skb, dev); skb->ip_summed = CHECKSUM_UNNECESSARY; /* 这个函数通往内核协议栈的通道 */ netif_rx(skb); return 0; } irqreturn_t test_interrupt(int irq, void *dev_id, struct pt_regs *pr) { unsigned char data[ETH_FRAME_LEN]; int len; struct net_device *dev; //read data //...... dev = dev_id; { struct test_priv *priv; priv = netdev_priv(dev); spin_lock(&priv->lock); len = priv->len; memcpy(data, priv->data, len); spin_unlock(&priv->lock); } test_rx(data, len, dev); return IRQ_HANDLED; } int test_hw_tx(unsigned char *data, int len, struct net_device *dev) { struct iphdr *ip ; unsigned long temp; ip = data + sizeof(struct ethhdr); temp = ip->saddr; ip->saddr = ip->daddr; ip->daddr = temp; ((unsigned char *)&ip->saddr)[2] ^= 1; ((unsigned char *)&ip->daddr)[2] ^= 1; ip->check = 0; ip->check = ip_fast_csum(ip, ip->ihl); //send data //...... { struct net_device *dest; struct test_priv *priv ; if (dev == test_dev[0]) { dest = test_dev[1]; } else { dest = test_dev[0]; } priv = netdev_priv(dest); /*这里有可能有竞态,当test_interrupt还没有处理完时 *又有一个数据到来 */ spin_lock(&priv->lock); priv->len = len; memcpy(priv->data, data, len); spin_unlock(&priv->lock); test_interrupt(0, dest, NULL); } return 0; } int test_tx(struct sk_buff *skb, struct net_device *dev) { int len = skb->len; unsigned char temp[ETH_ZLEN]; unsigned char *data; data = skb->data; /*我们至少要发送60个字节,这是由以太网决定的,所以 *如果数据不够60个字节,我们要自己填充 */ if (len < ETH_ZLEN) { memcpy(temp, data, len); memset(temp + len, 0x00, ETH_ZLEN - len); data = temp; len = ETH_ZLEN; } dev->trans_start = jiffies; test_hw_tx(data, len, dev); /*skb内核申请给我们用的,需要我们释放*/ dev_kfree_skb(skb); return 0; } int test_header(struct sk_buff *skb, struct net_device *dev, unsigned long type, void *saddr, void *daddr, unsigned long len) { struct ethhdr *eth; /* head--->+-------+ head--->+-------+ head--->+-------+\ | | | | | | IP | \ | | data--->+-------+\ data-' +-------+ \ | | | TCP | \ | TCP | len data--->+-------+\ +-------+ len +-------+ / |\\\\\\\| len |\\\\\\\| / |\\\\\\\| / tail--->+-------+/ tail--->+-------+/ tail--->+-------+/ | | | | | | | | | | | | | | | | | | end---->+-------+ end---->+-------+ end---->+-------+ sk_buff几个重要的数据域: data,tail分别指向先有数据的开头和结尾, head,end分别指向这个buff的开头和结束。 skb_push()是把data指针向上移动一定长度,然后返回新的位置,这个方法 是当我们要添加一个协议头的时候用的。 skb_put() 是把tail指针向下移动一定长度,然后返回旧的位置,这个方法 是当我们有头没有数据的时候添加数据用的。 我们这里不需要考虑data会不会超过head,或者是tail会不会超过end之类的问题, sk_buff帮我们解决,它自动调节各个指针的位置,类似环形buffer。 */ eth = skb_push(skb, ETH_HLEN); eth->h_proto = htons(type); memcpy(eth->h_source, saddr, 6); memcpy(eth->h_dest, saddr, 6); if (eth->h_dest[5] == '0') { eth->h_dest[5] = '1'; } else { eth->h_dest[5] = '0'; } return dev->hard_header_len; } /*------------------------------------*/ int test_open(struct net_device *dev) { printk("<0> ifconfg up.\n"); // setup mac address /* open最主要的工作是设置以太网地址,一般情况下是在硬件的rom中得到*/ memcpy(dev->dev_addr , "\0TEST0", 6); if (dev == test_dev[1]) { dev->dev_addr[5] += 1; } /*启用发送队列*/ netif_start_queue(dev); return 0; } int test_stop(struct net_device *dev) { printk("<0> ifconfig down.\n"); /*停止发送队列*/ netif_stop_queue(dev); return 0; } int test_timeout(struct net_device *dev) { /*重启发送队列*/ netif_wake_queue(dev); return 0; } void test_setup(struct net_device *dev) { struct test_priv *priv = NULL; ether_setup(dev); /* * struct net_device里面的主要函数: * open() ifconfig up * stop() ficonfig down * hard_start_xmit() 往硬件发送数据,协议栈在有数据传输的 * 时候自动调用这个函数。 * hard_header() 如果有这个函数,内核就不自己构造ether层 * 的内容,留给这个函数来建 * tx_timeout() 没有得到回应, watchdog_timeo以jiffers为单位 * 注意这个超时是指物理层上的,在网卡发出一个数据时它会在一条线上拉 * 低或高来表示数据发出了,在网线的另外一端的那个设备需要相应>地拉低 * 高另外一条线来表示我收到这个数据了,这是在物理层上面的握手,如果 * 对方设备不应答,网卡就会不断的发数据,这就是上面的电频>触发中断。 * 这个超时是指对方设备在物理层上面的不应答。 * 接收是在中断处理函数中,所以我们这里看不到。 */ dev->open = test_open; dev->stop = test_stop; dev->hard_start_xmit = test_tx; /*在调用hard_start_xmit之前调用*/ dev->hard_header = test_header; dev->tx_timeout = test_timeout; dev->watchdog_timeo = 5; dev->flags |= IFF_NOARP; dev->features |= NETIF_F_NO_CSUM; /*这是arp <-> ip的转换缓存,arp表*/ dev->hard_header_cache = NULL; priv = netdev_priv(dev); memset(priv, 0x00, sizeof(struct test_priv)); spin_lock_init(&priv->lock); } int test_init(void) { /* 创建两个net_device设备,每个net_device代表一张网卡 * 网络设备和字符设备和块设备都不一样,以前我们在定义 * 自己的设备的时候都是test_dev包含一个cdev。但是网络 * 设备是通过alloc_netdev()>来分配net_device的, 而且第 * 一个参数是我们对于这个设备的私有数据的大小,比如data,flag等, * alloc_netdev在分配net_device的同时也把 * 私有数据也分配了。可以通过netdev_priv()这个函数来得到私有变量。 * 参数一: 私有数据的大小 * 参数二:设备名字 * 参数三:在分配完这些数据结构之后使用这个函数来初始化 */ test_dev[0] = alloc_netdev(sizeof(struct test_priv), "test%d", test_setup ); test_dev[1] = alloc_netdev(sizeof(struct test_priv), "test%d", test_setup ); /* 设备注册到内核 */ register_netdev(test_dev[0]); register_netdev(test_dev[1]); return 0; } void test_exit(void) { /* 反注册 */ unregister_netdev(test_dev[0]); unregister_netdev(test_dev[1]); /* free 设备 */ free_netdev(test_dev[0]); free_netdev(test_dev[1]); } module_init(test_init); module_exit(test_exit);