m18317710169

gdb+gdbserver的远程调试笔记


创建时间:2017/3/9 15:37更新时间:2017/3/10 10:37作者:Robbie Jun


前言:

某些时候由于模拟环境的限制,调试必须要在目标板上进行。由于嵌入式系统资源比较有限,一般不能在目标板上直接构建GDB的调试环境,

这时我们通常采用gdb+gdbserver的远程调试方法:gdbserver在目标板中运行,而gdb则在主机上运行。


调试准备:

1.目标板与主机在同一个局域网:互相可以ping通

2.假设目标板的ip是:192.168.199.155  假设主机的ip是:192.168.199.118

3.交叉编译gdbserver并拷贝到目标板

4.交叉调试器arm-linux-gnueabihf-gdb可执行


运行应用程序:

gdbserver 192.168.199.118:5000 ./netflix    # 192.168.199.118主机ip;5000监听的端口;netflix应用程序名称

运行上面的应用程序打印如下:

Process ./netflix created; pid = 5018

Listening on port 5000


主机端执行:

arm-linux-gdb ./netflix    # arm-linux-gdb为gdb调试工具(名称不唯一);netflix应用程序名称,和上面是同一个bin

运行上面的程序后打印如下:

GNU gdb (crosstool-NG 1.20.0) 7.8

Copyright (C) 2014 Free Software Foundation, Inc.

License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software: you are free to change and redistribute it.

There is NO WARRANTY, to the extent permitted by law.  Type "show copying"

and "show warranty" for details.

This GDB was configured as "--host=x86_64-build_unknown-linux-gnu --target=arm-unknown-linux-gnueabi".

Type "show configuration" for configuration details.

For bug reporting instructions, please see:

<http://www.gnu.org/software/gdb/bugs/>.

Find the GDB manual and other documentation resources online at:

<http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".

Type "apropos word" to search for commands related to "word"...

Reading symbols from ./netflix...(no debugging symbols found)...done.

(gdb)


在(gdb)命令行执行如下:

target remote 192.168.199.155:5000   # 192.168.199.155为目标板ip

执行上面的指令后打印如下:

Remote debugging using 192.168.199.155:5000

warning: Unable to find dynamic linker breakpoint function.

GDB will be unable to debug shared library initializers

and track explicitly loaded dynamic code.

0xf70b2a40 in ?? ()

(gdb)


开始调试:

一般来说,GDB主要帮忙你完成下面四个方面的功能

1、启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。

2、可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式)

3、当程序被停住时,可以检查此时你的程序中所发生的事。

4、动态的改变你程序的执行环境。


 调试源码如下:

 root@vm:/home/xurenjun/share/debug# ls

 func.c  main.c  test


 func.c :

  1 #include <string.h>

  2 #include <stdlib.h>       

  3 #include <stdio.h>

  4

  5 void print2(int size,char* string){

  6     char* ptr;           

  7     ptr=(char*)malloc(size);

  8     memset(ptr,'\0',size);

  9     strcpy(ptr,string);

 10     printf("[%s] %s\n",__FUNCTION__,ptr);

 11     free(ptr);

 12     ptr=(void*)0;

 13 }


main.c :

  1 #include <stdio.h>

  2 #include <stdlib.h>       

  3 #include <string.h>

  4

  5 void print2(int,char*);   

  6   

  7 void print1(int* val){   

  8     printf("[%s] val = %d\n",__FUNCTION__,*val);

  9 }

 10

 11 int main(void){

 12     int i = 0;

 13     int res = 0;

 14     for(i=0; i<=100; i++){

 15         res += i;

 16     }

 17     print1(&res);

 18     print2(100,"this is test demo!");

 19     return 0;

 20 }


编译:

arm-linux-gnueabihf-gcc  -g -c main.c -o main.o

arm-linux-gnueabihf-gcc  -g -c func.c -o func.o

arm-linux-gnueabihf-gcc  -g main.o func.o -o test

注意此处编译带-g选项是为了产生调试的符号信息否则调试的时候会出现如下错误:No symbol table is loaded.  Use the "file" command.


GDB调试常用命令:

list(l)                               查看程序

break(b)  函数名             在某函数入口 加断点

run                                  开始执行程序

break(b)  行号                在指定行添加断点

break(b)  文件名:行号     在指定文件的指定行号加断点

info break                      查看所有设置的断 点

delete 某断点编号           删除某断点

next(n)                            单步运行程序(不进入子函数)

step(s)                             单步运行程序(进入子函数)

continue(c)                     继续运行程序

watch  变量名                   监视某一个变量

backtrace                       命令可以在遇到断点而暂停执行时显示栈帧。此外,backtrace 的别名还有 where 和 info stack

print(p)    变量名            查看指定变量值

set var=value                 设置变量的值

quit(q)                            退出gdb


针对上面的源码开始调试实验:

使用list或者l查看源码,enter键进入下一页:

(gdb) l ist

3       #include <string.h>

4

5       void print2(int,char*);

6

7       void print1(int* val){

8               printf("[%s] val = %d\n",__FUNCTION__,*val);

9       }

10

11      int main(void){

12              int i = 0;

(gdb)

13              int res = 0;

14              for(i=0; i<=100; i++){

15                      res += i;

16              }

17              print1(&res);

18              print2(100,"this is test demo!");

19              return 0;

20      }

(gdb)


使用list + 行号 表示显示从指定行号的位置为中心开始上下显示10行,enter键进入下一页 

(gdb) list 13

8               printf("[%s] val = %d\n",__FUNCTION__,*val);

9       }

10

