s3c2440裸机编程-nandflash

1 nandflash原理#

1.1 原理图#

1.2 引脚描述#

引脚名称 引脚功能
IO0~IO7 数据输入输出(命令、地址、数据共用数据总线)
CLE 命令使能
ALE 地址使能
/CE 芯片使能(片选)
/RE 读使能
/WE 写使能
R/B 就绪/忙输出信号(低电平表示操作还在进行中,高电平表示操作完成)

1.3 nand存储结构#

我们常见的Nand Flash,内部只有一个chip,每个chip只有一个plane。但也有些复杂的,容量更大的Nand Flash,内部有多个chip,每个chip有多个plane,这类的Nand Flash,其实就是多了一个主控将多块flash叠加在一起,如下图:

1.3.1 Block块#

一个Nand Flash由很多个块(Block)组成,块的大小一般有64K,128KB,256KB,512KB,Block是Nand Flash的擦除操作的基本/最小单位。Nand Flash中,一个块中含有1个或多个位是坏的,就称为其为坏块Bad Block

1.3.2 Page页#

每个块里面又包含了很多页(page)。每个页的大小,对于现在常见的Nand Flash多数是2KB,当然也有的nand flash的页大小为4KB、8KB等。页Page,是读写操作的基本单位

1.3.3 oob区域#

每一个page页,对应还附加了一块区域,一般为64byte,叫做空闲区域(spare area)/oob区域(Out Of Band),由于nandflash在操作过程中容易产生位反转,这是nandflash的物理特性决定的,所以必须要有对应的检测和纠错机制,这种机制被叫做Error Checking and Correcting,所以设计了多余的oob区域,用于放置数据的校验值。oob的读写操作,一般是随着页的操作一起完成的,即读写页的时候,对应地就读写了oob。

关于oob具体用途,总结起来有:

1
2
3
标记是否是坏快
存储ECC数据
存储一些和文件系统相关的数据。如jffs2就会用到这些空间存储一些特定信息

1.3.4 存储单位关系#

一般情况下:1block = 64page = 64 * (2K+64B) = 128K + 4K, 一个page包含2K数据和64B的oob。

1.4 Feature特性#

K9F2G08U0C这款nandflash为例:

可以看出此款nandflash特性如下:

1
2
3
4
5
6
1. 容量为256M,外加8M的冗余oob存储区。
2. page大小为2K,block大小为128K
3. 读一个page时顺序读取至少25*2048ns(数据可以每字节25ns的循环时间读出),随机读取不超过40us
4. 写一个page一般为250us
5. 擦除一个block一般为2ms
6. 封装上分为TSOP分装和FBGA封装(TSOP是指引脚在侧面,FBGA是引脚封在芯片底部,更能保障数据安全,有些客户为了保障数据安全性,防止被飞线进行数据破解,会要求用FBGA封装的flash)

1.5 常见的flash厂商#

常见的flash厂商有:Micron(镁光)、Toshiba(东芝)、Samsung(三星)、MXIC(旺宏)、dosilicon(东芯),(Winbond)华邦、ESMT等。

2 Nand控制器#

前面s3c2440裸机编程-内存控制器介绍讲到,我们知道nand没有独立地址线,cpu无法直接访问nand上的指令,所以nand不能片上执行。那么为何程序还能支持nand启动的呢?

为了支持NAND启动,S3C2440A配备了一个称为“ Steppingstone”的内部SRAM缓冲区,容量为4K。 开机时,Nandflash中的前4K数据将被加载到Steppingstone中,而引导代码将被加载到SRAM中将被执行,如下图所示:

2.1 OM启动介质选择#

我们知道s3c2440支持2种boot方式,nand或者nor,那么需要配置OM引脚来设置引导方式,如下图:

内存控制器的地址映射表如下:

当SW2闭合,OM0=1, OM[1:0]=01, 0地址对应nor,那么从nor启动。
当SW2断开,OM0=0, OM[1:0]=00, 0地址对应bootSRAM(4K),那么0地址对应该SRAM, 那么从nand启动。

2.2 nand控制器引脚配置#

当上电启动时,NAND Flash 控制器将通过下面的引脚配置来获取连接的 NAND Flash 的信息。

NCON:NAND Flash 存储器选择(普通/先进)

1
2
0:普通 NAND Flash(256 字或 512 字节页大小,34 个地址周期)
1:先进 NAND Flash(1K 字或 2K 字节页大小,45 个地址周期)

GPG13:NAND Flash 存储器page size选择

1
2
0:页=256 字(NCON=0)或页=1K 字(NCON=1
1:页=512 字节(NCON=0)或页=2K 字节(NCON=1

GPG14:NAND Flash 存储器地址周期选择

1
2
03 个地址周期(NCON=0)或 4 个地址周期(NCON=1
14 个地址周期(NCON=0)或 5 个地址周期(NCON=1

GPG15:NAND Flash 存储器总线宽度选择

1
2
08 位宽度
116 位宽度

如下表所示更直观:

3 Nand控制器访问时序#

nandflash访问时需要遵循一定的时序才能完成命令、地址、数据的发送。nandflash有8bit位宽数据总线,那么没有地址线它是怎么和cpu通信的呢?

1
2
3
4
5
6
7
8
9
1.nandflash是DATA0~DATA7上既传输数据,又传输地址,又传输命令;
①当ALE为高电平时传输的是地址;
②当CLE为高电平时传输的是命令;
③当ALE,CLE都为低电平表示传输的是数据

2. 先发送片选CS和WE/RE信号
3. 再发送CLE
4. 再发送ALE
5. 最后发送数据

下面分别介绍命令、地址、数据的发送过程。

3.1 命令/地址锁存时序(写命令/地址)#

  1. 首先看时钟,nand控制器的时钟源采用的是HCLK, 也就是AHB高速总线模式,可以参考s3c2440裸机编程-时钟体系那么HCLK=100Mhz, T=1/HCLK=10ns。
  2. 从上图可知命令、地址锁存的时序是一样的,复用一个时序图,当到达①的位置时,CLE/ALE=0;
  3. 当到达位置②时,CLE/ALE=1,表示命令/地址信号拉高,命令/地址开始使能,然后往数据总线DATA上放入命令或地址;
  4. 经过TACLS时间,到达位置③时,拉低nWE引脚,这时数据总线DATA上的命令/地址开始被锁存,锁存需要一定的时间,所以经过TWRPH0时间后,数据总线DATA上的命令/地址锁存完成;
  5. 到达位置④,此时释放nWE信号,nWE=1,这时还需要经过TWRPH1时间后,释放CLE/ALE,此时一个完整的命令/地址锁存过程完成。

上面分析了命令/地址的锁存时序过程,下面详细解释下上面几个时间参数的含义:

1
2
3
TACLS:CLE/ALE使能信号发送多久后才可以发送nWE信号
TWRPH0:nWE信号发送多久后数据(commamd/addr)才会被锁存成功
TWRPH1:nWE信号释放多久后才能释放CLE/ALE

这些时间参数要根据我们实际使用的具体nandflash型号和性能来配置我们的nand控制器。

3.1.1 K9F2G08U0C命令/地址锁存时序#

以K9F2G08U0C这款nandflash为例进行讲解,规格书上命令和地址锁存周期如下:

和nand控制器的命令/地址锁存时序图对比发现:

1
2
3
TACLS = max(tCLS,tALS) - tWP;
TWRPH0 = tWP;
TWRPH0 = max(tCLH,tALH);

nand控制器把命令、地址锁存时序复用成了一个时序图,其实命令和地址锁存时序参数基本一致,只不过发命令只需要一个周期就OK了,发地址需要5个时钟周期,为什么?

1
你想,命令多简单,无非就是读写擦,像我们这款nand数据位宽8bit,一个周期绰绰有余。但地址就不一样了,比如此款nandflash容量256M = 2^28,那么需要28根数据线来传输才能一个周期传输完,但这款nandflash的数据总线位宽只有8bit, 只有8根数据线,所以需要把地址拆分成多次发送,先发送col地址,再发送row地址,此款nandflash是用了5个周期发送地址。

3.2 数据锁存时序(写数据)#

从前面的命令地址锁存时序图中我们得知:

1
2
CLE信号拉高,ALE信号拉低时,表示发送的命令;
CLE信号拉低,ALE信号拉高时,表示发送的地址;

1
CLE信号拉低, ALE信号拉低时,表示发送的数据;
  1. 当到达①时,nWE还是高电平,写使能没有开启;
  2. 当到达②,③时,那么经过了tWP时间(TDS时间),数据开始被锁存;
  3. 到达④,经过tDH时间,数据锁存完成;
  4. 到达⑤,也就是数据开始锁存后再过了tWH时间后释放nWE信号;
  5. 重复②③④⑤过程,得到DIN0, DIN1, DIN final。

根据上面这三个图(手册上的命令、地址、数据锁存时序图),下面详细解释各个时间参数的含义:

3.3 数据顺序读时序(读数据)#

  1. ①处,表示要过tRR后才能发送读使能信号nRE进行访问(上一次的访问结束后,需要等待ready状态稳定后才可以进行下一次访问);
  2. 当到达②,需要经过rREA时间后nRE信号才有效(待nRE稳定);
  3. 当到达③,DATA总线上的读取被读取;
  4. 当到达④,nRE释放tREH时间后才允许下一次读使能;

我们看到连续顺序访问时,单次访问的时间为tRC,那么这些时间参数的值也可以从K9F2G08U0C datasheet中找到:为25ns

4 nandFlash初始化和识别#

4.1 nandFlash命令表#

找到K9F2G08U0C datasheet,对NAND FLASH的操作需要发出命令,下面有个NAND FLASH的命令表格,用此表格上的命令来访问我们的nandflash:

4.2 读ID时序#

命令表中的读id还不太直观,下图是从nand芯片手册中截取出的读id时序图:

  1. 第一条竖线位置,发送了nCE,CLE,nWE信号,所以90命令被锁存(readID命令);
  2. 第二条竖线位置,发送了nCE,ALE,nWE信号,所以地址00被锁存;继续往后,命令、地址都发完了,要read数据了,所以释放nWE,ALE,这里tAR表示ALE释放多久后才可以发送nRE信号,tREA表示nRE信号的建立时间;
  3. 第三条竖线位置,发送了nCE,nRE信号,所以数据被锁存,第一个访问周期锁存的数据为marker code,值为0xEC,第二个访问周期的数据为device code,值为0xDA。读id时读5个周期含义对应如下表:

该款nandflash的5个周期读出来的值对应如下:

第四个访问周期含义如下表:

第五个访问周期含义如下表:

根据第4、5个访问周期的结果0x15、0x44我们得知该flash的block_size=128K,page_size=2k, 有2个plane,plane_size=1Gb = 128M, 共256M。

4.3 初始化#

4.3.1 初始化nand控制器#

4.3.1.1 NFCONF-配置寄存器#

nand控制器要按照我们nandflash的实际型号和性能来设置初始值。NFCONF寄存器,也叫nand配置寄存器:

以K9F2G08U0C这款nandflash为例:

前面第3.1.1小节分析了TACLS = max(tCLS,tALS) - tWP,我们得知tCLS、tALS、tWP最小都可以取到12ns, 所以我们可以取TACLS=0;

TWRPH0 = tWP,我们的nand手册上要求tWP最少12ns, 那么取TWRPH0 =1, Duration = HCLK*(TWRPH0+1)=20ns>12ns,满足要求;

TWRPH0 = max(tCLH,tALH), 我们的nand手册上要求tCLH、tALH最少5ns, 那么取TWRPH1 =0, Duration = HCLK*(TWRPH1+1)=10ns>5ns,满足要求。

再配置BusWidth总线位宽为8bit;
所以NFCONF寄存器设置如下:

1
2
3
4
5
#define  TACLS   0
#define TWRPH0 1
#define TWRPH1 0
/*设置NAND FLASH的时序*/
NFCONF = (TACLS<<12) | (TWRPH0<<8) | (TWRPH1<<4);

4.3.1.2 NFCONT-控制寄存器#


1
2
MODE [0]: 设置为1,使能NAND控制器。
Reg_nCE [1]: 设置为1,禁止片选(等要使用的时候再使能片选信号)

所以NFCONF寄存器设置如下:

1
2
/*使能NAND FLASH控制器,禁止片选*/
NFCONT = (1<<1) | (1<<0);

4.4 识别nandflash#

4.4.1 NFCMMD-命令寄存器#

我们可以使用2440上的NAND FLASH控制器简化操作,只需要往NFCMMD寄存器写入要传输的命令就可以了,NAND FLASH控制器默认把上面复杂的时序发出来。

NFADDR-地址寄存器

发命令后,后面就需要发送地址了,当nWE和ALE有效的时候,表示锁存的是地址,往NFADDR寄存器中写值就可以了,比如:NFADDR=0x00。
我们得知地址需要用5个周期来发送,前2个周期为col地址,后三个周期为row(page)地址。前面第3小节已详细分析过了命令、地址、数据锁存时序过程。

1
2
1. column: 列地址A0~A10,就是页内地址,地址范围是从02047。(A11用来确定oob的地址,即2048-211164个字节的范围)
2. page:A12~A30,称作页号,page(row)编号。

4.4.2 NFDATA-数据寄存器#

当命令、地址都发送完后就可以从数据总线上DATA[7:0]获取数据或者写入数据。同样往NFDATA寄存器中写值或者读值就可以了,如unsigned char buf=NFDATA,由于是数据位宽是8位的,所以访问时数据组织形式如下:

从上图可以看出,当byte access时,只需一个时钟周期;当wold access的时候,需要4个时钟周期,小端模式下第一个时钟周期对应低字节,第四个时钟周期对应高字节。

识别nandflash代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/*初始化nand控制器*/
void nand_init(void) {
#define TACLS 0
#define TWRPH0 1
#define TWRPH1 0
NFCONF = (TACLS<<12) | (TWRPH0<<8) | (TWRPH1<<4);

NFCONT = (1<<1) | (1<<0);
}

/*使能片选*/
void nand_select(void) {
NFCONT &=~(1<<1);
}
/*禁止片选*/
void nand_deselect(void) {
NFCONT |= (1<<1);
}
/*发命令*/
void nand_cmd(unsigned char cmd) {
volatile int i;
NFCCMD = cmd;
for(i=0; i<10; i++);
}
/*发地址*/
void nand_addr_byte(unsigned char addr) {
volatile int i;
NFADDR = addr;
for(i=0; i<10; i++);
}
/*读数据*/
unsigned char nand_data(void) {
return NFDATA;
}
/*识别nandflash*/
void nand_chip_probe(void) {
unsigned char buf[5]={0};

nand_select();
nand_cmd(0x90);
nand_addr_byte(0x00);

buf[0] = nand_data();
buf[1] = nand_data();
buf[2] = nand_data();
buf[3] = nand_data();
buf[4] = nand_data();
nand_deselect();

printf("maker id = 0x%x\n\r",buf[0]);
printf("device id = 0x%x\n\r",buf[1]);
printf("3rd byte = 0x%x\n\r",buf[2]);
printf("4th byte = 0x%x\n\r",buf[3]);
printf("page size = %d kb\n\r",1 << (buf[3] & 0x03));
printf("block size = %d kb\n\r",64 << ((buf[3] >> 4) & 0x03));
printf("5th byte = 0x%x\n\r",buf[4]);
}

5 NandFlash数据操作#

5.1 读数据#

读数据以page为单位的。下图的表格,来说明NAND FLASH内部结构,前面2K(02047)表示页数据,后边64字节(20482111)表示oob。

CPU想读取,第2048个数据,它是哪以一个?

是Page1的第0个字节。CPU使用某个地址访问数据的时候,是在页数据空间来寻址的。

下图为读NAND FLASH的read时序操作:

  1. 首先需要锁存00命令,nCE、CLE、nWE有效,0x00命令被锁存;
  2. 此时CLE无效,ALE开始有效,地址被锁存(从NAND FLASH的地址周期中可以看出来,先发出2个周期的col列地址,再发出3个周期的Row行地址);
  3. 锁存0x30命令;
  4. 然后会有一个busy时间段,R/nB为低电平。tRR表示busy状态的持续时间(手册上最小为20ns)。
  5. 开始锁存数据,nRE使能,nand上的数据被同步到数据nand控制器上。我们的nand是8bit数据位宽,所以每隔一个read时钟周期(tRC),传输1byte数据。每传输1byte数据,地址会自动往后偏移1byte,一般我们会连续读取1page数据。

下面开始写代码:

5.1.1 NFSTAT-状态寄存器#

当发完命令、地址后再进行读数据前我们知道有一段时间tRR处于busy状态,我们可以通过查询NFSTAT寄存器来确定busy状态有没有结束,是不是已经ready了。

wait_ready函数等待NAND FLASH空闲,从上图可以看出当NFSTAT寄存器[0]的值为1时NAND FLASH是空闲的,我们可以通过该位来判断NAND FLASH是否繁忙。代码如下:

1
2
3
void wait_ready(void) {
while (!(NFSTAT & 1));
}

nand_read函数为NAND FLASH的读函数,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void nand_read(unsigned int addr, unsigned char *buf, unsigned int len) {
int i = 0;
int page = addr / 2048;
int col = addr & (2048 - 1);

nand_select();

while (i < len) {
/* 发出00h命令 */
nand_cmd(00);

/* 发出地址 */
/* col addr */
nand_addr_byte(col & 0xff);
nand_addr_byte((col>>8) & 0xff);

/* row/page addr */
nand_addr_byte(page & 0xff);
nand_addr_byte((page>>8) & 0xff);
nand_addr_byte((page>>16) & 0xff);

/* 发出30h命令 */
nand_cmd(0x30);

/* 等待就绪 */
wait_ready();

/* 读数据 */
for (; (col < 2048) && (i < len); col++)
buf[i++] = nand_data();

col = 0;
page++;
}

nand_deselect();
}

我们看到每read一个page,都要重新发送命令地址,因为这里是顺序访问,flash的读写都是以page为单位的。

5.1.2 自动识别是nor还是nand启动#

在init.c文件中,加上如下代码,用来判断所使用的FLASH是NOR FLASH还是NAND FLASH。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*我们知道nand启动0地址对应片内SRAM,可以像内存一样的写0地址;nor启动,0地址对应nor,nor不能像内存一样的写地址,
**所以往0地址写入数据成功表示nand启动,写不成功表示nor启动
*/
int isBootFromNorFlash(void) {
volatile unsigned int *p = (volatile unsigned int *)0;
unsigned int val = *p;

*p = 0x12345678;
if (*p == 0x12345678) {
/* 写成功, 对应nand启动 */
*p = val;
return 0;
} else
return 1;
}

下面是代码重定位时可以自动区分nand和nor启动,无论是nand启动还是nor启动,都能将程序重定位到sdram中去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void copy2sdram(void) {
/* 要从lds文件中获得 __code_start, __bss_start
* 然后从0地址把数据复制到__code_start
*/

extern int __code_start, __bss_start;

volatile unsigned int *dest = (volatile unsigned int *)&__code_start;
volatile unsigned int *end = (volatile unsigned int *)&__bss_start;
volatile unsigned int *src = (volatile unsigned int *)0;
unsigned int len = (unsigned int)(&__bss_start) - (unsigned int)(&__code_start);

if (isBootFromNorFlash()) {
while (dest < end)
*dest++ = *src++;
} else {
nand_init();
nand_read((unsigned int)src, dest, len);
}
}

5.2 擦数据#

擦除数据以block为单位的。block erase时序图的过程大致如下:

1.首先发送0x60命令
2.发送row地址(由于擦除是以block为单位的,所以无需知道页内地址,只需要知道要擦除哪个page、哪个block即可)
3.发送0xd0,执行擦除动作
4.然后会有一个busy时间段,R/nB为低电平
5.发送0x70命令,用来读取状态
6.判断NFDATA寄存器的第0位是否擦除成功

代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
int nand_erase(unsigned int addr, unsigned int len) {
int page = addr / 2048;

if (addr & (0x1FFFF)) {
printf("nand_erase err, addr is not block align\n\r");
return -1;
}

if (len & (0x1FFFF)) {
printf("nand_erase err, len is not block align\n\r");
return -1;
}

nand_select();

while (1) {
page = addr / 2048;

nand_cmd(0x60);

/* page addr */
nand_addr_byte(page & 0xff);
nand_addr_byte((page>>8) & 0xff);
nand_addr_byte((page>>16) & 0xff);

nand_cmd(0xD0);

wait_ready();

nand_cmd(0x70);
if (nand_data()&0x1) {
printf("nand_erase err, at addr:0x%x\n\r", addr);
return -1;
}

len -= (128*1024);
if (len == 0)
break;
addr += (128*1024);
}

nand_deselect();
return 0;
}

5.3 写数据#

写数据以page为单位。往NAND FLASH写数据时,只需要把要写的数据复制给NFDATA寄存器即可。代码如下:

1
2
3
void nand_w_data(unsigned char val) {
NFDATA = val;
}

page write的写时序图如下:

1.首先发送0x80命令
2.发送地址(5个周期)
3.发送数据
4.发送0x10命令,执行烧写动作
4.然后会有一个busy时间段,R/nB为低电平
5.发送0x70命令,用来读取状态
6.判断NFDATA寄存器的第0位是否烧写成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void nand_write(unsigned int addr, unsigned char *buf, unsigned int len) {
int page = addr / 2048;
int col = addr & (2048 - 1);
int i = 0;

nand_select();

while (1) {
nand_cmd(0x80);

/* 发出地址 */
/* col addr */
nand_addr_byte(col & 0xff);
nand_addr_byte((col>>8) & 0xff);

/* row/page addr */
nand_addr_byte(page & 0xff);
nand_addr_byte((page>>8) & 0xff);
nand_addr_byte((page>>16) & 0xff);

/* 发出数据 */
for (; (col < 2048) && (i < len); col++) //还需确认
nand_w_data(buf[i++]);

nand_cmd(0x10);
wait_ready();

nand_cmd(0x70);
if (nand_data() & 0x1) {
printf("nand_write err, at page:0x%x, addr:0x%x\n\r", page, page<<11);
return -1;
}

if (i == len)
break;

/* 开始下一个循环page */
col = 0;
page++;
}

nand_deselect();
}

我们看到每写一个page,都要重新发送命令地址,因为这里是顺序访问,flash的读写都是以page为单位的。

5.4 测试#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
void do_erase_nand_flash(void) {
unsigned int addr;

/* 获得地址 */
printf("Enter the address of sector to erase: ");
addr = get_uint();

printf("erasing ...\n\r");
nand_erase(addr, 128*1024);
}

void do_read_nand_flash(void) {
unsigned int addr;
volatile unsigned char *p;
int i, j;
unsigned char c;
unsigned char str[16];
unsigned char buf[64];

/* 获得地址 */
printf("Enter the address to read: ");
addr = get_uint();

nand_read(addr, buf, 64);
p = (volatile unsigned char *)buf;

printf("Data : \n\r");
/* 长度固定为64 */
for (i = 0; i < 4; i++) {
/* 每行打印16个数据 */
for (j = 0; j < 16; j++) {
/* 先打印数值 */
c = *p++;
str[j] = c;
printf("%02x ", c);
}

printf(" ; ");

for (j = 0; j < 16; j++) {
/* 后打印字符 */
if (str[j] < 0x20 || str[j] > 0x7e) /* 不可视字符 */
putchar('.');
else
putchar(str[j]);
}
printf("\n\r");
}
}

void do_write_nand_flash(void) {
unsigned int addr;
unsigned char str[100];
int i, j;
unsigned int val;

/* 获得地址 */
printf("Enter the address of sector to write: ");
addr = get_uint();

printf("Enter the string to write: ");
gets(str);

printf("writing ...\n\r");
nand_write(addr, str, strlen(str)+1);
}

说明:本节的读、写、擦都只涉及到页数据区,不涉及到oob区的操作。

5.5 坏快的标记和解除#

Nand Flash怎么标记某一个BLOCK是坏的? 如何识别一个flash中的坏快?

1
2
3
它使用该BLOCK中第1个扇区的OOB数据中某一个字节来标记: 其值为0xff表示该BLOCK是好的, 其值为非0xff表示该BLOCK是坏的。
在uboot中直接输入“nand bad ”命令即可识别某一个块是否为坏快,在linux用户态的情况下,需要用ioctl(MEMGETBADBLOCK)来获取该block是否为坏快。
有时候我们会误写这个OOB区的值导致有些BLOCK被误认为是"坏块",可以在u-boot中执行"nand scrub"后, 根据提示信息输入小写字母'y'并回车, 它会强制擦除整个Nand Flash(包括把OOB擦除为0xff), 这样就可以恢复被误标为坏块的区域了。

s3c2440裸机编程-ADC

1 ADC硬件原理#

模数转换器即A/D转换器,或简称ADC,通常是指一个将模拟信号转变为数字信号的电子元件。

如图,把可变电阻上的电压值变换的模拟信号通过ADC转换,输出数字信号。以s3c2440为例:

1.1 ADC属性#

对于数字信号我们需要得到它的2个属性:

  1. 转换精度:
    用多少位来存储这个数据(假如是10 bit)。那么最大值0b111111111对应3.3v, 0b0对应0v。

  2. 采样/转换速度:


可以看出s3c2440的转换精度可达10 bit, 转换速率可达 500ksps.

1.2 ADC框图#

下图是s3c2440芯片的ADC转换框图:

可以看出AD converter前有一个MUX选择器,用来选择模拟输入源,这里选择A[3:0]作为输入源。

下面是编写程序要做的步骤:

1.确定是哪一路信号:设置8:1MUX
2.设置工作时钟
3.启动ADC
4.读状态,判断ADC转换是否成功。
5.读数据

2 ADC寄存器#

2.1 控制寄存器(ADCCON)#

bit[15]:只读,用来表示转换是否完成
bit[14:6]:用来设置adc频率(如果是auto TSC则不用设置该位)
bit[5:3]:用来选择模拟输入源
bit[2]:用来设置是否待机
bit[0]:用来启动adc

2.2 启动延时寄存器(ADCDLY)#

2.3 数据寄存器(ADCDAT0)#

bit[9:0]表示转换后的数据

3 编程测试#

1
2
3
4
5
6
7
8
9
void adc_init(void) {
ADCCON = (1<<14) | (49<<6) | (0<<3); //设置输入源AIN0, ADC时钟为1Mhz,并且使能预分频
}
int adc_read_ain0(void) {
/* 启动ADC */
ADCCON |= (1<<0);
while (!(ADCCON & (1<<15))); /* 等待ADC结束 */
return ADCDAT0 & 0x3ff;
}

s3c2440裸机-异常中断

1 异常中断引入#

在arm架构的处理器中,cpu有7中工作模式,2中工作状态。

1.1 CPU模式#

7种Mode: 除了usr/sys,其他5种都是异常模式。我们知道中断属于异常的2中,中断有irq,fiq。
usr sys undefined(und) Supervisor(svc) Abort(abt) irq fiq
用户模式 系统模式 未定义指令异常模 svc管理模式 终止模式(1.指令预取终止(读写某条错误的指令导致终止运行);2.数据访问终止(读写某个非法地址程序终止)) irq中断 快中断

除了usr模式,其他6中为特权模式。 CPU无法从usr模式直接进入特权模式。不能直接进入特权模式,那么怎么进入特权模式呢?

可以通过设置CPSR进入其他模式。

1.2 工作State#

ARM state
Thumb state(几乎用不上)

1.3 ARM寄存器#

(1)通用寄存器:
(2)备份寄存器(banked register):
    CPSR:当前程序状态寄存器(Current Program Status Register) 反映程序处在那种状态
    SPSR:CPSR的备份寄存器 (Saved Program Status Register)  用来保存"被中断前的CPSR"

下图是我们arm状态下的通用寄存器和程序状态寄存器

R13是SP(栈指针)
R14是LR(link register),程序跳转或者发成异常时的返回地址
R15是PC(程序计数器)

假设cpu执行:

1
2
mov R0, R8
mov R0,R8_fiq

在usr/System 模式下访问的R8, 但是在FIQ模式下,访问R8是访问FIQ模式专属的R8寄存器,不是同一个物理上的寄存器。
在5种异常模式中每个模式都有自己专属的R13 R14寄存器,R13用作SP(栈), R14(LR)是用来保存发生异常时的指令地址。

为什么快中断(FIQ)有那么多专属寄存器?
这些寄存器称为备份寄存器,我们先看下中断处理流程

1 保存现场(保存被中断模式的寄存器)---(比如程序正在sys/usr模式下运行,当发生中断时,需要把R0-R14这些寄存器全部保存下来)
2 异常处理(去分辨是哪一个中断源产生了中断,去执行对应的中断服务程序)
3 恢复现场(恢复被中断时保存下来的寄存器R0-R14)

但如果是快中断,那么我就不需要保存系统/用户模式下的R8 ~ R12这几个寄存器,因为在FIQ模式下有自己专属的R8 ~ R12寄存器,省略保存寄存器的时间,加快处理速度,所以它才称得上快中断。

1.3.1 CPSR程序状态寄存器#

在异常中断中PSR寄存器会使用的很频繁,PSR寄存器的格式如下图:

我们再来看看下表,反映的是PSR的 M[4:0]与arm工作模式的关系:

我们可以按照上图的对应关系设置CPSR,让其进入与之对应的模式。

1.3.2 SPSR程序状态备份寄存器#

1.4 异常向量表#

异常向量:不同的异常有不同的入口函数,那么这个异常入口函数的地址就是存放在该异常向量的位置。从该异常向量读取到的数据就是异常入口函数的地址。
异常向量表:就是由异常向量组成的集合。

下图是从uboot源代码中截取的smdk2410 的异常向量表:

1
2
3
4
5
6
7
8
9
.globl _start
_start: b reset
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq
ldr pc, _fiq

异常向量表对应的地址如下图:

2 异常处理流程#

CPU是如何进入到中断模式,执行中断服务程序的?

2.1 中断前#

下图是中断未触发前的程序执行过程:

2.1.1 初始化中断#

1,设置中断源(使中断能够产生,让CPU知道是哪个中断)
2,设置中断控制器(设置中断屏蔽,中断优先级)
3,设置中断总开关CPSR (使能中断)

2.2 中断产生后#

举个栗子:按键按下,产生按键irq。

2.2.1 硬件上的处理流程#

cpu强制跳转到异常向量表上对应的_irq异常向量(0x18)去读取指令(这个是CPU强制执行的,不需要我们去控制)。

具体的进入中断向量和中断返回流程见下图:

1
2
3
4
5
6
7
8
9
进入:
(1) LR_异常=PC + offset(具体多少看下图)
2)SPSR_异常=被中断前的程序模式CPSR
(3) CPSR被设置成对应的异常模式
4)跳转到对应的异常向量去执行
退出(返回):进入和退出就是一个逆过程
1)PC= LR_异常 -offset
2)被中断前的程序模式CPSR = SPSR_异常
3)中断结束,清中断