11      int main(void){

12              int i = 0;

13              int res = 0;

14              for(i=0; i<=100; i++){

15                      res += i;

16              }

17              print1(&res);

(gdb)


使用break + 函数名 表示在函数名处设置断点:

(gdb) break print2

Breakpoint 1 at 0x4006ec: file ./func.c, line 7.

(gdb)


使用info break 查看设置的所有断点:

(gdb) info break

Num     Type           Disp Enb Address            What

1       breakpoint     keep y   0x00000000004006ec in print2 at ./func.c:7

        breakpoint already hit 1 time

2       breakpoint     keep y   <PENDING>          info

3       breakpoint     keep y   <PENDING>          info 


使用run即开始运行,遇到断点停止运行:

(gdb) break print2

Breakpoint 1 at 0x4006ec: file ./func.c, line 7.

(gdb) run

Starting program: /home/xurenjun/share/debug/test


[print1] val = 5050


Breakpoint 1, print2 (size=100, string=0x4007f3 "this is test demo!") at ./func.c:7

7               ptr=(char*)malloc(size);

(gdb)


使用next可以单步运行程序,如果不是在子程序中设置断点,那么next不会进入子程序(下面是进入子程序的情况):

(gdb) break print2

Breakpoint 1 at 0x4006ec: file ./func.c, line 7.

(gdb) run

Starting program: /home/xurenjun/share/debug/test


[print1] val = 5050


Breakpoint 1, print2 (size=100, string=0x4007f3 "this is test demo!") at ./func.c:7

7               ptr=(char*)malloc(size);

(gdb)

(gdb) next

8               memset(ptr,'\0',size);

(gdb) next

9               strcpy(ptr,string);

(gdb)


使用break(b)  文件名:行号在某个文件的某行打上断点;如在main.c的第8行打一个断点:

(gdb) break  main.c:8   

Breakpoint 1 at 0x40064c: file main.c, line 8.


使用delete 某断点编号 ;删除某个断点:

(gdb) info break

Num     Type           Disp Enb Address            What

1       breakpoint     keep y   0x000000000040064c main.c:8

        breakpoint already hit 1 time

2       breakpoint     keep y   <PENDING>          info

(gdb) delete 1

(gdb) info break

Num     Type           Disp Enb Address    What

2       breakpoint     keep y   <PENDING>  info

(gdb) delete 2

(gdb) info break

No breakpoints or watchpoints.


使用step可以单步运行程序,会进入子程序:

(gdb) break 9

Breakpoint 1 at 0x400668: file main.c, line 9.

(gdb) break 17

Breakpoint 2 at 0x40069e: file main.c, line 17.

(gdb) run

Starting program: /home/xurenjun/share/debug/test


Breakpoint 2, 0x000000000040069e in main ()

(gdb) step

Single stepping until exit from function main,

which has no line number information.

[print1] val = 5050


Breakpoint 1, 0x0000000000400668 in print1 ()

(gdb)

Single stepping until exit from function print1,

which has no line number information.

0x00000000004006aa in main ()

(gdb)

Single stepping until exit from function main,

which has no line number information.

[print2] this is test demo!

__libc_start_main (main=0x40066a <main>, argc=1, ubp_av=0x7fffffffe578, init=<optimized out>, fini=<optimized out>,

    rtld_fini=<optimized out>, stack_end=0x7fffffffe568) at libc-start.c:258

258     libc-start.c: No such file or directory.


使用continue(c)继续运行程序(为了方便观看main程序多加一句打印)

continue 命令继续运行程序。程序会在遇到断点后再次暂停运行。如果没有遇到断点,就会一直运行到结束。

(gdb) continue

(gdb) continue 次数:

main.c :

1 #include <stdio.h>

2 #include <stdlib.h>       

3 #include <string.h>

4

5 void print2(int,char*);   

6   

7 void print1(int* val){   

8     printf("[%s] val = %d\n",__FUNCTION__,*val);

9 }

10

11 int main(void){

12     int i = 0;

13     int res = 0;

14     for(i=0; i<=100; i++){

15         res += i;

16         printf("res=%d\n",res);       

17     }

18     print1(&res);

19     print2(100,"this is test demo!");

20     return 0;

21 }


(gdb) break 16

Breakpoint 1 at 0x400694: file main.c, line 16.

(gdb) break 20

Breakpoint 2 at 0x4006cd: file main.c, line 20.

(gdb) run

Starting program: /home/xurenjun/share/debug/test


Breakpoint 1, 0x0000000000400694 in main ()

(gdb) next

Single stepping until exit from function main,

which has no line number information.

res=0


Breakpoint 1, 0x0000000000400694 in main ()

(gdb)

Single stepping until exit from function main,

which has no line number information.

res=1


Breakpoint 1, 0x0000000000400694 in main ()

(gdb)

Single stepping until exit from function main,

which has no line number information.

res=3


Breakpoint 1, 0x0000000000400694 in main ()

(gdb) continue

Continuing.

res=6


Breakpoint 1, 0x0000000000400694 in main ()

(gdb) continue 100

Will ignore next 99 crossings of breakpoint 1.  Continuing.

res=10

res=15

res=21

res=28

res=36

res=45

res=55

res=66

res=78

res=91

res=105

res=120

res=136

res=153

res=171

res=190

res=210

res=231

res=253

res=276

res=300

res=325

res=351

res=378

res=406

res=435

res=465

res=496

res=528

res=561

res=595

res=630

res=666

res=703

res=741

res=780

res=820

res=861

res=903

res=946

res=990

res=1035

res=1081

res=1128

res=1176

res=1225

res=1275

res=1326

res=1378

res=1431

res=1485

res=1540

res=1596

res=1653

res=1711

res=1770

res=1830

res=1891

res=1953

res=2016

res=2080

res=2145

res=2211

res=2278

res=2346

res=2415

res=2485

res=2556

res=2628

res=2701

res=2775

res=2850

res=2926

res=3003

res=3081

res=3160

res=3240

res=3321

res=3403

res=3486

res=3570

res=3655

res=3741

res=3828

res=3916

res=4005

res=4095

res=4186

res=4278

res=4371

res=4465

res=4560

res=4656

res=4753

res=4851

res=4950

res=5050

[print1] val = 5050

[print2] this is test demo!


Breakpoint 2, 0x00000000004006cd in main ()   //再次遇到断点


print 变量名;查看变量的值:

main.c添加变量mval:


1 #include <stdio.h>

2 #include <stdlib.h>       

3 #include <string.h>

4

5 int mval;                 

6                           

7 void print2(int,char*);   

8   

9 void print1(int* val){   

10     printf("[%s] val = %d\n",__FUNCTION__,*val);

11 }

12

13 int main(void){

14     int i = 0;

15     int res = 0;

16     mval=100;

17     for(i=0; i<=100; i++){

18         res += i;

19         printf("res=%d\n",res);       

20     }

21     print1(&res);

22     print2(100,"this is test demo!");

23     return 0;

24 }


(gdb) break 14

Breakpoint 1 at 0x400672: file main.c, line 14.

(gdb) break 17

Breakpoint 2 at 0x40068a: main.c:17. (2 locations)

(gdb) print mval

$1 = 0

(gdb) run

Starting program: /home/xurenjun/share/debug/test


Breakpoint 1, 0x0000000000400672 in main ()

(gdb) print mval

$2 = 0

(gdb) next

Single stepping until exit from function main,

which has no line number information.


Breakpoint 2, 0x000000000040068a in main ()

(gdb) print mval

$3 = 100


watch  变量名;监视某一个变量:

(gdb) watch mval

Hardware watchpoint 1: mval

(gdb) run

Starting program: /home/xurenjun/share/debug/test

Hardware watchpoint 1: mval


Old value = 0

New value = 100

0x000000000040068a in main ()


backtrace 命令可以在遇到断点而暂停执行时显示栈帧。此外,backtrace 的别名还有 where 和 info stack

(gdb) break 17

Breakpoint 1 at 0x40068a: main.c:17. (2 locations)

(gdb) run

Starting program: /home/xurenjun/share/debug/test


Breakpoint 1, 0x000000000040068a in main ()

(gdb) backtrace

#0  0x000000000040068a in main ()

(gdb)







常见的0欧姆电阻与磁珠的作用


1,在电路中没有任何功能,只是在PCB上为了调试方便或兼容设计等原因。
2,可以做跳线用,如果某段线路不用,直接不贴该电阻即可(不影响外观)
3,在匹配电路参数不确定的时候,以0欧姆代替,实际调试的时候,确定参数,再以具体数值的元件代替。
4,想测某部分电路的耗电流的时候,可以去掉0ohm电阻,接上电流表,这样方便测耗电流。
5,在布线时,如果实在布不过去了,也可以加一个0欧的电阻
6,在高频信号下,充当电感或电容。(与外部电路特性有关)电感用,主要是解决EMC问题。如地与地,电源和IC Pin间
7,单点接地(指保护接地、工作接地、直流接地在设备上相互分开,各自成为独立系统。)
8,熔丝作用

磁珠
电感是储能元件,而磁珠是能量转换(消耗)器件。电感多用于电源滤波回路,侧重于抑止传导性干扰;磁珠多用于信号回路,主要用于EMI方面。磁珠用来吸收超高频信号,象一些RF电路,PLL,振荡电路,含超高频存储器电路(DDR,SDRAM,RAMBUS等)都需要在电源输入部分加磁珠,而电感是一种储能元件,用在LC振荡电路、中低频的滤波电路等,其应用频率范围很少超过50MHz。磁珠有很高的电阻率和磁导率,他等效于电阻和电感串联,但电阻值和电感值都随频率变化。 他比普通的电感有更好的高频滤波特性,在高频时呈现阻性,所以能在相当宽的频率范围内保持较高的阻抗,从而提高调频滤波效果。 
作为电源滤波,可以使用电感。磁珠的电路符号就是电感但是型号上可以看出使用的是磁珠在电路功能上,磁珠和电感是原理相同的,只是频率特性不同罢了


*模拟地和数字地单点接地*
  只要是地,最终都要接到一起,然后入大地。如果不接在一起就是"浮地",存在压差,容易积累电荷,造成静电。地是参考0电位,所有电压都是参考地得出的,地的标准要一致,故各种地应短接在一起。人们认为大地能够吸收所有电荷,始终维持稳定,是最终的地参考点。虽然有些板子没有接大地,但发电厂是接大地的,板子上的电源最终还是会返回发电厂入地。如果把模拟地和数字地大面积直接相连,会导致互相干扰。不短接又不妥,理由如上有四种方法解决此问题:1、用磁珠连接;2、用电容连接;3、用电感连接;4、用0欧姆电阻连接。
  磁珠的等效电路相当于带阻限波器,只对某个频点的噪声有显著抑制作用,使用时需要预先估计噪点频率,以便选用适当型号。对于频率不确定或无法预知的情况,磁珠不合。 
  电容隔直通交,造成浮地。
  电感体积大,杂散参数多,不稳定。


  0欧电阻相当于很窄的电流通路,能够有效地限制环路电流,使噪声得到抑制。电阻在所有频带上都有衰减作用(0欧电阻也有阻抗),这点比磁珠强。

0欧电阻
  *跨接时用于电流回路*
  当分割电地平面后,造成信号最短回流路径断裂,此时,信号回路不得不绕道,形成很大的环路面积,电场和磁场的影响就变强了,容易干扰/被干扰。在分割区上跨接0欧电阻,可以提供较短的回流路径,减小干扰。
  *配置电路*
  一般,产品上不要出现跳线和拨码开关。有时用户会乱动设置,易引起误会,为了减少维护费用,应用0欧电阻代替跳线等焊在板子上。
  空置跳线在高频时相当于天线,用贴片电阻效果好。
  *其他用途* 布线时跨线
  调试/测试用
  临时取代其他贴片器件
  作为温度补偿器件
更多时候是出于EMC对策的需要。另外,0欧姆电阻比过孔的寄生电感小,而且过孔还会影响地平面(因为要挖孔)。

关于ARM栈初始化起始地址

堆栈寄存器起始地址的设置:

    程序刚启动并没有启动内存管理单元MMU,真正的内存地址如下S3C2440的储存空间映射图:

 

 

    ARM 使用统一编址,所以,我们得把堆栈指针设置到内存地址范围内;

    NAND FLASH 启动时:

        堆栈寄存器可以设为片内RAM 的最大地址:0x1000(4K)

        或者64MSDRAM的最大地址0x34000000(64M SDRAM的地址空间映射到BANK6,那么内存地址范围就是 0x30000000~0x34000000)。


    之所以选择最大地址是因为栈的生长方向是向下的,所以选最高地址作为栈的起始地址可以预防堆栈曾涨覆盖数据域;


关于linux下运行应用程序时候提示: -/bin/sh: xxx:not found的解决办法

关于mini2440上-/bin/sh: 命令:not found的解决办法

我按照mini2440的移植手册移植了linux内核和文件系统不同的是我用的交叉编译器是最新的4.4.1而没有用天嵌科技提供的交叉编译器,当我移植好了yaffs文件系统,想写个helloworld程序在开发板上测试下,我把编译好的helloworld文件放到yaffs文件系统的/usr/bin目录下,但当我运行/usr/bin/helloworld命令是提示“-/bin/sh: /usr/bin/helloworld: not found”,一开始我以为是helloworld没有运行权限,不过我给了它运行权限还是提示同样的错误。我在网上搜了下找到了原因:只所以提示“-/bin/sh: /usr/bin/helloworld: not found”这个,是因为我没有拷helloworld所需的库文件。那怎么才能知道helloworld需要哪些库文件呢,可以这样,在命令行输入arm-linux-readelf -a helloworld 命令然后在输出的内容中找到Program Headers:节这里就有helloworld所需的库文件如下图:

关于linux下运行应用程序时候提示: -/bin/sh: xxx:not found的解决办法 - 徐仁俊 - 徐仁俊的博客

看来我们需要ld-linux.so.3这个库,在你的交叉编译器中找到这个库文件把它拷到我们文件系统的/lib目录中然后烧到开发板中再次运行/usr/bin/helloworld结果提示“/usr/bin/helloworld: error while loading shared libraries: libgcc_s.so.1: cannot open shared object file: No such file or directory”有效果了,最起码不是前一个错误提示了,这就证明方法对头,我们看一下上面的错误这次直接提示所需的库文件了,我们按提示把libgcc_s.so.1拷到文件系统的/lib中,然后再次运行,又提示“/usr/bin/helloworld: error while loading shared libraries: libc.so.6: cannot open shared object file: No such file or directory”还是少库文件,我们再把这个也拷到文件系统的/lib中,这次总算是行了,终于看到“hello world”。

 


linux设备驱动程序之简单字符设备驱动( 转载)

1、字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。
2、块设备:是指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。

  每一个字符设备或块设备都在/dev目录下对应一个设备文件。linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备。

 

二、字符设备驱动程序基础:
1、主设备号和次设备号(二者一起为设备号):
  一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。
  linux内核中,设备号用dev_t来描述,2.6.28中定义如下:
  typedef u_long dev_t;
  在32位机中是4个字节,高12位表示主设备号,低12位表示次设备号。

可以使用下列宏从dev_t中获得主次设备号:                   也可以使用下列宏通过主次设备号生成dev_t:
MAJOR(dev_t dev);                              MKDEV(int major,int minor);
MINOR(dev_t dev);

复制代码

//宏定义:
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

复制代码

 

2、分配设备号(两种方法):

(1)静态申请:
int register_chrdev_region(dev_t from, unsigned count, const char *name);

复制代码

/**
* register_chrdev_region() - register a range of device numbers
* @from: the first in the desired range of device numbers; must include
* the major number.
* @count: the number of consecutive device numbers required
* @name: the name of the device or driver.
*
* Return value is zero on success, a negative error code on failure.
*/

复制代码

(2)动态分配:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

复制代码

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
/**
* alloc_chrdev_region() - register a range of char device numbers
* @dev: output parameter for first assigned number
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: the name of the associated device or driver
*
* Allocates a range of char device numbers. The major number will be
* chosen dynamically, and returned (along with the first minor number)
* in @dev. Returns zero or a negative error code.
*/

复制代码

注销设备号

void unregister_chrdev_region(dev_t from, unsigned count);


创建设备文件
利用cat /proc/devices查看申请到的设备名,设备号。
(1)使用mknod手工创建:mknod filename type major minor
(2)自动创建;

  利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置。在驱动初始化代码里调用class_create为该设备创建一个class,再为每个设备调用device_create创建对应的设备。

 

3、字符设备驱动程序重要的数据结构
(1)struct file:代表一个打开的文件描述符,系统中每一个打开的文件在内核中都有一个关联的struct file。它由内核在open时创建,并传递给在文件上操作的任何函数,直到最后关闭。当文件的所有实例都关闭之后,内核释放这个数据结构。

//重要成员:
const struct file_operations *f_op; //该操作是定义文件关联的操作的。内核在执行open时对这个指针赋值。
off_t f_pos; //该文件读写位置。
void *private_data;//该成员是系统调用时保存状态信息非常有用的资源。

(2)struct inode:用来记录文件的物理信息。它和代表打开的file结构是不同的。一个文件可以对应多个file结构,但只有一个inode结构。inode一般作为file_operations结构中函数的参数传递过来。
  inode译成中文就是索引节点。每个存储设备或存储设备的分区(存储设备是硬盘、软盘、U盘 ... ... )被格式化为文件系统后,应该有两部份,一部份是inode,另一部份是Block,Block是用来存储数据用的。而inode呢,就是用来存储这些数据的信息,这些信息包括文件大小、属主、归属的用户组、读写权限等。inode为每个文件进行信息索引,所以就有了inode的数值。操作系统根据指令,能通过inode值最快的找到相对应的文件。

dev_t i_rdev; //对表示设备文件的inode结构,该字段包含了真正的设备编号。
struct cdev *i_cdev; //是表示字符设备的内核的内部结构。当inode指向一个字符设备文件时,该字段包含了指向struct cdev结构的指针。
//我们也可以使用下边两个宏从inode中获得主设备号和此设备号:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);

(3)struct file_operations

本部分来源于:http://blog.chinaunix.net/space.php?uid=20729583&do=blog&id=1884550,感谢chinahhucai的分享。

复制代码

struct file_operations ***_ops={
.owner = THIS_MODULE,
.llseek = ***_llseek,
.read = ***_read,
.write = ***_write,
.ioctl = ***_ioctl,
.open = ***_open,
.release = ***_release,
。。。 。。。
};

struct module *owner;
/*第一个 file_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指针.
这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为
THIS_MODULE, 一个在 <linux/module.h> 中定义的宏.这个宏比较复杂,在进行简单学习操作的时候,一般初始化为THIS_MODULE。*/


loff_t (*llseek) (struct file * filp , loff_t p, int orig);
/*(指针参数filp为进行读取信息的目标文件结构体指针;参数 p 为文件定位的目标偏移量;参数orig为对文件定位
的起始地址,这个值可以为文件开头(SEEK_SET,0,当前位置(SEEK_CUR,1),文件末尾(SEEK_END,2))
llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值.
loff_t 参数是一个"long offset", 并且就算在 32位平台上也至少 64 位宽. 错误由一个负返回值指示.
如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器( 在"file 结构" 一节中描述).*/

ssize_t (*read) (struct file * filp, char __user * buffer, size_t size , loff_t * p);
/*(指针参数 filp 为进行读取信息的目标文件,指针参数buffer 为对应放置信息的缓冲区(即用户空间内存地址),
参数size为要读取的信息长度,参数 p 为读的位置相对于文件开头的偏移,在读取信息后,这个指针一般都会移动,移动的值为要读取信息的长度值)
这个函数用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败.
一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型).*/

ssize_t (*aio_read)(struct kiocb * , char __user * buffer, size_t size , loff_t p);
/*可以看出,这个函数的第一、三个参数和本结构体中的read()函数的第一、三个参数是不同 的,
异步读写的第三个参数直接传递值,而同步读写的第三个参数传递的是指针,因为AIO从来不需要改变文件的位置。
异步读写的第一个参数为指向kiocb结构体的指针,而同步读写的第一参数为指向file结构体的指针,每一个I/O请求都对应一个kiocb结构体);
初始化一个异步读 -- 可能在函数返回前不结束的读操作.如果这个方法是 NULL, 所有的操作会由 read 代替进行(同步地).
(有关linux异步I/O,可以参考有关的资料,《linux设备驱动开发详解》中给出了详细的解答)*/

ssize_t (*write) (struct file * filp, const char __user * buffer, size_t count, loff_t * ppos);
/*(参数filp为目标文件结构体指针,buffer为要写入文件的信息缓冲区,count为要写入信息的长度,
ppos为当前的偏移位置,这个值通常是用来判断写文件是否越界)
发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.
(注:这个操作和上面的对文件进行读的操作均为阻塞操作)*/

ssize_t (*aio_write)(struct kiocb *, const char __user * buffer, size_t count, loff_t * ppos);
/*初始化设备上的一个异步写.参数类型同aio_read()函数;*/

int (*readdir) (struct file * filp, void *, filldir_t);
/*对于设备文件这个成员应当为 NULL; 它用来读取目录, 并且仅对文件系统有用.*/

unsigned int (*poll) (struct file *, struct poll_table_struct *);
/*(这是一个设备驱动中的轮询函数,第一个参数为file结构指针,第二个为轮询表指针)
这个函数返回设备资源的可获取状态,即POLLIN,POLLOUT,POLLPRI,POLLERR,POLLNVAL等宏的位“或”结果。
每个宏都表明设备的一种状态,如:POLLIN(定义为0x0001)意味着设备可以无阻塞的读,POLLOUT(定义为0x0004)意味着设备可以无阻塞的写。
(poll 方法是 3 个系统调用的后端: poll, epoll, 和 select, 都用作查询对一个或多个文件描述符的读或写是否会阻塞.
poll 方法应当返回一个位掩码指示是否非阻塞的读或写是可能的, 并且, 可能地, 提供给内核信息用来使调用进程睡眠直到 I/O 变为可能.
如果一个驱动的 poll 方法为 NULL, 设备假定为不阻塞地可读可写.
(这里通常将设备看作一个文件进行相关的操作,而轮询操作的取值直接关系到设备的响应情况,可以是阻塞操作结果,同时也可以是非阻塞操作结果)*/

int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
/*(inode 和 filp 指针是对应应用程序传递的文件描述符 fd 的值, 和传递给 open 方法的相同参数.
cmd 参数从用户那里不改变地传下来, 并且可选的参数 arg 参数以一个 unsigned long 的形式传递, 不管它是否由用户给定为一个整数或一个指针.
如果调用程序不传递第 3 个参数, 被驱动操作收到的 arg 值是无定义的.
因为类型检查在这个额外参数上被关闭, 编译器不能警告你如果一个无效的参数被传递给 ioctl, 并且任何关联的错误将难以查找.)
ioctl 系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道, 这不是读也不是写). 另外, 几个 ioctl 命令被内核识别而不必引用 fops 表.
如果设备不提供 ioctl 方法, 对于任何未事先定义的请求(-ENOTTY, "设备无这样的 ioctl"), 系统调用返回一个错误.*/

int (*mmap) (struct file *, struct vm_area_struct *);
/*mmap 用来请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL, mmap 系统调用返回 -ENODEV.
(如果想对这个函数有个彻底的了解,那么请看有关“进程地址空间”介绍的书籍)*/

int (*open) (struct inode * inode , struct file * filp ) ;
/*(inode 为文件节点,这个节点只有一个,无论用户打开多少个文件,都只是对应着一个inode结构;
但是filp就不同,只要打开一个文件,就对应着一个file结构体,file结构体通常用来追踪文件在运行时的状态信息)
尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知.
与open()函数对应的是release()函数。*/

int (*flush) (struct file *);
/*flush 操作在进程关闭它的设备文件描述符的拷贝时调用; 它应当执行(并且等待)设备的任何未完成的操作.
这个必须不要和用户查询请求的 fsync 操作混淆了. 当前, flush 在很少驱动中使用;
SCSI 磁带驱动使用它, 例如, 为确保所有写的数据在设备关闭前写到磁带上. 如果 flush 为 NULL, 内核简单地忽略用户应用程序的请求.*/

int (*release) (struct inode *, struct file *);
/*release ()函数当最后一个打开设备的用户进程执行close()系统调用的时候,内核将调用驱动程序release()函数:
void release(struct inode inode,struct file *file),release函数的主要任务是清理未结束的输入输出操作,释放资源,用户自定义排他标志的复位等。
在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL.*/

int(*synch)(struct file *,struct dentry *,int datasync);
//刷新待处理的数据,允许进程把所有的脏缓冲区刷新到磁盘。


int (*aio_fsync)(struct kiocb *, int);
/*这是 fsync 方法的异步版本.所谓的fsync方法是一个系统调用函数。系统调用fsync
把文件所指定的文件的所有脏缓冲区写到磁盘中(如果需要,还包括存有索引节点的缓冲区)。
相应的服务例程获得文件对象的地址,并随后调用fsync方法。通常这个方法以调用函数__writeback_single_inode()结束,
这个函数把与被选中的索引节点相关的脏页和索引节点本身都写回磁盘。*/

int (*fasync) (int, struct file *, int);
//这个函数是系统支持异步通知的设备驱动,下面是这个函数的模板:

static int ***_fasync(int fd,struct file *filp,int mode)
{
struct ***_dev * dev=filp->private_data;
return fasync_helper(fd,filp,mode,&dev->async_queue);//第四个参数为 fasync_struct结构体指针的指针。
//这个函数是用来处理FASYNC标志的函数。(FASYNC:表示兼容BSD的fcntl同步操作)当这个标志改变时,驱动程序中的fasync()函数将得到执行。
}
/*此操作用来通知设备它的 FASYNC 标志的改变. 异步通知是一个高级的主题, 在第 6 章中描述.
这个成员可以是NULL 如果驱动不支持异步通知.*/

int (*lock) (struct file *, int, struct file_lock *);
//lock 方法用来实现文件加锁; 加锁对常规文件是必不可少的特性, 但是设备驱动几乎从不实现它.

ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
/*这些方法实现发散/汇聚读和写操作. 应用程序偶尔需要做一个包含多个内存区的单个读或写操作;
这些系统调用允许它们这样做而不必对数据进行额外拷贝. 如果这些函数指针为 NULL, read 和 write 方法被调用( 可能多于一次 ).*/

ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *);
/*这个方法实现 sendfile 系统调用的读, 使用最少的拷贝从一个文件描述符搬移数据到另一个.
例如, 它被一个需要发送文件内容到一个网络连接的 web 服务器使用. 设备驱动常常使 sendfile 为 NULL.*/

ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
/*sendpage 是 sendfile 的另一半; 它由内核调用来发送数据, 一次一页, 到对应的文件. 设备驱动实际上不实现 sendpage.*/

unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
/*这个方法的目的是在进程的地址空间找一个合适的位置来映射在底层设备上的内存段中.
这个任务通常由内存管理代码进行; 这个方法存在为了使驱动能强制特殊设备可能有的任何的对齐请求. 大部分驱动可以置这个方法为 NULL.[10]*/

int (*check_flags)(int)
//这个方法允许模块检查传递给 fnctl(F_SETFL...) 调用的标志.

int (*dir_notify)(struct file *, unsigned long);
//这个方法在应用程序使用 fcntl 来请求目录改变通知时调用. 只对文件系统有用; 驱动不需要实现 dir_notify.

复制代码

 

三、字符设备驱动程序设计

1.设备注册
在linux2.6内核中,字符设备使用struct cdev来描述;

复制代码

struct cdev
{
struct kobject kobj;//内嵌的kobject对象
struct module *owner;//所属模块
struct file_operations *ops;//文件操作结构体
struct list_head list;
dev_t dev;//设备号,长度为32位,其中高12为主设备号,低20位为此设备号
unsigned int count;
};

复制代码

字符设备的注册分为三个步骤:

(1)分配cdev: struct cdev *cdev_alloc(void);
(2)初始化cdev: void cdev_init(struct cdev *cdev, const struct file_operations *fops);
(3)添加cdev: int cdev_add(struct cdev *p, dev_t dev, unsigned count)