进入异常和返回异常时pc和lr的关系如下图:

从图中我们发现进入不同异常,offset的值也是有差异的。

2.2.2 软件上的处理流程#

1.当跳转到irq异常向量(0x18)后,发现该处是一条跳转指令“ldr pc, _irq”,
那么会通过ldr绝对跳转指令跳到到真正的中断处理函数_irq去执行。
2.那么在_irq的函数中我们需要按照之前说的**中断处理流程**去执行:
    (1)保存现场
    (2)异常处理(去分辨是哪一个中断源产生了中断,去执行对应的中断服务程序)
    (3)恢复现场

流程图总结下中断产生后的详细处理过程:

3 中断实例#

3.1 und-未定义指令异常#

先来看下当cpu解析到什么样的指令才会触发未定义指令异常呢?

从上面的arm指令格式中可知,只要指令码属于划线的格式,就属于未定义指令异常。

3.1.1 汇编向c函数传参#

我们知道汇编给C语言函数传参是通过r0,r1,…通过堆栈的方式去传递的参数,比如r0=1, r1=2;那么在被调用的c函数中argv0就是r0, argv1就是r1…,那么我们如果通过汇编给C函数传递字符串呢?

声明und_string为一个字符串:

1
2
und_string:
.string "undefined instruction exception"

然后用ldr r1, =und_string,这样r1中就保存了und_string的地址。
这样调用我们的c函数就可以把und_string传入进去。

3.1.2 und异常程序示例#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.text
.global _start
_start:
b reset /* vector 0 : reset */
b do_und /* vector 4 : und (看中断向量表)*/
reset:
/*看门狗
时钟
sdram
设置SP
重定位*/
...
bl print1
und_code:
.word 0xdeadc0de; /*定义一条未定义指令*/
/*故意以一个数据的方式引入一条未定义指令,当cpu执行到这里,读取0xdeadc0de指令码的时候,
发现无法识别这条指令,就发生未定义指令异常,就跳转到0x4的中断向量去执行*/

bl print2
...

在未定义指令异常前后加上打印print1, print2,如果出现未定义指令异常后,就会跳到0x4的地方去读取指令,print2也就没法执行

当跳转到0x4的中断向量后,发现此处是一条跳转指令bl do_und, 我们再到未定义指令异常的服务程序do_und中打印出und_string这个字符串的内容。
现在开始写指令异常的服务程序do_und,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
do_und:
/* sp_und未设置, 先设置它 (由于之前一直处于管理模式,现在处在und状态)*/
ldr sp, =0x34000000

/* 保存现场 */
/* 在und异常处理函数中有可能会修改r0-r12, 所以先保存 */
/* lr是异常处理完后的返回地址, 也要保存 */
stmdb sp!, {r0-r12, lr} /*先减后存*/ /* 把栈中的值备份到r0-r12*/

/* 处理und异常 */
mrs r0, cpsr
ldr r1, =und_string /*保存und_string地址*/
bl printException

/* 恢复现场 */
ldmia sp!, {r0-r12, pc}^ /*(ldmia先读后加),把备份的值恢复到栈中,让pc=lr就可以恢复到异常前的指令地址。^会把spsr的值恢复到cpsr里 */

下面来分析一下这个未定义指令异常服务程序:

  1. 进入未定义指令异常服务do_und之前硬件自动完成的事情如下:

    1. lr_und保存有被中断模式中的下一条即将执行的指令的地址
    2. SPSR_und保存有被中断模式的CPSR
    3. CPSR中的M4-M0被设置为11011, 进入到und模式
    4. 跳到0x4的地方执行程序 (bl do_und)
  2. 进入指令异常服务程序do_und后,我们需要保存现场,处理und异常,恢复现场,注意:由于发生了cpu模式切换,如果要用到栈,那么先要设置对应模式的栈。由于栈的地址是向下生长的,这里我就用sdram的末位地址作为栈指针,把sp_und=0x34000000。

  3. 在und异常服务程序中有可能会用到栈, 所以先保存现场,通过stmdb sp!, {r0-r12, lr}语句把栈中的值备份到r0-r12和lr,然后恢复现场的时候通过ldmia sp!, {r0-r12, pc}^,详见上面的注释。

  4. 我们看到保存现场后,我们把cpsr的值放到r0, 把und_string放到r1, 然后用bl printException调用c函数,这样我们的c函数printException就能收到汇编传过来的参数,一个是cpsr模式(r0),一个是und_string汇编传过来的字符串(r1)。我们用C函数实现printException:

1
2
3
4
5
6
7
void printException(unsigned int cpsr, char *str) {
puts("Exception! cpsr = ");
printHex(cpsr);
puts(" ");
puts(str);
puts("\n\r");
}

完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
.text
.global _start

_start:
b reset /* vector 0 : reset */

b do_und /* vector 4 : und (看中断向量表)*/

do_und:
/* 执行到这里之前:
* 1. lr_und保存有被中断模式中的下一条即将执行的指令的地址
* 2. SPSR_und保存有被中断模式的CPSR
* 3. CPSR中的M4-M0被设置为11011, 进入到und模式
* 4. 跳到0x4的地方执行程序 (bl do_und)
*/


/* sp_und未设置, 先设置它 (由于之前一直处于管理模式,现在处在und状态)*/
ldr sp, =0x34000000

/* 保存现场 */
/* 在und异常处理函数中有可能会修改r0-r12, 所以先保存 */
/* lr是异常处理完后的返回地址, 也要保存 */
stmdb sp!, {r0-r12, lr} /*先减后存*/ /* 把栈中的值备份到r0-r12*/

/* 处理und异常 */
mrs r0, cpsr
ldr r1, =und_string /*保存und_string地址*/
bl printException

/* 恢复现场 */
ldmia sp!, {r0-r12, pc}^ /*(ldmia先读后加),把备份的值恢复到栈中,让pc=lr就可以恢复到异常前的指令地址。^会把spsr的值恢复到cpsr里 */

und_string:
.string "undefined instruction exception"

reset:
/* 关闭看门狗 */
/* 时钟 */
/* sdram */
bl copy2sdram
bl clean_bss

bl uart0_init

bl print1
/* 故意加入一条未定义指令 */
und_code:
.word 0xdeadc0de /* 未定义指令 */
bl print2

//bl main /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */
ldr pc, =main /* 绝对跳转, 跳到SDRAM */

halt:
b halt

测试结果如下:

打印出print1中的字符串‘abc’后,紧接着打印printException函数中的结果,cpsr=0x600000db,那么对应的M[4:0]=11011, 对应下图为und模式。然后从und异常返回,恢复原来的模式继续执行。

3.1.3 示例改进#

3.1.3.1 指令4字节对齐#

我们将上面的代码的und_string字符串修改一下:

1
2
3
4
5
6
7
8
...
und_string:
.string "undef instruction"

reset:
/* 关闭看门狗 */
/* 时钟 */
...

编译烧录再次运行,发现没有任何打印输出,这是为什么呢?我明明只是把und_string字符串改了一下呀。

查看反汇编:

我们发现reset的地址是0x30000032,竟然不是4字节对齐的,我们知道arm指令集是以4字节为基本单位的,那么这里没有对齐,肯定无法解析指令。那么我们手工改进代码如下:

1
2
3
...
und_string:
.string "undef instruction"
.align 4
1
2
3
4
reset:
/* 关闭看门狗 */
/* 时钟 */
...

我们再来看看反汇编,发现reset的地址是30000040,是以4字节对齐的,再次烧录运行,发现能够正常输出print1, 能够进入未定义指令异常。

3.1.3.2 绝对跳转进入异常向量#

如果我们程序非常大,中断向量入口代码的地址可能会大于sram的容量4k,比如do_und和do_swi,那么这个时候就需要用绝对跳转。

1
2
3
4
5
.text
.global _start
_start:
b reset /* vector 0 : reset */
b do_und /* vector 4 : und (看中断向量表)*/

将上面的相对跳转换成如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
 .text
.global _start

_start:
b reset
ldr pc, und_addr
ldr pc, swi_addr
...
...
und_addr:
.word do_und
swi_addr:
.word do_swi

这样我们的do_und, do_swi就可放在4k之外的地方, 放到sdram。

3.1.3.3 重定位后跳转sdram上执行#

我们现在不断增加的程序代码量,那么有可能在 ldr pc, =main 这条指令执行之前程序就已经超过4k。那么我们当从nand启动的时候,还没执行到ldr pc, =main这句来,就无法取指令执行了。nor同理超过2M也就无法取指令执行了。 所以我们干脆重定位完代码后就直接跳转到sdram上去执行,代码简要概述如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
...
reset:
/*看门狗
时钟
set SP
sdram_init
重定位*/
ldr pc, =sdram
sdram:
...
ldr pc, =main /* 绝对跳转, 跳到SDRAM */
halt:
b halt

我们再来分析下整个程序执行过程:

1.一上电,cpu从0地址执行,执行b reset(进行初始化硬件)
2.重定位程序
3.跳转到sdram去继续执行
4.执行到 deadc0de,发生未定义指令异常
5.跳转到异常向量表的0x4地址去执行
6.跳转到sdram上执行异常处理函数(do_und)
7.异常返回,继续执行

3.2 swi-软中断#

arm有7中工作模式,除了usr模式,其他6种都是特权模式。

我们知道usr模式无法修改CPSR直接进入其他特权模式,但linux应用程序一般运行在usr模式,既然usr模式权限非常低,是无法直接访问硬件寄存器的,那么它是如何访问硬件的呢?

1
linux应用程序是通过系统调用,从而进入内核态,运行驱动程序来访问的硬件,那么系统调用又是如何实现的呢,就是通过软中断swi指令来进入svc模式,进入到svc模式后当然就能访问硬件啦。

所以我们的应用程序在usr模式想访问硬件,必须切换模式:

有以下两种方式:

1
2
1. 发生异常或中断(被动的)
2. swi + 某个值(主动的)

3.2.1 进入软中断swi#

s3c2440 一上电会跳到0地址(reset复位)执行代码,此时CPU处于svc模式,2440异常向量表如下图所示:

为了验证usr模式能够主动的通过swi软中断指令来进入svc模式, 我们先将模式切换到usr模式,那么这个时候就不能访问硬件了,也不能直接修改cpsr直接进入其他模式。

从上图我们设置CPSR让M4-M0处在10000,这样就进入了usr模式。修改start.s如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
.global _start

_start:
b reset
ldr pc, und_addr
ldr pc, swi_addr
...
und_addr:
.word do_und
swi_addr:
.word do_swi

reset:
/*
看门狗
时钟
set SP
sdram_init
重定位
bl uart0_init
*/
/*先进入usr模式*/
mrs r0, cpsr /* 读出cpsr 读到r0 */
/*使用bic命令 bitclean 把低4位清零*/
bic r0, r0, #0xf /* 修改M4-M0为0b10000, 进入usr模式 */
msr cpsr, r0 /* 写入cpsr */

/* 设置usr模式下的栈sp_usr */
ldr sp, =0x33f00000
swi 0x123 /* 执行此命令, 触发SWI异常, 进入0x8执行 */
ldr pc, =main /* 绝对跳转, 跳到SDRAM */