复制代码

/**
* cdev_add() - add a char device to the system
* @p: the cdev structure for the device
* @dev: the first device number for which this device is responsible
* @count: the number of consecutive minor numbers corresponding to this
* device
*
* cdev_add() adds the device represented by @p to the system, making it
* live immediately. A negative error code is returned on failure.
*/

复制代码

 

2.设备操作的实现:file_operations函数集的实现(要明确某个函数什么时候被调用?调用来做什么操作?)
特别注意:驱动程序应用程序的数据交换:
  驱动程序和应用程序的数据交换是非常重要的。file_operations中的read()和write()函数,就是用来在驱动程序和应用程序间交换数据的。通过数据交换,驱动程序和应用程序可以彼此了解对方的情况。但是驱动程序和应用程序属于不同的地址空间。驱动程序不能直接访问应用程序的地址空间;同样应用程序也不能直接访问驱动程序的地址空间,否则会破坏彼此空间中的数据,从而造成系统崩溃,或者数据损坏。安全的方法是使用内核提供的专用函数,完成数据在应用程序空间和驱动程序空间的交换。这些函数对用户程序传过来的指针进行了严格的检查和必要的转换,从而保证用户程序与驱动程序交换数据的安全性。这些函数有:

unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
put_user(local,user);
get_user(local,user);

 

3.设备注销:void cdev_del(struct cdev *p);

四、字符设备驱动小结:

  字符设备是3大类设备(字符设备、块设备、网络设备)中较简单的一类设备,其驱动程序中完成的主要工作是初始化、添加和删除cdev结构体,申请和释放设备号,以及填充file_operation结构体中操作函数,并实现file_operations结构体中的read()、write()、ioctl()等重要函数。如图所示为cdev结构体、file_operations和用户空间调用驱动的关系。

 

五:字符设备驱动程序分析:

(1)memdev.h

复制代码

#ifndef _MEMDEV_H_
#define _MEMDEV_H_

#ifndef MEMDEV_MAJOR
#define MEMDEV_MAJOR 251 /*预设的mem的主设备号*/
#endif

#ifndef MEMDEV_NR_DEVS
#define MEMDEV_NR_DEVS 2 /*设备数*/
#endif

#ifndef MEMDEV_SIZE
#define MEMDEV_SIZE 4096
#endif

/*mem设备描述结构体*/
struct mem_dev
{
char *data;
unsigned long size;
};

#endif /* _MEMDEV_H_ */

复制代码

 

(2)memdev.c

复制代码

static mem_major = MEMDEV_MAJOR;

module_param(mem_major, int, S_IRUGO);

struct mem_dev *mem_devp; /*设备结构体指针*/

struct cdev cdev;

/*文件打开函数*/
int mem_open(struct inode *inode, struct file *filp)
{
struct mem_dev *dev;

/*获取次设备号*/
int num = MINOR(inode->i_rdev);

if (num >= MEMDEV_NR_DEVS)
return -ENODEV;
dev = &mem_devp[num];

/*将设备描述结构指针赋值给文件私有数据指针*/
filp->private_data = dev;

return 0;
}

/*文件释放函数*/
int mem_release(struct inode *inode, struct file *filp)
{
return 0;
}

/*读函数*/
static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos; /*记录文件指针偏移位置*/
unsigned int count = size; /*记录需要读取的字节数*/
int ret = 0; /*返回值*/
struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/

/*判断读位置是否有效*/
if (p >= MEMDEV_SIZE) /*要读取的偏移大于设备的内存空间*/
return 0;
if (count > MEMDEV_SIZE - p) /*要读取的字节大于设备的内存空间*/
count = MEMDEV_SIZE - p;

/*读数据到用户空间:内核空间->用户空间交换数据*/
if (copy_to_user(buf, (void*)(dev->data + p), count))
{
ret = - EFAULT;
}
else
{
*ppos += count;
ret = count;

printk(KERN_INFO "read %d bytes(s) from %d\n", count, p);
}

return ret;
}

/*写函数*/
static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/

/*分析和获取有效的写长度*/
if (p >= MEMDEV_SIZE)
return 0;
if (count > MEMDEV_SIZE - p) /*要写入的字节大于设备的内存空间*/
count = MEMDEV_SIZE - p;

/*从用户空间写入数据*/
if (copy_from_user(dev->data + p, buf, count))
ret = - EFAULT;
else
{
*ppos += count; /*增加偏移位置*/
ret = count; /*返回实际的写入字节数*/

printk(KERN_INFO "written %d bytes(s) from %d\n", count, p);
}

return ret;
}

/* seek文件定位函数 */
static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)
{
loff_t newpos;

switch(whence) {
case 0: /* SEEK_SET */ /*相对文件开始位置偏移*/
newpos = offset; /*更新文件指针位置*/
break;

case 1: /* SEEK_CUR */
newpos = filp->f_pos + offset;
break;

case 2: /* SEEK_END */
newpos = MEMDEV_SIZE -1 + offset;
break;

default: /* can't happen */
return -EINVAL;
}
if ((newpos<0) || (newpos>MEMDEV_SIZE))
return -EINVAL;

filp->f_pos = newpos;
return newpos;

}

/*文件操作结构体*/
static const struct file_operations mem_fops =
{
.owner = THIS_MODULE,
.llseek = mem_llseek,
.read = mem_read,
.write = mem_write,
.open = mem_open,
.release = mem_release,
};

/*设备驱动模块加载函数*/
static int memdev_init(void)
{
int result;
int i;

dev_t devno = MKDEV(mem_major, 0);

/* 申请设备号,当xxx_major不为0时,表示静态指定;当为0时,表示动态申请*/
/* 静态申请设备号*/
if (mem_major)
result = register_chrdev_region(devno, 2, "memdev");
else /* 动态分配设备号 */
{
result = alloc_chrdev_region(&devno, 0, 2, "memdev");
mem_major = MAJOR(devno); /*获得申请的主设备号*/
}

if (result < 0)
return result;

/*初始化cdev结构,并传递file_operations结构指针*/
cdev_init(&cdev, &mem_fops);
cdev.owner = THIS_MODULE; /*指定所属模块*/
cdev.ops = &mem_fops;

/* 注册字符设备 */
cdev_add(&cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS);

/* 为设备描述结构分配内存*/
mem_devp = kmalloc(MEMDEV_NR_DEVS * sizeof(struct mem_dev), GFP_KERNEL);
if (!mem_devp) /*申请失败*/
{
result = - ENOMEM;
goto fail_malloc;
}
memset(mem_devp, 0, sizeof(struct mem_dev));

/*为设备分配内存*/
for (i=0; i < MEMDEV_NR_DEVS; i++)
{
mem_devp[i].size = MEMDEV_SIZE;
mem_devp[i].data = kmalloc(MEMDEV_SIZE, GFP_KERNEL);
memset(mem_devp[i].data, 0, MEMDEV_SIZE);
}

return 0;

fail_malloc:
unregister_chrdev_region(devno, 1);

return result;
}

/*模块卸载函数*/
static void memdev_exit(void)
{
cdev_del(&cdev); /*注销设备*/
kfree(mem_devp); /*释放设备结构体内存*/
unregister_chrdev_region(MKDEV(mem_major, 0), 2); /*释放设备号*/
}

MODULE_AUTHOR("David Xie");
MODULE_LICENSE("GPL");

module_init(memdev_init);
module_exit(memdev_exit);

复制代码



(3)应用程序(测试文件):app-mem.c

复制代码

#include <stdio.h>