halt:
b halt

那么当执行到swi 0x123,就会触发SWI异常, 进入0x8的向量去执行,调用do_swi,我们参考do_und实现我们的软中断服务程序do_swi。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
do_swi:
/* 执行到这里之前:
* 1. lr_svc保存有被中断模式中的下一条即将执行的指令的地址
* 2. SPSR_svc保存有被中断模式的CPSR
* 3. CPSR中的M4-M0被设置为10011, 进入到svc模式
* 4. 跳到0x08的地方执行程序
*/

/* sp_svc未设置, 先设置它 */
ldr sp, =0x33e00000

/* 保存现场 */
/* 在swi异常处理函数中有可能会修改r0-r12, 所以先保存 */
/* lr是异常处理完后的返回地址, 也要保存 */
stmdb sp!, {r0-r12, lr}

/* 处理swi异常 */
mrs r0, cpsr
ldr r1, =swi_string /*这里r0, r1只是为了给printException传参*/
bl printException

/* 恢复现场 */
ldmia sp!, {r0-r12, pc}^ /* ^会把spsr的值恢复到cpsr里 */

swi_string:
.string "swi exception"

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
.global _start
b reset
ldr pc, und_addr
ldr pc, swi_addr
...
und_addr:
.word do_und
swi_addr:
.word do_swi
do_swi:
/* 执行到这里之前:
* 1. lr_svc保存有被中断模式中的下一条即将执行的指令的地址
* 2. SPSR_svc保存有被中断模式的CPSR
* 3. CPSR中的M4-M0被设置为10011, 进入到svc模式
* 4. 跳到0x08的地方执行程序
*/

/* sp_svc未设置, 先设置它 */
ldr sp, =0x33e00000

/* 保存现场 */
/* 在swi异常处理函数中有可能会修改r0-r12, 所以先保存 */
/* lr是异常处理完后的返回地址, 也要保存 */
stmdb sp!, {r0-r12, lr}

/* 处理swi异常 */
mrs r0, cpsr
ldr r1, =swi_string /*这里r0, r1只是为了给printException传参*/
bl printException

/* 恢复现场 */
ldmia sp!, {r0-r12, pc}^ /* ^会把spsr的值恢复到cpsr里 */

swi_string:
.string "swi exception"

.align 4

reset:
/*
看门狗
时钟
set SP
sdram_init
重定位
bl uart0_init
*/
/*先进入usr模式*/
mrs r0, cpsr /* 读出cpsr 读到r0 */
/*使用bic命令 bitclean 把低4位清零*/
bic r0, r0, #0xf /* 修改M4-M0为0b10000, 进入usr模式 */
msr cpsr, r0 /* 写入cpsr */

/* 设置usr模式下的栈sp_usr */
ldr sp, =0x33f00000
swi 0x123 /* 执行此命令, 触发SWI异常, 进入0x8执行 */
ldr pc, =main /* 绝对跳转, 跳到SDRAM */
halt:
b halt

do_swi中调用printException,打印出了软中断异常的字符串和CPSR对应的svc模式。

3.2.1.1 打印出swi软中断号#

我们要读出swi 0x123指令,我们知道当执行完swi 0x123指令以后,会发生swi异常,那么lr_svc = PC + offset。从下图看出offset是4:

修改中断服务函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
do_swi:
/* 执行到这里之前:
* 1. lr_svc保存有被中断模式中的下一条即将执行的指令的地址
* 2. SPSR_svc保存有被中断模式的CPSR
* 3. CPSR中的M4-M0被设置为10011, 进入到svc模式
* 4. 跳到0x08的地方执行程序
*/

/* sp_svc未设置, 先设置它 */
ldr sp, =0x33e00000

/* 保存现场 */
/* 在swi异常处理函数中有可能会修改r0-r12, 所以先保存 */
/* lr是异常处理完后的返回地址, 也要保存 */
stmdb sp!, {r0-r12, lr}

我们要把lr拿出来保存,因为bl printException会破坏lr,那么把lr保存在哪个个寄存器比较好呢?

我们知道当调用bl printException可能会修改某些寄存器,但是又会恢复这些寄存器,那么得知道它会保护哪些些寄存器。
来看下ATPCS规则:

img

在子程序中,使用R4~R11来保存局部变量,子程序进入时必须保存这些寄存器的值,在返回前必须恢复这些寄存器的值。所以对于 r4 ~ r11在C函数里会保存这几个寄存器,执行完C函数再把它释放掉并且恢复原来的值。我们把lr 保存在r4寄存器里,r4寄存器不会被C语言破坏。

1
2
3
4
5
mov r4, lr
/* 处理swi异常 */
mrs r0, cpsr
ldr r1, =swi_string
bl printException

当执行完swi 0x123指令后,会发生swi异常,swi异常模式里的lr寄存器会保存下一条指令的地址(即’ldr pc, =main’),我们把lr寄存器的地址减去4就是swi 0x123这条指令的地址。

把r4的寄存器赋给r0让后打印我们得写出打印函数:

1
2
3
4
5
6
7
8
9
10
    mov r0, r4

sub r0, r4, #4 //得到swi指令的地址
bl printSWIVal

/* 恢复现场 */
ldmia sp!, {r0-r12, pc}^ /* ^会把spsr的值恢复到cpsr里 */

swi_string:
.string "swi exception"

在uart.c添加printSWIVal打印函数:

1
2
3
4
5
void printSWIVal(unsigned int *pSWI) {
puts("SWI val = ");
printHEx(*pSWI & ~0xff000000); //高8位忽略掉
puts("\n\r");
}

3.3 irq-外部中断#

3.3.1 引入外部中断#

我们想实现一个按键点灯程序,我们知道有以下两种方案:

1
2
1.轮询方案:轮询检测按键的电平状态,当检测到被按下后,对应的gpio会拉低,点亮对应的led;(略)
2.中断方案:将按键配置成外部中断源,当有按键按下,触发中断,在中断服务程序(isr)中去完成点灯。

我们用按键作为外部中断源,我们把按键对应的gpio配置成中断引脚,当按键按下,相应的gpio产生了电平跳变,就会触发外部中断。

3.3.2 外部中断示例#

我们想达到按下按键灯亮, 松开按键灯灭这种效果(配成双边沿触发,按下的时候产生下降沿中断,进行点亮,松开产生上升沿中断,进行熄灭)。当然也可做成按一下点亮,再按一下熄灭的效果(设成单边沿触发,每来一次中断,对led电平进行一次取反)。
原理图如下:


从按键的原理图中得知,当按键没有按下时,接上拉电阻,按键为高电平状态。当按键按下时,电位被拉低,按键处于低电平状态。s2-s5分别对应GPF0,GPF2,GPG3,GPG11; D10-D12这3盏led所对应的gpio分别是GPF4,GPF5,GPF6。

那么我们让s2,s3,s4分别控制D10,D11,D12;s5对D10-D12同时控制(按下s5同时点亮3个led)。

3.3.1.1 配置GPIO和中断源#

配置D10-D12的gpio为输出模式,s2-s4的gpio为外部中断模式。

打开芯片手册找到第九章 IO ports,找到对应的gpio控制寄存器,将对应的gpio配置成中断模式。

  1. 配置GPF GPIO为中断引脚:

    同理GPG的寄存器类似。
1
2
3
4
5
GPFCON &= ~((3<<0) | (3<<4));	//先把eint0和eint2这两个引脚清零
GPFCON |= ((2<<0) | (2<<4)); //S2,S3被配置为中断引脚

GPGCON &= ~((3<<6) | (3<<22));
GPGCON |= ((2<<6) | (2<<22)); //S4,S5被配置为中断引脚
  1. 设置中断触发方式:

当电平从高变低时,此时表示按键按下,当电平由低变高,表示松开按键。不妨设置中断方式为双边沿触发,按下按键,触发下降沿中断,中断服务程序就可以去点亮led,反之,松开触发上升沿中断,就可以去熄灭led。


1
2
3
EXTINT0 |= (7<<0) | (7<<8);     /* S2,S3 */
EXTINT1 |= (7<<12); /* S4 */
EXTINT2 |= (7<<12); /* S5 */
  1. 设置外部中断屏蔽寄存器EINTMASK:

从上图我们知道外部中断0-3是直接连接到中断控制器,而外部中断4-7、外部中断8-23还要经过EINTMASK,那么我们需要配置EINTMASK来打开中断的通道:

1
EINTMASK &= ~((1<<11) | (1<<19));    //打开外部中断通道
  1. 外部中断挂起寄存器EINTPEND:

当一个外部中断(EINT4-EINT23)发生后,那么相应的位会被置1, 所以中断结束后需要清除对应位。这个寄存器可以用来区分外部中断4-23的哪一个中断源。

3.3.3.2 中断控制器设置#

我们先来看下中断控制器的总框图:

1. 首先是SRCPND:用来表示哪个中断源发出了中断请求。

先看下中断源:

从上图我们发现外部中断有24个外部中断,除了外部中断EINT,还有定时器中断,ADC中断,UART中断等…。

我们来认识下SRCPND寄存器:(用来表示哪个(哪些)中断源已产生中断请求,中断结束后要清中断)

从上图中我们发现EINT4-7共用1bit,EINT8-23共用1bit,那么肯定有其他寄存器来区分它们,那就是EINTPEND寄存器(后面5会讲)。

2. 然后到达INTMSK:(中断屏蔽寄存器)

我们需要把INTMSK寄存器配置成非屏蔽状态,默认是中断源时屏蔽的,见下图:

3.INTMOD(中断模式,是fiq还是irq)

4.Priroty:

5.INTPND:
INTPND 用来显示当前优先级最高的、正在发生的中断, 需要清除对应位。

中断发生后,SRCPND中会有bit置1,可能好几个(因为同时可能发生几个中断),这些中断会由优先级仲裁器选出一个最紧迫的,然后把INTPND中相应位置1。所以只有INTPND置1,CPU才会处理。

我们知道有可能同时出现多个中断请求,那么INTPND就挑选出当前优先级最高的、正在发生的中断。

当产生irq后,要去分辨是哪个中断源,根据不同的中断源去中断服务程序isr中做不同的事情,那么如何得知当前产生的中断是哪一个外部中断源产生的呢?那么就可以访问这个INTPND寄存器。

可是我们要去手工去解析INTPND里面的位,才能知道是哪个中断源产生了中断请求。那么有没有什么比较快捷的方式自动帮我们解析INTPND呢,直接返回中断号给我们?

当然有啦,有一个INTOFFSET寄存器的值就是代表哪个中断请求产生了,如果INTOFFSET=0表示EINT0产生了中断请求,INTOFFSET=2表示EINT2产生了中断请求。具体见下图:

我们从上图看到ENIT4-7共用一个offset, EINT8-23也共用一个offset,那么要通过访问EINTPEND寄存器来区分它们。

中断控制器设置代码入下:

1
2
3
4
5
6
/* 初始化中断控制器 */
void interrupt_init(void) {
//1是屏蔽我们需要清零,外部中断0 外部中断2 外部中8_23里面还有外部中断11到19
INTMSK &= ~((1<<0) | (1<<2) | (1<<5));
//INTMOD默认是irq,可以不设置
}

3.3.3.3 中断总开关#

CPSR有I位,是irq的总开关,我们需要把CPSR寄存器 bit7给清零,这是中断的总开关,如果bit7设置为1,CPU无法响应任何中断。

1
2
3
/* 把bit7这一位清零 */
bic r0, r0, #(1<<7) /* 清除I位, 使能中断 */
msr cpsr, r0

3.3.3.4 中断服务程序#

到这里中断前的初始化工作知识点就已经讲完了,当然要提前准备好led初始化工作(就是将led对应的gpio配置成输出模式,这个不讲解)。

那么中断产生后,我们之前讲过,会跳转到0x18异常向量,执行跳转指令ldr pc, =_irq,和之前的swi异常,und异常框架一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.text
.global _start

_start:
b reset /* vector 0 : reset */

ldr pc, und_addr /* vector 4 : und(绝对跳转) */
ldr pc, swi_addr /* vector 8 : swi */
b halt /* vector 0x0c : prefetch aboot */
b halt /* vector 0x10 : data abort */
b halt /* vector 0x14 : reserved */
ldr pc, irq_addr /* vector 0x18 : irq */
b halt /* vector 0x1c : fiq */

und_addr:
.word do_und
swi_addr:
.word do_swi
irq_addr:
.word do_irq

reset:
/* 关闭看门狗 */
/*初始化时钟*/
/*初始化sdram,设置栈*/
/*代码重定位,清bss*/

/* 把bit7这一位清零(打开中断总开关) */
bic r0, r0, #(1<<7) /* 清除I位, 使能中断 */
msr cpsr, r0

ldr pc, =main /* 绝对跳转, 跳到SDRAM */

halt:
b halt

1.我们在start.s中用汇编代码设置cpsr的I位,开启中断开关;

2.在main函数中初始化中断源key_eint_init,初始化中断控制器interrupt_init;

3.然后继续执行main主函数。

4.当中断产生,触发irq异常,进入0x18异常向量,执行do_irq。

do_irq实现如下(和do_und, do_swi类似):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
do_irq:
/* 执行到这里之前: */
/* 1. lr_irq保存有被中断模式中的下一条即将执行的指令的地址 */
/* 2. SPSR_irq保存有被中断模式的CPSR */
/* 3. CPSR中的M4-M0被设置为10010, 进入到irq模式 */
/* 4. 跳到0x18的地方执行程序 */
/* sp_irq未设置, 先设置它 */
ldr sp, =0x33d00000

/* 保存现场 */
/* 在irq异常处理函数中有可能会修改r0-r12, 所以先保存 */
/* lr-4是异常处理完后的返回地址, 也要保存 */
sub lr, lr, #4
stmdb sp!, {r0-r12, lr}

/* 处理irq异常 */
bl handle_irq_c

/* 恢复现场 */
ldmia sp!, {r0-r12, pc}^ /* ^会把spsr_irq的值恢复到cpsr里 */

handle_irq_c函数实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
void key_eint_irq(int irq) {
unsigned int val = EINTPEND;
unsigned int val1 = GPFDAT;
unsigned int val2 = GPGDAT;

if (irq == 0) /* eint0 : s2 控制 D12 */
{
if (val1 & (1<<0)) /* s2 --> gpf6 */
{
/* 松开 */
GPFDAT |= (1<<6);
} else
{
/* 按下 */
GPFDAT &= ~(1<<6);
}
}
else if (irq == 2) /* eint2 : s3 控制 D11 */
{
if (val1 & (1<<2)) /* s3 --> gpf5 */
{
/* 松开 */
GPFDAT |= (1<<5);
}
else
{
/* 按下 */
GPFDAT &= ~(1<<5);
}

}
else if (irq == 5) /* eint8_23, eint11--s4 控制 D10, eint19---s5 控制所有LED */
{
if (val & (1<<11)) /* eint11 */
{
if (val2 & (1<<3)) /* s4 --> gpf4 */
{
/* 松开 */
GPFDAT |= (1<<4);
}
else
{
/* 按下 */
GPFDAT &= ~(1<<4);
}
}
else if (val & (1<<19)) /* eint19 */
{
if (val2 & (1<<11))
{
/* 松开 */
/* 熄灭所有LED */
GPFDAT |= ((1<<4) | (1<<5) | (1<<6));
}
else
{
/* 按下: 点亮所有LED */
GPFDAT &= ~((1<<4) | (1<<5) | (1<<6));
}
}
}

EINTPEND = val; /* 清中断 : 源头*/
}

/*INTOFFSET中哪一位被设置成1,就表示哪一个 中断源*/
void handle_irq_c(void)
{
/* 分辨中断源 */
int bit = INTOFFSET;

/* 调用对应的处理函数 */
if (bit == 0 || bit == 2 || bit == 5) /* eint0,2,bit==5还需细分eint8_23 */
{
key_eint_irq(bit); /* 处理中断, 清中断源EINTPEND(eint11,2 eint11, eint11) */
}

/* 清中断 : 从源头开始清 */
SRCPND = (1<<bit);/*清EINT0,EINT2,EINT5*/
INTPND = (1<<bit);
}

3.4 irq-定时器中断#

3.4.1 引入看门狗定时器#

s3c2440共有2种定时器:

1.Watchdog看门狗定时器
2.PWM脉冲可调制定时器

下面详细介绍2种定时器的原理,来了解定时器是如何产生定时器中断的。

3.4.1.1 WatchDog定时器原理#

Watchdog定时器的原理很简单,寄存器很少,框图如下:

  1. 定时器,定时器那肯定是需要用到时钟的,从框图中可以看到Watchdog定时器采用的时钟源是PCLK,从s3c2440时钟体系中也可以体现出来,接的是APB总线。
  2. 然后到达一个8 bit的分频器,可以通过配置WTCON[15:8]来设置分频器的预设值。
  3. 再设置WTCON[4:3]来设置除数因子来进一步分频。
    所以最终的Watchdog定时器的时钟周期t_watchdog = 1/[ PCLK / (Prescaler value + 1) / Division_factor ]
  4. 到达WTCNT:看门狗递减寄存器。WTCNT里的数据就开始在输入时钟频率下递减。WTCNT的值由WTDAT寄存器提供。
  5. WTDAT:WTDAT寄存器用于指定计数器的初始值,也就是它的超时时间,系统上电之后硬件自动的将0x8000的初始值载入到WTCNT里,在发生了第一次超时操作时,WTDAT的值才会载入到WTCNT寄存器

当WTCNT的值减到0时,就会触发看门狗定时器中断,进而产生复位。中断框图中可以看到可以设置WTCON[2]来设置是否产生中断信号,可以设置WTCON[0]来设置是否产生复位信号。

3.4.1.1.1 WTCON寄存器#

3.4.1.1.2 WTCNT、WTDAT寄存器#

3.4.2 WatchDog定时器中断示例#

3.4.2.1 定时器初始化#

在之前的章节中,我们在start.s启动代码中首先做的就是关闭看门狗,把WTCON[5]=0,也就是把Watchdog timer给disable。那么Watchdog Timer就不再工作了,这样做是为了防止在启动代码进行硬件初始化的时候出现超时,发出复位信号又去重启硬件,这样就陷入了不断重启过程中。因为s3c2440芯片默认WTCON[5]是1,也就是Watchdog Timer默认是处于使能状态。

s3c2440时钟体系中配置了PCLK=50M Hz, 那么让WTDAT取默认值0x8000,那么根据公式算出从开机到触发复位重启的时间:

t=WTDAT*( 1/[ PCLK / (Prescaler value + 1) / Division_factor ])

根据WTCON寄存器配置Prescaler value=255,配置Division_factor=128,这样最终定时器分得的频率更低,那么减数器递减的更慢,也就代表从开机到触发复位重启的时间:

T=0x8000 * (1/[50*10^6/(255+1)/128]) = 21474836.48us = 21s

之前的start.s中把看门狗已经关闭了,那么我们在跳转到main函数中调用wtd_timer_init函数实现如下:

1
2
3
4
void wtd_timer_init(void) {
WTCON |= (1<<0) | (1<<5);//使能定时器,开启reset复位
WTCON |= (3<<3) | (255<<8);
}

我们查看测试结果:
果然初始化wtd_timer_init后,过21s后板子重启了,说明我们watchdog定时器功能已经OK了。

现在修改代码如下:

1
2
3
4
5
void wtd_timer_init2(void) {
WTCON |= (1<<0) | (1<<2);//使能定时器,开启watchdog定时器中断
WTCON |= (3<<3) | (255<<8);
WTDAT = 0x4000;
}

我们看到我们现在定时器的初值被修改成了0x4000, 相对于默认值少了一半,那么触发wtd_timer中断的时间应该减半,也就是约等于10s。

3.4.2.2 定时器中断服务程序#

那么需要写一个wtd_timer的中断服务程序,同样需要先在do_irq中去保护现场、调用handle_irq_c、恢复现场。查看INTOFFSET寄存器:

得知:
handle_irq_c代码修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void handle_irq_c(void)
{
/* 分辨中断源 */
int bit = INTOFFSET;

/* 调用对应的处理函数 */
if (bit == 0 || bit == 2 || bit == 5) /* eint0,2,bit==5还需细分eint8_23 */
{
key_eint_irq(bit); /* 处理中断, 清中断源EINTPEND(eint11,2 eint11, eint11) */
}
else if(bit == 9)
{
这里还需区分子中断源
}
/* 清中断 : 从源头开始清 */
SRCPND = (1<<bit);
INTPND = (1<<bit);
}

查看芯片手册查找“INT_WDT_AC97”如下图:

从上图可以看到SRCPND和SUBSRCPND的映射关系。
SUBSRCPND寄存器如下图:

我们可以读取SUBSRCPND来区分到底是哪一个子中断源产生了中断,当SUBSRCPND中哪一位被置1,表示对应的中断源发生了中断。

前面做完wtd_timer_init,还要进行中断控制器的初始化,查看INTMSK寄存器如下图:

查看INTSUBMSK寄存器如下图:

在interrupt_init中添加:

1
2
INTMSK &= ~(1<<9);//不屏蔽INT_WDT_AC97
INTSUBMSK &= ~(1<<14);//不屏蔽INT_AC97

修改handle_irq_c:

1
2
3
4
5
6
7
8
9
...
else if (bit == 9)//INTOFFSET==9
{
if (SUBSRCPND & 1<<14)
{
printf("watchdog timer interrupt occured.\n");
}
}
...

3.4.3 PWM脉冲宽度调制定时器#

PWM(Pulse Width Modulation),字面上是脉冲可调制的意思,就是可以调节占空比。

s3c2440有5个定时器,其中定时器0、1、2和3具有脉宽调制(PWM)功能。定时器4是一个无输出引脚的内部定时器。

先认识下s3c2440的pwm timer的框架:

1.时钟源为PCLK
2.pclk经过8 bit的预分频系数(Prescaler),和4 bit的时钟除数因子(clock divider),进行分频
3.经过MUX选择器选择用哪个定时器(5选1)
4.设置TCMPB0和TCNTB0和TCONn寄存器

3.4.1.1 pwm定时器原理#

pwm定时器的逻辑控制单元结构如下:

1
2
3
4
5
1 TCMPBn和TCNTBn寄存器中的值分别加载到TCMPn和TCNTn寄存器
2 每来一个clk(时钟)这个TCNTn减去1
3 当TCNTn == TCMPn时,可以产生中断,pwm输出引脚反转
4 TCNTn继续减1,当TCNTn == 0时,又产生一次中断,pwm引脚再次反转
5 重复1-4过程

设置TCNTBn寄存器来设置加载初值,设置后TCNTn中的值就会按照时钟周期递减。
设置TCMPBn寄存器来设置占空比,从而控制高低电平持续时间的比例。

3.4.3.2 pwm定时器编程实现#

要开始一个PWM定时器功能的步骤如下:(假设使用的是timer0)

3.4.3.2.1 初始化pwm定时器#

定义一个pwm_timer_init()函数。

  1. 设置时钟:

    分别设置定时器0的预分频器值(prescaler)和时钟分频值(clock divider),从而控制TCNT0减数器的频率。

根据公式:

1
pwm Timer clk = PCLK / {(预分频数)prescaler value+1} / {divider value(5.1MUX值)} 

PCLK是50M,设置prescaler value=99, divider value=16,所以pwm Timer clk= 50000000/(99+1)/16 = 31250 Hz

1
2
3
TCFG0 = 99; 
TCFG1 &= ~0xf;
TCFG1 |= 3;
  1. 设置初值:
1
2
3
/* 设置比较缓存寄存器TCMPB0和计数缓存寄存器TCNTB0的初始值*/
TCNTB0 = 31250 << 1; /* 2s中断一次 */
TCMPB0 = 31250 >> 1; /* 设置占空比*/
  1. 开启定时器0的手动更新TCNTB0&TCMPB0功能(设置TCON的第1位):

1
TCON |= (1<<1); //开始需要手工更新,这样才能将TCNTB0&TCMPB0同步到TCNT0&TCMP0
  1. 开启定时器0的自动加载:
1
2
TCON &= ~(1<<1); //开启自动加载要先清除手动更新
TCON |= (1<<3);
  1. 启动定时器0(设置TCON的第0位);

1
TCON |= (1<<0);
  1. 初始化中断控制器:


1
2
3
4
5
interrupt_init(){
...
INTMSK &= ~(1<<10); /* enable timer0 int */
...
}

做完这些初始化工作,就可以产生定时器中断了,同样我们需要在handle_irq_c函数中区分中断源:

3.4.3.2.2 pwm定时器中断服务程序#

我们可以通过查看TCNTO0寄存器来查看当前TCNT的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void handle_irq_c(void)
{
/* 分辨中断源 */
int bit = INTOFFSET;

/* 调用对应的处理函数 */
if (bit == 0 || bit == 2 || bit == 5) /* eint0,2,bit==5还需细分eint8_23 */
{
key_eint_irq(bit); /* 处理中断, 清中断源EINTPEND(eint11,2 eint11, eint11) */
}

else if(bit == 9) //INT_WDT_AC97
{
...
}

else if(bit == 10) //timer0
{
printf("timer0 interrupt occured.\n");
print_hex(TCNTO0);
}

/* 清中断 : 从源头开始清 */
SRCPND = (1<<bit);
INTPND = (1<<bit);
}

3.5 irq的优化改进#

我们对比irq外部中断, irq定时器中断,发现每增加一个中断源,又要去修改中断控制器的初始化interrupt_init()和handle_irq_c(),要在handle_irq_c()中去添加分支去执行不同的中断服务。

那么我们现在不去改变interrupt文件,在timer.c、key_eint.c中去注册自己的中断服务程序即可,这里我们使用函数指针数组,建立一个中断号和中断服务程序的映射关系。这样就可以根据中断号来执行对应的中断服务程序,即在handle_irq_c()中去回调不同类型的中断源注册下来的函数即可。

1
2
3
4
/* 定义函数指针数组 */
#define IRQ_NUM 32
typedef void(*irq_func)(int);
irq_func irq_array[IRQ_NUM];

然后实现一个register_irq(…)如下:

1
2
3
4
5
void register_irq (int irq, irq_func fp)
{
irq_array[irq] = fp;
INTMASK &= ~(1 << irq)
}

handle_irq_c()修改实现如下:

1
2
3
4
5
6
7
8
9
10
11
void handle_irq_c(void)
{
/* 分辨中断源 */
int bit = INTOFFSET;

irq_array[bit](bit); //根据中断号回调不同的中断处理函数

/* 清中断 */
SRCPND = (1<<bit);
INTPND = (1<<bit);
}

这样子我们的irq中断就被统一管理了起来,只要在其他各中断模块初始化的时候调用register_irq(…)注册即可。

s3c2440裸机编程-代码重定位和清bss

1 引入代码重定位#

s3c2440的cpu默认是从0地址开始取指令执行,当从nor启动时,0地址对应nor, nor可以像内存一样读,但不能像内存一样写,前面s3c2440裸机编程-内存控制器 讲过,写入norflash要进行按照spec进行命令表写入。因此我们能够从nor上取指令执行。

当nand启动的时候,我们nand中的前4K指令会变自动加载到sram中去,这时的0地址对应sram。那么我们的程序如果大于4K,要从nand启动,sram只拷贝了nand中的前4K代码,那么如何解决这个问题呢?

就需要重定位代码到dram(ddr)中去,dram的容量较大,又可以直接被cpu访问。

1.1 程序地址空间#

我们知道,程序包含:

1
2
3
4
5
代码段(.text)
数据段(.data):存放初始值不为0的全局变量/静态变量
rodata段(.rodata):const修饰的全局变量或静态变量
bss段(.bss):存放初始值为0或者未初始化的全局变量/静态变量
commen段(.commen):注释

下面展开一个实验引入为什么要代码重定位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "s3c2440_soc.h"
#include "uart.h"
#include "init.h"
int g_Char = 'A'; //.data
int g_CharB = 'B'; //.data
int g_CharC = 'C'; //.data
int g_CharD = 'D'; //.data
const int g_roval = 'C'; //.rodata
int g_A = 0; //bss
int g_B; //bss
int main(void) {
uart0_init();
while (1) {
putchar(g_Char);
g_Char++; /* nor启动时, 此代码无效,由于nor启动,nor上不可写 */
delay(1000000);
}
return 0;
}

我们将上面的代码编译出的裸机程序分别烧录到nand和nor flash,看看结果:

1
2
1. 烧录到nor flash, 程序一直输出`AAAA`。
2. 烧录到nand flash,程序无任何输出。

我们发现nor启动时, 对全局变量g_char++无效, nand启动程序无任何输出。我们对程序进行反汇编处理:

可以看到.text段是从0地址开始的,证明cpu的确从0地址取指令进行译码、执行。
当从nor启动时,0地址对应nor;当从nand启动时,0地址对应sram,所以无论从nand还是从nor启动cpu都能取指令执行。

再进一步分析反汇编:

可以看到.data段的起始地址是0x8474(即g_Char变量的地址为0x8474)。

1
2
1. 当把程序烧录进nor,  .data段在nor上的某一段区域, 由于nor能像内存一样读,但不能像内存一样直接写,因此对'g_Char'修改无效。
2. 当把程序烧录进nand, .data段在nand的某一区域,nand启动时硬件会自动把nand上的前4K数据copy到SRAM,然后cpu从sram取指令执行。但是.data段的起始地址0x8474>0x1000,超过了4K, cpu没法把.data段也copy到SRAM,所以当访问'g_Char'时,发生了异常(abt数据访问终止,这个异常后面有在下一节“异常与中断”里面专门讲解),因此程序卡死。

再仔细看看反汇编,发现.rodata段和.text段是连续的,但是.rodata段和.data段中间有一段”空洞”。用图形表示更形象,bin文件的内容分布如下所示:


那么我们怎么去掉空洞,让.data段了紧接着.rodata段呢?

1
2
1. 链接脚本(后面1.2有专门讲)
2. 直接在编译的时候用 "-Tdata 0x800",这样指定.data段基地址为0x800,这样nand启动时.data就能自动copy到SRAM了。

我们现在使用-Tdata 0x800编译出裸机程序,对应反汇编如下:

这时我们烧录程序到nand,从nand启动,发现能输出ABCDEFG了,这就对了,因为.data段数据从nand自动拷贝到了sram。

有人说为什么不吧.data段指向到dram呢,这样无论时nor启动还是nand启动不就都能对全局变量写了?
当然这个没错,我做了这个尝试,编译时用-Tdata 0x30000000, 发现编译出来的bin文件有800多M,为什么有这么大呢?由于我们指定.data段存放在0x30000000(sdram的基地址),这时bin文件的内部结构如下所示:

这么大的bin文件根本无法烧录。通过上面的例子,现在总结下为什么要代码重定位:

1
2
1.nand启动,前4K代码被自动copy到sram,当程序大于4K的时候需要重定位代码到sdram。
2.nor启动, 全局变量在nor上,不能像内存一样直接写该全局变量,那么也需要重定位到sdram。

1.2 链接脚本#

1.2.1 重定位data段#

我们发现arm-linux-ld -Ttext 0 -Tdata 0x30000000这种方式编译出来的bin文件有800多M,这肯定是不行的。可以通过AT参数指定.data段在编译时的存放位置,我们发现这样指定太不方便了,而且不好确定要放在bin文件的哪个位置。这里就要引入链接脚本,它可以帮我们解决这个不必要的麻烦。

1.2.1.1 链接脚本格式#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
SECTIONS {
. = 0x00000000; //表示当前地址为0
. = ALIGN(4); //设置当前位置让4字节对齐
.text :
{
cpu/arm920t/start.o (.text)
board/lyb2440/boot_init.o (.text)
*(.text)
} //表示.text段从0x4开始存放,其中可以手动调整代码段的位置,
//比如让start.o,boot_init.o中的函数放在最前面,然后存放剩余的代码段

. = ALIGN(4); //设置当前位置让4字节对齐
.rodata : { *(.rodata) } //从该位置开始存放所有的.rodata段

. = ALIGN(4); //设置当前位置让4字节对齐
.data : 0x30000000 : AT(0x800) { *(.data) } //从该位置开始存放所有的.data段 设置运行

__bss_start = .; //设置.bss段的起始位置
.bss : { *(.bss) } //从该位置开始存放所有的.bss段
_end = .;//设置.bss段的结束位置(也就是整个链接脚本的结束为止)
}

这是从uboot中裁剪过来的链接脚本,注释已经链接脚本的结构讲解的差不多了。这里.data段指定了程序的运行(链接)地址为sdram的base_addr(0x30000000),通过AT指定加载(在bin文件的存放)地址0x800

1.2.1.2 重定位data段例子#

对于nor启动时,我们可以直接从nor上取指令执行,所以可以只进行数据段的重定位(数据段需要写入),我们编写链接脚本sdram.lds如下所示:

1
2
3
4
5
6
SECTIONS {
.text 0 : { *(.text) }//所有文件的.text
.rodata : { *(.rodata) } //只读数据段
.data 0x30000000 : AT(0x800) { *(.data) } //放在0x800,但运行时在0x3000000
.bss : { *(.bss) *(.COMMON) }//所有文件的bss段,所有文件的.COMMON段
}

将程序烧录进nor flash,程序运行时会将.data拷贝到0x0x30000000也就是sdram中去。只重定位数据段的过程用下图更直观:

因此就可以对g_char进行写入了。Makefile如下:

1
2
3
4
5
6
7
8
9
10
all:
arm-linux-gcc -c -o led.o led.c
arm-linux-gcc -c -o uart.o uart.c
arm-linux-gcc -c -o init.o init.c
arm-linux-gcc -c -o main.o main.c
arm-linux-gcc -c -o start.o start.S
#arm-linux-ld -Ttext 0 -Tdata 0x30000000 start.o led.o uart.o init.o main.o -o sdram.elf
arm-linux-ld -T sdram.lds start.o led.o uart.o init.o main.o -o sdram.elf
arm-linux-objcopy -O binary -S sdram.elf sdram.bin
arm-linux-objdump -D sdram.elf > sdram.dis

修改start.s进行.data段的重定位。我们需要将以0x800为.data段基地址的整个数据段copy到0x30000000处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text
.global _start
_start:
/* 关闭看门狗 */
/* 初始化时钟 */
/* 设置栈 */
/*初始化sdram*/
...
/* 重定位data段,把加载地址0x800(bin文件中在nor中)的数据段的内容重定位到sdram的baseaddr */
mov r1, #0x800
ldr r0, [r1]
mov r1, #0x30000000
str r0, [r1]

bl main
halt:
b halt

用几行简单的数据加载存储指令即可实现数据段的重定位,这里是用的相对跳转指令bl main,因为还没有重定位整个完整的代码,所以不能用ldr绝对跳转。前面的初始化时钟、sdram我就不写了,参考s3c2440裸机编程-时钟体系 , s3c2440裸机编程-内存控制器

缺点:
这里只是人为的对.data段写死了,那么当我有多个全局变量时,还要计算重定位的次数,而且我们也不知道有多少个全局变量,所以这重定位方式有缺陷。那么我们对这种重定位.data断的方法做一个改进,将链接脚本修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
SECTIONS{
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x800)
{
data_load_addr = LOADADDR(.data); /* data段在bin文件中的地址, 加载地址 */
data_start = . ; /* data段在重定位地址, 运行时的地址 */
*(.data)
data_end = . ; /* data段结束地址 */
}
.bss : { *(.bss) *(.COMMON) }
}

链接脚本用一个变量data_load_addr指定了加载地址(data段在bin文件中的地址,即0x800),用变量data_start指定了运行地址(即为0x30000000),那么用data_end - data_start就是我们数据段的总长度。

对start.s重定位过程做出如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 重定位data段 */
ldr r1, =data_load_addr /* data段在bin文件中的地址, 加载地址 */
ldr r2, =data_start /* data段在重定位地址, 运行时的地址 */
ldr r3, =data_end /* data段结束地址 */
cpy:
ldrb r4, [r1]
strb r4, [r2] /*r2存入data_load_addr 0x400, 然后,r2,r1依次自加*/
add r1, r1, #1
add r2, r2, #1
cmp r2, r3
ble cpy

bl main
halt:
b halt

这里start.s中用到了链接脚本中的label地址。

1.2.2 重定位整个程序段#

由于我们的程序可能会大于SRAM或者nor的容量,那么就必须连代码段也一起进行重定位,这种重定位方式更好,在实际应用中也是用的这种方式去做的重定位。

1
2
3
4
5
6
7
8
9
10
11
12
13
SECTIONS {
. = 0x30000000;
. = ALIGN(4);
.text : { *(.text) }
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
_end = .;
}

我们将代码段的地址设置为0x3000_0004,然后紧接着放.rodata段,然后再紧接着放.data段。这样我们的bin文件就不再有“空洞”了。再来看重定位代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text
.global _start
_start:
...
/* 重定位text, rodata, data段整个程序 */
mov r1, #0
ldr r2, =_start /* 第1条指令运行时的地址,也就是.text段的runtime addr,在这里是0x3000_0004*/
ldr r3, =__bss_start /* bss段的起始地址,也就是整个程序的结束地址 */
cpy:
ldrb r4, [r1]
strb r4, [r2]
add r1, r1, #1
add r2, r2, #1
cmp r2, r3
ble cpy

bl main
halt:
b halt

整个bin文件程序的长度(.text + .rodata + .data)为__bss_start - _start,那么我们是把bin文件从存储介质的0地址copy到程序的运行地址0x3000_0004,这样我们访问.data段时就是访问sdram中重定位后的数据段了。

1.2.3 重定位代码优化#

1.2.3.1 strb/ldrb替换成str/ldr#

前面重定位时,我们使用的是ldrb命令从的Nor Flash读取1字节数据,再用strb命令将1字节数据写到SDRAM里面。
我们开发板的Nor Flash是16位,SDRAM是32位。 假设现在需要复制16byte数据。

不同的读写指令 cpu读取nor的次数 cpu写入sdram的次数
ldrb、strb 16 16
ldr、str 8 4

可以看出我们更换读写指令后读写次数变少了,提升了cpu的访问效率。修改如下:

1
2
3
4
5
6
7
8
9
...
cpy:
ldr r4, [r1]
str r4, [r2]
add r1, r1, #4 //r1加4
add r2, r2, #4 //r2加4
cmp r2, r3 //如果r2 =< r3继续拷贝
ble cpy
...

1.2.3.2 改成c代码重定位#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SECTIONS {
. = 0x30000000;
__code_start = .;
. = ALIGN(4);
.text : { *(.text) }
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
_end = .;
}
1
2
3
4
5
6
7
8
9
10
11
12
void copy2sdram(void) 	{
//要从lds文件中获得 __code_start, __bss_start
//然后从0地址把数据复制到__code_start
extern int __code_start, __bss_start;
volatile unsigned int *dest = (volatile unsigned int *)&__code_start;
volatile unsigned int *end = (volatile unsigned int *)&__bss_start;
volatile unsigned int *src = (volatile unsigned int *)0;

while (dest < end) {
*dest++ = *src++; //从0地址依次copy到__code_start(代码段的运行地址)
}
}

在start.s中设置栈指针sp后,即可执行bl copy2sdram进行重定位代码。如何设置栈指针参考s3c2440裸机编程-时钟体系 有实现,重复代码我就不贴上来了。

2 清除bss#

2.1 bss段介绍#

bss段是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "s3c2440_soc.h"
#include "uart.h"

char g_Char = 'A'; //.data
char g_Char3 = 'a';
const char g_Char2 = 'B'; //.rodata
int g_A = 0; //bss
int g_B; //bss

int main(void) {
uart0_init();

puts("\n\rg_A = ");
printHex(g_A);
puts("\n\r");

while (1) {
putchar(g_Char);
g_Char++; /* nor启动时, 此代码无效 ,重定位到sdram的baseaddr后有效*/
putchar(g_Char3);
g_Char3++;
delay(1000000);
}
return 0;
}

把程序烧进去,然后打印g_A,但是发现g_A这个值并不是0,而是一个随机值。我们学习linux时知道全局变量g_A, g_B输出肯定是0,裸机输出不是0,为什么呢?

1
2
原因:程序执行汇编过程做完了重定位后把代码copy到了sdram上,然后sdram上紧接着的地址就是.bss的基地址了,这时候bss段的这块内存没有经过任何处理,所以是随机的。
那么我们重定位完代码后需要进行清除sdram上.bss段的数据,因为我们知道bss是未初始化和初始值为0的全局变量。

2.2 清bss#

1
2
3
4
5
6
7
8
9
10
11
12
13
SECTIONS {
. = 0x30000000;
. = ALIGN(4);
.text :{ *(.text) }
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
_end = .;
}

清除bss段的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 清除BSS段 */
ldr r1, =__bss_start
ldr r2, =_end
mov r3, #0
clean:
strb r3, [r1]
add r1, r1, #1
cmp r1, r2
ble clean

bl main
halt:
b halt

我们把程序再烧进去,然后打印g_A,但是发现g_A的值是0了。本质上就是对重定位后的bss段数据清0。

2.3 清bss优化#

2.3.1 strb/ldrb替换成str/ldr#

1
2
3
4
5
6
7
8
9
10
11
    ldr r1, =__bss_start
ldr r2, =_end
mov r3, #0
clean:
str r3, [r1]
add r1, r1, #4
cmp r1, r2
ble clean
bl main
halt:
b halt

2.3.2 改成c代码清bss#

1
2
3
4
5
6
7
8
9
10
void clean_bss(void) {
/* 从lds文件中获得 __bss_start, _end*/
extern int _end, __bss_start;

volatile unsigned int *start = (volatile unsigned int *)&__bss_start;
volatile unsigned int *end = (volatile unsigned int *)&_end;

while (start <= end)
*start++ = 0;
}