int main()
{
FILE *fp0 = NULL;
char Buf[4096];

/*初始化Buf*/
strcpy(Buf,"Mem is char dev!");
printf("BUF: %s\n",Buf);

/*打开设备文件*/
fp0 = fopen("/dev/memdev0","r+");
if (fp0 == NULL)
{
printf("Open Memdev0 Error!\n");
return -1;
}

/*写入设备*/
fwrite(Buf, sizeof(Buf), 1, fp0);

/*重新定位文件位置(思考没有该指令,会有何后果)*/
fseek(fp0,0,SEEK_SET);

/*清除Buf*/
strcpy(Buf,"Buf is NULL!");
printf("BUF: %s\n",Buf);


/*读出设备*/
fread(Buf, sizeof(Buf), 1, fp0);

/*检测结果*/
printf("BUF: %s\n",Buf);

return 0;

}

复制代码

测试步骤:

1)cat /proc/devices看看有哪些编号已经被使用,我们选一个没有使用的XXX。2)insmod memdev.ko3)通过"mknod /dev/memdev0 c XXX 0"命令创建"/dev/memdev0"设备节点。4)交叉编译app-mem.c文件,下载并执行:#./app-mem,显示:Mem is char dev!

linux字符设备驱动开发基础知识(转载)

这章将介绍Linux系统的设 备,这样我们才能清楚的知道应用程序和设备驱动程序是如何的工作的,或者说应用程序是如何控制驱动程序的,进而知道应用程序是如何通过驱动程序操作设备 的,另外会详细的介绍设备号及设备文件。

 

Linux设备分类
Linux下的设备通常分为三类,字符设备,块设备和网络设 备。

字符设备

一个字符设 备是一种字节流设备,对设备的存取只能按顺序按字节的存取而不能随机访问,字符设备没有请求缓冲区,所有的访问请求都是按顺序执行的。Linux下的大多设备都是字符设备。应用程序是通过字符设备节点来访问 字符设备的。设备节点一般都由mknod命令都创 建在/dev目录下,下 面的例子显示了串口设备的设备节点。字符设备文件的第一个标志是前面的“c”标志。

root#ls -l /dev/ttyS[0-3]
crw-rw----  1 root  root 4, 64 Feb 18 23:34 /dev/ttyS0
crw-r-----  1 root  root 4, 65 Nov 17 10:26 /dev/ttyS1
crw-rw----  1 root  root 4, 66 Jul  5  2000 /dev/ttyS2
crw-rw----  1 root  root 4, 67 Jul  5  2000 /dev/ttyS3

字符设备是指那些只能按顺序一个字节一个字节读取的设备,但事实上现在一些高级 字符设备也可以从指定位置一次读取一块数据。字符设备是面向数据流的设备,每个字符设备都有一个设备号,设备号由主设备号和次设备号组成。同时Linux使用管理文件相同的方法来管理字符设备,所以每个字符设备在/dev/目录下都有一个对应的设备文件,即设备节点,它们包含了设备的 类型、主/次设备号以 及设备的访问权限控制等,系统通过设备文件来对字符设备进行操作,每个字符设备文件都有自己的与普通文件相同的文件操作函数组结构(struct file_operations)。字符设 备驱动通常至少需要实现文件操作函数组中的open、release、read和write四种操作方法。常见的字符设备有鼠标、键盘、串口、控制台等。

块设备

存储设备一 般属于块设备,块设备有请求缓冲区,并且支持随机访问而不必按照顺序去存取数据,比如你可以 先存取后面的数据,然后在存取前面的数据,这对字符设备来说是不可能的。Linux下的磁盘 设备都是块设备,尽管在Linux下有块设 备节点,但应用程序一般是通过文件系统及其高速缓存来访问块设备的,而不是直 接通过设备节点来读写块设备上的数据。块设备文件的第一个标志是前面的“b”标志。

root# ls -l /dev/hda[1-3]
brw-rw----  1 root  root  3, 1 Jul  5  2000 /dev/hda1
brw-rw----  1 root  root  3, 2 Jul  5  2000 /dev/hda2
brw-rw----  1 root  root  3, 3 Jul  5  2000 /dev/hda3

块设备是指那些可以从设备的任意位置读取任意长度数据的设备。每个块设备同样有 一个设备号,设备号由主设备号和次设备号组成。同时Linux也使用管 理文件相同的方法来管理块设备,每个块设备在/dev/目录下都 有一个对应的设备文件,即设备节点,它们包含了设备的类型、主/次设备号 以及设备的访问权限控制等,系统通过设备文件来对块设备进行操作,每个块设备文件都有自己的与普通文件相同的文件操作函数组结构(struct file_operations)。但块设 备需要实现的操作方法远比字符设备的操作方法多得多,也难得多。块设备既可以作为普通的裸设备用来存放任意数据,也可以将块设备按某种文件系统类型的格式 进行格式化,然后按照该文件系统类型的格式来读取块设备上的数据,但不管哪种方式,最后访问设备上的数据都必须通过调用设备本身的操作方法实现,区别在于 前者直接调用块设备的操作方法,而后者则间接调用块设备的操作方法。常见的块设备有各种硬盘、flash磁盘、RAM磁盘等。

网络设备

网络设备不 同于字符设备和块设备,它是面向报文的而不是面向流的,它不支持随机访问,也没有请求缓冲区。在Linux里一个网络设备也可以叫做一个网络接口,如eth0,应用程序是通过Socket而不是设备节点来访问网络设备,在系统里根本就不存在网络设备节点。

网络接口用来与其他设备交换数据,它可以是硬件设备,也可以是纯软件设备,如loopback接口就是一个纯软件设备。网络接口由内核中的网络 子系统驱动,负责发送和接收数据包,但它不需要了解每项事务如何映射到实际传送的数据包,许多网络连接(尤其是使用TCP协议的连接)是面向流的,但网络设备围绕数据包的传输和接收设 计。网络驱动程序不需要知道各个连接的相关信息,它只需处理数据包。网络接口没有像字符设备和块设备一样的设备号,只有一个唯一的名字,如eth0、eth1等,而这个名字也不需要与设备文件节点对应。内核使用一套与数据 包传输相关的函数来与网络设备驱动程序通信,它们不同于字符设备和块设备的read()和write()方法。


设备节点、设备驱动及设备的关联
     当我们访问 一个设备节点是,系统是如果知道使用哪个设备驱动及访问哪个设备的呢?这个是通过设备号来实现的。当我们创建一个设备节点时需要指定主设备号和次设备号。 对于设备节点来说,名字不是重要的,设备号才是最重要的,它实际指定了对应的驱动程序和对应的设备。

Linux的设备管理是和文件系统紧密结合的,各种设备都以文件的形式存 放在/dev目录下,称 为设备文件。应用程序可以打开、关闭和读写这些设备文件,完成对设备的操作,就像操作普通的数据文件一样。为了管理这些设备,系统为设备编了号,每个设备 号又分为主设备号和次设备号。主设备号用来区分不同种类的设备,而次设备号用来区分同一类型的多个设备。对于常用设备,Linux有约定俗成的编号,如硬盘的主设备号是3。

 

      Linux为所有的 设备文件都提供了统一的操作函数接口,方法是使用数据结构struct file_operations。这个数据结构中包括许多操作函数的指针,如open()、close()、read()和write()等,但由于外设的种类较多,操作方式各不相同。Struct file_operations结构体中的 成员为一系列的接口函数,如用于读/写的read/write函数和用于控制的ioctl等。打开一个文件就是调用这个文件file_operations中的open操作。不同类型的文件有不同的file_operations成员函数, 如普通的磁盘数据文件,接口函数完成磁盘数据块读写操作;而对于各种设备文件,则最终调用各自驱动程序中的I/O函数进行具体设备的操作。这样,应用程序根本不必考虑操作的是设 备还是普通文件,可一律当作文件处理,具有非常清晰统一的I/O接口。所 以file_operations是文件层 次的I/O接口。

 

主设备号

驱动程序在 初始化时,会注册它的驱动及对应主设备号到系统中,这样当应用程序访问设备节点时,系统就知道它所访问的驱动程序了。你可以通过/proc/devices文件来驱动 系统设备的主设备号。

次设备号

驱动程序遍 历设备时,每发现一个它能驱动的设备,就创建一个设备对象,并为其分配一个次设备号以区分不同的设备。这样当应用程序访问设备节点时驱动程序就可以根据次 设备号知道它说访问的设备了。

系统中的每一个字符设备和块设备(网络接口没有设备号)都有一个设备号,传统的UNIX以及早期版本Linux中的设备号是16位的,主次设备号都是8位的,低8位为次设备号,高8位为主设备号,因此系统最多分别支持65536个字符设备和65536个块设备,这个限制已经不能满足当 前层出不穷的各种新设备的需要,所以Linux2.6中对设备号已经进行了扩展,一个设备 号为32位,主设备号为12位,次设备号为20位,但是这32位设备号的编码方式有新旧两种,旧的 设备编号格式为:最高12位为主设备号,最低20位为次设备号;新的设备编号格式为:bit[19:8]是主设备号,bit[31:20]是次设备号的高12位,bit[7:0]是次设备号的低8位。如果知道了一个设备的主设备号major和次设备号minor,那么用MKDEV(major,minor)生成是该设备的旧格式的设备号,用new_encode_dev(MKDEV(major,minor))生成的则是新格式的设备号。Linux支持的各种设备的主设备号定义在include/linux/major.h文件中,而已经在官方注册的主设备 号和次设备号在Documentation/devices.txt文件中可以找到。

老式16位设备 号、32位旧格式设 备号以及32位新格式设 备号的转换操作函数如下:

new_encode_dev(dev_t dev)函数

将32位旧格式 设备号dev转换成32位新格式设备号。

new_decode_dev(u32 dev)函数

将32位新格式 设备号转换成32位旧格式设 备号。

old_encode_dev(dev_t dev)函数

将32位旧格式 设备号转换成老式16位设备号。

dev_t old_decode_dev(u16 val)函数

将老式16位设备号转换成32位旧格式设备号。

Linux中设备节点是通过“mknod”命令来创建 的。一个设备节点其实就是一个文件,Linux中称为设 备文件。有一点必要说明的是,在Linux中,所有 的设备访问都是通过文件的方式,一般的数据文件程序普通文件,设备节点称为设备文件。在Linux内核中网络设备也是通过文件操作的,称为网络设备文件,在用户空间是通过socket接口来访问的。socket号就是网络设备文件描述符。

如:mknod /dev/mydevice c 254 0

(c代表子都设备,254为主设备号,0为次设备号)

Open,close等操作/dev/下设备文件,内核根据文件的主设备号找到对应的设备驱动

主设备号可以分为动态和静态申请。

设备文件