注意:汇编代码获取的是链接脚本中的变量的地址,而C语言代码中获取的是链接脚本中的变量的值,所以这里的用C语言改进重定位还是清bss都是要加取址符。

2.3.3 每个段地址4字节对齐#

前面为了加快重定位和清bss的速度,用到了ldr,str这样以4字节为单位进行读写,但是还可能导致一个问题,假设现在链接脚本没有进行用ALIGN(4)让不同的段以4字节对齐,那么就会出现访问错乱的情况。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "s3c2440_soc.h"
#include "uart.h"
#include "init.h"
char g_Char = 'A'; //.data
char g_Char3 = 'a';
const char g_Char2 = 'B'; //.rodata
int g_A = 0; //bss
int g_B; //bss

int main(void) {
uart0_init();

puts("\n\rg_A = ");
printHex(g_A);
puts("\n\r");
putchar(g_Char);
return 0;
}

将链接脚本中.data段和.bss之间的**ALIGN(4)**去掉。那么我们会发现程序执行的时候输出的g_A=0,为什么呢,我们明明初始化g_A=‘A’呀?

打开反汇编分析:

我们的.bss段紧接着.data段后面,可知在对bss段进行清除的时候,由于我们是以4字节为单位操作的,所以我们清除g_A的时候,连带g_Char,g_Char的值也一起清除了。

所以data段和数据段之间添加ALIGN(4)。修改后就会发现bss段的地址以0x30000248开始了,如下图:

3 位置无关码#

3.1 绝对跳转与相对跳转指令#

3.1.1 相对跳转#

使用b, bl跳转指令。

bl sdram_test指令进行分析,查看反汇编, 代码段的链接地址为0x3000,0000。


这里的bl 3000036c不是跳转到3000036c,这个时候sdram并未初始化,那么这个物理地址是无法访问的.

为了验证,我们做另一个实验,修改连接脚本sdram.lds, 链接地址改为0x3000,0800,编译查看反汇编:

可以看到现在变成了bl 300003ec,但两个的机器码e1a0c00d都是一样的,机器码一样,执行的内容肯定都是一样的。 因此这里并不是跳转到显示的地址,而是跳转到: pc + offset,这个由链接器决定。

假设程序从0x30000000执行,当前指令地址:0x3000005c ,那么就是跳到0x3000036c;如果程序从0运行,当前指令地址:0x5c 跳到:0x000003ec。
因此:跳转到某个地址并不是由bl指令所决定,而是由当前pc值和offset偏移量决定。反汇编显示这个值只是为了方便读代码。

结论: 反汇编文件里, B或BL 某个值,只是起到方便查看的作用,并不是真的跳转。

3.1.2 绝对跳转#

1
2
//bl main  /*bl相对跳转,程序仍在NOR/sram执行*/
ldr pc, =main/*绝对跳转,跳到SDRAM*/

3.1.3 相对跳转与决定跳转比较#

怎么写位置无关码?

1
2
3
4
使用相对跳转命令 b或bl;
重定位之前,不可使用绝对地址(因为你sdram还没初始化,没有重定位代码过去,跳转过去不就死机了),也不可访问有初始值的数组(因为初始值放在rodata里,使用绝对地址来访问);
重定位之后,使用ldr pc = xxx,跳转到/runtime地址;
写位置无关码,其实就是不使用绝对地址

因此,前面的重定位和清bss例子,程序使用bl命令相对跳转,程序仍在NOR/sram执行,要想让main函数在SDRAM执行,需要修改代码:

1
2
//bl main  /*bl相对跳转,程序仍在NOR/sram执行*/
ldr pc, =main/*绝对跳转,跳到SDRAM*/

Linux内核-内核链表

1 内核链表#

内核链表本质就是一个双向循环链表:

image-20240802211111723

链表的实现仅用一个include/linux/list.h实现。

内核链表有别于传统链表就在节点本身不包含数据域,只包含指针域。故而可以很灵活的拓展数据结构。使用时包含在用户数据结构内部。

1.1 内核链表结构体#

1
2
3
struct list_head {
struct list_head *next, *prev;
};

image-20240802211751734

这里把内核经典的container_ofoffsetof实现也贴进来了。实际上一般使用container_of都用include\linux\kernel.h

1.2 list初始化#

1.2.1 用宏初始化-LIST_HEAD#

1
2
3
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)

image-20240802212312169

LIST_HEAD 定义一个list_head变量, 让next,和prev,也就是【前驱】和【后继】指针都指向自己,作为链表头指针。

例如:

1
LIST_HEAD(list); // struct list_head list = {.next = list, .prev = list};

1.2.2 用接口初始化-INIT_LIST_HEAD#

image-20240802213916603

INIT_LIST_HEAD函数用来对一个list_head指针初始化。WRITE_ONCE是一种内存屏障机制,只写入一次, 防止并发产生竞态,参考Linux内核-并发与同步 | Hexo (fuzidage.github.io)

linux内核下并发时同步机制 - fuzidage - 博客园 (cnblogs.com)

因此INIT_LIST_HEAD等效于:

1
2
3
4
static inline void INIT_LIST_HEAD(struct list_head *list) {
list->next = list;
list->prev = list;
}

例如:

1
2
struct list_head list;
INIT_LIST_HEAD(&list);

1.2.3 初始化完后头部节点图例#

image-20240803000855519

1.3 内核链表操作#

1.3.1 插入节点#

list_add总是在链表的头部插入, list_add_tail插在链表尾部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
WRITE_ONCE(prev->next, new);
}

static inline void list_add(struct list_head *new, struct list_head *head)
{
__list_add(new, head, head->next);
}

static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
__list_add(new, head->prev, head);
}

image-20240802220834774

1.3.1.1 创建一个节点#

1
2
3
4
5
6
7
8
9
struct my_data_list {
int data ;
struct list_head node;
};

struct my_data_list first_data = {
.val = 1,
.list = LIST_HEAD_INIT(first_data.node),//作为节点,其实可以不用初始化next和prev域
};

image-20240803150017826

1.3.1.2 头插节点-list_add#

list_add(&frist_data.node, &listHead);

list_add总是在链表的头部插入,先看插入第一个节点:

image-20240803001913257

插入第一个节点,就是让list_headnextprev都指向第一个节点,第一个节点的nextprev也都指向了list_head,构成一个单元素的环。

再插入第二个节点

image-20240803002115685

结合代码讲解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
WRITE_ONCE(prev->next, new);
}

//我们把__list_add(new, head, head->next);带入进去得:
{
head->next->prev = new; //①
new->next = head->next; //②
new->prev = head; //③
head->next = new; //④
}

①让节点1的prev指向节点2。

②让节点2的next指向节点1.

③让节点2的prev指向头节点。

④让头节点的next指向节点2。

1.3.1.2.1 头插入的要点总结#

总结1: head的next是指向链表中最新的节点,head的prev指向链表中最旧的节点。

总结2list_add函数作为头插本质:

​ ①把链表头的next剪掉,next去指向新节点;但是得提前将旧节点的prev剪掉,旧节点prev也去指向新节点。注意为什么要先操作旧节点?因为旧节点就是head->next啊。

​ ②让新节点前驱prev指向head, 后继next指向旧节点。

总结3:头插遍历总是先访问到最新的元素,类似于”栈stack“, ”先进后出“

1.3.1.3 尾插节点-list_add_tail#

list_add_tail(&frist_data.node, &listHead);

image-20240803150107150

image-20240803150405838

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
WRITE_ONCE(prev->next, new);
}
//我们把__list_add(new, head->prev, head);带入进去得:
{
head->prev = new; //①
new->next = head; //②
new->prev = head->prev; //③
head->prev->next = new; //④
}

①头节点的prev指向节点2。

②让节点2的next指向头节点.

③让节点2的prev指向头节点的prev。

④让节点1的next指向节点2。

1.3.1.3.1 尾插入的要点总结#

总结1: head的next是指向链表中最旧的节点,head的prev指向链表中最新的节点。

总结2list_add_tail函数作为尾插本质:

​ ①先把链表头的prev剪掉,prev去指向新节点;最后把旧节点(尾节点)的next指向新节点。这样插入的就变成了新的尾节点。

​ ②同时让新节点的next指向head, prev指向旧节点(尾节点)。

总结3:尾插遍历总是先访问到旧的元素,类似于”队列FIFO“, ”先进先出“

1.3.2 删除节点-list_del#

image-20240803153026556

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
WRITE_ONCE(prev->next, next);
}
static inline void __list_del_entry(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
}
static inline void list_del(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
entry->next = LIST_POISON1;
entry->prev = LIST_POISON2;
}

可以看到list_del__list_del_entry没有本质区别,核心都是__list_del。举个例子比如:

1
list_del(&frist_data.node);

非常好理解就不画图了,就是将下一个节点的prev指向前一个节点,同时反过来也要将前一个节点的next指向下一个节点。这个节点不就删除掉了。

注意:摘掉的节点prev、next 指针分别被设为 LIST_POSITION2LIST_POSITION1两个特殊值,这样设置是为了保证不在链表中的节点项不可访问–对LIST_POSITION1LIST_POSITION2的访问都将引起页故障。一访问就会立马出错,这样保证了数据安全性。来看下LIST_POSITION1LIST_POSITION2, 在include\linux\poison.h:

image-20240803155146829

1.3.3 链表删除与反初始化-list_del_init#

image-20240803155651544

可以看到调用了__list_del_entry摘除自己这个节点,同时INIT_LIST_HEAD用接口初始化自己这个节点。

1.3.4 链表遍历#

1.3.4.0 list_entry#

image-20240803162517683

遍历的关键就是这个list_entry 宏。它等效于container_of, 实现原理参考前面的讲解:

union和bit_field巧妙进行寄存器位操作 | Hexo (fuzidage.github.io)

union和bit field巧妙进行寄存器位操作 - fuzidage - 博客园 (cnblogs.com)

1.3.4.1 list_for_each(正向遍历)#

作用:传入头节点,去遍历里面的node

image-20240803162024761

head->next开始,也就是从第一个节点开始往后遍历,直到最后一个节点,这时pos就等于head了,循环结束。

1.3.4.1 list_for_each_safe#

下面这个list_for_each_safe, 多了一个tmp变量而已,提前用n去试探下一节点,安全情况下才赋给pos.

image-20240803205706719

1.3.4.2 list_for_each_entry#

作用:传入头节点,去遍历里面的node所对应的外层宿主结构体。举个例子比较好理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct xxx_dev {
struct list_head job_list;
char name[64];
int irq;
...
};
struct xxx_job {
atomic_t job_state;
struct list_head node;//add to xxx_dev job_list
atomic_t job_id;
...
};
struct xxx_dev dev;
struct xxx_job job;

list_add_tail(&job->node, &dev->job_list);

struct xxx_job *job_tmp;
list_for_each_entry(job_tmp, &dev->job_list, node) {
//从链表中取出job_tmp,do someting
}

首先定义一个链表job_list藏在xxx_dev里面,然后链表的节点宿主是xxx_job

list_add_tailxxx_jobnode即可加入xxx_devjob_list

list_for_each_entry即可根据xxx_jobnode成员取出宿主结构。

来分析一下list_for_each_entry函数:

image-20240803172745331

list_for_each_entry(job_tmp, &dev->job_list, node)那么首先进入for循环:

①代入list_first_entry(&dev->job_list, typeof(*job_tmp), node)

继续代入:看到了熟悉的list_entry(&dev->job_list->next, typeof(*job_tmp), node),是不是就是对应第一个节点的宿主结构xxx_job地址。

那么pos(也就是job_tmp)就指向了第一个节点的宿主结构。

②判断node是否达到head(也就是看有没有遍历到最后一个节点)

③此时pos已经是第一个节点的宿主结构,继续代入list_next_entry(第一个节点的宿主结构, node),看到了熟悉的list_entry(第一个节点的宿主结构->member->next, typeof(*job_tmp), node),这不就是下一个节点的宿主结构嘛,赋值给pos。

1.3.4.2 list_for_each_entry_safe#

image-20240803174314958

和前面的list_for_each_entry作用完全一样,可以看到实现也是基本一致。多了一个tmp变量而已,提前用n去试探下一节点,安全情况下才赋给pos.

1.3.4.3 list_for_each_prev(反向遍历)#

作用:传入头节点,反向去遍历里面的node

image-20240803174910449

没什么好说的,和list_for_each相反方向遍历。

1.3.4.3 list_for_each_prev_safe#

image-20240803210018006

1.3.4.4 list_for_each_entry_reverse#

作用:传入头节点,反向去遍历里面的node所对应的外层宿主结构体。和list_for_each_entry方向相反。

image-20240803175614268

1.3.4.4 list_for_each_entry_safe_reverse#

list_for_each_entry_reverse作用完全一样。

image-20240803175551253

1.3.4.5 list_for_each_entry_continue/list_for_each_entry_from(从中间某个节点开始遍历)#

image-20240803211441312

image-20240803212127987

1.3.4.5 list_for_each_entry_continue_reverse(从中间某个节点开始反向遍历)#

image-20240803211726153

1.3.5 判段链表是否为空#

1.3.5.1 list_empty#

image-20240803201525407

只要头节点的next还是自己,那就代表链表为空。

1.3.6 判段节点是否为最后一个节点#

1.3.6.1 list_is_last#

image-20240803201702932

只要传入的节点的next为头节点,那就是最后一个节点。

1.3.7 获取第一个节点的宿主结构#

1.3.7.1 list_first_entry/list_first_entry_or_null#

image-20240803204338002

1
2
struct xxx_job *job_tmp;
job_tmp = list_first_entry(&dev->job_list, typeof(*job_tmp), node)

前面1.3.4.2 list_for_each_entry小节其实已经分析过了,取出第一个节点的宿主结构指针。

下面这个list_first_entry_or_null多了一个判空,如果空链表,则会返回null。

image-20240803205148821

1.3.8 获取最后一个节点的宿主结构#

1.3.8.1 list_last_entry/list_first_entry_or_null#

image-20240803205532106

头节点的prev不就对应对应最后一个节点嘛。然后list_entry找到宿主结果。

1.3.9 获取上一个节点的宿主结构#

1.3.9.1 list_prev_entry#

image-20240803210650445

传入某个节点取prev不就是上一个节点嘛,然后list_entry找到宿主结果。

1.3.10 获取下一个节点的宿主结构#

1.3.10.1 list_next_entry#

image-20240803210742319

传入某个节点取next不就是下一个节点嘛,然后list_entry找到宿主结果。

1.4 内核链表进阶操作#

1.4.1 节点从A链表转移到B链表#

1.4.1.1 搬移到新链表头-list_move#

image-20240803212930512

可以看到就是从旧链表摘除节点,再头插到新链表。

1.4.1.2 搬移到新链表尾-list_move_tail#

image-20240803213156548

可以看到就是从旧链表摘除节点,再尾插到新链表。

1.4.2 链表A和链表B合并-list_splice#

image-20240803214326416

image-20240803215126160

分析:list_splice(list1, list2);

带入:__list_splice(list1, list2, list2->next),那么:

1
2
3
4
5
6
7
8
first = list1->next;
last = list1->prev;

list1->next = list2;
list2->next = list1->next;

list1->prev->next = list2->next;
list2->next->prev = list1->prev;

最后最好是将还要list1进行反初始化,这样list1才彻底和各个节点断链,比如list_splice_init函数:

image-20240803221224876

1.4.3 节点的替换-list_replace#

list_replace:将新的节点替换到旧的节点上。

list_replace_init:将新的节点替换到旧的节点上。同时将旧的节点的prev和next指向自己,反初始化。

image-20240803213753644

1
2
3
4
5
6
7
static inline void list_replace(struct list_head *old,
struct list_head *new) {
new->next = old->next;//①
new->next->prev = new;//②
new->prev = old->prev;//③
new->prev->next = new;//④
}

image-20240803223442214

可以看到虽然替换成功了,但是old还是有指向关系,我们再对old进行INIT_LIST_HEAD(old);断掉old的指向关系,也就是对应list_replace_init函数:

image-20240803223615205

old断链后图像那么最后就会变成:

image-20240803223811690

2 内核链表总结#

img

s3c2440裸机编程-内存控制器

1 内存接口概念#

1.1 不同类型的控制器#

S3C2440是个片上系统,有GPIO控制器(接有GPIO管脚(GPA-GPH)),有串口控制器 (接有TXD RXD引脚),有memory controller内存控制器,有Nand控制器等…

1
2
3
4
1)GPIO控制器属于门电路,不涉及到时序,相对简单。
2)串口控制器属于协议类接口,类似的协议类接口还有iic、iis、spi等。
3)前面的GPIO/门电路接口、协议类接口,都不会把地址输出到外部设备,仅仅只是将地址写入到相应的控制器。
接下来的内存类接口,会把地址输出到外部,cpu将地址写入内存控制器,内存控制器还需访问外部设备,比如NorFlash、网卡、SDRAM。

1.2 如何访问控制器地址#

GPIO/门电路接口、协议类接口、内存类接口都属于CPU的统一编址。但对于Nand Flash,它没有独立的地址线和cpu的地址总线相连接,因此它不参与CPU的统一编址。

1.2.1 门电路/协议类控制器#

对于门电路接口、协议类接口,直接访问寄存器即可。

1.2.2 内存类控制器#

对于内存类接口,交给内存控制器去处理。下面详细分析:

CPU只管发出一个地址,内存控制器根据该地址范围选择不同的模块,然后从模块中得到数据或者发送数据到模块中。
如下图,SDRAM、DM9000网卡、Nor Flash都接在s3c2440的数据总线和地址总线上,CPU把数据和地址发送出去,然后内存控制器根据地址范围确定要拉低选中哪个片选信号(nCS),再根据片选信号(nCS)选择相应的设备,进行收发地址和数据,互不干扰。

image-20240411202307277

1
2
3
4
5
6
(1)当CPU发出的指令的地址范围处于0x00000000 - 0x08000000
内存控制器就会使nGCS0处于低电平(片选引脚被选中),NorFlash被选中。(NorFlash启动时才行,nand启动时nGCS0这块对应SRAM)
(2)当CPU发出的指令的地址范围处于0x20000000 - 0x28000000
内存控制器就会使nGCS4处于低电平(片选引脚被选中),网卡DM9000被选中。
(3)当CPU发出的指令的地址范围处于0x30000000 - 0x38000000
内存控制器就会使nGCS6处于低电平(片选引脚被选中),SDRAM被选中

内存控制器根据不同的地址地址范围,发出不同的片选引脚,只有被片选引脚选中的芯片才能正常工作,不被选中的芯片就像不存在一样,不工作。

从s3c2440 datasheet中我们得知内存控制器可访问的地址范围有1G(0x0000,0000-0x4000,0000),8个bank,每个bank_size为128M。理论上需要2^30(30条地址线)来确定是哪个bank,哪个地址。但是实际上只用到了27条,那么是怎么确定是哪个bank被选中了呢?

cpu每次发给内存控制器的地址都是Addr[31:0],但是内存控制器发给外设(sdram,nor,dm9000)却只用到了A[26:0]。第[29:27]被用来确定要拉低哪个nGCS,即要选中哪个bank:

1
2
3
4
5
6
bit[29:27]		bit[26:0]
0b000 -> 0x000,0000-0x7ff,ffff (bank0被选中 (128M))
0b001 -> 0x800,0000-0xfff,ffff (bank1被选中 (128M))
... ...
0b110 -> 0x000,0000-0x7ff,ffff (bank6被选中 (128M))
0b111 -> 0x800,0000-0xfff,ffff (bank7被选中 (128M))

那么对于大容量的nandflash,理论上需要更多的地址线来确认访问地址,那既然没有地址线,cpu是如何访问nand的呢?当然是通过nand控制器,nand是地址、命令、数据都共用数据总线。这里只是引入一个话题,具体见s3c2440裸机-nand控制器

2 不同位宽内存与CPU地址连接关系#

s3c2440芯片手册上外设rom与CPU地址总线连接如下:

2.1 8bit x1 rom与CPU地址线连接#

2.2 8bit x2 rom与CPU地址线连接#

2.3 8bit x4 rom与CPU地址线连接#

2.4 16bit x1 rom与CPU地址线连接#

2.5 16bit x2 rom与CPU地址线连接#


从上面的图中,我们知道可以对2片位宽为8bit的内存扩展级联成1个16bit的内存,同理可用4片位宽为8bit的内存进行级联成1个32bit的内存。

3 不同位宽内存与CPU为什么要错位相连#

从上面的图中,我们还看见一个规律:

1
2
3
4
5
6
7
8
当外设总线位宽为8bit时, 外设A0接CPU的地址总线ADDR[0],
A[1]->ADDR[1] ...A[15]->ADDR[15]

当外设总线位宽为16bit时,外设A0接CPU的地址总线ADDR[1],
A[1]->ADDR[2] ...A[15]->ADDR[16]

当外设总线位宽为32bit时,外设A0接CPU的地址总线ADDR[2],
A[1]->ADDR[3] ...A[15]->ADDR[17]

为什么要这样设计呢?先看一个例子:

1
2
MOV R0, #3
LDRB R1, [R0] @ 从内存地址为3的地方,读出一个字节到R1

如图有8bitROM、16bitROM、32bitROM:

1
2
3
4
5
6
7
8
9
10
11
(1)对于8bitROM ,8bit是一次读写的最小单位,即0地址是第一个8bit,1地址是第二个8bit;
CPU发出地址3,即A0和A1都为1,8bitROM的A0和A1收到的也都是1,
于是找到了ROM上地址为3的8bit数据,包含了我们需要的数据。

(2)对于16bitROM ,16bit是一次读写的最小单位,即0地址是第一个16bit,里面有两个8bit数据;
CPU发出地址3,即A0和A1都为1,16bitROM的A0和A1分别收到的是1和0,
于是找到了ROM上地址为1的16bit数据,包含了我们需要的数据,最后内存控制器再帮我们挑选出所需的8bit数据。

(3)对于32bitROM ,32bit是一次读写的最小单位,即0地址是第一个32bit,里面有四个8bit数据;
CPU发出的地址3,即A0和A1都为0,32bitROM的A0和A1收到的都是0,
于是找到了ROM上地址为0的32bit数据,包含了我们需要的数据,最后内存控制器再帮我们挑选出所需的8bit数据。

用表格更好理解:

ROM/bit CPU发出地址 ROM收到地址 ROM返回数据 内存控制器挑选出数据给CPU
8bit(ROM) 0b000011 0b000011 编号3的存储单元中的8数据 编号3的存储单元中的8数据
16bit(ROM) 0b000011 0b000001 编号1的存储单元中的16数据 根据”A0=1”,挑出低8bit数据
32bit(ROM) 0b000011 0b000000 编号0的存储单元中的32数据 根据”A1A0=11”,挑出最低8bit数据

对上图的数据再次整理:

ROM/bit CPU发出地址 ROM收到地址(内存控制器转发给rom) ROM返回数据 内存控制器组装数据给CPU
8bit(ROM) 0b000100 0b000100 地址4的一个1byte数据 组装地址7、6、5、4数据成4字节数据
0b000101 地址5的一个1byte数据
0b000110 地址6的一个1byte数据
0b000111 地址7的一个1byte数据
16bit(ROM) 0b000100 0b00010 地址2的一个2byte数据 组装地址3、2的数据成4字节数据
0b00011 地址3的一个2byte数据
32bit(ROM) 0b000100 0b00001 地址1的一个4byte数据 直接返回4字节数据

这里牵扯到地址、内存中数据的排列存储,有点深入,如果实在无法理解,记住怎么去错位相连就好了。
结论:

1
2
1.和cpu地址总线相连的外设地址线确定了要访问外设的地址,即哪个存储单元;
2.然后内存控制器拿到外设存储单元中的数据后,再根据那几个错开的引脚[A1-A0]的值(CPU地址总线没接的那几个引脚的值),来挑出相应的数据给CPU。

再举一个例子, 假如读取一个32位的数据时,前面读的是8位数据:

1
2
MOV R0,   #4
LDR R1, [R0] @去地址4,读取4字节数据

我们知道CPU发出的是32bit地址,那么

1
2
3
对于32bit Rom,内存控制器会给它发1次,rom也会相应的接收1次;
对于16bit Rom,内存控制器会给它发2次,rom也会相应的接收2次;
对于8bit Rom,内存控制器给它发4次, rom接收4次,

3.1 配置内存控制器位宽#

接不同的rom外设,s3c2440内存控制器总线位宽要配置成不一样。位宽和等待控制寄存器如下:
BWSCON(BUSWIDTH&WAITCONTROLREGISTER):

img

我们SDRAM的位宽为32,DW6[25:24]设置成10, 没有使用等待信号,所以WS6[26]=0。 bank7跟随bank6的配置, 因此BWSCON寄存器的值为:0x22000000

4 内存控制器地址映射范围#

怎样确定芯片的访问地址?

1
2
1. 根据片选信号确定基地址
2. 根据芯片所接地址线确定范围

外设类型 接内存控制器的哪个片选 基地址 占用CPU的地址总线 地址范围(offset + size)
nor nGCS0 0x0000,0000 ADDR0-ADDR20 0x0000,0000 ~ 0x001f,ffff(2M)
dm9000网卡 nGCS4 0x2000,0000 ADDR0和ADDR2 0x2000,0000 ~ 0x2000,0005(5byte)
sdram nGCS6 0x3000,0000 ADDR0-ADDR25 0x3000,0000 ~ 0x3000,0000 + RAM_SIZE

这里再次提醒一下: 有人发现上图中nor没有和CPU的ADDR0相连接,sdram没有和CPU的ADDR0、ADDR1相连接。不要觉得ADDR0、ADDR1没用到,由于nor数据位宽是16bit,ADDR0是给内存控制器拆分数据用的,同样sdram数据位宽32bit,ADDR0、ADDR1也是给内存控制器拆分数据用的。这个上面已分析过,这也是什么要错位连接的原因。

5 SDRAM访问实例#

以EM63A165TS-6G这款外接SRAM存储来展开介绍。

5.1 SDRAM存储结构#

5.2 SDRAM引脚接线#

这里采用2片 EM63A165TS-6G 级联作为外接内存,关于EM63A165TS-6G的规则描述参考datasheet。可以看到该sdram是16bit 的,从接线可以看出第一片存储低16位数据,第二片存储高16位数据。

引脚说明:

1
2
3
4
5
6
7
8
9
A0-A12:地址总线
D0-D15:数据总线(位宽16,2片级联成位宽32
BA0-BA1:bank选择
nSCS:片选
nSRAS:行地址选择
nSCAS:列地址选择
nWE:写使能
SCLK:时钟
SCKE:时钟使能

5.3 SDRAM地址范围#

前面提到片选接了nGCS6,地址映射的base_addr=0x3000,0000,那么size是多大呢?

容量为:4M word x 16-bit x 4-bank = 32M,再看原理图我们是两片级联,所以容量为4M word x 32-bit x 4-bank=64M。所以地址范围是**[0x3000_0000 ~ 0x33ff_ffff]**

在对比另一款W9825G6KH SDRAM为例,地址总线A0-A12,数据总线D0-D15,因此内存大小:2^13 * 2^9 = = 4194304,等于4M,加上有4个bank,数据为宽16位,因此内存大小4Mx4x2字节。

image

image

框图翻译成中文形式:

image

1
2
3
4
5
6
7
8
9
10
CLK:时钟线,SDRAM 是同步动态随机存储器,“同步”的意思就是时钟,因此需要一根额外的时钟线,这是和 SRAM 最大的不同,SRAM 没有时钟线。
CKE:时钟使能信号线,SRAM 没有 CKE 信号。
CS:片选信号,这个和 SRAM 一样,都有片选信号。
RAS:行选通信号,低电平有效,SDRAM 和 SRAM 的寻址方式不同,SDRAM 按照行、列来确定某个具体的存储区域。因此就有行地址和列地址之分,行地址和列地址共同复用同一组地址线,要访问某一个地址区域,必须要先后发送行地址和列地址
CAS:列选通信号,和 RAS 类似,低电平有效,选中以后就可以发送列地址了。
WE:写使能信号,低电平有效
A0-A12: 地址线
DQ0-DQ15: 16位数据线
BS0-BS1: BANK 选择线
LDQM,UDQM: 高低字节数据选择线

5.4 SDRAM数据访问过程#

我们知道64M=2^20*2^6=2^26,那么需要26条地址线,再看看原理图,我们发现SDRAM的地址线A[12:0]只有13条,那么最多只能访问2^13=8K的数据,地址线明显配不上这么大的容量,那么它是如何解决的呢?

答:当然是拆分地址了,多次传输

我们从SDRAM的内部存储结构得知要确定SDRAM的一个存储单元,先确定是哪个bank,然后再确定在哪一行、哪一列即可。SDRAM有4个bank,由BA0、BA1决定选中哪个bank,查看SDRAM手册见下图:

通过选中nSRAS选中行地址,从而发送行地址;最后通过选中nSCAS选中列地址,从而发送列地址。例如:

1
2
ldr	r0, =0x30000000;
ldr r1, [r0]; 从SDRAM基地址读取4byte数据

过程如下:

1
2
1.发出片选信号nGCS6,选中SDRAM
2.发出bank选中信号(BA0,BA1),确定是SDRAM上的哪个bank,从原理图得知CPU的ADDR24、ADDR25对应SDRAM上的BA0,BA1,所以让BA0、BA1=00,选中bankA。

然后从sdram规格书确定行列地址的数目:

1
2
3.发出行地址信号nSRAS,使能行地址传输。传输行地址,确定是哪一行(看SDRAM手册确定行地址数(A12-A0)13条)
4.发出列地址信号nnSCAS,使能列地址传输。传输列地址,确定是哪一列(看SDRAM手册确定列地址(A8-A0)9条)

从而发送完整的0x30000000地址到了SDRAM,SDRAM返回4byte数据给CPU。

5.5 SDRAM驱动实例#

s3c2440内存控制器共有13个寄存器。我们要设置内存控制器参数,适配外接SDRAM。

1
2
BANK0--BANK5只需要设置BWSCON和BANKCONx(x为0~5)两个寄存器;
BANK6、BANK7外接SDRAM时,除BWSCON和BANKCONx(x为6、7)外,还要设置REFRESH、BANKSIZE、MRSRB6、MRSRB7等4个寄存器。

5.5.1 位宽寄存器#

BWSCON(BUSWIDTH&WAITCONTROLREGISTER)

我们SDRAM的位宽为32,DW6[25:24]设置成10, 没有使用等待信号,所以WS6[26]=0。 bank7跟随bank6的配置, 因此BWSCON寄存器的值为:0x22000000。

5.5.2 BANK控制寄存器#

BANKCONTROLREGISTER:

在8个BANK中,只有BANK6和BANK7可以外接SRAM或SDRAM。BANKCON6设置参数如下:

1
2
MT[16:15]:设置BANK是ROM/SRAM还是DRAM,我们用的SDRAM,属于DRAM。
Trcd[3:2]:行地址和列地址间隔多长时间,看SDRAM芯片手册时间间隔Trcd>18ns,我们HCLK=100MHZ,clocks为10ns,所以设置为2clocks即可。

5.5.3 刷新控制寄存器#

REFRESH(REFRESHCONTROLREGISTER)

1
2
3
4
5
6
7
8
9
REFEN[23]:设置开启SDRAM的刷新功能。
TREFMD[22]:SDRAM的刷新模式,0=CBR/AutoRefresh,选择自动刷新。

Trp[2120]:根据sdram手册Trp>18ns, 设为0(2 clocks)即可。
Tsrc[1918]: Tsrc = Trc - Trp = Trc-20, 根据sdram手册Trc>=60,我们取Trc =70, 则Tsrc= 50ns(5clocks)即可。
RefreshCounter[10:0]:Refresh period = (211-refresh_count+1)/HCLK,
RefreshCount = 211 + 1 - 100*Refresh period,看SDRAM手册“8192 refresh cycles/64ms”, Refresh period= 64000us/8192 = 7.8us,
RefreshCount取推荐值1269= 0x4f5.
综上,REFRESH寄存器设为0x8404F5

5.5.4 BANKSIZE寄存器#

BANKSIZEREG ISTER

1
2
3
4
5
BURST_EN[7]:0=ARM核禁上突发传输,1=ARM核支持突发传输(推荐);
SCKEEN[5]:0=不使用SCKE信号令SDRAM进入省电模式,1=使用SCKE信号令SDRAM进入省电模式(推荐);
SCLK-EN[4]:0=时刻发出SCLK信号,1=仅在访问SDRAM期间发出SCLK信号(推荐);
BK76MAP[2:0]:配置banksize成64M
因此,BANKSIZE寄存器设为0xB1

5.5.5 模式设置寄存器#

SDRAM MODE REGISTER SET REGISTER(MRSR)

1
2
CL[6:4]:表示发出行、列地址后,等多久才返回收到数据, 看SDRAM手册发现Tcas >=18ns,所以配置成2 clocks即可。
MRSRB6寄存器设置为0x20

5.5.6 测试代码#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void sdram_init(void) {
BWSCON = 0x22000000;
BANKCON6 = 0x18001;
BANKCON7 = 0x18001;
REFRESH = 0x8404f5;
BANKSIZE = 0xb1;
MRSRB6 = 0x20;
MRSRB7 = 0x20;
}

int sdram_test(void) {
volatile unsigned char *p = (volatile unsigned char *)0x30000000;//sdram base addr
int i;
// write sdram
for (i = 0; i < 1000; i++)
p[i] = 0x55;

// read sdram
for (i = 0; i < 1000; i++)
if (p[i] != 0x55)
return -1;

return 0;
}
int main(void) {
uart0_init();
sdram_init();

if (sdram_test() == 0)
led_test();
return 0;
}

当进行sdram_init后可已访问0x3000_0000地址的内容,led流水灯闪烁。
不初始化sdram_init,sdram_test执行会导致程序卡死。

6 NorFlash访问实例#

6.1 Flash种类特性介绍#

flash一般分为nand flash和nor flash,各自特性如下:

Nor NAND
XIP(片上执行) yes no
性能(擦除) 非常慢(5s,块太大) 快(3ms)
性能(写)
性能(读)
可靠性 一般(容易出现位反转)
可擦除次数 10000 ~ 100000 100000 ~ 1000000
接口 与ram类似,可直接访问任意地址 I/O接口(无地址线,必须串行访问,命令、地址、数据共用8位IO)
易用性 容易 复杂
主要用途 常用于保存代码和关键数据 用于保存数据
价格
容量
常用文件系统类型 jffs yaffs

nor有以下优缺点相对nand:

1
2
3
4
5
6
7
8
优点:
操作简单(可以像内存一样随机访问)
读取速度快
可靠性高,不易出现位反转
缺点:
容量小,价格贵
擦写慢
寿命短

6.2 NorFlash地址范围#

前面介绍内存控制器地址映射范围说了,得知nor接了bank 0,地址范围是0x0000,0000 ~ 0x001f,ffff。

6.3 NorFlash引脚描述#

下面是一款典型的nor flash原理图MX29LV800BBTC

引脚信息:

1
2
3
4
地址线(A0-A20)
数据线(DQ0-DQ15)
片选信号(nCE)
读写使能信号(LnOE/LnWE)

Nor Flash可以像内存一样读,但是不能像内存一样写,需要做一些特殊的操作才能进行写操作,这是因为nor是属于rom(只读存储器),不能像ram一样可以任意的写0写1,只能将存储介质中的电平由1变成0,不能将0变成1,所以要向nor中写入数据,必须先进行擦除动作。

6.4 NorFlash硬件连接#

6.5 NorFlash数据访问过程#

下图是S3C2440的内存控制器的可编程访问周期读写时序,里面的时间参数要根据外部norflash的性能进行配置。

时序含义:

1
2
3
4
5
6
Tacs: Address set-up time before nGCSn(表示地址信号A发出多久后才能发出nGCS片选)
Tcos: Chip selection set-up time before nOE(表示片选信号nGCS发出多久后才能发出读使能信号)
Tacc: access cycle(数据访问周期)
Tacp: page模式下的访问周期
Tcoh: Chip selection hold time after nOE (nOE信号释放多久后才能释放片选nGCS)
Tcah: Address hold time after nGCSn (片选nGCS释放多久后才能释放地址信号A)

下面我们根据此款norflash MX29LV160D手册中的访问时序图来分析,如下图:

从上面MX29LV160D手册的时序图中我们看见:

1
2
3
4
5
(1)先发送地址信号A
(2)发送片选CE
(3)发送读使能OE
(4)从数据总线上读出数据
(5)释放信号...
1
2
3
发出地址数据(Addresses)后,要等待Taa(要求大于等于70ns)时间,地址数据才有效;
发出片选信号(CE#)后,要等待Tce(要求大于等于70ns)时间,片选信号才有效;
发出读信号(OE#)后要等待Toe(要求大于等于30ns)时间,读信号才有效;

Tas(地址建立时间,也就是地址发送多久后才能继续发后面的片选信号)最小可以为0,那么说明地址信号(A)、片选(CE)、读(OE)使能信号可以一起发出。

为了简单我们把地址(Addresses),片选信号(CE#),读信号(OE#),同时发出,然后让它们都等待70ns即可(等待地址信号,片选信号,读写使能信号有效)。

我们再看看上面的nor访问时序图,释放地址、片选、读使能信号都没有时间差值dt要求,那么说明地址、片选、读使能信号可以同时释放。

6.6 NorFlash时序初始化#

打开s3c2440内存控制器。

6.6.1 BANK控制寄存器设置#

6.6.1.1 内存控制器时序设置#

6.6.1.1.1 Tacc#

Tacc表示数据访问周期:

从上图可以看到Tacc的默认值是111,对应14个clocks。s3c2440系统上电采用12MHz的晶振,HCLK=OSC=12MHz,那么Tacc=(1/(12*10^6)) * 14≈1166 ns,这个值很大,远超过了我们的nor手册上的Trc=70ns,几乎可以满足所有NorFlash的要求,这也是为什么我们不做初始化也能访问norflash的原因。

启动后,由于我们的时钟HCLK设置成了100MHz,T=1000/100=10ns,Tacc= 10ns*14 >70ns, 所以内存控制器不配置Tacc也是能访问该flash的。为了让访问速率加快,因此设置Tacc>70ns即可,配置成101,8个clocks即可。

6.6.1.1.2 Tacs/Tcos/Tcoh/Tcah#

从nor的分析中,我们得知地址、片选、读使能同时发出和同时释放,所以配置Tacs,Tcos,Tcoh,Tcah皆为0。

1
2
3
4
5
BANKCON0 = (*(volatile unsigned long *)(0x48000004));
void bank0_tacc_set(int val) {
BANKCON0 = val << 8;
}
bank0_tacc_set(0x5);

6.6.2 测试代码#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(void) {
unsigned char c;
uart0_init();//参考前面的uart编程
puts("Enter the Tacc val: \n\r");
while(1) {
c = getchar();
putchar(c);
if (c >= '0' && c <= '7') {
bank0_tacc_set(c - '0');
led_test();//跑马灯代码我就不贴了,谁都会
} else{
puts("Error, val should between 0~7\n\r");
puts("Enter the Tacc val: \n\r");
}
}
return 0;
}

6.6.2.1 测试结果#

输入0~4,Tacc小于70ns,无法读取Nor Flash上数据,LED不能闪烁。

输入5~7,Tacc大于70ns,可以读取Nor Flash上数据,LED不断闪烁,且值越小越快。

结论:我们的内存控制器默认配置的tacc一般都能兼容大多数市面上的norflash,一般都是可以访问的,无需进行对内存控制器进行多余的配置。

7 u-boot命令访问NorFlash#

前提
norflash初始化正常,能够正常从nor上执行。

对s3c2440而言,cpu总是从0地址读取指令执行程序。当cpu设置成nor启动时,0地址对应nor。cpu从nand启动时,0地址对应sram。

7.1 操作NorFlash#

将板子设为nor启动,那么0地址对应nor,我们先将uboot烧写到nor中,启动uboot。

打开这款MX29LV800BBTC norflash手册,找到操作flash的命令表:

7.1.1 reset#

往任何一个地址写入F0即可。

7.1.2 读ID#

很多的Nor Flash可以配置成位宽16bit(Word),位宽8bit(Byte),我们这款norflash数据位宽为16bit。下面我们按照nor手册上的命令表尝试一下:

1
2
3
4
5
6
往地址555H写入AAH(解锁)	
往地址2AAH写入55H(解锁)
往地址555H写入90H(命令)
0地址得到厂家ID(C2H)
1地址得到设备ID(22DAH或225BH)
退出读ID状态(给任意地址写F0H就可以了)

上面的地址是对于norflash的,那么我们CPU要怎么发送地址呢?从原理图接线我们知道CPU和nor的地址是错位相连的。

cpu地址 nor地址
A15~A1 A14~A0

那么可以看到cpu的地址实际相当于是nor地址左移了一位,那么比如要想给nor上的555H地址写入AAH,那么CPU要发出的地址应该为0x555<<1,也就是nor地址的2倍。

下面对在Nor Flash的操作,cpu的操作,U-BOOT上的操作进行比较,如下表:

Nor Flash的操作 cpu的操作 U-BOOT上的操作
往地址555H写入AAH(解锁) 往地址AAAH写入AAH(解锁) mw.w aaa aa
往地址2AAH写入55H(解锁) 往地址554H写入55H(解锁) mw.w 554 55
往地址555H写入90H(命令) 往地址AAAH写入90H(命令) mw.w aaa 90
读0地址得到厂家ID(C2H) 读0地址得到厂家ID(C2H) md.w 0 1 (1:表示读一次)
读1地址得到设备ID(22DAH或225BH) 读2地址得到设备ID(22DAH或225BH) md.w 2 1
退出读ID状态(给任意地址写F0H) 退出读ID状态(给任意地址写F0H) mw.w 0 f0

我们读出厂家id为c2,设备id为2249,和我们的nor手册上是一致的。我们发出f0命令,进行复位,这时读取的数据就不再是厂家id和设备id了,而是我们norflash中的实际的数据17 00 00 ea。

7.1.3 读数据#

前面说了,nor属于rom, 有独立地址线,可以像ram一样的读,只要做好内存控制器的初始化工作就可以直接读了。

我们再用二进制编辑器打开我们烧进去的uboot.bin,发现内容一样,说明我们从norflash中读出来的数据是正确的。

7.1.4 读属性#

通常Linux内核里面要识别一个 Nor Flash 有两种方法:

一种是 jedec 探测,就是在内核里面事先定义一个数组,该数组里面放有不同厂家各个芯片的一些参数,探测的时候将 flash 的 ID 和数组里面的 ID 一一比较,如果发现相同的,就使用该数组的参数。 jedec 探测的优点就是简单,只要通过flash的数组编号,即可访问该款flash属性,缺点是如果内核要支持的 flash 种类很多,这个数组就会很庞大。

一种是 CFI(common flash interface)探测,就是直接发各种命令来读取芯片的信息,比如 ID、容量等,芯片本身就包含了电压有多大,容量有有多少等信息。

我们的这款norflash属于cfi探测,下面对在Nor Flash上操作,s3c2440上操作,U-BOOT上进行cfi 探测(读取芯片信息)。

下图是从datasheet中检索出进入cfi模式后的一些flash属性查找表,可以按照表格命令查询norflash的一些属性(容量、电压、block信息等):

1
2
3
1.根据命令表往55H地址写入98H进入cfi模式
2.读取`qry`字符
3.获取属性
Nor Flash上操作cfi 2440上操作cfi U-BOOT上操作cfi
往55H地址写入98H(进入cfi模式) 往AAH地址写入98H mw.w aa 98
读地址10H得到0051(’q’) 读地址20H得到0051 md.w 20 1
读地址11H得到0052(‘r’) 读地址22H得到0052 md.w 22 1
读地址12H得到0059(‘y’) 读地址24H得到0059 md.w 24 1
读地址27H得到容量 读地址4EH得到容量 md.w 4e 1
读地址1BH得到VCCmin 读地址36H得到VCCmin md.w 36 1

从测试结果我们看到容量为2^21=2M,Vcc最小提供电压是2.7v。

7.1.5 写数据#

前面说了,nor属于rom, 有独立地址线,可以像ram一样的读,用md命令直接读取,不能像内存一样直接写,不信我们试试:

  1. 我们在Nor Flash地址0x10000读数据

    1
    由于我们的uboot只有162k,烧录到norflash后,norflash上的的0x100000地址还没有被写入数据,norflash的容量为2M(0~0x200000),所以读取NorFlash的0x10000的地址数据是0xffff...
  2. 在Nor flash的0x10000地址写数据0x1234,然后在这个地址读出数据:

    可以看到0x1234无法写进去,读出来还是0xfffff。为什么呢?要怎么才能将0x1234写进去。找到命令表:

    Nor Flash上写操作 2440上写操作 U-BOOT上写操作
    往地址555H写AAH(解锁) 往地址AAAH写AAH(解锁) mw.w aaa aa
    往地址2AAH写55H(解锁) 往地址554H写55H(解锁) mw.w 554 55
    往地址555H写A0H 往地址AAAH写A0H mw.w aaa a0
    往地址PA写PD 往地址0x100000写1234h mw.w 100000 1234

可以看到0x1234已被写入到地址0x100000。再次往0x100000地址处,写入0x5678:

这时我们发现0x100000地址处的数据不是0x5678,而是0x1230,为什么?

1
2
3
4
5
6
7
8
原因:flash有无法位反转的特性。
具体分析:
由于原来的数据已经是0x1234不是全0xffff,flash存储介质中只能1变成0,不能将0变成1。所以在0x1234的基础上不去擦出直接继续去写0x5678是会有问题的。
如果将0x1234不擦就去写成0x5678,过程如下:
0001 0010 0011 0100(0x1234)
0101 0110 0111 1000(0x5678)
----------------------------
0001 0010 0011 0000(0x1230)

所以得到就是0x1230, 因此flash写入前一定要先擦除。

7.1.5.1 擦除#

从datasheet找到擦除命令表:

Nor Flash擦操作 u-boot擦操作
往地址555H写AAH mw.w aaa aa
往地址2AAH写55H mw.w 554 55
往地址555H写80H mw.w aaa 80
往地址555H写AAH mw.w aaa aa
往地址2AAH写55H mw.w 554 55
往地址PA写30H mw.w 100000 30

擦除后再读取发现数据就已经变成了0xffff,后面就可以进行写操作了。

7.1.5.2 写入#

找到写入命令表,进行写入:

现在数据就变成我们的0x5678了。

注意:在写norflash时,要注意不要写0地址或者是uboot所在的地址,这样写入后norflash上的uboot程序就被破坏了。比如本测试就是写了0x100000地址,这个地址在uboot之外。

7.2 操作NorFlash-拓展#

7.2.1 地址位宽不对齐导致死机分析#

uboot发送md.w 0, md.w 2, md.w 4等偶地址命令能够读取norflash,但使用md.w 1, md.w 3,md.w 5就会出现死机,为什么?

1
由于我们的norflash是16bit数据位宽的,访问时要2byte对齐。如果不想以2byte为单位进行访问,那么要用uboot中用md.b 1,md.b 3这种单字节读取命令。

7.2.2 每次写都要先擦除#

操作norflash进行擦写的时候能够解锁一次,擦写多次吗?

1
不能,每次擦写都要进行解锁动作。

image-20240412144608560

7.2.3 擦除单位-块#

擦除那么是以块(block)为单位的,那么当进行擦除时发送的地址并不是以块对齐的,会有什么结果?

也能擦除成功,会根据地址范围确定在哪一个块中。

填入的地址是0x100009,也是擦除0x100000地址对应的块。

8 NorFlash驱动实例#

8.1 识别NorFlash#

我们知道要识别norflash属性,要让norflash进入cfi模式,然后按照手册上的表格发送一系列的命令就能获取norflash属性。

8.1.1 发命令#

实现一个cpu向nor发命令的一个函数nor_cmd()。我们的norflash是16bit位宽的,所以访问nor是以16位为单位访问的。

1
2
3
4
5
6
7
8
9
10
11
12
#define NOR_FLASH_BASE  0  /* s3c2440, nor-->cs0, base addr = 0 */
/* 比如: 55H 98
** 本意是: 往(0 + (0x55)<<1)写入0x98
*/
void nor_write_word(unsigned int base, unsigned int offset, unsigned int val) {
volatile unsigned short *p = (volatile unsigned short *)(base + (offset << 1));
*p = val;
}

void nor_cmd(unsigned int offset, unsigned int cmd) {
nor_write_word(NOR_FLASH_BASE, offset, cmd);
}

nor_cmd(0x55, 0x90);即可往norflash的0x55写入了0x98。

8.1.2 读一次数据#

1
2
3
4
5
6
7
unsigned short nor_read_word(unsigned int base, unsigned int offset) {
volatile unsigned short *p = (volatile unsigned short *)(base + (offset << 1));
return *p;
}
unsigned short nor_dat(unsigned int offset) {
return nor_read_word(NOR_FLASH_BASE, offset);
}

调用nor_dat(0x100000)即可得到该地址的数据。

8.1.3 识别函数#

有了发命令函数nor_cmd和读一次数据函数nor_dat,那么就就可以参考nor芯片手册的命令表进行操作norflash了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/* 进入NOR FLASH的CFI模式
* 读取flash属性
*/
void do_scan_nor_flash(void) {
char str[4];
unsigned int size;
int regions, i;
int region_info_base = 0x2d; //第0块region的基地址2d,第1块region的基地址31,第2块region的基地址35......(参考手册表4-3)
int block_addr=0, blocks, block_size, j;
int cnt = 0;

int vendor, device;

/* 打印厂家ID、设备ID */
nor_cmd(0x555, 0xaa); /* 解锁 */
nor_cmd(0x2aa, 0x55);
nor_cmd(0x555, 0x90); /* read id */
vendor = nor_dat(0);
device = nor_dat(1);
nor_cmd(0, 0xf0); /* reset */

nor_cmd(0x55, 0x98); /* 进入cfi模式 */
str[0] = nor_dat(0x10); //读地址10H得到0051('q')
str[1] = nor_dat(0x11); //读地址11H得到0052('r')
str[2] = nor_dat(0x12); //读地址12H得到0059('y')
str[3] = '\0';
printf("str = %s\n\r", str);

/* 打印容量 */
size = 1<<(nor_dat(0x27));
printf("vendor id = 0x%x, device id = 0x%x, nor size = 0x%x = %dM\n\r", vendor, device, size, size/(1024*1024));

/* 打印各个扇区的起始地址 */
/* 名词解释:
* region : 一个nor flash含有1个或多个region, 一个region含有1个或多个block(扇区).
* Erase block region[i] information:
* 前2字节+1 : 表示该region有多少个block
* 后2字节*256 : 表示block的大小
*/

printf("Block/Sector start Address:\n\r");
regions = nor_dat(0x2c); //读出region数量
for (i = 0; i < regions; i++) {
blocks = 1 + nor_dat(region_info_base) + (nor_dat(region_info_base+1)<<8);
block_size = 256 * (nor_dat(region_info_base+2) + (nor_dat(region_info_base+3)<<8));

printf("\n\rregion %d, blocks = %d, block_size = 0x%x, block_addr = 0x%x\n\r", i, blocks, block_size, block_addr);

for (j = 0; j < blocks; j++) {
/* 打印每个block的起始地址 */
printHex(block_addr);
putchar(' ');
cnt++;
if (cnt % 5 == 0)
printf("\n\r");

block_addr += block_size;
}

region_info_base += 4; /*得到region[i]的基地址*/
}
printf("\n\r");
/* 退出CFI模式 */
nor_cmd(0, 0xf0);
}

从测试结果来看每个region的block个数和block_size不一定一样,像region[0]只有一个block,block_size为4*64K;
region[1]有2个block,block_size=2*64K。

8.2 读数据#

由于NOR Flash是内存类接口,可以像内存一样读取,那么do_read_nor_flash函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void do_read_nor_flash(void){
unsigned int addr;
volatile unsigned char *p;
int i, j;
unsigned char c;
unsigned char str[16];

/* 获得地址 */
printf("Enter the address to read: ");
addr = get_uint();

p = (volatile unsigned char *)addr;

printf("Data : \n\r");

for (i = 0; i < 4; i++){
for (j = 0; j < 16; j++){
c = *p++;
str[j] = c;
printf("%02x ", c);
}

printf(" ; ");

for (j = 0; j < 16; j++){
if (str[j] < 0x20 || str[j] > 0x7e) /* 不可视字符 */
putchar('.');
else
putchar(str[j]);
}
printf("\n\r");
}
}

8.3 擦数据#

norflash擦写都是需要一定时间的,那么当我执行擦除或者写入动作后什么时候代表一次擦写动作已经完成了呢?

芯片手册提供了一个方法,每次擦除或者烧写过程中都可以查询数据总线上的第6位(Q6),当它保持稳定的时候表示一次擦除或者烧写动作完成,如下图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void wait_ready(unsigned int addr) {
unsigned int val;
unsigned int pre;

pre = nor_dat(addr>>1);
val = nor_dat(addr>>1);
while ((val & (1<<6)) != (pre & (1<<6))) {
pre = val;
val = nor_dat(addr>>1);
}
}
void do_erase_nor_flash(void) {
unsigned int addr;

printf("Enter the address of sector to erase: ");
addr = get_uint();

printf("erasing ...\n\r");
nor_cmd(0x555, 0xaa); /* 解锁 */
nor_cmd(0x2aa, 0x55);
nor_cmd(0x555, 0x80); /* erase sector */

nor_cmd(0x555, 0xaa); /* 解锁 */
nor_cmd(0x2aa, 0x55);
nor_cmd(addr>>1, 0x30); /* 发出扇区地址 */
wait_ready(addr);
}

可以看到擦除后这个block就是全0xffff了。

8.3 写数据#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
void do_write_nor_flash(void){
unsigned int addr;
unsigned char str[100];
int i, j;
unsigned int val;

/* 获得地址 */
printf("Enter the address of sector to write: ");
addr = get_uint();

printf("Enter the string to write: ");
gets(str);

printf("writing ...\n\r");

/* str[0],str[1]==>16bit
* str[2],str[3]==>16bit
*/
i = 0;
j = 1;
while (str[i] && str[j]) {
val = str[i] + (str[j]<<8);

/* 烧写 */
nor_cmd(0x555, 0xaa); /* 解锁 */
nor_cmd(0x2aa, 0x55);
nor_cmd(0x555, 0xa0); /* program */
nor_cmd(addr>>1, val);
/* 等待烧写完成 : 读数据, Q6无变化时表示结束 */
wait_ready(addr);

i += 2;
j += 2;
addr += 2;
}

val = str[i];
/* 烧写 */
nor_cmd(0x555, 0xaa); /* 解锁 */
nor_cmd(0x2aa, 0x55);
nor_cmd(0x555, 0xa0); /* program */
nor_cmd(addr>>1, val);
/* 等待烧写完成 : 读数据, Q6无变化时表示结束 */
wait_ready(addr);
}

由于我的norflash是位宽为16bit的,所以我们上面代码do_write_nor_flash进行写入时是以2byte(wold)为单位进行写入的。

总结:只要从spec中拿到了命令操作表,读写擦,识别就可以很轻松应对实现。

s3c2440裸机编程-UART体系

1 uart硬件介绍#

UART的全称是Universal Asynchronous Receiver and Transmitter(异步收发器)。 uart主要用于:

1
2
1.打印调试
2.数据传输

串口通过三根线即可,发送、接收、地线。

1
2
pc的TxD -> arm的RxD (UART write)
arm的TxD -> pc的RxD (UART read)

2 uart的参数和格式#

1
2
3
4
5
6
7
8
9
波特率:表示每秒传输多少bit,bits per second(bps).一般波特率都会有9600,19200,115200等选项。
起始位: 先发出一个逻辑”0”的信号,表示传输数据的开始。
数据位:可以是5~8位逻辑”0”或”1”。一般7位,刚好可以传输所有ASCII码。
校验位:
奇校验:(校验位+数据位)使得“1”的位数为奇数
偶校验:(校验位+数据位)使得“1”的位数为偶数
举个栗子:
‘A’的ASCII值是0x41,二进制就是01000001,那么奇校验就在校验位写‘1’,偶校验就在校验位写‘0’
停止位:它是一个字符数据的结束标志。

3 UART传输原理#

3.1 如何传输一个字符A#

‘A’的ASCII值是0x41#,二进制就是01000001,怎样把这8位数据发送给PC机呢?

1.双方约定好波特率(每一位占据的时间)
2.规定传输协议

现在处于这种模式:arm的TxD -> pc的RxD (UART read)

1.arm拉低uart总线1bit的时间(起始位)
2.arm根据数据位依次驱动TxD的电平,同时PC依次读取uart总线,数据到达PC的RxD引脚,pc依次获得数据位

为了能够进行远距离的传输数据,我们的PC是使用的RS-232逻辑电平,而arm开发板使用的TTL/CMOS逻辑电平。这里先讲解下什么是TTL逻辑电平,什么是RS-232逻辑电平。

TTL/CMOS逻辑电平:

0(低电平0-0.7v)表示逻辑'0'
1(高电平2-5v)  表示逻辑'1'

RS-232逻辑电平:

(+3V ~ +12V) 表示逻辑'0'
(-12V ~ -3V) 表示逻辑'1'

TTL逻辑电平的波形:

RS232逻辑电平的波形:

那么在起始信号开始后开始计时,arm每隔一个时钟往TxD放1bit数据,同时pc也从RxD get 1bit数据.

    arm				pc
TxD=data[0:],    data[0:]=RxD
TxD=data[1:],    data[1:]=RxD
...	
TxD=data[7:],    data[7:]=RxD

3.1.1 RS232#

我们知道RS232的逻辑’0’和逻辑’1’相差较大,比TTL/CMOS差距大,那么逻辑电平不容易出现反转,能传输更远的距离,在工业上用得比较多。

所以我们上面PC拿到的数据是不对的,那么需要一个TTL转RS232的电平转换芯片。

4 UART控制器#

发送数据:
内存将数据放入发送FIFO(64byte),通过发送移位器将数据一位一位的依次发送到TXDn,这样PC就可以从总线上依次get到数据。

接收数据:
当pc的TXDn端将数据发送到总线后,arm获取RXDn的引脚电平依次get到数据,逐位放进接收移位器,再放入FIFO,写入内存。

当然,也可不使用fifo,直接让内存与移位器交互,不过这样会造成浪费内存资源,内存的频率是很高滴,降低了内存的吞吐量。

5 UART控制器编程#

s3c2440支持3个UART串口,以uart0为例讲解。
那么我们需要实现以下这几个函数完成串口的最基本功能:

1
2
3
4
1)uart0_init()用于初始化串口
2putchar()用于发送一个字符
3)getchar()用于接收一个字符
4puts()用于发送一串字符

5.1 初始化UART#

5.1.1 引脚初始化#

配置uart0引脚

  1. 根据原理图GPH2,3用于TxD0, RxD0。
  2. 查看dataset,配置GPH控制寄存器,让GPH2,3配成uart模式;为了将其保持为高电平,先设置其为上拉。
    1
    2
    3
    GPHCON &= ~((3<<4) | (3<<6));
    GPHCON |= ((2<<4) | (2<<6));
    GPHUP &= ~((1<<2) | (1<<3)); /* 使能内部上拉 */

5.1.2 UART控制器初始化#

5.1.2.1 设置时钟源#

1
UCON0 = 0x00000005; /* 时钟源选择PCLK,中断/查询模式 */


5.1.2.2 设置波特率#

1
2
3
4
5
/* uart clock=50M,假设我们想要波特率=115200,
** 根据公式UBRDIVn = (int)(UART clock/( buad rate x 16) ) –1
** 得到UBRDIVn = (int)( 50000000 / ( 115200 x 16) ) –1 = 26
*/
UBRDIV0 = 26;

5.1.2.3 设置数据格式#


数据格式设置为常用的8n1,表示8个数据位, 无较验位, 1个停止位

1
ULCON0 = 0x00000003; /* 8n1: 8个数据位(数据+校验), 无较验位, 1个停止位 */

5.2 putchar/getchar#

putchar就是向发送寄存器(UTXH0)写入值进去。
getchar就是从接受寄存器(URXH0)取出值。
无论是getchar还是putchar都可以通过读取状态寄存器(UTRSTAT0)来作为传输结束判断标志。

查询其第2位判断发送buff是否为空,即上一次发送是否完成,如果完成即向UTXH0写入要发送的新数据;
查询其第0位判断接收buff是否有数据接受到,如果有数据接收到,返回接收buffer的值。
1
2
3
4
5
6
7
8
int putchar(int c){
while (!(UTRSTAT0 & (1<<2)));
UTXH0 = (unsigned char)c;
}
int getchar(void){
while (!(UTRSTAT0 & (1<<0)));
return URXH0;
}

5.3 puts#

1
2
3
4
5
6
int puts(const char *s){
while (*s){
putchar(*s);
s++;
}
}

s3c2440裸机编程-时钟体系

1 总线框架#

下图是s3c2440的总线框架,其中有AHB(Advanced High performance Bus)高速总线,APB(Advanced Peripheral Bus)外围总线。

不同总线对应不同的时钟。

SOC <-> FCLK
AHB <-> HCLK	
APB <-> PCLK 

其中:

1.使用AHB总线的有:LCD控制器、usb控制器、中断控制器、内存控制器等…
2.使用APB总线的有:i2c、spi、timer、gpio、adc等…具体上图。

2 时钟框架#

下图表示s3c2440 Soc的时钟框图:

下面从细节上讲解时钟体系:

2.1 如何选择时钟源#

s3c2440时钟源有2个,一个是OSC,一个是外部时钟EXTCLK,上面的时钟框图有标记,当然大家也可以查看手册“Figure 7-1. Clock Generator Block Diagram”。

那么如何选择是OSC还是EXTCLK呢?

打开原理图,OM3,OM2的引脚接地,那么OM[3,2]=00.所以根据手册“Table 7-1. Clock Source Selection at Boot-Up”可知时钟源为OSC晶振。

2.2 如何得到HCLK,PCLK,UCLK#

先了解下PLL, DIV

PLL:用锁相环进行倍频
DIV:用分频器进行分频

如下图:

生成的MPLL(Main PLL)和UPLL(USB PLL),MPLL直接提供给FCLK,通过HDIVN分频给HCLK,通过PDIVN分频给PCLK,再传给下面的各个设备。
osc经过UPLL(USB PLL)得到UCLK。

3 配置时钟控制器#

3.1 s3c2440时钟时序#

下图是2440时钟配置时序:

1.上电后,nRESET复位信号拉低,此时cpu还无法取指令工作。
2.nRESET复位信号结束后变为高电平,此时cpu开始工作。此时cpu主频FCLK=osc。
3.此时可以配置PLL,经过lock time后,FCLK倍频成新的时钟。

3.2 如何配置时钟#

在参考手册的特性里介绍了S3C2440的工作频率,Fclk最高400MHz,Hclk最高136MHz,Pclk最高68MHz。那么 我们干脆配置FCLK:HCLK:PCLK= 400:100:50 (MHz).

3.2.1 配置lock time#

我们取芯片手册上的推荐值。
1
2
3
4
/* LOCKTIME(0x4C000000) = 0xFFFFFFFF */
ldr r0, =0x4C000000
ldr r1, =0xFFFFFFFF
str r1, [r0]

3.2.2 配置MPLL得到FCLK#

也可参考配置表取值

1
2
3
4
5
6
7
8
9
10
11
12
/*
** MPLL(FCLK) = 2*m*Fin/(p*2^s)
** 不妨取:
** m = MDIV+8 = 92+8=100
** p = PDIV+2 = 1+2 = 3
** s = SDIV = 1
** 则:
** FCLK= 2*100*12/(3*2^1)=400M
*/
ldr r0, =0x4C000004
ldr r1, =(92<<12)|(1<<4)|(1<<0)
str r1, [r0]

注意:如果也要配置了UPLL,那么先配置UPLL,要个7个NOP后才能再配置MPLL.

###3,配置CLKDIVN得到HCLK,PCLK

1
2
3
4
/* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8  */
ldr r0, =0x4C000014
ldr r1, =0x5
str r1, [r0]

注意:如果HDIV设置为非0,CPU的总线模式要进行改变,默认情况下FCLK = HCLK,CPU工作在fast bus mode快速总线模式下,HDIV设置为非0后, FCLK与HCLK不再相等,要将CPU改为asynchronous bus mod异步总线模式.

1
2
3
4
/* 设置CPU工作于异步模式 */
mrc p15, 0, r1, c1, c0, 0         /* 读取CP15 C1寄存器 */ 
orr r1, r1, #0xc0000000         /* 设置CPU总线模式 */ 
mcr p15, 0, r1, c1, c0, 0         /* 写回CP15 C1寄存器 */

3.2.2 完整汇编代码#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
.text
.global _start

_start:
/* 关闭看门狗 */
ldr r0, =0x53000000
ldr r1, =0
str r1, [r0]

/* 设置MPLL, FCLK : HCLK : PCLK = 400m : 100m : 50m */
/* LOCKTIME(0x4C000000) = 0xFFFFFFFF */
ldr r0, =0x4C000000
ldr r1, =0xFFFFFFFF
str r1, [r0]

/* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8 */
ldr r0, =0x4C000014
ldr r1, =0x5
str r1, [r0]

/* 设置CPU工作于异步模式 */
mrc p15,0,r0,c1,c0,0
orr r0,r0,#0xc0000000 //R1_nF:OR:R1_iA
mcr p15,0,r0,c1,c0,0

/* 设置MPLLCON(0x4C000004) = (92<<12)|(1<<4)|(1<<0)
* m = MDIV+8 = 92+8=100
* p = PDIV+2 = 1+2 = 3
* s = SDIV = 1
* FCLK = 2*m*Fin/(p*2^s) = 2*100*12/(3*2^1)=400M
*/
ldr r0, =0x4C000004
ldr r1, =(92<<12)|(1<<4)|(1<<0)
str r1, [r0]

/* 一旦设置PLL, 就会锁定lock time直到PLL输出稳定
* 然后CPU工作于新的频率FCLK
*/

/* 设置内存: sp 栈 */
/* 分辨是nor/nand启动
* 写0到0地址, 再读出来
* 如果得到0, 表示0地址上的内容被修改了, 它对应ram, 这就是nand启动
* 否则就是nor启动
*/
mov r1, #0
ldr r0, [r1] /* 读出原来的值备份 */
str r1, [r1] /* 0->[0] */
ldr r2, [r1] /* r2=[0] */
cmp r1, r2
ldr sp, =0x40000000+4096
moveq sp, #4096 /* r1=r2, nand启动,修改栈指针=4096(此时sram对应0地址) */
streq r0, [r1] /* 恢复原来的值 */

bl main

halt:
b halt

循环队列FIFO原理图解

1 循环队列FIFO介绍#

循环队列是把顺序队列首尾相连,把存储队列元素的表从逻辑上看成一个环,成为循环队列。

img

入队时尾指针向前追赶头指针;出队时头指针向前追赶尾指针。

1.1 循环队列结构#

1
2
3
4
5
#define FIFO_HEAD(name, type)                                          \
struct name { \
struct type *fifo; \
int front, tail, capacity; \
}
1
2
3
4
front表示首元素索引
tail表示最后一个元素索引
capacity表示队列的长度
struct type fifo表示该队列中的元素指针,可以指向任意结构体指针

举个例子:

1
2
3
4
5
6
7
8
9
10
11
struct person{
int age;
int id;
char name[20];
};
FIFO_HEAD(person_q, person*);
//等价于
struct person_q { \
struct person* *fifo; \
int front, tail, capacity; \
}

1.2 FIFO初始化#

分配一个连续的空间存储队列元素。用户自定义队列容量。

img

1
2
3
4
5
#define FIFO_INIT(head, _capacity) do {                                  \
(head)->fifo = malloc(sizeof(*(head)->fifo) * _capacity); \
(head)->front = (head)->tail = -1; \
(head)->capacity = _capacity; \
} while (0)

1.3 FIFO销毁#

1
2
3
4
5
6
#define FIFO_EXIT(head) do {                                                \
(head)->front = (head)->tail = -1; \
(head)->capacity = 0; \
if ((head)->fifo) \
free((head)->fifo); \
} while (0)

1.4 入队列#

入队列就是尾元素的索引++,也就是tail++,让新元素放进队列的尾部。

1
2
3
4
5
6
7
8
#define FIFO_PUSH(head, elm) do {                                      \
if (FIFO_EMPTY(head)) \
(head)->front = (head)->tail = 0; \
else \
(head)->tail = ((head)->tail == (head)->capacity - 1) \
? 0 : (head)->tail + 1; \
(head)->fifo[(head)->tail] = elm; \
} while (0)

如果队列是空的,则第一个元素入队列,front和tail索引都指向第一个元素,front = tail = 0;
img

其他情况入队,让tail++
img

img

img

1.5 出队列#

出队列就是让font对应的元素丢出去,font++。

1
2
3
4
5
6
7
8
#define FIFO_POP(head, pelm) do {                                      \
*(pelm) = (head)->fifo[(head)->front]; \
if ((head)->front == (head)->tail) \
(head)->front = (head)->tail = -1; \
else \
(head)->front = ((head)->front == (head)->capacity - 1)\
? 0 : (head)->front + 1; \
} while (0)

当front追上tail后,表示队列空了,重新设置起始点,需要将front = tail = -1 。

img

其他情况出队,丢出front元素,让front++

img

1.6 FIFO判空#

1
#define FIFO_EMPTY(head)    ((head)->front == -1)

①队列初始化时,队列是空的,会让front为-1
②出队列时,font++, 当font追上tail表示空了,则可以重新设置起始点,令front = tail = -1
综合①②所以可以用-1判断

1.6 FIFO判满#

1
#define FIFO_FULL(head)     (((head)->front == ((head)->tail + 1)%(head)->capacity))

①当front=0时,那么tail到达capacity-1表示FIFO full。

img

②否则,tail追上front后(front = tail + 1)表示FIFO full。

img

1.7 FIFO容量#

1
#define FIFO_CAPACITY(head) ((head)->capacity)

1.8 FIFO中有效元素个数#

1
2
#define FIFO_SIZE(head)     (FIFO_EMPTY(head) ? \
0 : ((((head)->tail + (head)->capacity - (head)->front) % (head)->capacity) + 1))

用tail - front就表示有效元素个数,不过由于循环FIFO,可能tail<front,这个时候就需要取余运算,如下图:

img

1.9 FIFO遍历#

1
2
3
4
#define FIFO_FOREACH(var, head, idx) \
  for (idx = (head)->front, var = (head)->fifo[idx]; \
    idx < (head)->front + FIFO_SIZE(head); \
    var = (head)->fifo[++idx % (head)->capacity])

1.10 队列元素获取#

1.10.1 第一个元素#

1
#define FIFO_GET_FRONT(head, pelm) (*(pelm) = (head)->fifo[(head)->front])

1.10.2 最后一个元素#

1
#define FIFO_GET_TAIL(head, pelm) (*(pelm) = (head)->fifo[(head)->tail])

2 测试用例#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#include "fifo.h"
#include <stdio.h>
struct person{
int age;
int id;
char name[20];
};

FIFO_HEAD(person_q, person*);
struct person_q person1_queue;
struct person_q person2_queue;

int main(void){
FIFO_INIT(&person1_queue, 1);
FIFO_INIT(&person2_queue, 5);

if (FIFO_CAPACITY(&person1_queue) != 1) {
printf( "FIFO_CAPACITY 1 NG.\n");
return -1;
}

if (FIFO_CAPACITY(&person2_queue) != 5) {
printf( "FIFO_CAPACITY 2 NG.\n");
return -1;
}

if (FIFO_SIZE(&person1_queue) != 0) {
printf( "FIFO_SIZE 1 NG.\n");
return -1;
}

if (FIFO_SIZE(&person2_queue) != 0) {
printf( "FIFO_SIZE 2 NG.\n");
return -1;
}

if (!FIFO_EMPTY(&person1_queue)) {
printf( "FIFO_EMPTY 1 NG.\n");
return -1;
}

if (!FIFO_EMPTY(&person2_queue)) {
printf( "FIFO_EMPTY 2 NG.\n");
return -1;
}

struct person *person_a = malloc(sizeof(*person_a));
person_a->age = 20;
person_a->id = 1001;
FIFO_PUSH(&person1_queue, person_a);//把person_a这个结构体指针元素丢进FIFO,
//后面对它pop出来又能拿到它,所以不用担心地址弄丢导致无法释放.

if (!FIFO_FULL(&person1_queue)) {
printf( "FIFO_FULL 1 NG.\n");
return -1;
}

person_a = malloc(sizeof(*person_a));
person_a->age = 30;
person_a->id = 1002;
FIFO_PUSH(&person2_queue, person_a);

if (FIFO_FULL(&person2_queue)) {
printf( "FIFO_FULL 2 NG.\n");
return -1;
}

if (FIFO_SIZE(&person1_queue) != 1) {
printf( "FIFO_SIZE 3 NG.\n");
return -1;
}

if (FIFO_SIZE(&person2_queue) != 1) {
printf( "FIFO_SIZE 4 NG.\n");
return -1;
}

FIFO_POP(&person1_queue, &person_a);
if (person_a->age != 20) {
printf( "FIFO_POP content NG.\n");
return -1;
}
free(person_a);

if (FIFO_SIZE(&person1_queue) != 0) {
printf( "FIFO_SIZE 5 NG.\n");
return -1;
}

person_a = malloc(sizeof(*person_a));
person_a->age = 40;
person_a->id = 1003;
FIFO_PUSH(&person2_queue, person_a);

FIFO_GET_FRONT(&person2_queue, &person_a);
if (person_a->age != 30) {
printf( "FIFO_GET_FRONT NG.\n");
return -1;
}

FIFO_GET_TAIL(&person2_queue, &person_a);
if (person_a->age != 40) {
printf( "FIFO_GET_TAIL NG.\n");
return -1;
}

FIFO_POP(&person2_queue, &person_a);
if (person_a->age != 30) {
printf( "FIFO_POP content NG.\n");
return -1;
}
free(person_a);

if (FIFO_SIZE(&person2_queue) != 1) {
printf( "FIFO_SIZE 6 NG.\n");
return -1;
}

FIFO_POP(&person2_queue, &person_a);
if (person_a->age != 40) {
printf( "FIFO_POP content NG.\n");
return -1;
}
free(person_a);

if (FIFO_SIZE(&person2_queue) != 0) {
printf( "FIFO_SIZE 7 NG.\n");
return -1;
}

struct person *person_arr[5];
int i=0;

while (!FIFO_FULL(&person2_queue)) {
person_arr[i] = malloc(sizeof(*person_arr[0]));
person_arr[i]->age = i;
person_arr[i]->id = 1000 + i;
FIFO_PUSH(&person2_queue, person_arr[i]);
i++;
}

while (!FIFO_EMPTY(&person2_queue) {
FIFO_POP(&person2_queue, &person_a);
printf( "age:%d, id:%d.\n", person_a->age, person_a->id);
free(person_a);
}

FIFO_EXIT(&person1_queue);
FIFO_EXIT(&person2_queue);
return 0;
}

结果如下:

img

tailq队列

1 tailq队列介绍#

TAILQ队列是FreeBSD内核中的一种队列数据结构,主要是把队列头抽象成一个单独的结构体。它实现在Linux queue中。

1.1 queue 简介#

img

可以include <sys/queue.h>后直接使用。queue 分为 SLIST、LIST、STAILQ、TAILQ、CIRCLEQ 。queue 的所有源码都是宏定义,因此完全包含于queue.h当中,无需编译为库文件。

可以从toolchains或者系统路径/usr/include/x86_64-linux-gnu/sys/queue.h找到实现。

img

1.2 SLIST#

SLIST 是Singly-linked List 的缩写,意为单向无尾链表。

img

1.3 STAILQ#

单向有尾链表,节点n为尾节点。

img

1.4 LIST#

双向无尾链表。

img

1.5 TAILQ#

双向有尾链表。

img

1.6 CIRCLEQ#

双向循环链表。

img

2 TAILQ实现原理图解#

双向有尾链表,也就是有一个表头和表尾,表头指向节点1和尾节点。

2.1 描述前一个和下一个元素的结构#

1
2
3
4
5
6
7
8
9
10
11
12
13
#define TAILQ_ENTRY(type)                                                   \
struct { \
struct type *tqe_next; /* next element */ \
struct type **tqe_prev; /* address of previous next element */ \
}

/*tqe_next是指向下一个元素的指针,tqe_prev是指向前一个元素的tqe_next地址,对它解引用后
(*tqe_priv)指向当前元素的地址。*/
如:
struct item{
  int val;
  TAILQ_ENTRY(item) entries;
};

img

2.2 队列头#

1
2
3
4
5
6
#define    TAILQ_HEAD(name, type)                        \
struct name { \
struct type *tqh_first; /* first element */ \
struct type **tqh_last; /* addr of last next element */ \
}
STAILQ_HEAD(my_tailq, tailq_entry) queue_head;

img

先看TAILQ_HEAD:

1
2
3
tqh_first为队列第一个元素的地址;
tqh_last为最后一个元素tqe_next的地址;
tqh_last指向的指针为0

再看TAILQ_ENTRY:

1
2
3
tqe_next为队列下一个元素的地址;
tqe_prev为队列上一个元素tqe_next的地址;
tqe_prev指向的指针为当前元素的地址;

2.3 初始化#

img

1
2
3
4
#define TAILQ_INIT(head) do {                                               \
(head)->tqh_first = NULL; \
(head)->tqh_last = &(head)->tqh_first; \
} while (0)

2.4 插入元素#

1
2
3
4
5
6
#define TAILQ_INSERT_TAIL(head, elm, field) do {                  \
(elm)->field.tqe_next = NULL; \
(elm)->field.tqe_prev = (head)->tqh_last; \
*(head)->tqh_last = (elm); \
(head)->tqh_last = &(elm)->field.tqe_next; \
} while (0)

2.4.1 插入1个元素#

  1. 将要插入的node加入到尾部:

    img

1
2
(elm)->field.tqe_next = NULL;                         
(elm)->field.tqe_prev = (head)->tqh_last;//将要插入的节点prev指向最后一个node
  1. 更新头节点:

    img

1
2
*(head)->tqh_last = (elm);          
(head)->tqh_last = &(elm)->field.tqe_next;

2.4.2 同理插入多个元素#

同理多个元素时尾插。

  1. 将要插入的node加入到尾部:

    img

  2. 更新头节点:

img

1
2
*(head)->tqh_last = (elm);           //尾节点指向新的尾巴
(head)->tqh_last = &(elm)->field.tqe_next; //head的last指向新的尾巴

2.5 删除元素#

1
2
3
4
5
6
7
#define TAILQ_REMOVE(head, elm, field) do {                       \
if (((elm)->field.tqe_next) != NULL) \
(elm)->field.tqe_next->field.tqe_prev = (elm)->field.tqe_prev; \
else \
(head)->tqh_last = (elm)->field.tqe_prev; \
*(elm)->field.tqe_prev = (elm)->field.tqe_next; \
} while (0)

我们现在要把val=3的elm删除:
elm中的tqe_next不为空,表示elm不是尾节点。那么

1
2
(elm)->field.tqe_next->field.tqe_prev = (elm)->field.tqe_prev;
*(elm)->field.tqe_prev = (elm)->field.tqe_next;

这2句执行完后:

img

然后free掉该elm,

img

同理再删除val=2的elm:

img

然后free掉该elm,

img

最后如果要把val=4的elm删除:

elm中的tqe_next为空,表示elm是尾节点。那么,

1
2
(head)->tqh_last = (elm)->field.tqe_prev;               //让head的last指向新的尾巴        
*(elm)->field.tqe_prev = (elm)->field.tqe_next; //让elm的前一个node的next指向该elm的后一个node

img

2.6 第一个元素#

1
#define        TAILQ_FIRST(head)                ((head)->tqh_first)

2.7 最后一个元素#

1
2
#define        TAILQ_LAST(head, headname) \
(*(((struct headname *)((head)->tqh_last))->tqh_last))

这个实现看起来有点绕,我们先做一个实验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
typedef struct _QUEUE_ITEM {
int value;
TAILQ_ENTRY(QUEUE_ITEM) entries;
}QUEUE_ITEM;

TAILQ_HEAD(TAIL_QUEUE, QUEUE_ITEM) queue_head;

int main(int argc, char **argv) {
QUEUE_ITEM *item[5];
TAILQ_INIT(&queue_head);

int i = 0;
for (i = 0; i < 5; i += 1) {
item[i] = (struct QUEUE_ITEM*)malloc(sizeof(QUEUE_ITEM));
item[i]->value = i;
TAILQ_INSERT_TAIL(&queue_head, item[i], entries);
}

for(i = 0; i < 5; i += 1) {
printf("item[%d]: item:%#x, next:%#x,&next:%#x, prev:%#x, *prev:%#x\n",
        i, item[i], item[i]->entries.tqe_next, &(item[i]->entries.tqe_next), item[i]->entries.tqe_prev, *(item[i]->entries.tqe_prev));
}

printf("queue_head:%#x, first:%#x, last:%#x\n", &queue_head, queue_head.tqh_first, queue_head.tqh_last);
printf("last item:%p\n", TAILQ_LAST(&queue_head, TAIL_QUEUE));
}

打印结果如下:
img

可以用图形来描述:
img

TAILQ_LAST(&queue_head, TAIL_QUEUE);这句话展开:
(*(((struct TAIL_QUEUE*)((&queue_head)->tqh_last))->tqh_last))

((struct TAIL_QUEUE*)((&queue_head)->tqh_last))这句话,我们把地址0x601060代入进去得0x602098,即为:

img

然后(((struct TAIL_QUEUE*)((&queue_head)->tqh_last))->tqh_last)得到0x602078,
认真的同学此时已经发现,此时对应倒数第二元素的next地址,

最后取(*(((struct TAIL_QUEUE*)((&queue_head)->tqh_last))->tqh_last))得到0x602090,这就是最后一个元素的地址。

总结:这里核心其实就是把最后一个元素的entries成员当成head指针来使用。因为本质上最后一个节点的TAILQ_ENTRY域和TAILQ_HEAD是同样的结构。

2.8 下一个元素#

1
#define        TAILQ_NEXT(elm, field)                ((elm)->field.tqe_next)

2.9 前一个元素#

1
2
#define        TAILQ_PREV(elm, headname, field) \
(*(((struct headname *)((elm)->field.tqe_prev))->tqh_last))

这里和TAILQ_LAST原理一样,将0x602090代入进去得:
img
然后对*(0x602058)得0x602070,即得到了前一个node的地址。

2.10 判空#

1
#define        TAILQ_EMPTY(head)                ((head)->tqh_first == NULL)

2.11 判满#

1
#define        TAILQ_FIRST(head)                ((head)->tqh_first)

2.12 遍历#

1
2
3
4
#define        TAILQ_FOREACH(var, head, field)                                        \
for ((var) = ((head)->tqh_first); \
(var); \
(var) = ((var)->field.tqe_next))

2.13 倒遍历#

1
2
3
4
#define        TAILQ_FOREACH_REVERSE(var, head, headname, field)                \
for ((var) = (*(((struct headname *)((head)->tqh_last))->tqh_last)); \
(var); \
(var) = (*(((struct headname *)((var)->field.tqe_prev))->tqh_last)))

当看懂之前的最后一个元素原理时,倒遍历的实现是不是超级简单。