Linux使用对文 件一样管理方式来管理设备,所以对于系统中的每个字符设备或者块设备都必须为其创建一个设备文件,这个设备文件就是放在/dev/目录下的设备节点,它包含了该设备的设备类型(块设备或字符设 备)、设备号(主设备号和次设备号)以及设备访问控制属性等。设备文件可以通过手工用mknod命令生成也可以由udev用户工具 软件在系统启动后根据/sys目录下每个 设备的实际信息创建,使用后一种方式可以为每个设备动态分配设备号,而不必分配固定的设备号,如果系统中的设备不多,而且设备类型又是常见的,可以使用手 工方式生成设备文件,为常用设备创建一个已经分配号的设备号对应的设备文件,这样比较方便。如果系统很大,系统中的设备太多,那么最好动态分配设备号,由udev在系统启动之后根据设备实际信息自动创建设备文件。

Linux下的大部分驱动程序都是字符设备驱动程序,通过下面的学习我们将 会了解到字符设备是如何注册到系统中的,应用程序是如何访问驱动程序的数据的,及字符驱动程序是如何工作的。


设备号
通过前面的 学习我们知道应用程序是通过设备节点来访问驱动程序及设备的,其根本是通过设备节点的设备号(主设备号及从设备号)来关联驱动程序及设备的,字符设备也不 例外(其实字符设备只能这样访问)。这里我们详细讨论Linux内部如何管 理设备号的。

  • 设备号类型

Linux内核里用“dev_t”来表示设备 号,它是一个32位的无符号 数,其高12位用来表示主 设备号,低20位用来表示从 设备号。它被定义在<linux/types.h>头文件里。 内核里提供了操作“dev_t”的函数,驱动 程序中通过这些函数(其实是宏,定义在<linux/kdev_t.h>文件中)来 操作设备号。

#define MINORBITS    20
#define MINORMASK    ((1U << MINORBITS) - 1)
#define MAJOR(dev)    ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev)    ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))


MAJOR(dev)用于获取主设备号,MINOR(dev)用于获取从设备号,而MKDEV(ma,mi)用于通过主设备号和从设备号构造"dev_t"数据。
另一点需要 说明的是,dev_t数据类型支持2^12个主设备号,每个主设备号(通常是一个设备驱动)可以支持2^20个设备,目前来说这已经足够大了,但谁又能说将来还能满足要求 呢?一个良好的编程习惯是不要依赖dev_t这个数据类 型,切记必须使用内核提供的操作设备号的函数。

  • 字符设备号注册

内核提供了字符设备号管理的函数接口,作为一个良好的编程习惯,字符设备驱动程 序应该通过这些函数向系统注册或注销字符设备号。

int register_chrdev_region(dev_t from, unsigned count, const char *name)
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
            const char *name)
void unregister_chrdev_region(dev_t from, unsigned count)


register_chrdev_region用于向内核 注册已知可用的设备号(次设备号通常是0)范围。由 于历史的原因一些设备的设备号是固定的,你可以在内核源代码树的Documentation/devices.txt文件中找到 这些静态分配的设备号。

alloc_chrdev_region用于动态分 配的设备号并注册到内核中,分配的设备号通过dev参数返回。 作为一个良好的内核开发习惯,我们推荐你使用动态分配的方式来生成设备号。
unregister_chrdev_region用于注销一 个不用的设备号区域,通常这个函数在驱动程序卸载时被调用。


字符设备
Linux2.6内核使用“struct cdev”来记录字符设 备的信息,内核也提供了相关的函数来操作“struct cdev”对象,他们 定义在<linux/cdev.h>头文件中。 可见字符设备及其操作函数接口定义的很简单。

struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
};

void cdev_init(struct cdev *, const struct file_operations *);
struct cdev *cdev_alloc(void);
void cdev_put(struct cdev *p);
int cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev *);


对于Linux 2.6内核来说,struct cdev是内核字符设备的基础结构,用来表示一个字符设备, 包含了字符设备需要的全部信息。

  • kobj:struct kobject对象数据,用 来描述设备的引用计数,是Linux设备模型的 基础结构。我们在后面的“Linux设备模型”在做详细的介绍。
  • owner:struct module对象数据,描 述了模块的属主,指向拥有这个结构的模块的指针,显然它只有对编译为模块方式的驱动才由意义。一般赋值位“THIS_MODULE”。
  • ops:struct file_operations对象数据,描 述了字符设备的操作函数指针。对于设备驱动来说,这是一个很重要的数据成员,几乎所有的驱动都要用到这个对象,我们会在下面做详细介绍。
  • dev:dev_t对象数据,描述了字符设备的设备号。

内核提供了操作字符设备对象“struct cdev”的函数,我们 只能通过这些函数来操作字符设备,例如:初始化、注册、添加、移除字符设备。

  • cdev_alloc:用于动态 分配一个新的字符设备 cdev 对象,并对其 进行初始化。采用cdev_alloc分配的cdev对象需要显示的初始化owner和ops对象。

// 参考drivers/scsi/st.c:st_probe 函数
struct cdev *cdev = NULL;
cdev = cdev_alloc();
// Error Processing
cdev->owner = THIS_MODULE;
cdev->ops = &st_fops;

 

  • cdev_init:用于初始 化一个静态分配的cdev对象,一般这 个对象会嵌入到其他的对象中。cdev_init会自动初始 化ops数据,因此应 用程序只需要显示的给owner对象赋值。cdev_init的功能与cdev_alloc基本相同,唯 一的区别是cdev_init初始化一个 已经存在的cdev对象,并且这 个初始化会影响到字符设备删除函数(cdev_del)的行为, 请参考cdev_del函数。
  • cdev_add:向内核系 统中添加一个新的字符设备cdev,并且使它立 即可用。
  • cdev_del:从内核系 统中移除cdev字符设备。如 果字符设备是由cdev_alloc动态分配的, 则会释放分配的内存。
  • cdev_put:减少模块 的引用计数,一般很少会有驱动程序直接调用这个函数。

文件操作对象
Linux中的所有设备都是文件,内核中用“struct file”结构来表示一 个文件。尽管我们的驱动不会直接使用这个结构中的大部分对象,其中的一些数据成员还是很重要的,我们有必要在这里做一些介绍,具体的内容请参考内核源代码 树<linux/fs.h>头文件。

// struct file 中的一些重要数据成员
const struct file_operations    *f_op;
unsigned int         f_flags;
mode_t            f_mode;
loff_t            f_pos;
struct address_space    *f_mapping;


这里我们不对struct file做过多的介绍,另一篇struct file将做详细介绍。这个结构中的f_ops成员是我们的驱动所关心的,它是一个struct file_operations结构。Linux里的struct file_operations结构描述了一 个文件操作需要的所有函数,它定义在<linux/fs.h>头文件中。

struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    int (*readdir) (struct file *, void *, filldir_t);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, struct dentry *, int datasync);
    int (*aio_fsync) (struct kiocb *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*dir_notify)(struct file *filp, unsigned long arg);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **);
};


这是一个很大的结构,包含了所有的设备操作函数指针。当然,对于 一个驱动,不是所有的接口都需要来实现的。对于一个字符设备来说,一般实现open、release、read、write、mmap、ioctl这几个函数就足够了。
这里需要指 出的是,open和release函数的第一个参数是一个struct inode对象。这是一个内核文件系统索引节点对象,它包含 了内核在操作文件或目录是需要的全部信息。对于字符设备驱动来说,我们关心的是从struct inode对象中获取设备号(inode的i_rdev成员)内核提供了两个函数来做这件事。

static inline unsigned iminor(const struct inode *inode)
{
    return MINOR(inode->i_rdev);
}
static inline unsigned imajor(const struct inode *inode)
{
    return MAJOR(inode->i_rdev);
}


尽管我们可 以直接从inode->i_rdev获取设备 号,但是尽量不要这样做。我们推荐你调用内核提供的函数来获取设备号,这样即使将来inode->i_rdev有所变化, 我们的程序也会工作的很好。

 

字符设备驱 动可以参考Linux 设备驱动程序 第三版和linux设备驱动开发 详解,其中linux设备驱动程序 第三版中讲的:

主次编号

一些重要数据结构

字符设备注册

Open和release

读和写

一些头文件和结构体;

都非常经典, 都理解字符驱动设备很重要,很值得参考!

Linux内核链表简单应用

#include<linux/init.h>

#include<linux/module.h>

#include<linux/list.h>


/*Linux内核中有list_head原型如下

struct list_head 

{

struct list_head *next, *prev;

};

*/


//创建一个结构体保存学生信息

struct score

{

int num;

int math;

int eng;

struct list_head list;

};


struct score stu1,stu2,stu3;


struct list_head score_head;


struct list_head *pos;


struct score *tmp;


MODULE_LICENSE("GPL"); //申明遵守的许可证协议

MODULE_AUTHOR("XRJ"); //申明作者

MODULE_DESCRIPTION("First Module Program!"); //功能描述

MODULE_VERSION("V1.0"); //程序版本



static int mylist_init(void)

{

INIT_LIST_HEAD(&score_head); //Linux内核API创建链表

stu1.num=1;

stu1.math=90;

stu1.eng=80;

list_add_tail((&stu1.list),&score_head); //Linux内核API插入链表尾部

stu2.num=2;

stu2.math=100;

stu2.eng=80;

list_add_tail((&stu2.list),&score_head); //Linux内核API插入链表尾部

stu3.num=3;

stu3.math=60;

stu3.eng=99;

list_add_tail((&stu3.list),&score_head); //Linux内核API插入链表尾部

list_for_each(pos,&score_head); //Linux内核API遍历链表

{

tmp=list_entry(pos,struct score,list);

printk("num %d,eng is %d,math is %d\n",tmp->num,tmp->eng,tmp->math);

}

return 0;

}



static void mylist_exit(void)

{

list_del(&(stu1.list)); //Linux内核API链表节点删除

list_del(&(stu2.list)); //Linux内核API链表节点删除

list_del(&(stu3.list)); //Linux内核API链表节点删除

}


module_init(mylist_init);

module_exit(mylist_exit);

MDK生成编译文件大小探索---STM32为例(转载)

一般在stm32工程使用keil编译之后,keil的build output栏目下面会出现如图所示的输出信息,其中会显示code 大小 RO-data、RW-data 、ZI-data的大小。一般别人不怎么会在意这个的大小。

出于好奇我百度了下网上关于这些段的介绍,援引自http://mcuos.com/thread-2843-1-1.html,上面的介绍是这样说的:

ARM程序的组成
此处所说的“ARM程序”是指在ARM系统中正在执行的程序,而非保存在ROM中的bin映像(image)文件,这一点清注意区别。
一个ARM程序包含3部分:RO,RW和ZI。RO是程序中的指令和常量RW是程序中的已初始化变量;ZI是程序中的未初始化的变量.
            由以上3点说明可以理解为:RO就是readonly,RW就是read/write,ZI就是zero

ARM映像文件的组成
            所谓ARM映像文件就是指烧录到ROM中的bin文件,也称为image文件。以下用Image文件来称呼它。
Image文件包含了RO和RW数据。之所以Image文件不包含ZI数据,是因为ZI数据都是0,没必要包含,只要程序运行之前将ZI数据所在的区域一律清零即可。包含进去反而浪费存储空间
            Q:为什么Image中必须包含RO和RW?
            A:因为RO中的指令和常量以及RW中初始化过的变量是不能像ZI那样“无中生有”的。

ARM程序的执行过程
从以上两点可以知道,烧录到ROM中的image文件与实际运行时的ARM程序之间并不是完全一样的。因此就有必要了解ARM程序是如何从ROM中的image到达实际运行状态的。
            实际上,RO中的指令至少应该有这样的功能:
            1. 将RW从ROM中搬到RAM中,因为RW是变量,变量不能存在ROM中。
            2. 将ZI所在的RAM区域全部清零,因为ZI区域并不在Image中,所以需要程序根据编译器给出的ZI地址及大小来将相应得RAM区域清零。ZI中也是变量,同理:变量不能存在ROM中

在程序运行的最初阶段,RO中的指令完成了这两项工作后C程序才能正常访问变量。否则只能运行不含变量的代码。

 

VQJGFXYY$H7PG)T$Z]Q0901

按照上面的我标红色的部分的解释,这样的话RO-data、RW-data和code的大小加起来就是最终的烧入程序的大小,但是好像事情不是这么简单的。看截图:

3X{_`31Z7KL2Z6QPZDGOQRE

 

从我截图的hex程序来看程序的大小是1.5K和我们编译出来的RO-data+RW-data+code的大小是304+252=556字节,557字节和1.5K相差了很多,这又是怎么回事呢?

这时候我想起了hex和bin这两种文件的格式,我在想是不是因为格式的原因在到鬼呢?

百度了下,http://wenku.baidu.com/link?url=jnO4kGRmKoGA8SGl6wN9nZboEAPUqnZGs0_XYk743E47wCTF5a7CRjbpRaJaeG92Voe92dqWOxYKsRRRP3PC4wYMZA65udxGU25EBcR3vmW

这是百度文库里面一篇关于单片机编译生成的各种文件的格式的介绍,从介绍中我们很明显的hex文件的大小并不等于最终烧入到单片机的程序大小,因为hex里面有很多的ASSII码的信息,最终的程序大小是可以从bin文件中看出来的。怎么样子把hex文件转化为bin文件呢?

在keil的安装目录下面有一个小工具:C:\Keil_v5\ARM\ARMCC\bin目录下面有一个fromelf.exe的小工具,是可以将axf文件转换成bin文件的一个小工具。我在命令行下面进行了这个操作。

FJ5F$(F6OX8`}]E)E(XT`MQ

上面是运行的过程,然后我们可以看生成的bin文件大小。

Z@S43Q{2`FMIP8V6%%{$JCH

从上面的图上看出,生成的bin文件和我们之前计算的一样了。其实如果是有特殊的需要的话,可以在keil里面设置调用fromelf就可以了。具体的自己百度。

努力才可成功!信仰不是别人给你说一百遍的一句话,而是你被撞的头破血流后的幡然悔悟!有时候一个道理你会花几年才会明白。虽然这只是一个简单的道理!

Cortex-M3快速开关中断PRIMASK与 FAULTMASK,以及CPSID/CPSIE

0.前言

本文想解决的问题有:

  • 如何开启、关闭中断
  • 如何开启、关闭异常
  • LPC177x/8x支持的中断优先级个数
  • 复位后,异常/中断默认的优先级
  • 如何设置异常/中断的优先级
  • 什么是优先级组,如何设置优先级组,复位后的优先级组
1. Cortex-M3的异常/中断屏蔽寄存器组

注:只有在特权级下,才允许访问这3个寄存器。

名 字

功能描述

PRIMASK

只有单一比特的寄存器。置为1后,就关掉所有可屏蔽异常,只剩下NMI和硬Fault可以响应。默认值是0,表示没有关闭中断。

FAULTMASK

只有单一比特的寄存器。置为1后,只有NMI可以响应。默认值为0,表示没有关异常。

BASEPRI

该寄存器最多有9位(由表达优先级的位数决定)。定义了被屏蔽优先级的阈值。当它被设置为某个值后,所有优先级号大于等于此值的中断都被关。若设置成0,则不关断任何中断,0为默认值。

注:寄存器BASEPRI的有效位数受系统中表达优先级的位数影响,如果系统中只使用3个位来表达优先级,则BASEPRI有意义的值仅为0x00、0x20、0x40、0x60、0x80、0xA0、0xC0和0xE0

使用MRS/MSR指令访问这三个寄存器,比如:

MRS R0, BASEPRI ;读取BASEPRI到R0中 MSR BASEPRI, R0 ;将R0数据写入到BASEPRI中

为了快速的开关中断,CM3还专门设置了一条CPS指令,有四种用法:

CPSID I ;PRIMASK=1,关中断 CPSIE I ;PRIMASK=0,开中断 CPSID F ;FAULTMASK=1,关异常 CPSIE F ;FAULTMASK=0,开异常

CMSIS-M3微控制器软件接口标准中的core_cm3.h给出了开关中断或异常的函数:

1.1 开/关中断

1: /** 2: * @brief Set the Priority Mask value 3: * 4: * @param priMask PriMask 5: * 6: * Set the priority mask bit in the priority mask register 7: */ 8: static __INLINE void __set_PRIMASK(uint32_t priMask) 9: { 10: register uint32_t __regPriMask __ASM("primask"); 11: __regPriMask = (priMask); 12: }

       使用__set_PRIMASK(1)关闭中断;__setPRIMASK(0)开启中断。

       一些说明:__INLINE是宏定义,对应__inline,这是keil编译器自定义关键字,表示这个函数是内联函数,但并不是强制性内联,编译器最终决定是否内联。

       __ASM(“primask”): __ASM也是一个宏,对应__asm,这是keil编译器自定义关键字,关于这个关键字,有相当多的用法,可以在C中内嵌汇编语言、内嵌汇编函数、指定汇编标号以及本代码中的声明一个已命名寄存器变量。这里,已命名的寄存器是("primask"),也就是说寄存器变量__regPriMask等同于编译器已命名的primask。语法为:

register type var-name __asm(reg);

keil编译器已命名的寄存器变量为:

寄存器

__asm修饰的字符串

处理器

APSR

"apsr"

All processors

CPSR

"cpsr"

All processors

BASEPRI

"basepri"

Cortex-M3, Cortex-M4

BASEPRI_MAX

"basepri_max"

Cortex-M3, Cortex-M4

CONTROL

"control"

Cortex-M0, Cortex-M1, Cortex-M3, Cortex-M4

DSP

"dsp"

Cortex-M0, Cortex-M1, Cortex-M3, Cortex-M4

EAPSR

"eapsr"

Cortex-M0, Cortex-M1, Cortex-M3, Cortex-M4

EPSR

"epsr"

Cortex-M0, Cortex-M1, Cortex-M3, Cortex-M4

FAULTMASK

"faultmask"

Cortex-M3, Cortex-M4

IAPSR

"iapsr"

Cortex-M0, Cortex-M1, Cortex-M3, Cortex-M4

IEPSR

"iepsr"

Cortex-M0, Cortex-M1, Cortex-M3, Cortex-M4

IPSR

"ipsr"

Cortex-M0, Cortex-M1, Cortex-M3, Cortex-M4

MSP

"msp"

Cortex-M0, Cortex-M1, Cortex-M3, Cortex-M4

PRIMASK

"primask"

Cortex-M0, Cortex-M1, Cortex-M3, Cortex-M4

PSP

"psp"

Cortex-M0, Cortex-M1, Cortex-M3, Cortex-M4

PSR

"psr"

Cortex-M0, Cortex-M1, Cortex-M3, Cortex-M4

r0 to r12

"r0" to "r12"

All processors

r14 or lr

"r14" or "lr"

All processors

r13 or sp

"r13" or "sp"

All processors

r15 or pc

"r15" or "pc"

All processors

SPSR

"spsr"

All processors, apart from Cortex-M series processors.

XPSR

"xpsr"

Cortex-M0, Cortex-M1, Cortex-M3, Cortex-M4

1.2 开/关异常

1: /** 2: * @brief Set the Fault Mask value 3: * 4: * @param faultMask faultMask value 5: * 6: * Set the fault mask register 7: */ 8: static __INLINE void __set_FAULTMASK(uint32_t faultMask) 9: { 10: register uint32_t __regFaultMask __ASM("faultmask"); 11: __regFaultMask = (faultMask & 1); 12: }

使用__set_FAULTMASK(1)来关闭中断和异常;使用__set_FAULTMASK(0)开启中断和异常.

1.3 更精确的优先级屏蔽

1: /** 2: * @brief Set the Base Priority value 3: * 4: * @param basePri BasePriority 5: * 6: * Set the base priority register 7: */ 8: static __INLINE void __set_BASEPRI(uint32_t basePri) 9: { 10: register uint32_t __regBasePri __ASM("basepri"); 11: __regBasePri = (basePri & 0xff); 12: }

比如想屏蔽优先级不高于0x60的中断,则使用代码:__set_BASEPRI(0x60);如果想取消中断屏蔽,则使用__set_BASEPRI(0)即可。

2.异常/中断和优先级

       Cortex-M3的异常包括系统异常和外设中断,系统异常是Cortex-M3内核自带的一些异常,比如复位、总线Fault和SysTick等等(见表2-1),外设中断是指制造CPU的厂家加入的,比如串口、定时器中断等等(见表2-2)。

注:关于异常和中断,想要分个清清楚楚实在有点困难。异常和中断都可以“中断”正常执行的代码流,区别在于,异常是Cortex-M3内核产生的“中断”信号,而中断是Cortex-M3内核外部(片上外设或外部中断信号)产生的“中断”信号。希望你看懂了,有时候你心里明白,但要讲的清清楚楚着实难!

表2-1:系统异常

编号

类型

优先级

简介

0

N/A

N/A

1

复位

-3(最高)

复位

2

NMI

-2

不可屏蔽中断(来自外部NMI输入脚)

3

硬Fault

-1

只要FAULTMASK没有置位,硬Fault服务例程会被强制执行

4

存储器管理Fault

可编程

MPU访问违例以及访问非法位置均可引发。企图在“非执行区”取址也会引发此Fault。

5

总线Fault

可编程

总线收到了错误响应,原因可以使预取流产或数据流产,企图访问协处理器也会引发此Fault

6

用法Fault

可编程

由于程序错误导致的异常。通常是使用了一条无效指令,或者是非法的状态转换,例如尝试切换到ARM状态

7~10

保留

保留

保留

11

SVCall

可编程

执行系统服务调用指令(SVC)引发的异常

12

调试监视器

可编程

调试器(断点、数据观察点,或者是外部调试请求)

13

保留

保留

保留

14

PendSV

可编程

为系统设备而设的“可挂起请求”

15

SysTick

可编程

系统节拍时钟定时器(SysTick)

表2-2:外设中断

编号类型优先级简介16

IRQ #0

可编程

外设中断#0

17

IRQ #1

可编程

外设中断#1

...

...

可编程

...

255

IRQ #239

可编程

外设中断#239

注:表2-1和2-2中的“编号”有着特殊的意义,一是特殊功能寄存器IPSR中会记录当前正在服务的异常并给出了它的编号;二是优先级完全相同的多个异常同时挂起时,则先响应异常编号最小的那一个。

        一个发生的异常如果不能被立即响应,就称它被“挂起”,值得一提的是,对于被挂起的中断/异常,中断/异常信号不必由其产生者保持,NVIC的挂起状态寄存器会来保持这个信号。所以哪怕后来挂起的中断源释放了中断请求信号,曾经的中断请求也不会丢失。

        除了复位、NMI和硬Fault三个异常具有固定的优先级外,其它所有异常和中断的优先级都是可以编程的。这就涉及到优先级配置寄存器。Cortex-M3优先级配置寄存器共8位,所以可以有256级的可编程优先级。但是大多数Cortex-M3芯片都会精简设计。

        LPC177x/8x使用了优先级配置寄存器的5位,所以有32级可编程优先级。复位后,对于所有优先级可编程的异常,其优先级都被初始化为0(最高优先级)

2.1 设置异常/中断的优先级2.1.1 系统异常优先级设置

        SHPR1-SHPR3寄存器用于设置有可编程优先级的系统异常,可设置的优先级为0到31。SHPR1-SHPR3可按字节访问。为了提高软件效率,CMSIS简化了SCB寄存器的表述。在CMSIS中,字节数组SHP[0] 到SHP[12]对应于寄存器SHPR1至SHPR3。

表2-3:SHPR1寄存器的位分配

名称

功能

[31:24]

PRI_7

保留

[23:16]

PRI_6

系统处理程序6的优先级,用法Fault

[15:8]

PRI_5

系统处理程序5的优先级,总线Fault

[7:0]

PRI_4

系统处理程序4的优先级,存储器管理Fault

表2-3:SHPR2寄存器的位分配

名称

功能

[31:24]

PRI_11

系统处理程序11的优先级,SVCall

[23:0]

-

保留

表2-4:SHPR3寄存器的位分配

名称

功能

[31:24]

PRI_15

系统处理程序15的优先级,SysTick 异常

[23:16]

PRI_14

系统处理程序14的优先级,PendSV

[15:0]

-

保留

注:每个PRI_N域为8位宽,但是处理器仅实现每个域的位[7:3],位[2:0]读取值为零并忽略写入值

2.1.2 外设中断优先级设置

         LPC177x/8x微处理器的中断优先寄存器IPR0~IPR10用于设置外设中断优先级,控制41个外设中断。每个IPRx可以按字节访问,在CMSIS中,字节数组IP[0] 到IP[40]对应于寄存器IPR0~IPR10。

2.1.3 系统异常/外设中断优先级设置C代码

1: /** 2: * @brief Set the priority for an interrupt 3: * 4: * @param IRQn The number of the interrupt for set priority 5: * @param priority The priority to set 6: * 7: * Set the priority for the specified interrupt. The interrupt 8: * number can be positive to specify an external (device specific) 9: * interrupt, or negative to specify an internal (core) interrupt. 10: * 11: * Note: The priority cannot be set for every core interrupt. */ 12: static __INLINE void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority) 13: { 14: if(IRQn < 0) { /* set Priority for Cortex-M3 System Interrupts */ 15: SCB->SHP[((uint32_t)(IRQn) & 0xF)-4] = ((priority << (8 - __NVIC_PRIO_BITS)) & 0xff); 16: } 17: else { /* set Priority for device specific Interrupts */ 18: NVIC->IP[(uint32_t)(IRQn)] = ((priority << (8 - __NVIC_PRIO_BITS)) & 0xff); 19: } 20: }

        其中,参数IRQn为中断ID号,可以为负,也可以为正。当IRQn为负时,设置系统异常的优先级,当IRQn大于等于0时,设置外设中断优先级。__NVIC_PRIO_BITS是指使用到的优先级配置寄存器的位数,LPC177x/8x使用了5位。为什么要使用(8-__NVIC_PRIO_BITS)呢?这是因为优先级配置寄存器是高位对齐的(MSB),这主要方面不同CPU间的移植。参数priority为要设置的优先级值,为0~31,数值越低,表示优先级越大。LPC177x/8x的中断ID为:

系统异常ID:

标号

中断ID

描述

NonMaskableInt_IRQn

-14

不可屏蔽中断

MemoryManagement_IRQn

-12

Cortex-M3内存管理中断

BusFault_IRQn

-11

Cortex-M3 总线Fault中断

UsageFault_IRQn

-10

Cortex-M3 用法Fault 中断

SVCall_IRQn

-5

Cortex-M3 SV Call中断

DebugMonitor_IRQn

-4

Cortex-M3 调试监视中断

PendSV_IRQn

-2

Cortex-M3 Pend SV中断

SysTick_IRQn

-1

Cortex-M3 系统Tick中断

外设中断ID:

标号中断ID描述标号中断ID描述WDT_IRQn0看门狗EINT3_IRQn21外中断3TIMER0_IRQn1定时器0ADC_IRQn22AD转换TIMER1_IRQn2定时器1BOD_IRQn23欠压检测TIMER2_IRQn3定时器2USB_IRQn24USBTIMER3_IRQn4定时器3CAN_IRQn25CANUART0_IRQn5UART0DMA_IRQn26通用DMAUART1_IRQn6UART1I2S_IRQn27I2SUART2_IRQn7UART2ENET_IRQn28以太网UART3_IRQn8UART3MCI_IRQn29SD/MMC卡I/FPWM1_IRQn9PWM1MCPWM_IRQn30电机控制PWMI2C0_IRQn10I2C0QEI_IRQn31正交编码接口I2C1_IRQn11I2C1PLL1_IRQn32PLL1锁存I2C2_IRQn12I2C2USBActivity_IRQn33USB活动Reserved0_IRQn13保留CANActivity_IRQn34CAN活动SSP0_IRQn14SSP0UART4_IRQn35UART4SSP1_IRQn15SSP1SSP2_IRQn36SSP2PLL0_IRQn16PLL0锁存LCD_IRQn37LCDRTC_IRQn17RTCGPIO_IRQn38GPIOEINT0_IRQn18外中断0PWM0_IRQn39PWM0EINT1_IRQn19外中断1EEPROM_IRQn40EEPROMEINT2_IRQn20外中断2   

2.2 设置异常/中断的优先级组

        Cortex-M3的异常/中断是可以抢占的,高抢占优先级中断可以抢占低抢占优先级中断。NVIC中有个名字叫做“应用程序中断及复位控制寄存器(AIRCR)”的寄存器,该寄存器的bit[10:8]称为优先级分组(PRIGROUP)段,表示的值为0~7,分别对应8个不同的抢占优先级设置。比如优先级分组段为0时,则8位优先级配置寄存器(LPC177x/8x只使用了其中的5位)的bit[7:1]表示抢占优先级,bit[0:0]表示非抢占优先级;再比如优先级分组段为1时,则8位优先级配置寄存器的bit[7:2]表示抢占优先级,bit[1:0]表示非抢占优先级,依次类推。

        复位后,优先级分组(PRIGROUP)段默认值为0,也就是则8位优先级配置寄存器(LPC177x/8x只使用了其中的5位)的bit[7:1]表示抢占优先级,bit[0:0]表示非抢占优先级。而LPC177x/8x只使用了8位优先级配置寄存器其中的bit[7:3],所以对于LPC177x/8x微处理器而言,复位后默认32级优先级全部为可抢占优先级。

2.2.1 设置优先级寄存器组的C代码

1: /** 2: * @brief Set the Priority Grouping in NVIC Interrupt Controller 3: * 4: * @param PriorityGroup is priority grouping field 5: * 6: * Set the priority grouping field using the required unlock sequence. 7: * The parameter priority_grouping is assigned to the field 8: * SCB->AIRCR [10:8] PRIGROUP field. Only values from 0..7 are used. 9: * In case of a conflict between priority grouping and available 10: * priority bits (__NVIC_PRIO_BITS) the smallest possible priority group is set. 11: */ 12: static __INLINE void NVIC_SetPriorityGrouping(uint32_t PriorityGroup) 13: { 14: uint32_t reg_value; 15: /* only values 0..7 are used */ 16: uint32_t PriorityGroupTmp = (PriorityGroup & 0x07); 17: 18: reg_value = SCB->AIRCR; /* read old register configuration */ 19: /* clear bits to change */ 20: reg_value &= ~(SCB_AIRCR_VECTKEY_Msk | SCB_AIRCR_PRIGROUP_Msk); 21: /* Insert write key and priorty group */ 22: reg_value = (reg_value | 23: (0x5FA << SCB_AIRCR_VECTKEY_Pos) | 24: (PriorityGroupTmp << 8)); 25: SCB->AIRCR = reg_value; 26: }

        其中,参数PriorityGroup为要设置的优先级分组(PRIGROUP)段的值,取值范围为0~7.由于操作AIRCR寄存器需要访问钥匙,所以要把0x05FA写入到该寄存器的bit[31:16]中,否则写入的值会被忽略。

        需要注意的是,在一个设计好的产品中,如果没有十足的把握,不要修改优先级组,不然会有大恶魔纠缠你的。