ini_parse配置解析功能移植

1 ini_parse移植#

1.1 下载ini解析源码#

源码的github地址https://github.com/benhoyt/ini

1
git clone https://github.com/benhoyt/inih.git

img

1.2 使用ini_parse功能#

img

img

img

可以看到核心就是一个ini_parse函数。用户自定义一个callback函数去解析自己的配置ini。测试代码如下:

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
#include <stdio.h>
#include "ini.h"
#include <string.h>
typedef struct _SAMPLE_INI_CFG_S {
char name[100];
int bus_id;
int age;
char name2[100];
int bus_id2;
int age2;
} SAMPLE_INI_CFG_S;
static int parse_handler(void *user, const char *section, const char *name, const char *value) {
SAMPLE_INI_CFG_S *cfg = (SAMPLE_INI_CFG_S *)user;
if (strcmp(section, "person1") == 0) {
if (strcmp(name, "name") == 0) {
strcpy(cfg->name, value);
} else if (strcmp(name, "bus_id") == 0) {
cfg->bus_id = atoi(value);
} else if (strcmp(name, "age") == 0) {
cfg->age = atoi(value);
} else {
/* unknown section/name */
}
} else if (strcmp(section, "person2") == 0) {
if (strcmp(name, "name") == 0) {
strcpy(cfg->name2, value);
} else if (strcmp(name, "bus_id") == 0) {
cfg->bus_id2 = atoi(value);
} else if (strcmp(name, "age") == 0) {
cfg->age2 = atoi(value);
} else {
/* unknown section/name */
}
} else {
/* unknown section/name */
}
return 1;
}
int main(int argc, char **argv) {
SAMPLE_INI_CFG_S ini_cfg;
int ret = ini_parse("./sensor_cfg.ini", parse_handler, &ini_cfg);
if (ret > 0) {
printf("Parse err in %d line.\n", ret);
return ret;
}
if (ret == 0) {
printf("Parse incomplete, use default cfg ./sensor_cfg.ini\n");
printf("%s, %d, %d\n", ini_cfg.name, ini_cfg.bus_id, ini_cfg.age);
printf("%s, %d, %d\n", ini_cfg.name2, ini_cfg.bus_id2, ini_cfg.age2);
}
return 0;
}

1.2.1 自定义ini文件#

ini配置文件sensor_cfg.ini如下:

img

gcc test.c ini.c。我的callback定义是parse_handler,从ini中解析section,每个section会调用一次callback,解析出所有的section。

运行代码:

img

1.2.2 支持语法检测#

ini_parse还支持语法检测。但ini写的不和语法规范会报错。手工制造ini语法错误,测试结果如下:

img

img

2 minIni移植使用#

MiniINI 是一个用来解析 INI/CFG 配置文件的 C++ 库,主要特点是可移植性、性能和小体积。支持上千种 INI 格式配置,易用简单。

2.1 下载minIni#

GitHub - compuphase/minIni: A small and portable INI file library with read/write support

2.2 特征#

  • minIni 支持读取senction外部的key,因此它支持不使用section的配置文件(但在其他方面与 INI 文件兼容)。
  • 可以使用冒号分隔键和值;冒号等价于等号。也就是说,字符串“Name: Value”和“Name=Value”具有相同的含义。
  • minIni 不需要标准 C/C++ 库中的 文件 I/O 函数,且允许通过宏配置要选择文件 I/O 接口。
  • 哈希字符 (“#”) 是分号开始注释的替代方法。允许尾随注释(即在一行上的键/值对后面)。
  • key名称和val周围的前导和尾随空格将被忽略。
  • 当写入包含注释字符(“;”或“#”)的值时,该值将自动放在双引号之间;读取值时,将删除这些引号。当设置中出现双引号本身时,这些字符将被转义。
  • 支持section和key枚举。
  • 您可以选择设置 minIni 将使用的行终止符(对于文本文件)。(这是编译时设置,而不是运行时设置)。
  • 由于写入速度远低于闪存(SD/MMC 卡、U 盘)中的读取速度,因此 minIni 以双倍“文件读取”为代价将“文件写入”降至最低。
  • 内存占用是确定性的。没有动态内存分配。

2.3 INI 文件语法#

1
2
3
4
[Network]  #section
hostname=My Computer #key = val
address=dhcp
dns = 192.168.1.1

2.4 minIni支持文件系统#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

#define INI_FILETYPE FILE*
#define ini_openread(filename,file) ((*(file) = fopen((filename),"r")) != NULL)
#define ini_openwrite(filename,file) ((*(file) = fopen((filename),"w")) != NULL)
#define ini_close(file) (fclose(*(file)) == 0)
#define ini_read(buffer,size,file) (fgets((buffer),(size),*(file)) != NULL)
#define ini_write(buffer,file) (fputs((buffer),*(file)) >= 0)
#define ini_rename(source,dest) (rename((source), (dest)) == 0)
#define ini_remove(filename) (remove(filename) == 0)

#define INI_FILEPOS fpos_t
#define ini_tell(file,pos) (fgetpos(*(file), (pos)) == 0)
#define ini_seek(file,pos) (fsetpos(*(file), (pos)) == 0)

2.5 minIni的API介绍#

image-20240602144015881

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "minIni.h"
#define sizearray(a) (sizeof(a) / sizeof((a)[0]))
const char inifile[] = "example.ini";
int main(void){
char str[100];
char section[50];
long n;

n = ini_gets("Network", "address", "dummy", str, sizearray(str), inifile);
if (n >= 0) printf("Network/address=%s", str);

n = ini_getl("Network", "timeout", -1, inifile);
printf("Network/timeout=%ld\n", n);
}

example.ini如下:

1
2
3
4
5
[Network]
hostname=My Computer
address=dhcp
dns=192.168.1.1
timeout=10

运行结果:

1
2
Network/address=dhcp
Network/timeout=10

2.5.1 ini_gets()#

获取字符串类型的值.

1
2
3
4
5
6
7
8
9
10
int   ini_gets(const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *DefValue, mTCHAR *Buffer, int BufferSize, const mTCHAR *Filename);
/*
参数 1 是 Section;
参数 2 是 Key;
参数 3 是获取不到值时的默认值;
参数 4 是用于保存目标键值的 Buffer;
参数 5 是 Buffer 的长度;
参数 6 是 INI 文件的路径;
*/

image-20240602144722018

先打开文件,然后用 getkeystring() 找到目标键值,最后拷贝给调用者。

2.5.1.1 getkeystring#

image-20240602150058567

image-20240602145619533

  1. 用 fgets 进行逐行读取,用 strrchr 找到包含 ‘[‘ 和 ‘]’ 的行,然后再用 strncasecmp 找到目标 Section 所在的行。
  2. 继续用 fgets 进行逐行读取,用 strrchr 找到包含 ‘=’ 的行,然后再用 strncasecmp 找到目标 Key 所在的行。
  3. 用 strncpy 将目标 Key 的值拷贝给调用者。

大致就是这3个关键步骤,当然还有很多其他异常处理,语法检测和边界判断的逻辑,这里不做展示。

2.5.2 ini_getl()#

ini_getl() 用于获取整型类型的值,也是间接调用int_gets, 最后将字符串转换成数字。

2.5.3 ini_puts()#

写出参数到ini,保存到ini。

1
2
3
4
5
6
7
8
9
10
11
12
/** ini_puts()
* \param Section the name of the section to write the string in
* \param Key the name of the entry to write, or NULL to erase all keys in the section
* \param Value a pointer to the buffer the string, or NULL to erase the key
* \param Filename the name and full path of the .ini file to write to
*
* \return 1 if successful, otherwise 0
*/
int ini_puts(const TCHAR *Section, const TCHAR *Key, const TCHAR *Value, const TCHAR *Filename)
//eg:
ini_putl("second", "age", 20, inifile);
n = ini_puts("first", "alt", NULL, inifile);//当val等于NULL表示删除该key

2.5.4 ini_putl()#

ini_putl() 用于写出整型类型的值,也是间接调用int_puts, 将数字转换成字符串,然后保存字符串到ini。

image-20240602171648913

2.5.5 section/key enumeration#

1
2
3
4
5
6
7
8
printf("4. Section/key enumeration, file structure follows\n");

for (s = 0; ini_getsection(s, section, sizearray(section), inifile) > 0; s++) {
printf(" [%s]\n", section);
for (k = 0; ini_getkey(section, k, str, sizearray(str), inifile) > 0; k++) {
printf("\t%s\n", str);
}
}//对section和key进行枚举

2.5.6 section/key存在性检查#

1
2
3
4
5
6
/* section/key presence check */
assert(ini_hassection("first", inifile));//检查是否有first 段
assert(!ini_hassection("fourth", inifile));
assert(ini_haskey("first", "val", inifile));//检查first段是否有val这个key
assert(!ini_haskey("first", "test", inifile));
printf("5. checking presence of sections and keys passed\n");

2.5.7 配置文件阅读打印#

ini_browse用来打印出每个段的每个key的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
int Callback(const char *section, const char *key, const char *value, void *userdata) {
(void)userdata; /* this parameter is not used in this example */
printf(" [%s]\t%s=%s\n", section, key, value);
return 1;
}

/* browsing through the file */
printf("6. browse through all settings, file field list follows\n");
ini_browse(Callback, NULL, inifile);

if (access(filename, F_OK) != 0) {
perror("open %s fail", filename);
}

image-20240602173647362

通用裸机-传感器

1 DHT11温湿度传感器#

img

MCU通过一条数据线与DH11连接,MCU通过这条线发命令给DH11,DH11再通过这条线把数据发送给MCU。只需要一根GPIO就OK了。

核心就是MCU发给DH11的命令格式和DH11返回的数据格式。

1.1 时序解析#

img

  1. MCU发送一个开始信号S,这个开始信号是一个低脉冲,然后再拉高。等待DHT11应答。

  2. DH11拉低,做出一个响应信号,再拉高,准备发送数据。

  3. 接着就是DH11返回的数据。

1.1.1 数据格式#

这些数据一共有40bit,高位先出。(8bit湿度整数数据+8bit湿度小数数据+8bi温度整数数据+8bit温度小数数据+8bit校验和)

数据有40bit: 8bit湿度整数数据+8bit湿度小数数据+8bit温度整数数据+8bit温度小数数据+8bit校验和

1.1.2 时序参数#

img

MCU必须先拉低至少18ms, 然后再拉高20-40us, DH11再拉低80us以响应,最后再拉高80us。

1.1.3 数据传输#

img

Bit0:1bit 50us开始后,DHT11拉低数据时间为30us以内。

img

Bit1: 1bit 50us开始后,DHT11拉低数据时间为超过70us。

1.1.4 代码实现#

img

img

使用GPG5引脚:

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
static void dht11_data_cfg_as_output(void) {
GPGCON &= ~(3<<10);
GPGCON |= (1<<10);
}
static void dht11_data_cfg_as_input(void) {
GPGCON &= ~(3<<10);
}

static void dht11_data_set(int val) {
if (val)
GPGDAT |= (1<<5);
else
GPGDAT &= ~(1<<5);
}
static int dht11_data_get(void) {
if (GPGDAT & (1<<5))
return 1;
else
return 0;
}
//DHT11操作:
void dht11_init(void) {
dht11_data_cfg_as_output();
dht11_data_set(1);
mdelay(2000);
}

static void dht11_start(void) {
dht11_data_set(0);
mdelay(20);
dht11_data_cfg_as_input();
}

static int dht11_wait_ack(void) {
udelay(60);
return dht11_data_get();
}

static int dht11_recv_byte(void) {
int i;
int data = 0;

for (i = 0; i < 8; i++) {
if (dht11_wait_for_val(1, 1000)) {
printf("dht11 wait for high data err!\n\r");
return -1;
}
udelay(40);
data <<= 1;
if (dht11_data_get() == 1)
data |= 1;

if (dht11_wait_for_val(0, 1000)) {
printf("dht11 wait for low data err!\n\r");
return -1;
}
}
return data;
}

static int dht11_wait_for_val(int val, int timeout_us) {
while (timeout_us--) {
if (dht11_data_get() == val)
return 0; /* ok */
udelay(1);
}
return -1; /* err */
}

int dht11_read(int *hum, int *temp) {
unsigned char hum_m, hum_n;
unsigned char temp_m, temp_n;
unsigned char check;

dht11_start();

if (0 != dht11_wait_ack()) {
printf("dht11 not ack, err!\n\r");
return -1;
}

if (0 != dht11_wait_for_val(1, 1000)) { /* 等待ACK变为高电平, 超时时间是1000us */
printf("dht11 wait for ack high err!\n\r");
return -1;
}

if (0 != dht11_wait_for_val(0, 1000)) { /* 数据阶段: 等待低电平, 超时时间是1000us */
printf("dht11 wait for data low err!\n\r");
return -1;
}

hum_m = dht11_recv_byte();
hum_n = dht11_recv_byte();
temp_m = dht11_recv_byte();
temp_n = dht11_recv_byte();
check = dht11_recv_byte();

dht11_data_cfg_as_output();
dht11_data_set(1);

if (hum_m + hum_n + temp_m + temp_n == check) {
*hum = hum_m;
*temp = temp_m;
mdelay(2000); /* 读取周期是2S, 不能读太频繁 */
return 0;
} else {
printf("dht11 checksum err!\n\r");
return -1;
}
}

void dht11_test(void) {
int hum, temp;
dht11_init();
while (1) {
if (dht11_read(&hum, &temp) != 0) {
printf("\n\rdht11 read err!\n\r");
dht11_init();
} else {
printf("\n\rDHT11 : %d humidity, %d temperature\n\r", hum, temp);
}
}
}

通用裸机-arm汇编

1 GNU 汇编格式#

1
label:instruction @ comment

label 即标号,表示地址位置,有些指令前面可能会有标号,这样就可以通过这个标号得到指令的地址,标号也可以用来表示数据地址。注意 label 后面的“:”,任何以“:”结尾的标识符都会被识别为一个标号。
instruction 即指令,也就是汇编指令或伪指令。
@符号,表示后面的是注释,就跟 C 语言里面的/**/一样,其实在 GNU 汇编文件中我们也可以使用\**/来注释。
comment 就是注释内容。

1
2
add:
MOVS R0, #0X12 @设置 R0=0X12

注意: ARM 中的指令、伪指令、伪操作、寄存器名等可以全部使用大写,也可以全部使用小写,但是不能大小写混用

1.1伪操作#

1.1.1 .section#

来定义一个段,汇编系统预定义了一些段名:

1
2
3
4
.text 表示代码段。
.data 初始化的数据段。
.bss 未初始化的数据段。
.rodata 只读数据段。

定义一个 testsetcion 段

1
.section .testsection

同时,还可以指定该段的属性,对应的属性见下表:

1
2
3
4
5
6
7
8
9
a section is allocatable
d section is a GNU_MBIND section
e section is excluded from executable and shared library.
w section is writable
x section is executable
M section is mergeable
S section contains zero terminated strings
G section is a member of a section group
T section is used for thread-local-storage

属性可以组合, 比如:

1
2
.section .foo,"aex"
text...

汇编程序的默认入口标号是_start,不过我们也可以在链接脚本中使用 ENTRY 来指明其它的入口点。

1.1.2 .global#

1
2
3
.global _start
_start:
ldr r0, =0x12 @r0=0x12

.global 是伪操作,表示_start 是一个全局标号,类似 C 语言里面的全局变量一样,常见的伪操作有:

1
2
3
4
5
6
7
.byte 定义单字节数据,比如.byte 0x12
.short 定义双字节数据,比如.short 0x1234
.long 定义一个 4 字节数据,比如.long 0x12345678
.equ 赋值语句,格式为:.equ 变量名,表达式,比如.equ num, 0x12,表示 num=0x12
.align 数据字节对齐,比如:.align 4 表示 4 字节对齐。
.end 表示源文件结束。
.global 定义一个全局符号,格式为:.global symbol,比如:.global _start。

1.1.3 .comm#

.comm 表示目标文件中的 common symbol,表示公共的符号:
.comm symbol,length

这和 GNU 中的强弱符号机制相关,未初始化的变量表示为弱符号,初始化的变量为强符号,当不同源文件中存在多个同名变量时,强符号会覆盖弱符号而不会报错,这是 gcc 的扩展语法,所以实际上未初始化的全局变量是作为公共符号保存的,当多个文件中的comm符号出现冲突时,需要将其以一定规则融合. 实际上,C 语言中未定义的全局变量(也就是 comm 符号)并非是存放到 bss 段中的,而是保存在 COMMON 段.

1.1.4 .byte , .word#

伪汇编中有一系列的数据放置指令,表示在当前位置放置某些数据,相对应的有:

1
2
3
4
5
6
7
.byte : 放置一个字节
.hword:放置半字,在 32 位平台中对应两个字节,64 位对应四字节
.short:放置一个 short 类型数据,两个字节.word:放置一个字,在 32 位平台中对应四个字节,64 位对应八字节
.int : 放置一个 int 类型的数据,数据长度根据平台而定,16位平台为两字节,32位和64位平台为四字节
.long:放置一个 long 类型的数据,数据长度根据平台而定,32位平台为四字节,64位平台为八字节
.float:放置一个 float 类型数据,四字节
.double:放置一个 double 类型数据,八字节

1.1.5 .set, .eqv, .equ, .equiv#

set:设置一个符号的值, C 中的宏。通过设置一个符号的值,在后续的代码中可以重复使用该符号的值。

1
2
.set sym_set,0x100
mov r0,#sym_set

1.1.6 .weak#

定义一个 weak 类型的符号,当这个符号在相同作用域的地方存在定义,当前符号会被忽略,如果这个符号之前不存在,这个符号就会被使用. 这和 C 语言中的 weak 机制是一样的.

1.2 函数定义#

1
2
3
函数名:
函数体
返回语句

GNU 汇编函数返回语句不是必须的,如下代码就是用汇编写的Cortex-A7 中断服务函数:

1
2
3
4
5
6
7
8
9
10
11
12
/* 未定义中断 */
Undefined_Handler:
ldr r0, =Undefined_Handler
bx r0
/* SVC 中断 */
SVC_Handler:
ldr r0, =SVC_Handler
bx r0
/* 预取终止中断 */
PrefAbort_Handler:
ldr r0, =PrefAbort_Handler
bx r0

以函数 Undefined_Handler 为例我们来看一下汇编函数组成,Undefined_Handler就是函数名,ldr r0, =Undefined_Handler是函数体,bx r0是函数返回语句,bx指令是返回指令,函数返回语句不是必须的.

2 ARMv7汇编指令#

2.1 数据移动指令#

数据移动指令都是cpu内部寄存器之间的数据拷贝。

2.1.1 MOV#

1
2
MOV R0,R1 @将寄存器 R1 中的数据传递给 R0,即 R0=R1
MOV R0, #0X12 @将立即数 0X12 传递给 R0 寄存器,即 R0=0X12

2.1.2 MRS#

读取特殊寄存器的数据只能使用 MRS 指令:

1
MRS R0, CPSR @将 CPSR 里面的数据传递给 R0,即 R0=CPSR

2.1.3 MSR#

和 MRS 刚好相反,通用寄存器写入到特殊寄存器

1
MSR CPSR, R0 @将 R0 中的数据复制到 CPSR 中,即 CPSR=R0

举个例子利用MRS MSR进行清bss (_bss_start, __bss_end定义在链接脚本):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.global _start
.global _bss_start
.global _bss_end

_bss_start:
.word __bss_start
_bss_end:
.word __bss_end

_start:
//disable watchdog, disable icache dcache
//init_clk
//enter svc mode
/*clear bss*/
ldr r0, _bss_start
ldr r1, _bss_end
move r2, 0
clr_bss:
stmia r0!, {r2} //复制一r2中的数据给r0, 并将指针r0增加4
cmp r0, r1
ble clr_bss /*if r0<r1, b clr_bss*/

2.1.4 CPS#

特权模式下(除了用户模式,剩余的模式都是特权模式),可以通过CPS指令直接修改CPSR寄存器的M[4:0],让处理器进入不同的模式。
image

1
2
CPS #0x12 /*irq mode*/
CPS #0x13 /*svc mode*/

2.2 数据存取指令(访问存储器RAM)#

2.2.1 LDR#

数据加载指令,从指定地址读取到cpu寄存器。

1
2
LDR R0, =0X0209C004 @将寄存器地址 0X0209C004 加载到 R0 中,即 R0=0X0209C004
LDR R1, [R0] @读取地址 0X0209C004 中的数据到 R1 寄存器中

2.2.2 STR#

数据存放指令,从cpu寄存器写入指定地址。

1
2
3
LDR R0, =0X0209C004 @将寄存器地址 0X0209C004 加载到 R0 中,即 R0=0X0209C004
LDR R1, =0X20000002 @R1 保存要写入到寄存器的值,即 R1=0X20000002
STR R1, [R0] @将 R1 中的值写入到 R0 中所保存的地址中

LDR 和 STR 都是按照4 byte进行读取和写入的,也就是操作的 32 位数据,如果要按照字节、半字进行操作的话可以在指令“LDR”后面加上 B 或 H,比如按字节操作的指令就是 LDRB 和STRB,按半字(16位)操作的指令就是 LDRH 和 STRH。

2.2.3 多寄存器加载存储指令LDMIA,STMIA等#

1.LDMIA指令、LDMIB指令、LDMDB指令、LDMDA指令
LDM是LDR指令的增强型 , 将连续的数据加载到多组寄存器。
DB (Decrement Before)栈指针先减小再操作、DA(Decrement After)栈指针先操作再减小。
IB(Increment Before)栈指针先增加再操作、IA(Increment After)栈指针先操作再增加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LDMIA指令,IA表示每次传送后地址加4
LDMIB指令,IB表示每次传送前地址加4
LDMDA指令,DA表示每次传送后地址减4
LDMDB指令,DB表示每次传送前地址减4

LDMIA R14,{R0-R3,R12} /*从R14寄存器指向的地址取出5个32位数据分别存进到R0-R4以及R12*/

//等效于
//R0=*R14
//R1=*(R14+4)
//R2=*(R14+8)
//R3=*(R14+12)
//R12=*(R14+16)

LDMIA R1!,{R4-R11} /*从R1指向的地址取8个32位数据存入R4-R11, 每取一次,让R1指针加4,因此最后R1指针加了32*/

2.STMIA指令、STMIB指令、STMDB指令、STMDA指令
同理,STM是STR指令的增强型 , 将多组寄存器数据保存进连续地址空间。

1
STMIA R13!,{R0-R1} /*将R0,R1寄存器中的数据存入R13指向的栈空间, r13指向的地址存入R0数据,再地址+4后存入R1的数据*/

2.3 入栈出栈指令#

函调调用过程中离不开现场的保护和恢复。保存 R0~R15 寄存器的操作就叫做现场保护,恢复 R0~R15 寄存器的操作就叫做恢复现场。

2.3.1 PUSH#

比如要将 R0~R3 和 R12 这 5 个寄存器压栈,当前的 SP (stack pointer)指针指向 0X80000000,我们知道栈空间的地址是向下增长的,堆空间地址向上增长。

1
PUSH {R0~R3, R12} @将 R0~R3 和 R12 压栈

那么压栈完成以后的堆栈如下:入栈保护现场完这5个寄存器后,SP指向0X7FFFFFEC(每压栈一个寄存器,SP地址减4)
image
再次保存LR寄存器,进行压栈:
image

1
PUSH {LR} @将 LR 进行压栈

2.3.2 POP#

1
2
POP {LR} @先恢复 LR
POP {R0~R3,R12} @在恢复 R0~R3,R12

可以看出入栈出栈本质都是对SP指针进行加减,入栈减,出栈加,入栈把寄存器依次保存进SP指向的地址去,出栈从SP地址依次取出数据。

2.3.3 STMFD和LDMFD#

入栈出栈的另外一种写法是“STMFD SP!”和“LDMFD SP!”。

1
2
3
4
5
STMFD SP!,{R0~R3, R12} @R0~R3,R12 入栈
STMFD SP!,{LR} @LR 入栈
bl xxx
LDMFD SP!, {LR} @先恢复 LR
LDMFD SP!, {R0~R3, R12} @再恢复 R0~R3, R12

STMFD 可以分为两部分:STM 和 FD,同理,LDMFD 也可以分为 LDM 和 FD。前面我们讲了 LDR 和 STR,这两个是数据加载和存储指令,但是每次只能读写存储器中的一个数据。STM 和 LDM 就是多存储和多加载,可以连续的读写存储器中的多个连续数据。
FD 是 Full Descending 的缩写,即满递减的意思。根据 ATPCS 规则,ARM 使用的 FD 类型的堆栈,SP 指向最后一个入栈的数值,堆栈是由高地址向下增长的,也就是前面说的向下增长的堆栈,因此最常用的指令就是 STMFD 和 LDMFD。STM 和 LDM 的指令寄存器列表中编号小的对应低地址,编号高的对应高地址.

2.4 跳转指令#

2.4.1 B 指令#

B 指令会将 PC 寄存器的值设置为跳转目标地址,如果要调用的函数不会再返回到原来的执行处,那就可以用 B 指令.

1
2
3
_start:
ldr sp,=0X80200000 @设置栈指针
b main @跳转到 main 函数

在汇编中初始化 C 运行环境,然后跳转到 C 文件的 main 函数中运行,上述代码只是初始化了 SP 指针,有些处理器还需要做其他的初始化,比如初始化 DDR 等等.

2.4.2 BL 指令#

有返回的跳转,跳转之前会在寄存器 LR(R14)中保存当前 PC 寄存器值,所以可以通过将 LR 寄存器中的值重新加载到 PC 中来继续从跳转之前的代码处运行,这是子程序调用一个基本但常用的手段。
比如 Cortex-A 处理器的 irq 中断服务函数都是汇编写的,主要用汇编来实现现场的保护和恢复、获取中断号等。但是具体的中断处理过程都是 C 函数,所以就会存在汇编中调用 C 函数的问题。而且当 C 语言版本的中断处理函数执行完成以后是需要返回到irq 汇编中断服务函数,因为还要处理其他的工作,一般是恢复现场。

1
2
3
4
5
6
7
8
push {r0, r1} @保存 r0,r1
cps #0x13 @进入 SVC 模式,允许其他中断再次进去

bl system_irqhandler @加载 C 语言中断处理函数到 r2 寄存器中

cps #0x12 @进入 IRQ 模式
pop {r0, r1}
str r0, [r1, #0X10] @中断执行完成,写 EOIR

跳转指令总结:
有多种跳转操作,比如:
①、直接使用跳转指令 B、BL、BX 等。
②、直接向 PC 寄存器里面写入数据。
image

2.5 算数运算指令#

加减乘除,常用的运算指令用法:
image

2.6 逻辑运算指令#

与或非指令用法:
image
来看一个例子利用arm汇编进行初始化C语言环境。让arm进入svc模式,才能访问特殊寄存器如cpsr, spsr, sp指针。

1
2
3
4
5
6
7
8
9
10
.global _start
_start:
/* 进入SVC模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x13 /* r0或上0x13,表示使用SVC模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */

ldr sp, =0X80200000 /* 设置栈指针 */
b main /* 跳转到main函数 */

2.7 内存屏障指令#

2.7.1 Data Memory Barrier(DMB):数据内存屏障#

DMB指令确保在DMB之前的所有显式数据内存传输指令都已经在内存中读取或写入完成,同时确保任何后续的数据内存传输指令都将在DMB执行之后开始执行,否则有些数据传输指令可能会提前执行。保证了两个内存访问能按正确的顺序执行。
应用场景:
(1)DMA
在使用DMA控制器时,需要在CPU内存访问和DMA操作之间插入DMB屏障,以确保CPU当前的内存读写操作在DMA开始之前完成。

(2)多核系统中的信号量
在多核系统中,使用信号量进行核间同步。需要使用DMB来强制指定内存执行顺序,以避免潜在的竞态条件或数据不一致性。当一个核要访问共享资源之前,它会先检查信号量的状态。如果信号量已经被另一个核获取,当前核就必须等待,直到信号量状态变为可用。这个等待过程需要保证在一个核释放信号量之后,其他核能够立即看到信号量状态的变化,而不是因为处理器优化或缓存导致的无效读取而产生错误。

2.7.2 Data Synchronization Barrier(DSB):数据同步屏障#

在多线程编程中,两个线程同时对共享的内存进行读写操作,由于读/写操作的重排序,就会导致数据的不一致, DSB指令时,它确保在DSB之前的所有显式数据内存传输指令都已经在内存中读取或写入完成,同时确保任何后续的指令都将在DSB执行之后开始执行。
应用场景:
例如启用或禁用特定的中断、配置时钟、设置系统控制位等。为了确保对SCS的修改在下一条指令执行之前生效,需要使用DSB指令进行数据同步。一些特殊的指令如SVC(Supervisor Call,特权级调用)、WFI(Wait For Interrupt,等待中断)、WFE(Wait For Event,等待事件)等操作,涉及到特权级的转换或者等待系统事件发生,需要使用DSB指令。

2.7.3 Instruction Synchronization Barrier(ISB):指令同步屏障#

插入ISB指令,处理器会将流水线中的指令全部刷新,从而确保之前的指令不会影响后续指令的执行,并且后续指令将从正确的上下文开始重新获取。
应用场景:
在进行异常进入之前,处理器会执行ISB操作。这样做的目的是刷新指令流水线,确保异常处理程序的指令是从正确的地址开始执行,避免异常之前的指令对异常处理程序造成干扰。
在进行异常返回之前,处理器同样会执行ISB操作。这样做的目的是刷新指令流水线,确保返回时从正确的地址重新获取指令,避免异常处理程序的指令对正常任务造成干扰。

3 arm-v7 cpu运行模式#

以前的 ARM 处理器有 7 中运行模型:User、FIQ、IRQ、Supervisor(SVC)、Abort、Undef和 System,其中 User 是非特权模式,其余 6 中都是特权模式。

到了Cortex-A7 处理器有 9 种处理模式:

模式 描述
User(USR) 用户模式,非特权模式,大部分程序运行的时候就处于此模式。
FIQ 快速中断模式,进入 FIQ 中断异常
IRQ 一般中断模式。
Supervisor(SVC) 超级管理员模式,特权模式,供操作系统使用。
Monitor(MON) 监视模式?这个模式用于安全扩展模式。
Abort(ABT) 数据访问终止模式,用于虚拟存储以及存储保护。
Hyp(HYP) Hyp(HYP) 超级监视模式?用于虚拟化扩展。
Undef(UND) Undef(UND) 未定义指令终止模式。
System(SYS) System(SYS) 系统模式,用于运行特权级的操作系统任务

九种模式所对应的寄存器:
image
arm920t cpu模式或者查看s3c2440裸机-异常中断 | Hexo (fuzidage.github.io)的CPU模式。

4 arm-v7 cpu通用和特殊寄存器#

ARM 架构提供了 16 个 32 位的通用寄存器(R0~R15)供软件使用,前 15 个(R0~R14)可以用作通用的数据存储,R15 是程序计数器 PC,用来保存将要执行的指令。ARM 还提供了一个当前程序状态寄存器 CPSR 和一个备份程序状态寄存器 SPSR,SPSR 寄存器就是 CPSR 寄存器的备份。
image

4.1 通用寄存器#

4.1.1 通用寄存器分类#

R0~R15 就是通用寄存器,通用寄存器可以分为以下三类:
①、未备份寄存器,即 R0~R7
②、备份寄存器,即 R8~R14
③、程序计数器 PC,即 R15。

4.1.2 未备份寄存器R0-R7#

未备份寄存器指的是 R0~R7 这 8 个寄存器,因为在所有的处理器模式下这 8 个寄存器都是同一个物理寄存器,在不同的模式下,这 8 个寄存器中的数据就会被破坏.

4.1.3 备份寄存器#

4.1.3.1 R8~R12#

R8~R12 这 5 个寄存器有2种物理寄存器.在快速中断模式下(FIQ)它们对应着 Rx_irq(x=8~12)物理寄存器,其他模式下对应着 Rx(8~12)物理寄存器. FIQ 模式下的 R8~R12 是独立的,因此中断处理程序可以不用执行保存和恢复中断现场的指令,从而加速中断的执行过程。

4.1.3.2 R13 (SP)#

R13 一共有 8 个物理寄存器,其中一个是用户模式(User)和系统模式(Sys)共用的,剩下的 7 个分别对应 7 种不同的模式。R13 也叫做 SP,用来做为栈指针。基本上每种模式
都有一个自己的 R13 物理寄存器,应用程序会初始化 R13,使其指向该模式专用的栈地址,这就是常说的初始化 SP 指针.

4.1.3.2 R14 (LR)#

R14 一共有 7 个物理寄存器,其中一个是用户模式(User)、系统模式(Sys)和超级监视模式(Hyp)所共有的,剩下的 6 个分别对应 6 种不同的模式.
LR被叫做链接寄存器:
①用来存放子函数的返回地址。
在子函数中,将 R14(LR)中的值赋给 R15(PC)即可完成子函数返回,比如在子程序中可以使用如下代码:

1
MOV PC, LR

②当异常发生以后,该异常模式对应的 R14寄存器被设置成该异常模式将要返回的地址.

1
subs pc, lr, #4				/* 将lr-4赋给pc */

比如下面代码示例:

1
2
3
0X2000 MOV R1, R0 ;执行
0X2004 MOV R2, R3 ;译指
0X2008 MOV R4, R5 ;取值 PC

当前正在执行 0X2000地址处的指令MOV R1, R0,但是 PC 里面已经保存了 0X2008 地址处的指令MOV R4, R5。假设此时发生了中断,中断发生的时候保存在 lr 中的是 pc 的值,也就是地址 0X2008

4.1.3.2 R15 (PC)#

R15 保存着当前执行的指令地址值加 8 个字节,这是因为 ARM的流水线机制导致的。ARM 处理器 3 级流水线:取指->译码->执行,这三级流水线循环执行,比如当前正在执行第一条指令的同时也对第二条指令进行译码,第三条指令也同时被取出存放在 R15(PC)中.
对于arm32位处理器:

1
R15 (PC)值 = 当前执行的程序位置 + 8 个字节

4.2 特殊寄存器#

4.2.1 CPSR#

当前程序状态寄存器(current program status register),所有模式共用一个 CPSR 物理寄存器,因此 CPSR 可以在任何模式下被访问。
image
N(bit31):当两个补码表示的 有符号整数运算的时候,N=1 表示运算对的结果为负数,N=0表示结果为正数。
Z(bit30):Z=1 表示运算结果为零,Z=0 表示运算结果不为零,对于 CMP 指令,Z=1 表示进行比较的两个数大小相等。
C(bit29):在加法指令中,当结果产生了进位,则 C=1,表示无符号数运算发生上溢,其它情况下 C=0。在减法指令中,当运算中发生借位,则 C=0,表示无符号数运算发生下溢,其它情况下 C=1。对于包含移位操作的非加/减法运算指令,C 中包含最后一次溢出的位的数值,对于其它非加/减运算指令,C 位的值通常不受影响。
V(bit28):对于加/减法运算指令,当操作数和运算结果表示为二进制的补码表示的带符号数时,V=1 表示符号位溢出,通常其他位不影响 V 位。
Q(bit27):仅 ARM v5TE_J 架构支持,表示饱和状态,Q=1 表示累积饱和,Q=0 表示累积不饱和。
IT1:0:和 IT7:2一起组成 IT[7:0],作为 IF-THEN 指令执行状态。
J(bit24):仅 ARM_v5TE-J 架构支持,J=1 表示处于 Jazelle 状态,此位通常和 T(bit5)位一起表示当前所使用的指令集:

J T 描述
0 0 ARM
0 1 Thumb
1 1 ThumbEE

GE3:0:SIMD 指令有效,大于或等于。
IT7:2:参考 IT[1:0]。
E(bit9):大小端控制位,E=1 表示大端模式,E=0 表示小端模式。
A(bit8):禁止异步中断位,A=1 表示禁止异步中断。
I(bit7):I=1 禁止 IRQ,I=0 使能 IRQ。
F(bit6):F=1 禁止 FIQ,F=0 使能 FIQ。
T(bit5):控制指令执行状态,表明本指令是 ARM 指令还是 Thumb 指令,通常和 J(bit24)一起表明指令类型,参考 J(bit24)位。
M[4:0]:处理器模式控制位
cpsr最常用就是来控制处理器模式

M[4:0] 处理器模式
10000 User 模式
10001 FIQ 模式
10010 IRQ 模式
10011 Supervisor(SVC)模式
10110 Monitor(MON)模式
10111 Abort(ABT)模式
11010 Hyp(HYP)模式
11011 Undef(UND)模式
11111 System(SYS)模式
1
2
3
4
5
6
/* 进入SVC模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x13 /* r0或上0x13,表示使用SVC模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0X80200000 /* 设置SVC模式下的栈首地址为0X80200000,大小为2MB */

4.2.2 SPSR#

除了 User 和 Sys 这两个模式以外,其他 7 个模式每个都配备了一个专用的物理状态寄存器,叫做 SPSR(备份程序状态寄存器),当特定的异常中断发生时,SPSR 寄存器用来保存当前程序状态寄存器(CPSR)的值,当异常退出以后可以用 SPSR 中保存的值来恢复 CPSR。User 和 Sys 这两个模式不是异常模式,所以并没有配备 SPSR,因此不能在 User 和Sys 模式下访问 SPSR。

5 CP15协处理器#

CP15 协处理器一般用于存储系统管理,但是在中断中也会使用到,比如进入reset复位异常向量时,需要利用协处理器命令进行ICache DCache的开关。CP15 协处理器一共有16 个 32 位寄存器(C0-C15),MRC和MCR用来访问CP15协处理器。

5.1 协处理器指令#

MRC: 将 CP15 协处理器中的寄存器数据读到 ARM 寄存器中。
MCR: 将 ARM 寄存器的数据写入到 CP15 协处理器寄存器中。
格式如下:

1
MCR{cond} p15, <opc1>, <Rt>, <CRn>, <CRm>, <opc2>

cond:指令执行的条件码,如果忽略的话就表示无条件执行。
opc1:协处理器要执行的操作码。
Rt:ARM 源寄存器,要写入到 CP15 寄存器的数据就保存在此寄存器中。
CRn:CP15 协处理器的目标寄存器。
CRm:协处理器中附加的目标寄存器或者源操作数寄存器,如果不需要附加信息就将CRm 设置为 C0,否则结果不可预测。
opc2:可选的协处理器特定操作码,当不需要的时候要设置为 0

例如:

1
MRC p15, 0, r0, c0, c0, 0 //将 CP15 中 C0 寄存器的值读取到 R0 寄存器

5.2 c0寄存器(MIDR)#

使用 MRC 或者 MCR 指令访问c0-c15寄存器的时候,指令中的CRn、opc1、CRm 和 opc2通过不同的搭配,其得到的寄存器含义不同:
image
CRn=c0,opc1=0,CRm=c0,opc2=0 的时候就表示此时的 c0 就是 MIDR 寄存器,也就是主 ID 寄存器,这个也是 c0 的基本作用。来看下c0作为MDIR寄存器时的含义:
image
bit31:24:厂商编号,0X41,ARM。
bit23:20:内核架构的主版本号,ARM 内核版本一般使用rnpn来表示,比如 r0p1,其中 r0
后面的 0 就是内核架构主版本号。
bit19:16:架构代码,0XF,ARMv7 架构。
bit15:4:内核版本号,0XC07,Cortex-A7 MPCore 内核。
bit3:0:内核架构的次版本号,rnpn 中的 pn,比如r0p1p1 后面的 1 就是次版本号。

5.3 c1寄存器(SCTLR)#

image
CRn=c1,opc1=0,CRm=c0,opc2=0 的时候就表示此时的 c1 就是 SCTLR 寄存器,也就是系统控制寄存器,这个是 c1 的基本作用。
SCTLR 寄存器主要是完成控制功能的,比如使能或者禁止 MMU、I/D Cache 等。 SCTLR 寄存器展开如下:
image

bit13:V , 中断向量表基地址选择位,为 0 的话中断向量表基地址为 0X00000000,软件可以使用VBAR来重映射此基地址,也就是中断向量表重定位。为 1 的话中断向量表基地址为0XFFFF0000,此基地址不能被重映射。
bit12:I,I Cache 使能位,为 0 的话关闭 I Cache,为 1 的话使能 I Cache。
bit11:Z,分支预测使能位,如果开启 MMU 的话,此位也会使能。
bit10:SW,SWP 和 SWPB 使能位,当为 0 的话关闭 SWP 和 SWPB 指令,当为 1 的时候就使能 SWP 和 SWPB 指令。
bit9:3:未使用,保留。
bit2:C,D Cache 和缓存一致性使能位,为 0 的时候禁止 D Cache 和缓存一致性,为 1 时使能。
bit1:A,内存对齐检查使能位,为 0 的时候关闭内存对齐检查,为 1 的时候使能内存对齐检查。
bit0:M,MMU 使能位,为 0 的时候禁止 MMU,为 1 的时候使能 MMU。
举个读写SCTLR的例子:

1
2
3
MRC p15, 0, <Rt>, c1, c0, 0 ;读取 SCTLR 寄存器,数据保存到 Rt 中。

MCR p15, 0, <Rt>, c1, c0, 0 ;将 Rt 中的数据写到 SCTLR(c1)寄存器中。

再来一个关闭MMU,ICache,DCache的例子:

1
2
3
4
5
6
7
mrc     p15, 0, r0, c1, c0, 0     /* 读取CP15的C1寄存器到R0中       		        	*/
bic r0, r0, #(0x1 << 12) /* 清除C1寄存器的bit12位(I位),关闭I Cache */
bic r0, r0, #(0x1 << 2) /* 清除C1寄存器的bit2(C位),关闭D Cache */
bic r0, r0, #0x2 /* 清除C1寄存器的bit1(A位),关闭对齐 */
bic r0, r0, #(0x1 << 11) /* 清除C1寄存器的bit11(Z位),关闭分支预测 */
bic r0, r0, #0x1 /* 清除C1寄存器的bit0(M位),关闭MMU */
mcr p15, 0, r0, c1, c0, 0 /* 将r0寄存器中的值写入到CP15的C1寄存器中 */

5.4 c12寄存器(VBAR)#

image
CRn=c12,opc1=0,CRm=c0,opc2=0 的时候就表示此时 c12 为 VBAR 寄存器,也就是中断向量表基地址寄存器

比如代码链接到DDR的某个位置作为起始地址,起始地址为0X87800000,而中断向量表肯定要放到最前面,也就是 0X87800000 这个地址处。所以就需要设置 VBAR0X87800000,设置命令如下:

1
2
3
4
5
6
dsb
isb
ldr r0, =0x87800000 ; r0=0x87800000
MCR p15, 0, r0, c12, c0, 0 ;将 r0 里面的数据写入到 c12 中,即 c12=0X87800000
dsb
isb

5.5 c15寄存器(CBAR)#

image
image

CBAR寄存器中保存了GIC(Generic Interrupt Controller)的基地址。GIC基地址偏移0x1000分发器 block, 偏移0x2000是CPU 接口端 block

1
MRC p15, 4, r1, c15, c0, 0 ; 获取 GIC 基础地址,基地址保存在 r1 中

6 arm官方参考链接#

6.1 armv7与armv8架构#

armv7都是32位处理器,典型的有CorTex-A A7 A8 A9 A15 A17。armv8采用64位处理器,典型的比如手机处理器,有Cortex A53 A76 A77都是64位架构。

6.2 armv7参考手册#

https://developer.arm.com/documentation/ddi0406/cd?lang=en
image

6.3 armv7 编程手册#

https://developer.arm.com/documentation/den0013/d/?lang=en
image

6.4 Cortex-A7 MPCore 技术参考手册#

https://developer.arm.com/documentation/ddi0464/latest/
image

7 附件1:armv7裸机启动汇编示例:#

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
.global _start  				/* 全局标号 */

_start:
ldr pc, =Reset_Handler /* 复位中断 */
ldr pc, =Undefined_Handler /* 未定义中断 */
ldr pc, =SVC_Handler /* SVC(Supervisor)中断 */
ldr pc, =PrefAbort_Handler /* 预取终止中断 */
ldr pc, =DataAbort_Handler /* 数据终止中断 */
ldr pc, =NotUsed_Handler /* 未使用中断 */
ldr pc, =IRQ_Handler /* IRQ中断 */
ldr pc, =FIQ_Handler /* FIQ(快速中断)未定义中断 */

/* 复位中断 */
Reset_Handler:

cpsid i /* 关闭全局中断 */

/* 关闭I,DCache和MMU
* 采取读-改-写的方式。
*/
mrc p15, 0, r0, c1, c0, 0 /* 读取CP15的C1寄存器到R0中 */
bic r0, r0, #(0x1 << 12) /* 清除C1寄存器的bit12位(I位),关闭I Cache */
bic r0, r0, #(0x1 << 2) /* 清除C1寄存器的bit2(C位),关闭D Cache */
bic r0, r0, #0x2 /* 清除C1寄存器的bit1(A位),关闭对齐 */
bic r0, r0, #(0x1 << 11) /* 清除C1寄存器的bit11(Z位),关闭分支预测 */
bic r0, r0, #0x1 /* 清除C1寄存器的bit0(M位),关闭MMU */
mcr p15, 0, r0, c1, c0, 0 /* 将r0寄存器中的值写入到CP15的C1寄存器中 */

#if 0
/* 汇编版本设置中断向量表偏移 */
ldr r0, =0X87800000

dsb
isb
mcr p15, 0, r0, c12, c0, 0
dsb
isb
#endif

/* 设置各个模式下的栈指针,
* 注意:IMX6UL的堆栈是向下增长的!
* 堆栈指针地址一定要是4字节地址对齐的!!!
* DDR范围:0X80000000~0X9FFFFFFF
*/
/* 进入IRQ模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x12 /* r0或上0x13,表示使用IRQ模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0x80600000 /* 设置IRQ模式下的栈首地址为0X80600000,大小为2MB */

/* 进入SYS模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x1f /* r0或上0x13,表示使用SYS模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0x80400000 /* 设置SYS模式下的栈首地址为0X80400000,大小为2MB */

/* 进入SVC模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x13 /* r0或上0x13,表示使用SVC模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0X80200000 /* 设置SVC模式下的栈首地址为0X80200000,大小为2MB */

cpsie i /* 打开全局中断 */
#if 0
/* 使能IRQ中断 */
mrs r0, cpsr /* 读取cpsr寄存器值到r0中 */
bic r0, r0, #0x80 /* 将r0寄存器中bit7清零,也就是CPSR中的I位清零,表示允许IRQ中断 */
msr cpsr, r0 /* 将r0重新写入到cpsr中 */
#endif

b main /* 跳转到main函数 */

/* 未定义中断 */
Undefined_Handler:
ldr r0, =Undefined_Handler
bx r0

/* SVC中断 */
SVC_Handler:
ldr r0, =SVC_Handler
bx r0

/* 预取终止中断 */
PrefAbort_Handler:
ldr r0, =PrefAbort_Handler
bx r0

/* 数据终止中断 */
DataAbort_Handler:
ldr r0, =DataAbort_Handler
bx r0

/* 未使用的中断 */
NotUsed_Handler:

ldr r0, =NotUsed_Handler
bx r0

IRQ_Handler:
push {lr} /* 保存lr地址 */
push {r0-r3, r12} /* 保存r0-r3,r12寄存器 */

mrs r0, spsr /* 读取spsr寄存器 */
push {r0} /* 保存spsr寄存器 */

mrc p15, 4, r1, c15, c0, 0 /* 从CP15的C0寄存器内的值到R1寄存器中
* 参考文档ARM Cortex-A(armV7)编程手册V4.0.pdf P49
* Cortex-A7 Technical ReferenceManua.pdf P68 P138
*/
add r1, r1, #0X2000 /* GIC基地址加0X2000,也就是GIC的CPU接口端基地址 */
ldr r0, [r1, #0XC] /* GIC的CPU接口端基地址加0X0C就是GICC_IAR寄存器,
* GICC_IAR寄存器保存这当前发生中断的中断号,我们要根据
* 这个中断号来绝对调用哪个中断服务函数
*/
push {r0, r1} /* 保存r0,r1 */

cps #0x13 /* 进入SVC模式,允许其他中断再次进去 */

push {lr} /* 保存SVC模式的lr寄存器 */
ldr r2, =system_irqhandler /* 加载C语言中断处理函数到r2寄存器中*/
blx r2 /* 运行C语言中断处理函数,带有一个参数,保存在R0寄存器中 */

pop {lr} /* 执行完C语言中断服务函数,lr出栈 */
cps #0x12 /* 进入IRQ模式 */
pop {r0, r1}
str r0, [r1, #0X10] /* 中断执行完成,写EOIR */

pop {r0}
msr spsr_cxsf, r0 /* 恢复spsr */

pop {r0-r3, r12} /* r0-r3,r12出栈 */
pop {lr} /* lr出栈 */
subs pc, lr, #4 /* 将lr-4赋给pc */

/* FIQ中断 */
FIQ_Handler:

ldr r0, =FIQ_Handler
bx r0

8 附件2:GIC指令和寄存器定义(内联汇编方式)#

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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
#ifndef __CORTEX_CA7_H
#define __CORTEX_CA7_H
/***************************************************************
文件名 : core_ca7.h
描述 : Cortex-A7内核通用文件。
其他 : 本文件主要实现了对GIC操作函数
论坛 : www.wtmembed.com
***************************************************************/
#include <stdint.h>
#include <string.h>


#define FORCEDINLINE __attribute__((always_inline))
#define __ASM __asm /* GNU C语言内嵌汇编关键字 */
#define __INLINE inline /* GNU内联关键字 */
#define __STATIC_INLINE static inline


#define __IM volatile const /* 只读 */
#define __OM volatile /* 只写 */
#define __IOM volatile /* 读写 */
#define __STRINGIFY(x) #x

/* C语言实现MCR指令 */
#define __MCR(coproc, opcode_1, src, CRn, CRm, opcode_2) \
__ASM volatile ("MCR " __STRINGIFY(p##coproc) ", " __STRINGIFY(opcode_1) ", " \
"%0, " __STRINGIFY(c##CRn) ", " __STRINGIFY(c##CRm) ", " \
__STRINGIFY(opcode_2) \
: : "r" (src) )

/* C语言实现MRC指令 */
#define __MRC(coproc, opcode_1, CRn, CRm, opcode_2) \
({ \
uint32_t __dst; \
__ASM volatile ("MRC " __STRINGIFY(p##coproc) ", " __STRINGIFY(opcode_1) ", " \
"%0, " __STRINGIFY(c##CRn) ", " __STRINGIFY(c##CRm) ", " \
__STRINGIFY(opcode_2) \
: "=r" (__dst) ); \
__dst; \
})

/* 其他一些C语言内嵌汇编 */
__attribute__( ( always_inline ) ) __STATIC_INLINE void __set_APSR(uint32_t apsr)
{
__ASM volatile ("MSR apsr, %0" : : "r" (apsr) : "cc");
}

__attribute__( ( always_inline ) ) __STATIC_INLINE uint32_t __get_CPSR(void)
{
uint32_t result;

__ASM volatile ("MRS %0, cpsr" : "=r" (result) );
return(result);
}

__attribute__( ( always_inline ) ) __STATIC_INLINE void __set_CPSR(uint32_t cpsr)
{
__ASM volatile ("MSR cpsr, %0" : : "r" (cpsr) : "cc");
}

__attribute__( ( always_inline ) ) __STATIC_INLINE uint32_t __get_FPEXC(void)
{
uint32_t result;

__ASM volatile ("VMRS %0, fpexc" : "=r" (result) );
return result;
}

__attribute__( ( always_inline ) ) __STATIC_INLINE void __set_FPEXC(uint32_t fpexc)
{
__ASM volatile ("VMSR fpexc, %0" : : "r" (fpexc));
}


/*******************************************************************************
* 一些内核寄存器定义和抽象
定义如下几个内核寄存器:
- CPSR
- CP15
******************************************************************************/

/* CPSR寄存器
* 参考资料:ARM Cortex-A(armV7)编程手册V4.0.pdf P46
*/
typedef union
{
struct
{
uint32_t M:5; /*!< bit: 0.. 4 Mode field */
uint32_t T:1; /*!< bit: 5 Thumb execution state bit */
uint32_t F:1; /*!< bit: 6 FIQ mask bit */
uint32_t I:1; /*!< bit: 7 IRQ mask bit */
uint32_t A:1; /*!< bit: 8 Asynchronous abort mask bit */
uint32_t E:1; /*!< bit: 9 Endianness execution state bit */
uint32_t IT1:6; /*!< bit: 10..15 If-Then execution state bits 2-7 */
uint32_t GE:4; /*!< bit: 16..19 Greater than or Equal flags */
uint32_t _reserved0:4; /*!< bit: 20..23 Reserved */
uint32_t J:1; /*!< bit: 24 Jazelle bit */
uint32_t IT0:2; /*!< bit: 25..26 If-Then execution state bits 0-1 */
uint32_t Q:1; /*!< bit: 27 Saturation condition flag */
uint32_t V:1; /*!< bit: 28 Overflow condition code flag */
uint32_t C:1; /*!< bit: 29 Carry condition code flag */
uint32_t Z:1; /*!< bit: 30 Zero condition code flag */
uint32_t N:1; /*!< bit: 31 Negative condition code flag */
} b; /*!< Structure used for bit access */
uint32_t w; /*!< Type used for word access */
} CPSR_Type;


/* CP15的SCTLR寄存器
* 参考资料:Cortex-A7 Technical ReferenceManua.pdf P105
*/
typedef union
{
struct
{
uint32_t M:1; /*!< bit: 0 MMU enable */
uint32_t A:1; /*!< bit: 1 Alignment check enable */
uint32_t C:1; /*!< bit: 2 Cache enable */
uint32_t _reserved0:2; /*!< bit: 3.. 4 Reserved */
uint32_t CP15BEN:1; /*!< bit: 5 CP15 barrier enable */
uint32_t _reserved1:1; /*!< bit: 6 Reserved */
uint32_t B:1; /*!< bit: 7 Endianness model */
uint32_t _reserved2:2; /*!< bit: 8.. 9 Reserved */
uint32_t SW:1; /*!< bit: 10 SWP and SWPB enable */
uint32_t Z:1; /*!< bit: 11 Branch prediction enable */
uint32_t I:1; /*!< bit: 12 Instruction cache enable */
uint32_t V:1; /*!< bit: 13 Vectors bit */
uint32_t RR:1; /*!< bit: 14 Round Robin select */
uint32_t _reserved3:2; /*!< bit:15..16 Reserved */
uint32_t HA:1; /*!< bit: 17 Hardware Access flag enable */
uint32_t _reserved4:1; /*!< bit: 18 Reserved */
uint32_t WXN:1; /*!< bit: 19 Write permission implies XN */
uint32_t UWXN:1; /*!< bit: 20 Unprivileged write permission implies PL1 XN */
uint32_t FI:1; /*!< bit: 21 Fast interrupts configuration enable */
uint32_t U:1; /*!< bit: 22 Alignment model */
uint32_t _reserved5:1; /*!< bit: 23 Reserved */
uint32_t VE:1; /*!< bit: 24 Interrupt Vectors Enable */
uint32_t EE:1; /*!< bit: 25 Exception Endianness */
uint32_t _reserved6:1; /*!< bit: 26 Reserved */
uint32_t NMFI:1; /*!< bit: 27 Non-maskable FIQ (NMFI) support */
uint32_t TRE:1; /*!< bit: 28 TEX remap enable. */
uint32_t AFE:1; /*!< bit: 29 Access flag enable */
uint32_t TE:1; /*!< bit: 30 Thumb Exception enable */
uint32_t _reserved7:1; /*!< bit: 31 Reserved */
} b; /*!< Structure used for bit access */
uint32_t w; /*!< Type used for word access */
} SCTLR_Type;

/* CP15 寄存器SCTLR各个位定义 */
#define SCTLR_TE_Pos 30U /*!< SCTLR: TE Position */
#define SCTLR_TE_Msk (1UL << SCTLR_TE_Pos) /*!< SCTLR: TE Mask */

#define SCTLR_AFE_Pos 29U /*!< SCTLR: AFE Position */
#define SCTLR_AFE_Msk (1UL << SCTLR_AFE_Pos) /*!< SCTLR: AFE Mask */

#define SCTLR_TRE_Pos 28U /*!< SCTLR: TRE Position */
#define SCTLR_TRE_Msk (1UL << SCTLR_TRE_Pos) /*!< SCTLR: TRE Mask */

#define SCTLR_NMFI_Pos 27U /*!< SCTLR: NMFI Position */
#define SCTLR_NMFI_Msk (1UL << SCTLR_NMFI_Pos) /*!< SCTLR: NMFI Mask */

#define SCTLR_EE_Pos 25U /*!< SCTLR: EE Position */
#define SCTLR_EE_Msk (1UL << SCTLR_EE_Pos) /*!< SCTLR: EE Mask */

#define SCTLR_VE_Pos 24U /*!< SCTLR: VE Position */
#define SCTLR_VE_Msk (1UL << SCTLR_VE_Pos) /*!< SCTLR: VE Mask */

#define SCTLR_U_Pos 22U /*!< SCTLR: U Position */
#define SCTLR_U_Msk (1UL << SCTLR_U_Pos) /*!< SCTLR: U Mask */

#define SCTLR_FI_Pos 21U /*!< SCTLR: FI Position */
#define SCTLR_FI_Msk (1UL << SCTLR_FI_Pos) /*!< SCTLR: FI Mask */

#define SCTLR_UWXN_Pos 20U /*!< SCTLR: UWXN Position */
#define SCTLR_UWXN_Msk (1UL << SCTLR_UWXN_Pos) /*!< SCTLR: UWXN Mask */

#define SCTLR_WXN_Pos 19U /*!< SCTLR: WXN Position */
#define SCTLR_WXN_Msk (1UL << SCTLR_WXN_Pos) /*!< SCTLR: WXN Mask */

#define SCTLR_HA_Pos 17U /*!< SCTLR: HA Position */
#define SCTLR_HA_Msk (1UL << SCTLR_HA_Pos) /*!< SCTLR: HA Mask */

#define SCTLR_RR_Pos 14U /*!< SCTLR: RR Position */
#define SCTLR_RR_Msk (1UL << SCTLR_RR_Pos) /*!< SCTLR: RR Mask */

#define SCTLR_V_Pos 13U /*!< SCTLR: V Position */
#define SCTLR_V_Msk (1UL << SCTLR_V_Pos) /*!< SCTLR: V Mask */

#define SCTLR_I_Pos 12U /*!< SCTLR: I Position */
#define SCTLR_I_Msk (1UL << SCTLR_I_Pos) /*!< SCTLR: I Mask */

#define SCTLR_Z_Pos 11U /*!< SCTLR: Z Position */
#define SCTLR_Z_Msk (1UL << SCTLR_Z_Pos) /*!< SCTLR: Z Mask */

#define SCTLR_SW_Pos 10U /*!< SCTLR: SW Position */
#define SCTLR_SW_Msk (1UL << SCTLR_SW_Pos) /*!< SCTLR: SW Mask */

#define SCTLR_B_Pos 7U /*!< SCTLR: B Position */
#define SCTLR_B_Msk (1UL << SCTLR_B_Pos) /*!< SCTLR: B Mask */

#define SCTLR_CP15BEN_Pos 5U /*!< SCTLR: CP15BEN Position */
#define SCTLR_CP15BEN_Msk (1UL << SCTLR_CP15BEN_Pos) /*!< SCTLR: CP15BEN Mask */

#define SCTLR_C_Pos 2U /*!< SCTLR: C Position */
#define SCTLR_C_Msk (1UL << SCTLR_C_Pos) /*!< SCTLR: C Mask */

#define SCTLR_A_Pos 1U /*!< SCTLR: A Position */
#define SCTLR_A_Msk (1UL << SCTLR_A_Pos) /*!< SCTLR: A Mask */

#define SCTLR_M_Pos 0U /*!< SCTLR: M Position */
#define SCTLR_M_Msk (1UL << SCTLR_M_Pos) /*!< SCTLR: M Mask */

/* CP15的ACTLR寄存器
* 参考资料:Cortex-A7 Technical ReferenceManua.pdf P113
*/
typedef union
{
struct
{
uint32_t _reserved0:6; /*!< bit: 0.. 5 Reserved */
uint32_t SMP:1; /*!< bit: 6 Enables coherent requests to the processor */
uint32_t _reserved1:3; /*!< bit: 7.. 9 Reserved */
uint32_t DODMBS:1; /*!< bit: 10 Disable optimized data memory barrier behavior */
uint32_t L2RADIS:1; /*!< bit: 11 L2 Data Cache read-allocate mode disable */
uint32_t L1RADIS:1; /*!< bit: 12 L1 Data Cache read-allocate mode disable */
uint32_t L1PCTL:2; /*!< bit:13..14 L1 Data prefetch control */
uint32_t DDVM:1; /*!< bit: 15 Disable Distributed Virtual Memory (DVM) transactions */
uint32_t _reserved3:12; /*!< bit:16..27 Reserved */
uint32_t DDI:1; /*!< bit: 28 Disable dual issue */
uint32_t _reserved7:3; /*!< bit:29..31 Reserved */
} b; /*!< Structure used for bit access */
uint32_t w; /*!< Type used for word access */
} ACTLR_Type;

#define ACTLR_DDI_Pos 28U /*!< ACTLR: DDI Position */
#define ACTLR_DDI_Msk (1UL << ACTLR_DDI_Pos) /*!< ACTLR: DDI Mask */

#define ACTLR_DDVM_Pos 15U /*!< ACTLR: DDVM Position */
#define ACTLR_DDVM_Msk (1UL << ACTLR_DDVM_Pos) /*!< ACTLR: DDVM Mask */

#define ACTLR_L1PCTL_Pos 13U /*!< ACTLR: L1PCTL Position */
#define ACTLR_L1PCTL_Msk (3UL << ACTLR_L1PCTL_Pos) /*!< ACTLR: L1PCTL Mask */

#define ACTLR_L1RADIS_Pos 12U /*!< ACTLR: L1RADIS Position */
#define ACTLR_L1RADIS_Msk (1UL << ACTLR_L1RADIS_Pos) /*!< ACTLR: L1RADIS Mask */

#define ACTLR_L2RADIS_Pos 11U /*!< ACTLR: L2RADIS Position */
#define ACTLR_L2RADIS_Msk (1UL << ACTLR_L2RADIS_Pos) /*!< ACTLR: L2RADIS Mask */

#define ACTLR_DODMBS_Pos 10U /*!< ACTLR: DODMBS Position */
#define ACTLR_DODMBS_Msk (1UL << ACTLR_DODMBS_Pos) /*!< ACTLR: DODMBS Mask */

#define ACTLR_SMP_Pos 6U /*!< ACTLR: SMP Position */
#define ACTLR_SMP_Msk (1UL << ACTLR_SMP_Pos) /*!< ACTLR: SMP Mask */


/* CP15的CPACR寄存器
* 参考资料:Cortex-A7 Technical ReferenceManua.pdf P115
*/
typedef union
{
struct
{
uint32_t _reserved0:20; /*!< bit: 0..19 Reserved */
uint32_t cp10:2; /*!< bit:20..21 Access rights for coprocessor 10 */
uint32_t cp11:2; /*!< bit:22..23 Access rights for coprocessor 11 */
uint32_t _reserved1:6; /*!< bit:24..29 Reserved */
uint32_t D32DIS:1; /*!< bit: 30 Disable use of registers D16-D31 of the VFP register file */
uint32_t ASEDIS:1; /*!< bit: 31 Disable Advanced SIMD Functionality */
} b; /*!< Structure used for bit access */
uint32_t w; /*!< Type used for word access */
} CPACR_Type;

#define CPACR_ASEDIS_Pos 31U /*!< CPACR: ASEDIS Position */
#define CPACR_ASEDIS_Msk (1UL << CPACR_ASEDIS_Pos) /*!< CPACR: ASEDIS Mask */

#define CPACR_D32DIS_Pos 30U /*!< CPACR: D32DIS Position */
#define CPACR_D32DIS_Msk (1UL << CPACR_D32DIS_Pos) /*!< CPACR: D32DIS Mask */

#define CPACR_cp11_Pos 22U /*!< CPACR: cp11 Position */
#define CPACR_cp11_Msk (3UL << CPACR_cp11_Pos) /*!< CPACR: cp11 Mask */

#define CPACR_cp10_Pos 20U /*!< CPACR: cp10 Position */
#define CPACR_cp10_Msk (3UL << CPACR_cp10_Pos) /*!< CPACR: cp10 Mask */


/* CP15的DFSR寄存器
* 参考资料:Cortex-A7 Technical ReferenceManua.pdf P128
*/
typedef union
{
struct
{
uint32_t FS0:4; /*!< bit: 0.. 3 Fault Status bits bit 0-3 */
uint32_t Domain:4; /*!< bit: 4.. 7 Fault on which domain */
uint32_t _reserved0:2; /*!< bit: 8.. 9 Reserved */
uint32_t FS1:1; /*!< bit: 10 Fault Status bits bit 4 */
uint32_t WnR:1; /*!< bit: 11 Write not Read bit */
uint32_t ExT:1; /*!< bit: 12 External abort type */
uint32_t CM:1; /*!< bit: 13 Cache maintenance fault */
uint32_t _reserved1:18; /*!< bit:14..31 Reserved */
} b; /*!< Structure used for bit access */
uint32_t w; /*!< Type used for word access */
} DFSR_Type;

#define DFSR_CM_Pos 13U /*!< DFSR: CM Position */
#define DFSR_CM_Msk (1UL << DFSR_CM_Pos) /*!< DFSR: CM Mask */

#define DFSR_Ext_Pos 12U /*!< DFSR: Ext Position */
#define DFSR_Ext_Msk (1UL << DFSR_Ext_Pos) /*!< DFSR: Ext Mask */

#define DFSR_WnR_Pos 11U /*!< DFSR: WnR Position */
#define DFSR_WnR_Msk (1UL << DFSR_WnR_Pos) /*!< DFSR: WnR Mask */

#define DFSR_FS1_Pos 10U /*!< DFSR: FS1 Position */
#define DFSR_FS1_Msk (1UL << DFSR_FS1_Pos) /*!< DFSR: FS1 Mask */

#define DFSR_Domain_Pos 4U /*!< DFSR: Domain Position */
#define DFSR_Domain_Msk (0xFUL << DFSR_Domain_Pos) /*!< DFSR: Domain Mask */

#define DFSR_FS0_Pos 0U /*!< DFSR: FS0 Position */
#define DFSR_FS0_Msk (0xFUL << DFSR_FS0_Pos) /*!< DFSR: FS0 Mask */


/* CP15的IFSR寄存器
* 参考资料:Cortex-A7 Technical ReferenceManua.pdf P131
*/
typedef union
{
struct
{
uint32_t FS0:4; /*!< bit: 0.. 3 Fault Status bits bit 0-3 */
uint32_t _reserved0:6; /*!< bit: 4.. 9 Reserved */
uint32_t FS1:1; /*!< bit: 10 Fault Status bits bit 4 */
uint32_t _reserved1:1; /*!< bit: 11 Reserved */
uint32_t ExT:1; /*!< bit: 12 External abort type */
uint32_t _reserved2:19; /*!< bit:13..31 Reserved */
} b; /*!< Structure used for bit access */
uint32_t w; /*!< Type used for word access */
} IFSR_Type;

#define IFSR_ExT_Pos 12U /*!< IFSR: ExT Position */
#define IFSR_ExT_Msk (1UL << IFSR_ExT_Pos) /*!< IFSR: ExT Mask */

#define IFSR_FS1_Pos 10U /*!< IFSR: FS1 Position */
#define IFSR_FS1_Msk (1UL << IFSR_FS1_Pos) /*!< IFSR: FS1 Mask */

#define IFSR_FS0_Pos 0U /*!< IFSR: FS0 Position */
#define IFSR_FS0_Msk (0xFUL << IFSR_FS0_Pos) /*!< IFSR: FS0 Mask */


/* CP15的ISR寄存器
* 参考资料:ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf P1640
*/
typedef union
{
struct
{
uint32_t _reserved0:6; /*!< bit: 0.. 5 Reserved */
uint32_t F:1; /*!< bit: 6 FIQ pending bit */
uint32_t I:1; /*!< bit: 7 IRQ pending bit */
uint32_t A:1; /*!< bit: 8 External abort pending bit */
uint32_t _reserved1:23; /*!< bit:14..31 Reserved */
} b; /*!< Structure used for bit access */
uint32_t w; /*!< Type used for word access */
} ISR_Type;

#define ISR_A_Pos 13U /*!< ISR: A Position */
#define ISR_A_Msk (1UL << ISR_A_Pos) /*!< ISR: A Mask */

#define ISR_I_Pos 12U /*!< ISR: I Position */
#define ISR_I_Msk (1UL << ISR_I_Pos) /*!< ISR: I Mask */

#define ISR_F_Pos 11U /*!< ISR: F Position */
#define ISR_F_Msk (1UL << ISR_F_Pos) /*!< ISR: F Mask */


/* Mask and shift a bit field value for use in a register bit range. */
#define _VAL2FLD(field, value) ((value << field ## _Pos) & field ## _Msk)

/* Mask and shift a register value to extract a bit filed value. */
#define _FLD2VAL(field, value) ((value & field ## _Msk) >> field ## _Pos)


/*******************************************************************************
* CP15 访问函数
******************************************************************************/

FORCEDINLINE __STATIC_INLINE uint32_t __get_SCTLR(void)
{
return __MRC(15, 0, 1, 0, 0);
}

FORCEDINLINE __STATIC_INLINE void __set_SCTLR(uint32_t sctlr)
{
__MCR(15, 0, sctlr, 1, 0, 0);
}

FORCEDINLINE __STATIC_INLINE uint32_t __get_ACTLR(void)
{
return __MRC(15, 0, 1, 0, 1);
}

FORCEDINLINE __STATIC_INLINE void __set_ACTLR(uint32_t actlr)
{
__MCR(15, 0, actlr, 1, 0, 1);
}

FORCEDINLINE __STATIC_INLINE uint32_t __get_CPACR(void)
{
return __MRC(15, 0, 1, 0, 2);
}

FORCEDINLINE __STATIC_INLINE void __set_CPACR(uint32_t cpacr)
{
__MCR(15, 0, cpacr, 1, 0, 2);
}

FORCEDINLINE __STATIC_INLINE uint32_t __get_TTBR0(void)
{
return __MRC(15, 0, 2, 0, 0);
}

FORCEDINLINE __STATIC_INLINE void __set_TTBR0(uint32_t ttbr0)
{
__MCR(15, 0, ttbr0, 2, 0, 0);
}

FORCEDINLINE __STATIC_INLINE uint32_t __get_TTBR1(void)
{
return __MRC(15, 0, 2, 0, 1);
}

FORCEDINLINE __STATIC_INLINE void __set_TTBR1(uint32_t ttbr1)
{
__MCR(15, 0, ttbr1, 2, 0, 1);
}

FORCEDINLINE __STATIC_INLINE uint32_t __get_TTBCR(void)
{
return __MRC(15, 0, 2, 0, 2);
}

FORCEDINLINE __STATIC_INLINE void __set_TTBCR(uint32_t ttbcr)
{
__MCR(15, 0, ttbcr, 2, 0, 2);
}

FORCEDINLINE __STATIC_INLINE uint32_t __get_DACR(void)
{
return __MRC(15, 0, 3, 0, 0);
}

FORCEDINLINE __STATIC_INLINE void __set_DACR(uint32_t dacr)
{
__MCR(15, 0, dacr, 3, 0, 0);
}

FORCEDINLINE __STATIC_INLINE uint32_t __get_DFSR(void)
{
return __MRC(15, 0, 5, 0, 0);
}

FORCEDINLINE __STATIC_INLINE void __set_DFSR(uint32_t dfsr)
{
__MCR(15, 0, dfsr, 5, 0, 0);
}

FORCEDINLINE __STATIC_INLINE uint32_t __get_IFSR(void)
{
return __MRC(15, 0, 5, 0, 1);
}

FORCEDINLINE __STATIC_INLINE void __set_IFSR(uint32_t ifsr)
{
__MCR(15, 0, ifsr, 5, 0, 1);
}

FORCEDINLINE __STATIC_INLINE uint32_t __get_DFAR(void)
{
return __MRC(15, 0, 6, 0, 0);
}

FORCEDINLINE __STATIC_INLINE void __set_DFAR(uint32_t dfar)
{
__MCR(15, 0, dfar, 6, 0, 0);
}

FORCEDINLINE __STATIC_INLINE uint32_t __get_IFAR(void)
{
return __MRC(15, 0, 6, 0, 2);
}

FORCEDINLINE __STATIC_INLINE void __set_IFAR(uint32_t ifar)
{
__MCR(15, 0, ifar, 6, 0, 2);
}

FORCEDINLINE __STATIC_INLINE uint32_t __get_VBAR(void)
{
return __MRC(15, 0, 12, 0, 0);
}

FORCEDINLINE __STATIC_INLINE void __set_VBAR(uint32_t vbar)
{
__MCR(15, 0, vbar, 12, 0, 0);
}

FORCEDINLINE __STATIC_INLINE uint32_t __get_ISR(void)
{
return __MRC(15, 0, 12, 1, 0);
}

FORCEDINLINE __STATIC_INLINE void __set_ISR(uint32_t isr)
{
__MCR(15, 0, isr, 12, 1, 0);
}

FORCEDINLINE __STATIC_INLINE uint32_t __get_CONTEXTIDR(void)
{
return __MRC(15, 0, 13, 0, 1);
}

FORCEDINLINE __STATIC_INLINE void __set_CONTEXTIDR(uint32_t contextidr)
{
__MCR(15, 0, contextidr, 13, 0, 1);
}

FORCEDINLINE __STATIC_INLINE uint32_t __get_CBAR(void)
{
return __MRC(15, 4, 15, 0, 0);
}

/*******************************************************************************
* GIC相关内容
*有关GIC的内容,参考:ARM Generic Interrupt Controller(ARM GIC控制器)V2.0.pdf
******************************************************************************/

/*
* GIC寄存器描述结构体,
* GIC分为分发器端和CPU接口端
*/
typedef struct
{
uint32_t RESERVED0[1024];
__IOM uint32_t D_CTLR; /*!< Offset: 0x1000 (R/W) Distributor Control Register */
__IM uint32_t D_TYPER; /*!< Offset: 0x1004 (R/ ) Interrupt Controller Type Register */
__IM uint32_t D_IIDR; /*!< Offset: 0x1008 (R/ ) Distributor Implementer Identification Register */
uint32_t RESERVED1[29];
__IOM uint32_t D_IGROUPR[16]; /*!< Offset: 0x1080 - 0x0BC (R/W) Interrupt Group Registers */
uint32_t RESERVED2[16];
__IOM uint32_t D_ISENABLER[16]; /*!< Offset: 0x1100 - 0x13C (R/W) Interrupt Set-Enable Registers */
uint32_t RESERVED3[16];
__IOM uint32_t D_ICENABLER[16]; /*!< Offset: 0x1180 - 0x1BC (R/W) Interrupt Clear-Enable Registers */
uint32_t RESERVED4[16];
__IOM uint32_t D_ISPENDR[16]; /*!< Offset: 0x1200 - 0x23C (R/W) Interrupt Set-Pending Registers */
uint32_t RESERVED5[16];
__IOM uint32_t D_ICPENDR[16]; /*!< Offset: 0x1280 - 0x2BC (R/W) Interrupt Clear-Pending Registers */
uint32_t RESERVED6[16];
__IOM uint32_t D_ISACTIVER[16]; /*!< Offset: 0x1300 - 0x33C (R/W) Interrupt Set-Active Registers */
uint32_t RESERVED7[16];
__IOM uint32_t D_ICACTIVER[16]; /*!< Offset: 0x1380 - 0x3BC (R/W) Interrupt Clear-Active Registers */
uint32_t RESERVED8[16];
__IOM uint8_t D_IPRIORITYR[512]; /*!< Offset: 0x1400 - 0x5FC (R/W) Interrupt Priority Registers */
uint32_t RESERVED9[128];
__IOM uint8_t D_ITARGETSR[512]; /*!< Offset: 0x1800 - 0x9FC (R/W) Interrupt Targets Registers */
uint32_t RESERVED10[128];
__IOM uint32_t D_ICFGR[32]; /*!< Offset: 0x1C00 - 0xC7C (R/W) Interrupt configuration registers */
uint32_t RESERVED11[32];
__IM uint32_t D_PPISR; /*!< Offset: 0x1D00 (R/ ) Private Peripheral Interrupt Status Register */
__IM uint32_t D_SPISR[15]; /*!< Offset: 0x1D04 - 0xD3C (R/ ) Shared Peripheral Interrupt Status Registers */
uint32_t RESERVED12[112];
__OM uint32_t D_SGIR; /*!< Offset: 0x1F00 ( /W) Software Generated Interrupt Register */
uint32_t RESERVED13[3];
__IOM uint8_t D_CPENDSGIR[16]; /*!< Offset: 0x1F10 - 0xF1C (R/W) SGI Clear-Pending Registers */
__IOM uint8_t D_SPENDSGIR[16]; /*!< Offset: 0x1F20 - 0xF2C (R/W) SGI Set-Pending Registers */
uint32_t RESERVED14[40];
__IM uint32_t D_PIDR4; /*!< Offset: 0x1FD0 (R/ ) Peripheral ID4 Register */
__IM uint32_t D_PIDR5; /*!< Offset: 0x1FD4 (R/ ) Peripheral ID5 Register */
__IM uint32_t D_PIDR6; /*!< Offset: 0x1FD8 (R/ ) Peripheral ID6 Register */
__IM uint32_t D_PIDR7; /*!< Offset: 0x1FDC (R/ ) Peripheral ID7 Register */
__IM uint32_t D_PIDR0; /*!< Offset: 0x1FE0 (R/ ) Peripheral ID0 Register */
__IM uint32_t D_PIDR1; /*!< Offset: 0x1FE4 (R/ ) Peripheral ID1 Register */
__IM uint32_t D_PIDR2; /*!< Offset: 0x1FE8 (R/ ) Peripheral ID2 Register */
__IM uint32_t D_PIDR3; /*!< Offset: 0x1FEC (R/ ) Peripheral ID3 Register */
__IM uint32_t D_CIDR0; /*!< Offset: 0x1FF0 (R/ ) Component ID0 Register */
__IM uint32_t D_CIDR1; /*!< Offset: 0x1FF4 (R/ ) Component ID1 Register */
__IM uint32_t D_CIDR2; /*!< Offset: 0x1FF8 (R/ ) Component ID2 Register */
__IM uint32_t D_CIDR3; /*!< Offset: 0x1FFC (R/ ) Component ID3 Register */

__IOM uint32_t C_CTLR; /*!< Offset: 0x2000 (R/W) CPU Interface Control Register */
__IOM uint32_t C_PMR; /*!< Offset: 0x2004 (R/W) Interrupt Priority Mask Register */
__IOM uint32_t C_BPR; /*!< Offset: 0x2008 (R/W) Binary Point Register */
__IM uint32_t C_IAR; /*!< Offset: 0x200C (R/ ) Interrupt Acknowledge Register */
__OM uint32_t C_EOIR; /*!< Offset: 0x2010 ( /W) End Of Interrupt Register */
__IM uint32_t C_RPR; /*!< Offset: 0x2014 (R/ ) Running Priority Register */
__IM uint32_t C_HPPIR; /*!< Offset: 0x2018 (R/ ) Highest Priority Pending Interrupt Register */
__IOM uint32_t C_ABPR; /*!< Offset: 0x201C (R/W) Aliased Binary Point Register */
__IM uint32_t C_AIAR; /*!< Offset: 0x2020 (R/ ) Aliased Interrupt Acknowledge Register */
__OM uint32_t C_AEOIR; /*!< Offset: 0x2024 ( /W) Aliased End Of Interrupt Register */
__IM uint32_t C_AHPPIR; /*!< Offset: 0x2028 (R/ ) Aliased Highest Priority Pending Interrupt Register */
uint32_t RESERVED15[41];
__IOM uint32_t C_APR0; /*!< Offset: 0x20D0 (R/W) Active Priority Register */
uint32_t RESERVED16[3];
__IOM uint32_t C_NSAPR0; /*!< Offset: 0x20E0 (R/W) Non-secure Active Priority Register */
uint32_t RESERVED17[6];
__IM uint32_t C_IIDR; /*!< Offset: 0x20FC (R/ ) CPU Interface Identification Register */
uint32_t RESERVED18[960];
__OM uint32_t C_DIR; /*!< Offset: 0x3000 ( /W) Deactivate Interrupt Register */
} GIC_Type;


/*
* GIC初始化
* 为了简单使用GIC的group0
*/
FORCEDINLINE __STATIC_INLINE void GIC_Init(void)
{
uint32_t i;
uint32_t irqRegs;
GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);

irqRegs = (gic->D_TYPER & 0x1FUL) + 1;

/* On POR, all SPI is in group 0, level-sensitive and using 1-N model */

/* Disable all PPI, SGI and SPI */
for (i = 0; i < irqRegs; i++)
gic->D_ICENABLER[i] = 0xFFFFFFFFUL;

/* Make all interrupts have higher priority */
gic->C_PMR = (0xFFUL << (8 - __GIC_PRIO_BITS)) & 0xFFUL;

/* No subpriority, all priority level allows preemption */
gic->C_BPR = 7 - __GIC_PRIO_BITS;

/* Enable group0 distribution */
gic->D_CTLR = 1UL;

/* Enable group0 signaling */
gic->C_CTLR = 1UL;
}

/*
* 使能指定的中断
*/
FORCEDINLINE __STATIC_INLINE void GIC_EnableIRQ(IRQn_Type IRQn)
{
GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);
gic->D_ISENABLER[((uint32_t)(int32_t)IRQn) >> 5] = (uint32_t)(1UL << (((uint32_t)(int32_t)IRQn) & 0x1FUL));
}

/*
* 关闭指定的中断
*/

FORCEDINLINE __STATIC_INLINE void GIC_DisableIRQ(IRQn_Type IRQn)
{
GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);
gic->D_ICENABLER[((uint32_t)(int32_t)IRQn) >> 5] = (uint32_t)(1UL << (((uint32_t)(int32_t)IRQn) & 0x1FUL));
}

/*
* 返回中断号
*/
FORCEDINLINE __STATIC_INLINE uint32_t GIC_AcknowledgeIRQ(void)
{
GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);
return gic->C_IAR & 0x1FFFUL;
}

/*
* 向EOIR写入发送中断的中断号来释放中断
*/
FORCEDINLINE __STATIC_INLINE void GIC_DeactivateIRQ(uint32_t value)
{
GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);
gic->C_EOIR = value;
}

/*
* 获取运行优先级
*/
FORCEDINLINE __STATIC_INLINE uint32_t GIC_GetRunningPriority(void)
{
GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);
return gic->C_RPR & 0xFFUL;
}

/*
* 设置组优先级
*/
FORCEDINLINE __STATIC_INLINE void GIC_SetPriorityGrouping(uint32_t PriorityGroup)
{
GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);
gic->C_BPR = PriorityGroup & 0x7UL;
}

/*
* 获取组优先级
*/
FORCEDINLINE __STATIC_INLINE uint32_t GIC_GetPriorityGrouping(void)
{
GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);

return gic->C_BPR & 0x7UL;
}

/*
* 设置优先级
*/
FORCEDINLINE __STATIC_INLINE void GIC_SetPriority(IRQn_Type IRQn, uint32_t priority)
{
GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);
gic->D_IPRIORITYR[((uint32_t)(int32_t)IRQn)] = (uint8_t)((priority << (8UL - __GIC_PRIO_BITS)) & (uint32_t)0xFFUL);
}

/*
* 获取优先级
*/
FORCEDINLINE __STATIC_INLINE uint32_t GIC_GetPriority(IRQn_Type IRQn)
{
GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);
return(((uint32_t)gic->D_IPRIORITYR[((uint32_t)(int32_t)IRQn)] >> (8UL - __GIC_PRIO_BITS)));
}
#endif

8.1 内联汇编原理#

先看一个错误的示例:

1
2
3
4
5
6
7
void func(void)
{
...
asm("mov r1,r0");
__asm__("mov r2,r1");
...
}

这种操作可能产生负面的影响,因为 r0~r2 寄存器很可能正在被程序的其它部分使用而在这里被意外地修改。
这就需要使用到嵌入汇编的另一种表达形式:
asm(code : output operand list : input operand list : clobber list);
这种嵌入汇编的形式一共分为四个部分:

1
2
3
4
* code
* [attr]output operand list
* [attr]input operand list
* clobber list

code:汇编的操作代码,一条或者多条指令,如果是多条指令,需要在指令间使用\n\t隔开。
与通用的汇编代码有一些不同:因为支持 C 变量的操作,所以在操作由第二、三部分提供
的操作数时,使用 %n 来替代操作数。
output operand list:表示输出的操作数,通常是一个或者多个 C 函数中的变量。
input operand list:表示输入的操作数,通常是一个或者多个 C 函数中的变量,attr部分表示操作数的属性,以字符串的形式提供,是必须的参数。
clobber list:被破坏的列表,这部分我们放到后面讨论。

1
2
3
4
5
6
7
8
9
void func(void)
{
int val1 = 111,val2 = 222;
asm("mov %0,%1"
:"+r"(val1)
:"r"(val2)
:);
printf("val1 = %d\n",val1);
}

func 函数的输出结果为:val1 = 222.
分析:

1
2
3
4
5
6
7
8
9
输出操作数为 val1,属性为 "=r"。
输入操作数为 val2,属性为 "r"
code 部分为 mov %1,%0, %0 表示输入输出列表中的第一个操作数,
%1 表示操作数列表中提供的第二个操作数,以此类推,这条汇编指
令很明显就是将第二个操作数(val2)赋值给第一个操作数(val1),所以最后的结果为 val1 = 222.

code 中的操作数命名顺序为输出操作书列表递增,输入操作数列表递增,比如增加一个操作数的代码为:
int val1 = 111,val2 = 222,val3=333;
asm("mov %0,%2" :"+r"(val1) :"r"(val2),"r"(val3) :);

8.1.1 操作数属性#

在上述的示例中,输入操作数和输出操作数都会使用到 attr 字段,即操作数的属性,下面是其属性对应的列表:

  • “=” 表示只写,通常用于所有输出操作数的属性
  • “+” 表示读写,只能被列为输出操作数的属性,否则编译会报错。

8.1.2 clobber list#

clobber 的意思为破坏,在这里的意思是:这段汇编指令将会破坏哪些寄存器的值。

在目前的 gcc 设计中,编译分为4个过程:预编译、编译、汇编、链接。 其中,编译就是将 C 代码编译成汇编代码,而通用的汇编代码在这个过程是不会处理的,也就是说,嵌入汇编代码的解析只涉及到输入输出操作数的替换,对于不包含输入输出操作数的部分不会解析,所以在编译阶段,编译器不会知道嵌入汇编代码中静态地使用到哪些寄存器,而是自顾自地编译 C 代码,从而导致 C 代码和嵌入汇编代码操作到同一个寄存器,而出现错误。例如:

1
2
3
void func(void) {
asm("mov lr,#1");
}

嵌入汇编代码过程中,将lr的值修改为 1,当前函数返回的时候也就返回到 1 地址,不出意料: 程序出现段错误:

1
Segmentation fault (core dumped)

解决办法是:将程序修改一下,用clobber list这种标准格式:

1
2
3
void func(void) {
asm("mov lr,#1":::"lr");
}

为了进一步追求真相,我们来对比它们的反汇编代码:

不添加 clobber list 的反汇编代码:

1
2
3
4
5
6
7
000083f4 <func3>:
83f4: b480 push {r7}
83f6: af00 add r7, sp, #0
83f8: f04f 0e01 mov.w lr, #1
83fc: 46bd mov sp, r7
83fe: f85d 7b04 ldr.w r7, [sp], #4
8402: 4770 bx lr

添加 clobber list 的反汇编代码:

1
2
3
4
5
6
7
8
void func3(void)
{
83f4: b580 push {r7, lr}
83f6: af00 add r7, sp, #0
83f8: f04f 0e01 mov.w lr, #1
}
83fc: bd80 pop {r7, pc}
83fe: bf00 nop

对比可以看出,对于不将"lr"添加到 clobber list 中的代码,返回指令为 bx lr,因为lr被修改为 1,所以返回一定出错.
第二个代码则不一样,在函数调用之初,就将lr寄存器使用 push 指令保存到了栈上,在最后返回的时候再将栈上的原 lr 数据 pop 到 pc 指针中,其中lr的修改没有任何影响。

通常情况下,clobber list 对应着寄存器的修改,所以我们只需要将寄存器的名字作为参数添加到clobber list中,比如"r0" "lr"等,有两个特殊的参数需要关注,就是 **"cc" 和 "memory"**。

"cc" 对应的并非是普通寄存器,而是 CPU 的状态寄存器,如果某些指令将状态寄存器修改了,需要在clobber list中添加"cc"来声明这个事情。

"memory" 对应内存操作,这从名称也可以看出,当clobber list中包含"memory"时,表示嵌入汇编代码会对内存进行一些操作,gcc 生成的代码会将特定的寄存器的值写回到内存中以保证内存中的值是最新的,这样做的原因是 gcc 经常会将内存数据缓存在寄存器中,如果不及时写回,嵌入汇编代码读到的内存就是原来的值。

imx6ull裸机-SPI

1 SPI介绍#

s3c2440裸机编程-SPI | Hexo (fuzidage.github.io)有详细介绍SPI协议。

1.1 imx6ull SPI控制器介绍#

NXP的6ull参考手册第Chapter 20介绍了SPI控制器,Enhanced Configurable SPI (ECSPI)

1.1.1 特点#

①、全双工同步串行接口。
②、可配置的主/从模式。
③、四个硬件片选信号,支持多从机。
④、发送和接收都有一个 32x64 的 FIFO。
⑤、片选信号 SS/CS,时钟信号 SCLK 的极性相位(CPOL,CPHA)可配置。
⑥、支持 DMA
⑦、SCK最高可以到输入参考时钟高达60Mhz

1.1.2 框图#

image
最右边是引脚,SCLK,MISO,MOSI等,上面是外围总线,通过APB总线进行寄存器读写,INTREG,CONREG等等。TXDATA和TXDATA寄存器存放了要发送的数据和接收的收据。
时钟源来自Reference Clock or Low Frequency Clock。可选时钟源如下:这里选用ecspi_clk_root
image
image
① CSCDR2的ECSPI_CLK_SEL位设置为0,选择出PLL3_SW_CLK 进行8分频作为 ECSPI 根时钟源。PLL3_SW_CLK=480MHz,8分频就是60MHz。
② CSCDR2 的 ECSPI_CLK_PODF位再次进行分频,ECSPI_CLK_PODF位设置成0,表示2^0分频,也就是1分频。
③ 最后ECSPI_CLK_ROOT就为60MHz

1.1.3 时序#

CPOL时钟极性 和CPHA时钟相位组合成了4种模式:

CPOL:表示SPI CLK的初始电平(空闲状态时电平),0为低电平,1为高电平
CPHA:表示相位,即第一个还是第二个时钟沿采样数据,0为第一个时钟沿,1为第二个时钟沿

image

1.2 SPI控制器寄存器#

1.2.1 控制器初始化流程#

CONREG[EN]:复位,0表示复位
CCM开启ECSPI时钟
CONREG[EN]:复位,1表示反选复位
image

1.2.2 寄存器介绍#

1.2.2.1 RXDATA#

RXDATA寄存器:接收数据寄存器,RR位的状态决定接受数据是否就绪
image

1.2.2.2 TXDATA#

TXDATA寄存器:发送数据寄存器,实际传输的位数由相应SPI控制寄存器的BURST_LENGTH位来决定。
image

1.2.2.3 CONREG#

CONREG寄存器:控制寄存器
image
EN:使能位,1为使能
SMC:为1表示当数据写入TXFIFO时,立即启动SPI突发;这里使用该模式
CHANNEL_MODE:硬件片选模式选择,bit[7:4]分别表示通道3到通道0,这里采用通道0设定为Master mode.因此bit[7:4]配置成1
POST_DIVIDER:后分频,0到15表示2^n次方分频,比如0就是1分频,15就是2^15分频
PRE_DIVIDER:前分频,0到15表示1到16分频
前面spi clk的时钟源为ECSPI_CLK_ROOT 60MHz,这里我们用6MHz,因此可以设置POST_DIVIDER=0,PRE_DIVIDER=9,表示10分频。
CHANNEL_SELECT:通道选择,也就是硬件片选SS选择,这里选择SS0,通道0
BURST_LENGTH:突发访问长度,这里我们用一次突发8bit, 配置成0x7

1.2.2.4 CONFIGREG#

CONFIGREG寄存器:配置寄存器
image
SCLK_PHA:时钟相位,SCLK_PHA[3:0]分别对应通道3~0,设置为0表示第一个时钟沿采集数据,设置成1表示第二个时钟沿采集数据。(同POL组成4种模式)
SCLK_POL:时钟极性,表示时钟初始空闲时的电平,0为低电平,1为高电平。(同PHA组成4种模式)
SS_CTL:硬件片选的wave form select,这个用不上设置成0
SS_POL:硬件片选的极性选择,用不上设置成0
DATA_CTL:数据线空闲时电平状态,我们设置成0表示高电平
SCLK_CTL:时钟线空闲时电平状态,我们设置成0表示低电平(POL设置了时钟初始空闲时的电平为低电平)
HT_LENGTH: HT Mode不用,无需配置

1.2.2.5 STATREG#

STATREG寄存器:状态寄存器
image
TE:TXFIFO empty, 为1表示TXFIFO为空,0表示TXFIFO还没空,因此往TXDATA发送数据时,需要先等待TXFIFO为空。
RR: RXFIFO Ready,1表示有数据,0表示数据还没ready.读取RXDATA需要等RXFIFO先ready。

1.2.2.6 PERIODREG#

PERIODREG寄存器:采样周期寄存器
image
SAMPLE_ PERIOD:突发访问时的等待周期,表示等待多少个时钟周期后进行一下次突发访问。我们设置为0x2000。
image
CSRC: 等待周期的单位,0表示以SPI clk为单位, 1表示以low-frequency reference clk 32.768KHz为单位。
CSD_CTL:硬件片选延时,表示片选后多少个时钟周期才可以进行数据传输。(这里不用,我们用软件片选)

1.3 SPI控制器代码编写#

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
void spi_init(ECSPI_Type *base) {
/* 配置CONREG寄存器
* bit0 : 1 使能ECSPI
* bit3 : 1 当向TXFIFO写入数据以后立即开启SPI突发。
* bit[7:4] : 0001 SPI通道0主模式,根据实际情况选择,
* 开发板上的ICM-20608接在SS0上,所以设置通道0为主模式
* bit[19:18]: 00 选中通道0(其实不需要,因为片选信号我们我们自己控制)
* bit[31:20]: 0x7 突发长度为8个bit。
*/
base->CONREG = 0; /* 先清除控制寄存器 */
base->CONREG |= (1 << 0) | (1 << 3) | (1 << 4) | (7 << 20); /* 配置CONREG寄存器 */

/*
* ECSPI通道0设置,即设置CONFIGREG寄存器
* bit0: 0 通道0 PHA为0
* bit4: 0 通道0 SCLK高电平有效
* bit8: 0 通道0片选信号 当SMC为1的时候此位无效
* bit12: 0 通道0 POL为0
* bit16: 0 通道0 数据线空闲时高电平
* bit20: 0 通道0 时钟线空闲时低电平
*/
base->CONFIGREG = 0; /* 设置通道寄存器 */

/*
* ECSPI通道0设置,设置采样周期
* bit[14:0] : 0X2000 采样等待周期,比如当SPI时钟为10MHz的时候
* 0X2000就等于1/10000 * 0X2000 = 0.8192ms,也就是连续
* 读取数据的时候每次之间间隔0.8ms
* bit15 : 0 采样时钟源为SPI CLK
* bit[21:16]: 0 片选延时,可设置为0~63
*/
base->PERIODREG = 0X2000; /* 设置采样周期寄存器 */

/*
* ECSPI的SPI时钟配置,SPI的时钟源来源于pll3_sw_clk/8=480/8=60MHz
* 通过设置CONREG寄存器的PER_DIVIDER(bit[11:8])和POST_DIVEDER(bit[15:12])来
* 对SPI时钟源分频,获取到我们想要的SPI时钟:
* SPI CLK = (SourceCLK / PER_DIVIDER) / (2^POST_DIVEDER)
* 比如我们现在要设置SPI时钟为6MHz,那么PER_DIVEIDER和POST_DEIVIDER设置如下:
* PER_DIVIDER = 0X9。
* POST_DIVIDER = 0X0。
* SPI CLK = 60000000/(0X9 + 1) = 60000000=6MHz
*/
base->CONREG &= ~((0XF << 12) | (0XF << 8)); /* 清除PER_DIVDER和POST_DIVEDER以前的设置 */
base->CONREG |= (0X9 << 12); /* 设置SPI CLK = 6MHz */
}

/*
* @description : SPI通道0发送/接收一个字节的数据
* @param - base : 要使用的SPI
* @param - txdata : 要发送的数据
* @return : 无
*/
unsigned char spich0_readwrite_byte(ECSPI_Type *base, unsigned char txdata) {
uint32_t spirxdata = 0;
uint32_t spitxdata = txdata;

/* 选择通道0 */
base->CONREG &= ~(3 << 18);
base->CONREG |= (0 << 18);

while((base->STATREG & (1 << 0)) == 0){} /* 等待发送FIFO为空 */
base->TXDATA = spitxdata;

while((base->STATREG & (1 << 3)) == 0){} /* 等待接收FIFO有数据 */
spirxdata = base->RXDATA;
return spirxdata;
}

2 SPI 应用#

2.1 6轴陀螺仪加速度传感器ICM-20608-G#

2.1.1 ICM-20608-G概述#

The ICM-20608-G is a 6-axis MotionTracking device that combines a 3-axis gyroscope, and a 3-axis accelerometer in a small 3x3x0.75mm (16-pin LGA) package. The gyroscope has a programmable full-scale range of ±250, ±500, ±1000, and ±2000 degrees/sec. The accelerometer has a user programmable accelerometer full-scale range of ±2g, ±4g, ±8g, and ±16g. Other industry-leading features include on-chip 16-bit ADCs, programmable digital filters, an embedded temperature sensor, and programmable interrupts. The device features I2 C and SPI serial interfaces, a VDD operating range of 1.71 to 3.45V, and a separate digital IO supply, VDDIO from 1.71V to 3.45V. Communication with all registers of the device is performed using either I2 C at 400kHz or SPI at 8MHz.
1.包含3轴陀螺仪数据和3轴加速度数据。
2.陀螺仪和加速度量程可设定,陀螺仪量程可设定位+-250,+-500,+-1000, +-2000角度每秒。加速度同理也可设定量程。
3.精度为16bit ADC转换。
4.使用I2C/SPI接口通信,I2C速率高达400KHz, SPI高达8MHz。

2.1.2 应用场景#

image

2.1.3 陀螺仪和加速度特性#

image

2.1.4 电器特性#

image
image
可以看到FS_SEL,AFS_SEL用来选择陀螺仪和加速度计的量程。举个例子,当角速度量程为+-250时,那么ADC的数据为多少表示为1度呢?已知ADC精度16bit, 数据范围[0,65535], 假如ADC的数据为x, 那么x/65636 = 1/500,算出x= 131.272x,对应表格数据中的131。加速度的换算公式也是同理, 当AFS_SEL=0时,x/65536 = 1/4, x=16384。

2.1.5 交流电器特性#

image
当用i2c通信,AD0引脚决定i2c从地址是0x68还是0x69。可以看到power-on reset上电时序,需要Valid power-on RESET时间最少0.01ms, 从启动到寄存器读写等11ms。

2.1.6 工作模式#

image

2.1.7 SPI方式寄存器访问#

image
数据上升沿锁存,下降沿数据发生改变。最大高达8MHz时钟,一次读写需要16个或者更多时钟周期,第一个字节传输寄存器地址,第二个字节传输数据。首字节的首位表示是读还是写。

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
#define ICM20608_CSN(n)    (n ? gpio_pinwrite(GPIO1, 20, 1) : gpio_pinwrite(GPIO1, 20, 0))   /* SPI片选信号	 */
/*
* @description : 写ICM20608指定寄存器
* @param - reg : 要读取的寄存器地址
* @param - value: 要写入的值
* @return : 无
*/
void icm20608_write_reg(unsigned char reg, unsigned char value) {
/* ICM20608在使用SPI接口的时候寄存器地址
* 只有低7位有效,寄存器地址最高位是读/写标志位
* 读的时候要为1,写的时候要为0。
*/
reg &= ~0X80;

ICM20608_CSN(0); /* 使能SPI传输 */
spich0_readwrite_byte(ECSPI3, reg); /* 发送寄存器地址 */
spich0_readwrite_byte(ECSPI3, value); /* 发送要写入的值 */
ICM20608_CSN(1); /* 禁止SPI传输 */
}
/*
* @description : 读取ICM20608寄存器值
* @param - reg : 要读取的寄存器地址
* @return : 读取到的寄存器值
*/
unsigned char icm20608_read_reg(unsigned char reg) {
unsigned char reg_val;

/* ICM20608在使用SPI接口的时候寄存器地址
* 只有低7位有效,寄存器地址最高位是读/写标志位
* 读的时候要为1,写的时候要为0。
*/
reg |= 0x80;

ICM20608_CSN(0); /* 使能SPI传输 */
spich0_readwrite_byte(ECSPI3, reg); /* 发送寄存器地址 */
reg_val = spich0_readwrite_byte(ECSPI3, 0XFF); /* 读取寄存器的值 */
ICM20608_CSN(1); /* 禁止SPI传输 */
return(reg_val); /* 返回读取到的寄存器值 */
}

2.2 ICM-20608-G寄存器描述#

image
ICM-20608-G寄存器的地址和数据都是单字节。

2.2.1 控制寄存器#

控制配置寄存器0x1a,0x1b,0x1c,0x1d,设置量程等配置。
image
0x19设置分频,不分频,配成0
image
0x1a设置陀螺仪低通滤波带宽BW=20Hz,配成0x4.
image
0x1b设置gyro量程,配成最大0x18.
image
0x1c设置加速度计的量程,也配成最大0x18.
image
0x1d设置加速度计低通滤波BW=21.2Hz
image
0x1e设置low power,配成0,关闭低功耗.
image
0x23设置fifo功能,这里配置0x0,禁用fifo.

设定量程,配置相关参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define	ICM20_SMPLRT_DIV			0x19
#define ICM20_CONFIG 0x1A
#define ICM20_GYRO_CONFIG 0x1B
#define ICM20_ACCEL_CONFIG 0x1C
#define ICM20_ACCEL_CONFIG2 0x1D
#define ICM20_LP_MODE_CFG 0x1E
#define ICM20_FIFO_EN 0x23
icm20608_write_reg(ICM20_SMPLRT_DIV, 0x00); /* 输出速率是内部采样率 */
icm20608_write_reg(ICM20_GYRO_CONFIG, 0x18); /* 陀螺仪±2000dps量程 */
icm20608_write_reg(ICM20_ACCEL_CONFIG, 0x18); /* 加速度计±16G量程 */
icm20608_write_reg(ICM20_CONFIG, 0x04); /* 陀螺仪低通滤波BW=20Hz */
icm20608_write_reg(ICM20_ACCEL_CONFIG2, 0x04); /* 加速度计低通滤波BW=21.2Hz */
icm20608_write_reg(ICM20_PWR_MGMT_2, 0x00); /* 打开加速度计和陀螺仪所有轴 */
icm20608_write_reg(ICM20_LP_MODE_CFG, 0x00); /* 关闭低功耗 */
icm20608_write_reg(ICM20_FIFO_EN, 0x00); /* 关闭FIFO */

2.2.2 数据寄存器#

数据寄存器0x3b0x48表示加速度和陀螺仪数据,可以看到该传感器的寄存器地址都是单字节,ADC精度16bit,因此需要2个寄存器来表示一个轴的坐标数据。
image
0x3b-0x40表示加速度计3轴数据。
image
0x42 温度数据
image
image
0x43
0x48陀螺仪3轴数据。

2.2.3 WHO_AM_I寄存器#

image
寄存器表示设备ID,默认0xAF.

2.2.4 PWR_MGMT_1/PWR_MGMT_2寄存器#

电源管理模式寄存器
image
可以看到bit6默认是一个sleep mode, bit7是复位信号,复位后,默认bit6会变成1,进入睡眠模式。Bit4 陀螺仪待机,bit3关闭温度传感器等等都不要开启,设置成0,bit[2:0]时钟选择自动。
image
可以看到设置成0,6轴数据全使能

复位初始化:

1
2
3
4
5
6
7
8
#define	ICM20_PWR_MGMT_1			0x6B
#define ICM20_WHO_AM_I 0x75
icm20608_write_reg(ICM20_PWR_MGMT_1, 0x80); /* 复位,复位后为0x40,睡眠模式 */
delayms(50);
icm20608_write_reg(ICM20_PWR_MGMT_1, 0x01); /* 关闭睡眠,自动选择时钟 */
delayms(50);
regvalue = icm20608_read_reg(ICM20_WHO_AM_I);
printf("icm20608 id = %#X\r\n", regvalue);

2.3 代码解析#

icm20608.h:

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
/* ICM20608寄存器 
*复位后所有寄存器地址都为0,除了
*Register 107(0X6B) Power Management 1 = 0x40
*Register 117(0X75) WHO_AM_I = 0xAF或0xAE
*/
/* 陀螺仪和加速度自测(出产时设置,用于与用户的自检输出值比较) */
#define ICM20_SELF_TEST_X_GYRO 0x00
#define ICM20_SELF_TEST_Y_GYRO 0x01
#define ICM20_SELF_TEST_Z_GYRO 0x02
#define ICM20_SELF_TEST_X_ACCEL 0x0D
#define ICM20_SELF_TEST_Y_ACCEL 0x0E
#define ICM20_SELF_TEST_Z_ACCEL 0x0F

/* 陀螺仪静态偏移 */
#define ICM20_XG_OFFS_USRH 0x13
#define ICM20_XG_OFFS_USRL 0x14
#define ICM20_YG_OFFS_USRH 0x15
#define ICM20_YG_OFFS_USRL 0x16
#define ICM20_ZG_OFFS_USRH 0x17
#define ICM20_ZG_OFFS_USRL 0x18

#define ICM20_SMPLRT_DIV 0x19
#define ICM20_CONFIG 0x1A
#define ICM20_GYRO_CONFIG 0x1B
#define ICM20_ACCEL_CONFIG 0x1C
#define ICM20_ACCEL_CONFIG2 0x1D
#define ICM20_LP_MODE_CFG 0x1E
#define ICM20_ACCEL_WOM_THR 0x1F
#define ICM20_FIFO_EN 0x23
#define ICM20_FSYNC_INT 0x36
#define ICM20_INT_PIN_CFG 0x37
#define ICM20_INT_ENABLE 0x38
#define ICM20_INT_STATUS 0x3A

/* 加速度输出 */
#define ICM20_ACCEL_XOUT_H 0x3B
#define ICM20_ACCEL_XOUT_L 0x3C
#define ICM20_ACCEL_YOUT_H 0x3D
#define ICM20_ACCEL_YOUT_L 0x3E
#define ICM20_ACCEL_ZOUT_H 0x3F
#define ICM20_ACCEL_ZOUT_L 0x40

/* 温度输出 */
#define ICM20_TEMP_OUT_H 0x41
#define ICM20_TEMP_OUT_L 0x42

/* 陀螺仪输出 */
#define ICM20_GYRO_XOUT_H 0x43
#define ICM20_GYRO_XOUT_L 0x44
#define ICM20_GYRO_YOUT_H 0x45
#define ICM20_GYRO_YOUT_L 0x46
#define ICM20_GYRO_ZOUT_H 0x47
#define ICM20_GYRO_ZOUT_L 0x48

#define ICM20_SIGNAL_PATH_RESET 0x68
#define ICM20_ACCEL_INTEL_CTRL 0x69
#define ICM20_USER_CTRL 0x6A
#define ICM20_PWR_MGMT_1 0x6B
#define ICM20_PWR_MGMT_2 0x6C
#define ICM20_FIFO_COUNTH 0x72
#define ICM20_FIFO_COUNTL 0x73
#define ICM20_FIFO_R_W 0x74
#define ICM20_WHO_AM_I 0x75

/* 加速度静态偏移 */
#define ICM20_XA_OFFSET_H 0x77
#define ICM20_XA_OFFSET_L 0x78
#define ICM20_YA_OFFSET_H 0x7A
#define ICM20_YA_OFFSET_L 0x7B
#define ICM20_ZA_OFFSET_H 0x7D
#define ICM20_ZA_OFFSET_L 0x7E

/*
* ICM20608结构体
*/
struct icm20608_dev_struc {
signed int gyro_x_adc; /* 陀螺仪X轴原始值 */
signed int gyro_y_adc; /* 陀螺仪Y轴原始值 */
signed int gyro_z_adc; /* 陀螺仪Z轴原始值 */
signed int accel_x_adc; /* 加速度计X轴原始值 */
signed int accel_y_adc; /* 加速度计Y轴原始值 */
signed int accel_z_adc; /* 加速度计Z轴原始值 */
signed int temp_adc; /* 温度原始值 */

/* 下面是计算得到的实际值,扩大100倍 */
signed int gyro_x_act; /* 陀螺仪X轴实际值 */
signed int gyro_y_act; /* 陀螺仪Y轴实际值 */
signed int gyro_z_act; /* 陀螺仪Z轴实际值 */
signed int accel_x_act; /* 加速度计X轴实际值 */
signed int accel_y_act; /* 加速度计Y轴实际值 */
signed int accel_z_act; /* 加速度计Z轴实际值 */
signed int temp_act; /* 温度实际值 */
};

struct icm20608_dev_struc icm20608_dev; /* icm20608设备 */

icm20608.h定义了该模块的6轴数据寄存器地址和值。
连续顺序读写模块:前一个字节得写入寄存器地址,然后每次突发读取1字节数据,注意:这里不用每次都发送寄存器地址,顺序访问时,地址自动增长,即可顺序依次访问寄存器。如:向0x00~0x05地址依次发送6 byte数据,icm20608_read_len(0x00, buf, 6);

1
2
3
4
5
6
7
8
9
10
11
12
void icm20608_read_len(unsigned char reg, unsigned char *buf, unsigned char len) {  
unsigned char i;
/* ICM20608在使用SPI接口的时候寄存器地址,只有低7位有效,
* 寄存器地址最高位是读/写标志位读的时候要为1,写的时候要为0。
*/
reg |= 0x80;
ICM20608_CSN(0); /* 使能SPI传输 */
spich0_readwrite_byte(ECSPI3, reg); /* 发送寄存器地址 */
for(i = 0; i < len; i++) /* 顺序读取寄存器的值 */
buf[i] = spich0_readwrite_byte(ECSPI3, 0XFF);
ICM20608_CSN(1); /* 禁止SPI传输 */
}

icm20608_gyro_scaleget()icm20608_accel_scaleget()是获取陀螺仪和加速度计的最小单位:

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
float icm20608_gyro_scaleget(void) {
unsigned char data;
float gyroscale;
data = (icm20608_read_reg(ICM20_GYRO_CONFIG) >> 3) & 0X3;
switch(data) {
case 0:
gyroscale = 131;
break;
case 1:
gyroscale = 65.5;
break;
case 2:
gyroscale = 32.8;
break;
case 3:
gyroscale = 16.4;
break;
}
return gyroscale;
}

/*
* @description : 获取加速度计的分辨率
* @param : 无
* @return : 获取到的分辨率
*/
unsigned short icm20608_accel_scaleget(void) {
unsigned char data;
unsigned short accelscale;
data = (icm20608_read_reg(ICM20_ACCEL_CONFIG) >> 3) & 0X3;
switch(data) {
case 0:
accelscale = 16384;
break;
case 1:
accelscale = 8192;
break;
case 2:
accelscale = 4096;
break;
case 3:
accelscale = 2048;
break;
}
return accelscale;
}
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
/*
* @description : 读取ICM20608的加速度、陀螺仪和温度原始值
* @param : 无
* @return : 无
*/
void icm20608_getdata(void) {
float gyroscale;
unsigned short accescale;
unsigned char data[14];

icm20608_read_len(ICM20_ACCEL_XOUT_H, data, 14);

gyroscale = icm20608_gyro_scaleget();
accescale = icm20608_accel_scaleget();

icm20608_dev.accel_x_adc = (signed short)((data[0] << 8) | data[1]);
icm20608_dev.accel_y_adc = (signed short)((data[2] << 8) | data[3]);
icm20608_dev.accel_z_adc = (signed short)((data[4] << 8) | data[5]);
icm20608_dev.temp_adc = (signed short)((data[6] << 8) | data[7]);
icm20608_dev.gyro_x_adc = (signed short)((data[8] << 8) | data[9]);
icm20608_dev.gyro_y_adc = (signed short)((data[10] << 8) | data[11]);
icm20608_dev.gyro_z_adc = (signed short)((data[12] << 8) | data[13]);
/* 计算实际值 */
icm20608_dev.gyro_x_act = ((float)(icm20608_dev.gyro_x_adc) / gyroscale) * 100;
icm20608_dev.gyro_y_act = ((float)(icm20608_dev.gyro_y_adc) / gyroscale) * 100;
icm20608_dev.gyro_z_act = ((float)(icm20608_dev.gyro_z_adc) / gyroscale) * 100;

icm20608_dev.accel_x_act = ((float)(icm20608_dev.accel_x_adc) / accescale) * 100;
icm20608_dev.accel_y_act = ((float)(icm20608_dev.accel_y_adc) / accescale) * 100;
icm20608_dev.accel_z_act = ((float)(icm20608_dev.accel_z_adc) / accescale) * 100;

icm20608_dev.temp_act = (((float)(icm20608_dev.temp_adc) - 25 ) / 326.8 + 25) * 100;
}

由于前面设置的陀螺仪和加速度计量程都是拉满的设置的0x18,因此gyroscale读出来就是对应16.4(最小单位),accescale读出来就是对应2048(最小单位)
然后读出14 byte数据,组装成short类型数据,16位ADC, 一轴数据刚好16位数据。最后转成人眼直观的实际的陀螺仪和加速度计数据,放大了100倍,放大一百倍目的是为了能够将小数的部分也能记录下来。
以陀螺仪为例:量程位+-2000时,换算出16.4为。同理以加速度计为例:量程为+-16是,换算出2048为1g。

可以看到用到了浮点运算,那么IMX6ULL属于armv7,支持硬件浮点运算:执行浮点运算前调用imx6ul_hardfpu_enable()函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
* @description : 使能I.MX6U的硬件NEON和FPU
* @param : 无
* @return : 无
*/
void imx6ul_hardfpu_enable(void) {
uint32_t cpacr;
uint32_t fpexc;

/* 使能NEON和FPU */
cpacr = __get_CPACR();
cpacr = (cpacr & ~(CPACR_ASEDIS_Msk | CPACR_D32DIS_Msk))
| (3UL << CPACR_cp10_Pos) | (3UL << CPACR_cp11_Pos);
__set_CPACR(cpacr);
fpexc = __get_FPEXC();
fpexc |= 0x40000000UL;
__set_FPEXC(fpexc);
}

打开Cortex-A7 MPCore Technical Reference Manual4.3.34 Non-Secure Access Control Register介绍:开启硬件NEON和FPU
image
image
打开ARM®Architecture Reference Manual ARMv7-A and ARMv7-R edition介绍FPEXC寄存器, bit30置1,使能浮点运算
image

打开IM6ULL 参考手册:可见IMX6U支持浮点单元:
image
编译选项开启硬件浮点编译:

1
2
$(COBJS) : obj/%.o : %.c
`$(CC) -Wall **-march=armv7-a -mfpu=neon-vfpv4 -mfloat-abi=hard -Wa,-mimplicit-it=thumb** -nostdlib -fno-builtin -c -O2 $(INCLUDE) -o $@ $<`

2.3.1 测试效果#

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
/*
* @description : 指定的位置显示小数数据,比如5123,显示为51.23
* @param - x : X轴位置
* @param - y : Y轴位置
* @param - size: 字体大小
* @param - num : 要显示的数据,实际小数扩大100倍,
* @return : 无
*/
void decimals_display(unsigned short x, unsigned short y, unsigned char size, signed int num) {
signed int integ; /* 整数部分 */
signed int fract; /* 小数部分 */
signed int uncomptemp = num;
char buf[200];

if(num < 0)
uncomptemp = -uncomptemp;
integ = uncomptemp / 100;
fract = uncomptemp % 100;

memset(buf, 0, sizeof(buf));
if(num < 0)
sprintf(buf, "-%d.%d", integ, fract);
else
sprintf(buf, "%d.%d", integ, fract);
lcd_fill(x, y, x + 60, y + size, tftlcd_dev.backcolor);
lcd_show_string(x, y, 60, size, size, buf);
}

image
静止时,有一个z方向的加速度2048,也就是1g,刚好时重力加速度。静止时,陀螺仪几乎没有角速度,因此3轴数据都几乎为0°。

image
左右晃动时,陀螺仪数据明显增加。

imx6ull裸机-定时器

1 RTC定时器#

1.1 RTC定时器介绍#

RTC定时器被叫做实时时钟(real time clock)。 CPU内部有很多定时器,像看门狗WDT,PWM定时器,高精度定时器Timer等等, 只在“启动”即“通电时”运行,断电时停止。当然,如果时钟不能连续跟踪时间,则必须手动设置。那么当关机后就没办法自动计数统计时间了。
定时器的本质就是计数器,有向上计数,也有向下计数。RTC有一个与主机单独分离的电源,如纽扣电池(备用电池),即使主机电源关闭,它也保持计数定时功能。这也是为什么我们手机关机后时间还能保持准确。再比如以前的老诺基亚手机,拆掉电池就时间不准了,因为rtc电源被切断了,无法在计数,RTC定时器的计数器会被清0,需要手动设置当前时间。
RTC一般都是用纽扣电池给外部晶振和电路供电。
!image

1.2 RTC定时器原理#

以IMX6U芯片的RTC定时器为例,I.MX6U 内部也有 个 RTC 模块,但是不叫作“RTC”,而是叫做“SNVS”。
RTC模块结构图如下:
image
SNVS 分为两个子模块:SNVS_HP 和 SNVS_LP,也就是高功耗域(SNVS_HP)和低功耗域(SNVS_LP),这两个域的电源来源如下:

1
2
SNVS_LP:专用的 always-powered-on 电源域,系统主电源和备用电源都可以为其供电。
SNVS_HP:系统(芯片)电源。

系统主电源断电以后 SNVS_HP 也会断电,但是在备用电源支持下,SNVS_LP 是不会断电的,而且 SNVS_LP 是和芯片复位隔离开的,因此 SNVS_LP 相关的寄存器的值会一直保存着, 也就是low Power Domain是不受系统电源影响。
上图各个序号含义如下:

1
2
3
4
1. VDD_HIGH_IN 是系统(芯片)主电源,这个电源会同时供给给 SNVS_HP 和 SNVS_LP。
2. VDD_SNVS_IN 是纽扣电池供电的电源,这个电源只会供给给 SNVS_LP,保证在系统主电源 VDD_HIGH_IN 掉电以后 SNVS_LP 会继续运行。
3. SNVS_HP 部分。
4. SNVS_LP 部分,此部分有个 SRTC,这个就是要使用的 RTC。

SRTC 需要外界提供一个 32.768KHz 的时钟,I.MX6U-ALPHA 核心板上的 32.768KHz 的晶振就是提供这个时钟的。
image

1.3 RTC定时器寄存器#

1
2
3
4
5
SNVS_SRTCMR[14:0]代表SRTC计数器的高15
SNVS_SRTCLR[31:15]代表SRTC计数器的低17
注意:是以 197011000秒为起点,加上经过的总秒数即可得到现在的时间点。
SNVS_HPCOMR[31], NPSWA_EN位,非特权软件访问控制位,如果非特权软件要访问 SNVS 的话此位必须为 1
SNVS_LPCR[0], SRTC_ENV位,使能 STC 计数器。

1.4 RTC裸机源码展示#

NXP 官方 SDK 包是针对 I.MX6ULL 编写的,因此文件 MCIMX6Y2.h中的结构体 SNVS_Type 里面的寄存器是不全的,我们需要在其中加入本章实验所需要的寄存器,修改 SNVS_Type 为如下所示:

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
/*!
* @addtogroup SNVS_Peripheral_Access_Layer SNVS Peripheral Access Layer
* @{
*/
/** SNVS - Register Layout Typedef */
typedef struct {
__IO uint32_t HPLR; /**< SNVS_HP Lock register, offset: 0x0 */
__IO uint32_t HPCOMR; /**< SNVS_HP Command register, offset: 0x4 */
__IO uint32_t HPCR; /**< SNVS_HP Control register, offset: 0x8 */
__IO uint32_t HPSICR; /**< SNVS_HP Control register, offset: 0x8 */
__IO uint32_t HPSVCR;
__IO uint32_t HPSR;
__IO uint32_t HPSVSR;
__IO uint32_t HPHACIVR;
__IO uint32_t HPHACR;
__IO uint32_t HPRTCMR;
__IO uint32_t HPRTCLR;
__IO uint32_t HPTAMR;
__IO uint32_t HPTALR;
__IO uint32_t LPLR;
__IO uint32_t LPCR;
__IO uint32_t LPMKCR;
__IO uint32_t LPSVCR;
__IO uint32_t LPTGFCR;
__IO uint32_t LPTDCR;
__IO uint32_t LPSR;
__IO uint32_t LPSRTCMR;
__IO uint32_t LPSRTCLR;
__IO uint32_t LPTAR;
__IO uint32_t LPSMCMR;
__IO uint32_t LPSMCLR;
}SNVS_Type;
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
#ifndef _BSP_RTC_H
#define _BSP_RTC_H
#include "imx6ul.h"

/* 相关宏定义 */
#define SECONDS_IN_A_DAY (86400) /* 一天86400秒 */
#define SECONDS_IN_A_HOUR (3600) /* 一个小时3600秒 */
#define SECONDS_IN_A_MINUTE (60) /* 一分钟60秒 */
#define DAYS_IN_A_YEAR (365) /* 一年365天 */
#define YEAR_RANGE_START (1970) /* 开始年份1970年 */
#define YEAR_RANGE_END (2099) /* 结束年份2099年 */

/* 时间日期结构体 */
struct rtc_datetime {
unsigned short year; /* 范围为:1970 ~ 2099 */
unsigned char month; /* 范围为:1 ~ 12 */
unsigned char day; /* 范围为:1 ~ 31 (不同的月,天数不同).*/
unsigned char hour; /* 范围为:0 ~ 23 */
unsigned char minute; /* 范围为:0 ~ 59 */
unsigned char second; /* 范围为:0 ~ 59 */
};

/* 函数声明 */
void rtc_init(void);
void rtc_enable(void);
void rtc_disable(void);
unsigned int rtc_coverdate_to_seconds(struct rtc_datetime *datetime);
unsigned int rtc_getseconds(void);
void rtc_setdatetime(struct rtc_datetime *datetime);
void rtc_getdatetime(struct rtc_datetime *datetime);
#endif
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
#include "bsp_rtc.h"
#include "stdio.h"

void rtc_init(void) {
/*
* 设置HPCOMR寄存器
* bit[31] 1 : 允许访问SNVS寄存器,一定要置1
* bit[8] 1 : 此位置1,需要签署NDA协议才能看到此位的详细说明,
* 这里不置1也没问题
*/
SNVS->HPCOMR |= (1 << 31) | (1 << 8);

#if 0
struct rtc_datetime rtcdate;
rtcdate.year = 2018U;
rtcdate.month = 12U;
rtcdate.day = 13U;
rtcdate.hour = 14U;
rtcdate.minute = 52;
rtcdate.second = 0;
rtc_setDatetime(&rtcdate); //初始化时间和日期
#endif
rtc_enable(); //使能RTC
}

void rtc_enable(void) {
/*
* LPCR寄存器bit0置1,使能RTC
*/
SNVS->LPCR |= 1 << 0;
while(!(SNVS->LPCR & 0X01));//等待使能完成

}

void rtc_disable(void) {
/*
* LPCR寄存器bit0置0,关闭RTC
*/
SNVS->LPCR &= ~(1 << 0);
while(SNVS->LPCR & 0X01);//等待关闭完成
}

/*
* @description : 判断指定年份是否为闰年,闰年条件如下:
* @param - year: 要判断的年份
* @return : 1 是闰年,0 不是闰年
*/
unsigned char rtc_isleapyear(unsigned short year) {
unsigned char value=0;

if(year % 400 == 0)
value = 1;
else {
if((year % 4 == 0) && (year % 100 != 0))
value = 1;
else
value = 0;
}
return value;
}

/*
* @description : 将时间转换为秒数
* @param - datetime: 要转换日期和时间。
* @return : 转换后的秒数
*/
unsigned int rtc_coverdate_to_seconds(struct rtc_datetime *datetime) {
unsigned short i = 0;
unsigned int seconds = 0;
unsigned int days = 0;
unsigned short monthdays[] = {0U, 0U, 31U, 59U, 90U, 120U, 151U, 181U, 212U, 243U, 273U, 304U, 334U};

for(i = 1970; i < datetime->year; i++) {
days += DAYS_IN_A_YEAR; /* 平年,每年365天 */
if(rtc_isleapyear(i)) days += 1;/* 闰年多加一天 */
}

days += monthdays[datetime->month];
if(rtc_isleapyear(i) && (datetime->month >= 3)) days += 1;/* 闰年,并且当前月份大于等于3月的话加一天 */

days += datetime->day - 1;
seconds = days * SECONDS_IN_A_DAY +
datetime->hour * SECONDS_IN_A_HOUR +
datetime->minute * SECONDS_IN_A_MINUTE +
datetime->second;
return seconds;
}

/*
* @description : 设置时间和日期
* @param - datetime: 要设置的日期和时间
* @return : 无
*/
void rtc_setdatetime(struct rtc_datetime *datetime) {
unsigned int seconds = 0;
unsigned int tmp = SNVS->LPCR;
rtc_disable(); /* 设置寄存器HPRTCMR和HPRTCLR的时候一定要先关闭RTC */
/* 先将时间转换为秒 */
seconds = rtc_coverdate_to_seconds(datetime);
SNVS->LPSRTCMR = (unsigned int)(seconds >> 17); /* 设置高16位 */
SNVS->LPSRTCLR = (unsigned int)(seconds << 15); /* 设置地16位 */
/* 如果此前RTC是打开的在设置完RTC时间以后需要重新打开RTC */
if (tmp & 0x1)
rtc_enable();
}

/*
* @description : 将秒数转换为时间
* @param - seconds : 要转换的秒数
* @param - datetime: 转换后的日期和时间
* @return : 无
*/
void rtc_convertseconds_to_datetime(u64 seconds, struct rtc_datetime *datetime) {
u64 x;
u64 secondsRemaining, days;
unsigned short daysInYear;

/* 每个月的天数 */
unsigned char daysPerMonth[] = {0U, 31U, 28U, 31U, 30U, 31U, 30U, 31U, 31U, 30U, 31U, 30U, 31U};
secondsRemaining = seconds; /* 剩余秒数初始化 */
days = secondsRemaining / SECONDS_IN_A_DAY + 1; /* 根据秒数计算天数,加1是当前天数 */
secondsRemaining = secondsRemaining % SECONDS_IN_A_DAY; /*计算天数以后剩余的秒数 */
/* 计算时、分、秒 */
datetime->hour = secondsRemaining / SECONDS_IN_A_HOUR;
secondsRemaining = secondsRemaining % SECONDS_IN_A_HOUR;
datetime->minute = secondsRemaining / 60;
datetime->second = secondsRemaining % SECONDS_IN_A_MINUTE;
/* 计算年 */
daysInYear = DAYS_IN_A_YEAR;
datetime->year = YEAR_RANGE_START;
while(days > daysInYear) {
/* 根据天数计算年 */
days -= daysInYear;
datetime->year++;
/* 处理闰年 */
if (!rtc_isleapyear(datetime->year))
daysInYear = DAYS_IN_A_YEAR;
else /*闰年,天数加一 */
daysInYear = DAYS_IN_A_YEAR + 1;
}
/*根据剩余的天数计算月份 */
if(rtc_isleapyear(datetime->year)) /* 如果是闰年的话2月加一天 */
daysPerMonth[2] = 29;

for(x = 1; x <= 12; x++) {
if (days <= daysPerMonth[x]) {
datetime->month = x;
break;
} else {
days -= daysPerMonth[x];
}
}
datetime->day = days;
}

/*
* @description : 获取RTC当前秒数
* @param : 无
* @return : 当前秒数
*/
unsigned int rtc_getseconds(void) {
unsigned int seconds = 0;
seconds = (SNVS->LPSRTCMR << 17) | (SNVS->LPSRTCLR >> 15);
return seconds;
}

/*
* @description : 获取当前时间
* @param - datetime: 获取到的时间,日期等参数
* @return : 无
*/
void rtc_getdatetime(struct rtc_datetime *datetime) {
//unsigned int seconds = 0;
u64 seconds;
seconds = rtc_getseconds();
rtc_convertseconds_to_datetime(seconds, datetime);
}

可以看到RTC定时器是以秒为计时单位的,每过1s SRTC计数器的值加1。
首先调用rtc_init初始化并启动,然后调用rtc_setdatetime设定当前日期时间,调用rtc_getdatetime获取当前日期时间,期间会利用rtc_convertseconds_to_datetime把总秒数转换成当前的日期和时间。

2 PWM定时器#

2.1 pwm定时器介绍#

imx6ull一共有 8 路 PWM 信号,每个 PWM 包含一个 16 位的计数器和一个 4 x 16 的数据 FIFO。一路框图如下:
image

1
2
3
4
5
6
7
①、此部分是一个选择器,用于选择 PWM 信号的时钟源,一共有三种时钟源:ipg_clk,pg_clk_highfreq 和 ipg_clk_32k。
②、这是一个 12 位的分频器,可以对①中选择的时钟源进行分频。
③、这是 PWM 的 16 位计数器寄存器,保存着 PWM 的计数值。
④、这是 PWM 的 16 位周期寄存器,此寄存器用来控制 PWM 的频率。
⑤、这是 PWM 的 16 位采样寄存器,此寄存器用来控制 PWM 的占空比。
⑥、此部分是 PWM 的中断信号,PWM 是提供中断功能的,如果使能了相应的中断的话就会产生中断。
⑦、此部分是 PWM 对应的输出 IO,产生的 PWM 信号就会从对应的 IO 中输出。

2.2 PWM控制器#

2.2.1 PWMx_PWMPR寄存器-周期设置#

PWM 的 16 位计数器是个上计数器,此计数器会从 0X0000 开始计数,直到计数值等于寄存器PWMx_PWMPR(x=1~8)+ 1,然后计数器就会重新从0X0000 开始计数,如此往复。PWMx_PWMPR设置频率。PWM周期公式如下:
PWM_FRE = PWM_CLK / (PERIOD + 2)
也就是PWMO(Hz) = PCLK(Hz) / (PERIOD + 2)
image

比如当前PWM_CLK=1MHz, 要产生1KHz的PWM,那么PERIOD = 1000000/1K - 2 = 998。,如下设置1000,即可得到PERIOD=998,也就是1khz.

1
2
3
4
5
6
7
8
void pwm1_setperiod_value(unsigned int value) {
unsigned int regvalue = 0;
if(value < 2)
regvalue = 2;
else
regvalue = value - 2;
PWM1->PWMPR = (regvalue & 0XFFFF);
}

2.2.2 PWMx_PWMSAR寄存器-占空比#

设置Sample采样寄存器,Sample数据会写入到FIFO中。当计数器的值小于 SAMPLE 的时候输出高电平(或低电平)。当计数器值大于等于 SAMPLE,小于寄存器PWM1_PWMPR 的 PERIO 的时候输出低电平(或高电平)。
假如我们要设置 PWM 信号的占空比为 50%,那么就可以将 SAMPLE 设置为(PERIOD + 2) / 2 = 1000 / 2=500。
image

如下设置50,即可得到sample=500,也就是占空比50%.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct backlight_dev_struc {	
unsigned char pwm_duty; /* 占空比 */
};
struct backlight_dev_struc backlight_dev;
void pwm1_setsample_value(unsigned int value) {
PWM1->PWMSAR = (value & 0XFFFF);
}
void pwm1_setduty(unsigned char duty) {
unsigned short preiod;
unsigned short sample;
backlight_dev.pwm_duty = duty;
preiod = PWM1->PWMPR + 2;
sample = preiod * backlight_dev.pwm_duty / 100;
pwm1_setsample_value(sample);
}

2.2.3 PWMCR 控制寄存器#

image

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
FWM(bit27:26):FIFO 水位线,用来设置 FIFO 空余位置为多少的时候表示 FIFO 为空。
设置为 0 的时候表示 FIFO 空余位置大于等于 1 的时候 FIFO 为空;
设置为 1 的时候表示 FIFO 空余位置大于等于 2 的时候 FIFO 为空;
设置为 2 的时候表示 FIFO 空余位置大于等于 3 的时候FIFO 为空;
设置为 3 的时候表示 FIFO 空余位置大于等于 4 的时候 FIFO 为空。
STOPEN(bit25):此位用来设置停止模式下 PWM 是否工作,为 0 的话表示在停止模式下PWM 不工作,为 1 的话表示停止模式下激活 PWM。
DOZEN(bit24):此位用来设置休眠模式下 PWM 是否工作,为 0 的话表示在休眠模式下PWM 不工作,为 1 的话表示休眠模式下激活 PWM。
WAITEN(bit23):此位用来设置等待模式下 PWM 是否工作,为 0 的话表示在等待模式下PWM 不工作,为 1 的话表示等待模式下激活 PWM。
DEGEN(bit22):此位用来设置调试模式下 PWM 是否工作,为 0 的话表示在调试模式下PWM 不工作,为 1 的话表示调试模式下激活 PWM。
BCTR(bit21):字节交换控制位,用来控制 16 位的数据进入 FIFO 的字节顺序。为 0 的时候不进行字节交换,为 1 的时候进行字节交换。
HCRT(bit20):半字交换控制位,用来决定从 32 位 IP 总线接口传输来的哪个半字数据写入采样寄存器的低 16 位中。
POUTC(bit19:18):PWM 输出控制控制位,用来设置 PWM 输出模式,
0 的时候表示PWM 先输出高电平,当计数器值和采样值相等的话就输出低电平。
1 的时候相反,当为 2 或者 3 的时候 PWM 信号不输出。本章我们设置为 0
也就是一开始输出高电平,当计数器值和采样值相等的话就改为低电平,这样采样值越大高电平时间就越长,占空比就越大。
CLKSRC(bit17:16):PWM 时钟源选择,
0 的话关闭;
1 的话选择 ipg_clk 为时钟源;
2 的话选择 ipg_clk_highfreq 为时钟源;
3 的话选择 ipg_clk_32k 为时钟源。本章我们设置为 1,也就是选择 ipg_clk 为 PWM 的时钟源,因此 PWM 时钟源频率为 66MHz。
PRESCALER(bit15:4):分频值,可设置为 0~4095,对应着 1~4096 分频。
SWR(bit3):软件复位,向此位写 1 就复位 PWM,此位是自清零的,当复位完成以后此位会自动清零。
REPEAT(bit2:1):重复采样设置,此位用来设置 FIFO 中的每个数据能用几次。
可设置 0~3,分别表示 FIFO 中的每个数据能用 1~4 次。本章我们设置为 0,即 FIFO 中的每个数据只能用一次。
EN(bit0):PWM 使能位,为 1 的时候使能 PWM,为 0 的时候关闭 PWM。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void pwm1_enable(void) {
PWM1->PWMCR |= 1 << 0;
}
void pwm1_init(void) {
PWM1->PWMCR = 0; /* 寄存器先清零 */
PWM1->PWMCR |= (1 << 26) | (1 << 16) | (65 << 4);

/* 设置PWM周期为1000,那么PWM频率就是1M/1000 = 1KHz。 */
pwm1_setperiod_value(1000);

/* 设置占空比,默认50%占空比 ,写四次是因为有4个FIFO */
backlight_dev.pwm_duty = 50;
for(i = 0; i < 4; i++) {
pwm1_setduty(backlight_dev.pwm_duty);
}

/* 使能FIFO空中断,设置寄存器PWMIR寄存器的bit0为1 */
PWM1->PWMIR |= 1 << 0;
system_register_irqhandler(PWM1_IRQn, (system_irq_handler_t)pwm1_irqhandler, NULL); /* 注册中断服务函数 */
GIC_EnableIRQ(PWM1_IRQn); /* 使能GIC中对应的中断 */
PWM1->PWMSR = 0; /* PWM中断状态寄存器清零 */

pwm1_enable(); /* 使能PWM1 */
}
1
2
3
4
5
6
7
8
9
10
11
12
13
bit[27:26]	: 01  当FIFO中空余位置大于等于2的时候FIFO空标志值位
bit[25] :0 停止模式下PWM不工作
bit[24] : 0 休眠模式下PWM不工作
bit[23] : 0 等待模式下PWM不工作
bit[22] : 0 调试模式下PWM不工作
it[21] : 0 关闭字节交换
bit[20] : 0 关闭半字数据交换
bit[19:18] : 00 PWM输出引脚在计数器重新计数的时候输出高电平,在计数器计数值达到比较值以后输出低电平
bit[17:16] : 01 PWM时钟源选择IPG CLK = 66MHz
bit[15:4] : 65 分频系数为65+1=66,PWM时钟源 = 66MHZ/66=1MHz
bit[3] : 0 PWM不复位
bit[2:1] : 00 FIFO中的sample数据每个只能使用一次。
bit[0] : 0 先关闭PWM,后面再使能

2.2.4 PWM1_PWMIR中断控制寄存器#

image
CIE(bit2):比较中断使能位,为 1 的时候使能比较中断,为 0 的时候关闭比较中断。
RIE(bit1):翻转中断使能位,当计数器值等于采样值并回滚到 0X0000 的时候就会产生此中断,为 1 的时候使能翻转中断,为 0 的时候关闭翻转中断。
FIE(bit0):FIFO 空中断,为 1 的时候使能,为 0 的时候关闭。前面代码写的是使能FIFO空中断.

1
2
/* 使能FIFO空中断,设置寄存器PWMIR寄存器的bit0为1 */
PWM1->PWMIR |= 1 << 0;

2.2.5 PWM1_PWMSR 状态寄存器#

image
FWE(bit6):FIFO 写错误事件,为 1 的时候表示发生了 FIFO 写错误。
CMP(bit5):FIFO 比较事件发标志位,为 1 的时候表示发生 FIFO 比较事件。
ROV(bit4):翻转事件标志位,为 1 的话表示翻转事件发生。
FE(bit3):FIFO 空标志位,为 1 的时候表示 FIFO 位空。
FIFOAV(bit2:0):此位记录 FIFO 中的有效数据个数,有效值为 04,分别表示 FIFO 中有04 个有效数据

初始化先清0,中断服务程序读取状态,并且清中断。FIFO 中的采样值每个周期都会少一个,所以需要不断的向 FIFO 中写入采样值,防止其为空。我们可以使能 FIFO 空中断,这样当 FIFO 为空的时候就会触发相应的中断,然后在中断处理函数中向 FIFO 写入采样值。

1
2
3
4
5
6
7
8
9
10
11
12
13
void pwm1_irqhandler(void) {
if(PWM1->PWMSR & (1 << 3)) /* FIFO为空中断 */
{
/* 将占空比信息写入到FIFO中,其实就是设置占空比 */
pwm1_setduty(backlight_dev.pwm_duty);
PWM1->PWMSR |= (1 << 3); /* 写1清除中断标志位 */
}
}

system_register_irqhandler(PWM1_IRQn, (system_irq_handler_t)pwm1_irqhandler, NULL); /* 注册中断服务函数 */
GIC_EnableIRQ(PWM1_IRQn); /* 使能GIC中对应的中断 */
PWM1->PWMSR = 0; /* PWM中断状态寄存器清零 */
pwm1_enable(); /* 使能PWM1 */

2.3 测试#

初始化时设置占空比为50%,测试代码读取按键,每次该按键按下就对占空比加10%,如果占空比超过100%,重新从10%开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
while(1) {
keyvalue = key_getvalue();
if(keyvalue == KEY0_VALUE)
{
duty += 10; /* 占空比加10% */
if(duty > 100) /* 如果占空比超过100%,重新从10%开始 */
duty = 10;
lcd_shownum(50 + 72, 90, duty, 3, 16);
pwm1_setduty(duty); /* 设置占空比 */
}

delayms(10);
}

占空比10%时亮度波形如下,亮度很暗。
image
image
占空比90%时亮度如下:
image

imx6ull裸机-ADC

1 IMX6ULL寄存器#

1.1 ADCx_CFG(x=1~2) 配置寄存器#

image
OVWREN (bit16):数据复写使能位,为 1 的时候使能复写功能,为 0 的时候关闭复写功能。
AVGS(bit15:14):硬件平均次数,只有当 ADC1_GC 寄存器的 AVGE 位为 1 的时候才有效
image
ADTRG(bit13):转换触发选择。为 0 的时候选择软件触发,为 1 的时候,不选择软件触发。
REFSEL(bit12:11):参考电压选择,为 00 时选择 VREFH/VREFL 这两个引脚上的电压为参考电压,正点原子 ALPHA 开发板上 VREFH 为 3.3V,VREFL 为 0V。
ADHSC(bit10):高速转换使能位,当为 0 时为正常模式,为 1 时为高速模式。
ADSTS(bit9:8):设置 ADC 的采样周期,与 ADLSMP 位一起决定采样周期:
image
image
ADLSMP(bit4):长采样周期使能位,当值为 0 时为短采样周期模式,为 1 时为长采样周期模式。搭配 ADSTS 位一起控制 ADC 的采样周期。
MODE(bit3:2):选择转换精度:
image
ADICLK(bit1:0):输入时钟源选择,为 00 的时候选择 IPG Clock,为 01 的时候选择 IPG Clock/2,为 10 的时候无效,为 11 的时候选择呢 ADACK。本教程我们设置为 11,也就是选择ADACK 为 ADC 的时钟源。

1.2 ADCx_GC 通用控制寄存器#

image
CAL(bit7):当该位写入 1 时,硬件校准功能将会启动,校准过程中该位会一直保持 1,校准完成后会清 0,校准完成后需要检查一下ADC_GS[CALF]位,确认校准结果。
ADCO(bit6):连续转换使能位,只有在开启了硬件平均功能时有效,为 0 时只能转换一次或一组,当 ADCO 为 1 时可以连续转换或多组。
AVGE(bit5):硬件平均使能位。为 0 时关闭,为 1 时使能。
ACFE(bit4):比较功能使能位。为 0 时关闭,为 1 时使能。
ACFGT(bit3):配置比较方法,如果为 0 的话就比较转换结果是否小于 ADC_CV 寄存器值,如果为 1 的话就比较装换结果是否大于或等于 ADC_CV 寄存器值。
ACREN(bit2):范围比较功能使能位。为 0 的话仅和 ADC_CV 里的 CV1 比较,为 1 的话和 ADC_CV 里的 CV1、CV2 比较。
ACREN(bit2):范围比较功能使能位。为 0 的话仅和 ADC_CV 里的 CV1 比较,为 1 的话和 ADC_CV 里的 CV1、CV2 比较。
DMAEN(bit1):DMA 功能使能位,为 0 是关闭,为 1 是开启
ADACKEN(bit0):异步时钟输出使能位,为 0 是关闭,为 1 时开启

1.3 ADCx_GS 通用状态寄存器#

image
AWKST(bit2):异步唤醒中断状态,为 1 时表示发生了异步唤醒中断。为 0 时没有发生异步中断。
CALF(bit1):校准失败标志位,为 0 的时候表示校准正常完成,为 1 的时候表示校准失败。
ADACT(bit0):转换活动标志,为 0 的时候表示转换没有进行,为 1 的时候表示正在进行转换。

1.4 ADCx_HS 状态寄存器#

COCO0表示转换完成.

COCO0(bit0):每次转换完成此位就会被置 1。

1.5 ADCx_HC0 控制寄存器#

image
AIEN(bit7):转换完成中断控制位,为 1 的时候打开转换完成中断,为 0 的时候关闭。
ADCH(bit4:0):转换通道选择,可以设置为 00000~01111 分别对应通道 0~15。11001 为内部通道,用于 ADC 自测。

1.6 ADCx_R0 数据寄存器#

image

2 流程代码#

1
2
3
4
5
6
7
8
1、初始化 ADC1_CH1
//初始化 ADC1_CH1,配置 ADC 位数,时钟源,采样时间等。
2、校准 ADC
//ADC 在使用之前需要校准一次。
3、使能 ADC
//配置好 ADC 以后就可以开启了。
4、读取 ADC 值
//ADC 正常工作以后就可以读取 ADC 值。

2.1 初始化#

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
void adc1ch1_init(void) {
ADC1->CFG = 0;
ADC1->CFG |= (2 << 2) | (3 << 0);
ADC1->GC = 0;
ADC1->GC |= 1 << 0;
}
/* CFG寄存器
* bit16 0 关闭复写功能
* bit15:14 00 硬件平均设置为默认值,00的时候4次平均,
* 但是得ADC_GC寄存器的AVGE位置1来使能硬件平均
* bit13 0 软件触发
* bit12:1 00 参考电压为VREFH/VREFL,也就是3.3V/0V
* bit10 0 正常转换速度
* bit9:8 00 采样时间2/12,ADLSMP=0(短采样)的时候为2个周期
* ADLSMP=1(长采样)的时候为12个周期
* bit7 0 非低功耗模式
* bit6:5 00 ADC时钟源1分频
* bit4 0 短采样
* bit3:2 10 12位ADC
* bit1:0 11 ADC时钟源选择ADACK
*/
/* GC寄存器
* bit7 0 先关闭校准功能,后面会校准
* bit6 0 关闭持续转换
* bit5 0 关闭硬件平均功能
* bit4 0 关闭比较功能
* bit3 0 关闭比较的Greater Than功能
* bit2 0 关闭比较的Range功能
* bit1 0 关闭DMA
* bit0 1 使能ADACK
*/

2.2 自动校准#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
status_t adc1_autocalibration(void) {
status_t ret = kStatus_Success;
ADC1->GS |= (1 << 2); /* 清除CALF位,写1清零 */
ADC1->GC |= (1 << 7); /* 使能校准功能 */

/* 校准完成之前GC寄存器的CAL位会一直为1,直到校准完成此位自动清零 */
while((ADC1->GC & (1 << 7)) != 0) {
/* 如果GS寄存器的CALF位为1的话表示校准失败 */
if((ADC1->GS & (1 << 2)) != 0) {
ret = kStatus_Fail;
break;
}
}
/* 校准成功以后HS寄存器的COCO0位会置1 */
if((ADC1->HS & (1 << 0)) == 0)
ret = kStatus_Fail;
/* 如果GS寄存器的CALF位为1的话表示校准失败 */
if((ADC1->GS & (1 << 2)) != 0)
ret = kStatus_Fail;
return ret;
}

2.3 获取ADC原始值#

1
2
3
4
5
6
7
unsigned int getadc_value(void) {
/* 配置ADC通道1 */
ADC1->HC[0] = 0; /* 关闭转换结束中断 */
ADC1->HC[0] |= (1 << 0); /* 通道1 */
while((ADC1->HS & (1 << 0)) == 0); /* 等待转换完成 */
return ADC1->R[0]; /* 返回ADC值 */
}

2.4 获取ADC原始值(多次取平均)#

1
2
3
4
5
6
7
8
9
unsigned short getadc_average(unsigned char times) {
unsigned int temp_val = 0;
unsigned char t;
for(t = 0; t < times; t++){
temp_val += getadc_value();
delayms(5);
}
return temp_val / times;
}

2.5 获取模数转换后的电压#

由于精度为12 bit, ADC范围为[0, 4095]。同时电压满输出时为3.3v,因此当ADC数据拉满,得到3300mv,也就是3.3v

1
2
3
4
5
6
7
unsigned short getadc_volt(void) {
unsigned int adcvalue=0;
unsigned int ret = 0;
adcvalue = getadc_average(5);
ret = (float)adcvalue * (3300.0f / 4096.0f); /* 获取计算后的带小数的实际电压值 */
return ret;
}

s3c2440裸机编程-电阻触摸屏

1 电阻触摸屏原理#

触摸屏包含上下叠合的两个透明层,一般覆盖在lcd表面,两个透明层是由均匀的电阻介质组成,如下图:

当触摸屏表面受到的压力(如通过笔尖或手指进行按压)足够大时,顶层与底层之间的薄膜会产生接触,此时会形成x方向和y方向的坐标。那么x,y坐标的值是怎么得来的呢?本质上就是通过ADC转换得来的。

触摸屏的等效电路可以看成如下图:

计算触点的X,Y坐标分为如下两步:

1.1 计算Y坐标#

在Y+电极施加驱动电压Vdrive, Y-电极接地,由于上下两层膜形成触点,X+做为触点的引出端,测量得到接触点的电压,触点电压与Vdrive电压之比等于触点Y坐标与屏高度之比。如下图:

1.2 计算X坐标#

在X+电极施加驱动电压Vdrive, X-电极接地,由于上下两层膜形成触点,Y+做为触点的引出端,测量得到接触点的电压,Y+做为引出端测量得到接触点的电压,触点电压与Vdrive电压之比等于触点X坐标与屏宽度之比。如下图:

2 电阻触摸屏的几种模式#

2.1 等待中断模式#

平时的时候上下两层膜并不粘在一起,我们把这种状态称为“等待中断模式”, 等效电路如下图的右边那幅图:

s5、s4闭合,s1、s2、s3断开,这个时候Y_ADC/XP通过S5接上拉电阻,处于高电平状态,X_ADC/YP接地。没法读取x,y坐标。

2.2 读取x坐标模式#

给X方向通电,也就是让S1、S3开关闭合,s2、s4断开,那么当屏幕按下,触点YP的电平就对应x坐标。(XP到XM之间是均匀的电阻介质)

x_adc电压/vcc = x坐标/width, 所以x坐标= width * x_adc电压/vcc

2.3 读取y坐标模式#

给Y方向通电,也就是让S2、S4开关闭合,s1、s3断开,那么当屏幕按下,触点XP的电平就对应y坐标。(YP到YM之间是均匀的电阻介质)

y_adc电压/vcc = Y坐标/height, 所以y坐标= height * y_adc电压/vcc

2.4 TS中断流程#

总结一下单次触发TS中断,使用触摸屏的流程:

1
2
3
4
5
1. 按下触摸屏,产生TS中断
2. 启动ADC(目的是获取x,y方向上的坐标值)
3. ADC转换完成,产生adc中断(adc转换需要一定的时间)
4. ADC中断中来读取x y坐标
5. 松开,结束

我们知道,现在的手机都是支持屏幕滑动翻页和长按的功能。那么这些功能是如何做到的呢?

2.4.1 中断加入定时器#

如何让触摸屏支持长按或者滑动操作(多次触发TS中断)?

答案:定时器,当长按屏幕,会产生多次TS中断,因此我们需要用定时器来判断,当定时一段时间后,还有TS中断产生,那么我们认为是长按操作,进行中断响应。滑动也是类似的道理,当定时时间到后,如果还有TS中断产生,且坐标发生了改变,就认为是滑动操作。

<5> 启动定时器
<6> 一段时间后,定时器中断发生,判断触摸屏是否仍被按下(是否有定时器中断产生),如果有就循环上述过程<2><3><4><5>

可以用如下流程图概括TSC的整个SW flow.

2.4.2 带定时器的TS中断处理流程#

image

3 触摸屏接口模式#

3.1 Normal Conversion Mode#

正常转换模式,一般情况下可以配置ADCCON和ADCDAT0来读取数据。

3.2 Separate X/Y position conversion Mode#


x,y坐标分离转换格式,x坐标会写入ADCDAT0, y坐标会写入ADCDAT1,所以会产生2次中断开分开完成x,y的坐标转换。

3.3 Auto(Sequential) X/Y Position Conversion Mode#

自动转换模式,当触摸屏按下后,会一次性对x,y方向的坐标进行转换,x坐标会写入ADCDAT0, x坐标会写入ADCDAT1。会产生一次中断进行x,y坐标的自动转换。

3.4 Waiting for Interrupt Mode#

等待中断模式 。可以设置rADCTSC=0xd3;也就是对应下图寄存器 // XP_PU, XP_Dis, XM_Dis, YP_Dis, YM_En.当产生中断信号(INT_TC)后,等待中断模式必须清除.(即XY_PST sets to the No operation Mode).

4 触摸屏控制器#

4.1 TS控制寄存器#

电阻触摸屏的原理本质上就是ADC,ADC相关寄存器介绍详见s3c2440裸机-ADC编程或者s3c2440裸机编程-ADC | Hexo (fuzidage.github.io)
TSC相比ADC多了一个ADCTSC寄存器,如下图:
image
当bit[2]=0,normal mode时,那么bit[1:0]需要配置成01或者10进行手工测量x,y.
当bit[2]=1,auto mode时,那么bit[1:0]需要配置成0,进行自动测量。

4.2 DATA寄存器#

4.2.1 x坐标ADCDATA0#

image

4.2.2 y坐标ADCDATA1#

image

4.3 松开按下检测寄存器#

这个寄存器可以检测是否有触摸中断产生,是按下触摸屏了,还是松开触摸屏了。
image

5 触摸屏编程示例#

5.1 ADC中断产生#

img

5.1.1 中断源#

ADC和TSC共用一个中断源,如下:

img

SRCPND表示哪个中断源产生了中断请求。

img

img

5.1.2 中断模式#

img

img

5.1.3 中断屏蔽寄存器#

img

img

5.1.4 中断挂起寄存器#

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

img

img

从SRCPND寄存器可以读到ADC和TSC复用的同一个中断源,那么如何区分呢?

可以从SUBSRCPND寄存器配置,如下:

5.1.4.1 SUBSRCPND寄存器#

img

img

当bit 9被置1时,表示TSC中断。那么我们需要打开subsrcmask寄存器:

5.1.4.2 INTSUBMSK寄存器#

img

所以TSC中断的产生流程如下:

img

5.2 TS触摸屏编程流程#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1. 初始化TSC,ADCTSC寄存器
2. 设定TSC处于“等待中断模式”
3. 使能TSC中断
      INTSUBMSK
      MSK/MODE
4. 按下,进入TSC中断
      进入自动采集转换模式
      启动ADC
5. ADC中断
      读数据
      再次进入”等待中断模式“
      启动定时器(为了处理长按或者滑动操作)
6. 定时器中断
      若松开,结束
      如任然按下,进入步骤4的启动ADC流程

img

5.2.1 初始化#

1
2
3
4
5
6
7
8
void touchscreen_init(void) {
/* 设置触摸屏接口:寄存器 */
adc_ts_reg_init();
/* 设置中断 */
adc_ts_int_init();
/* 让触摸屏控制器进入"等待中断模式" */
enter_wait_pen_down_mode();
}

5.2.1.1 ts寄存器初始化#

主要是设置预分频,产生ADC clk = 1MHz。

1
2
3
4
5
6
7
8
9
10
11
void adc_ts_reg_init(void) {
/* [15] : ECFLG, 1 = End of A/D conversion
* [14] : PRSCEN, 1 = A/D converter prescaler enable
* [13:6]: PRSCVL, adc clk = PCLK / (PRSCVL + 1)
* [5:3] : SEL_MUX, 000 = AIN 0
* [2] : STDBM
* [0] : 1 = A/D conversion starts and this bit is cleared after the startup.
*/
ADCCON = (1<<14) | (49<<6) | (0<<3);
ADCDLY = 0xff;
}

5.2.1.2 ts 中断初始化#

为了将中断源开启,这里设置SUBSRCPND 和INTSUBMSK让中断源开启。通过register_irq()注册中断号和中断服务程AdcTsIntHandle,查表得出中断号为31,这样当硬件产生中断后可以从INTOFFSET区分是哪个中断号。如下图:

img

1
2
3
4
5
6
7
8
void adc_ts_int_init(void) {
SUBSRCPND = (1<<TC_INT_BIT) | (1<<ADC_INT_BIT);/*清中断*/
/* 注册中断处理函数 */
register_irq(31, AdcTsIntHandle); /*31号中断*/
/* 使能中断 */
INTSUBMSK &= ~((1<<ADC_INT_BIT) | (1<<TC_INT_BIT));//防止屏蔽(SUBMSK)
//INTMSK &= ~(1<<INT_ADC_TC);//reg_irq已经使能了31中断号
}

5.2.1.3 进入”等待中断模式”#

img

img

img

进入等待中断模式,YM闭合, YP, XP, XM断开,需要pull up,WAIT_PEN_DOWN表示要等待的是按下中断,当触摸屏按下时就会产生一个TSC irq,反之WAIT_PEN_UP表示要等待的是松开中断。

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
#define ADC_INT_BIT (10)
#define TC_INT_BIT (9)
#define INT_ADC_TC (31)
/* ADCTSC's bits */
#define WAIT_PEN_DOWN (0<<8) /*触摸笔按下*/
#define WAIT_PEN_UP (1<<8) /*触摸笔松开*/
#define YM_ENABLE (1<<7)
#define YM_DISABLE (0<<7)
#define YP_ENABLE (0<<6)
#define YP_DISABLE (1<<6)
#define XM_ENABLE (1<<5)
#define XM_DISABLE (0<<5)
#define XP_ENABLE (0<<4)
#define XP_DISABLE (1<<4)
#define PULLUP_ENABLE (0<<3)
#define PULLUP_DISABLE (1<<3)
#define AUTO_PST (1<<2) /*自动转换*/
#define WAIT_INT_MODE (3) /*等待中断模式*/
#define NO_OPR_MODE (0) /*禁止模式*/

void enter_wait_pen_down_mode(void)/*等待按下模式*/ {
ADCTSC = WAIT_PEN_DOWN | PULLUP_ENABLE | YM_ENABLE | YP_DISABLE | XP_DISABLE | XM_DISABLE | WAIT_INT_MODE;
}
void enter_wait_pen_up_mode(void)/*等待松开模式*/ {
ADCTSC = WAIT_PEN_UP | PULLUP_ENABLE | YM_ENABLE | YP_DISABLE | XP_DISABLE | XM_DISABLE | WAIT_INT_MODE;
}

5.2.2 ts中断服务程序#

SUBSRCPND的bit9, bit10可以区分是TC中断还是ADC中断。

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void Isr_Tc(void)/*触摸屏中断服务程序*/ {
   printf("ADCUPDN = 0x%x, ADCDAT0 = 0x%x, ADCDAT1 = 0x%x, ADCTSC = 0x%x\n\r", ADCUPDN, ADCDAT0, ADCDAT1, ADCTSC);
   if (ADCDAT0 & (1<<15)) { //dat寄存器的第15位判断按下还是松开
     printf("pen up\n\r");
     enter_wait_pen_down_mode();
   } else {
     printf("pen down\n\r");
     /* 进入"等待触摸笔松开的模式" */
     enter_wait_pen_up_mode();
   }
}
void AdcTsIntHandle(int irq) {
   if (SUBSRCPND & (1<<TC_INT_BIT)) /* 如果是触摸屏中断 */
     Isr_Tc();
   // if (SUBSRCPND & (1<<ADC_INT_BIT)) /* ADC中断 */
   // Isr_Adc();
   SUBSRCPND = (1<<TC_INT_BIT) | (1<<ADC_INT_BIT);/*清中断*/
   //SRCPND = 1<<31;/*在interrupt.c已经清中断了*/
}
1
2
AdcTsIntHandle函数: 这里先注解掉ADC中断,只检测单独的按下松开触摸屏操作。那当isr处理完后为了能够正常响应下一次中断,需要清中断,否则会一直触发interrupt。
Isr_Tc函数:ADCDAT0 寄存器的第15位判断按下还是松开。那么当按下后,要将控制器进入”等待松开模式“,当松开后,要将控制器配置进入”等待按下模式“。

img

5.2.2.1 获取触摸屏坐标#

5.2.2.1.1 进入自动测量模式#

Auto(Sequential) X/Y Position Conversion Mode。打开TS控制寄存器,也就是ADCTSC寄存器:

img

让bit[2] =1, bit[1:0]=00,则会进入auto measurement。如果bit[2]=0,则需配置bit[1::0]=01 or 10是手动测量x,y坐标。

1
2
3
4
5
6
#define AUTO_PST         (1<<2) /*自动转换*/
#define WAIT_INT_MODE (3) /*等待中断模式*/
#define NO_OPR_MODE (0) /*禁止模式*/
void enter_auto_measure_mode(void) {
  ADCTSC = AUTO_PST | NO_OPR_MODE;
}
5.2.2.1.2 启动ADC#

触摸屏坐标就是通过ADC获取的。

img

1
ADCCON |= (1<<0);

所以TSC isr程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
void Isr_Tc(void) {/*触摸屏中断服务程序*/
if (ADCDAT0 & (1<<15)) {
printf("pen up\n\r");
enter_wait_pen_down_mode();
} else {
printf("pen down\n\r");
/* 进入"自动测量"模式 */
enter_auto_measure_mode();
/* 启动ADC */
ADCCON |= (1<<0);
}
}

那么当检测到按下后,需要进入auto measure mode,启动adc,然后就会进行自动坐标转换,转换结束后又会触发ADC中断,再次进入AdcTsIntHandle函数,进而进入Isr_Adc,SUBSRCPND可以区分中断源 。如下:

1
2
3
4
5
6
7
void AdcTsIntHandle(int irq) {
  if (SUBSRCPND & (1<<TC_INT_BIT)) /* 如果是触摸屏中断 */
    Isr_Tc();
  if (SUBSRCPND & (1<<ADC_INT_BIT)) /* ADC中断 */
    Isr_Adc();
  SUBSRCPND = (1<<TC_INT_BIT) | (1<<ADC_INT_BIT);/*清中断*/
}

我们知道ADC进行坐标转换结束后,那么会产生ADC中断,在Isr_Adc中即可获取我们的x,y坐标数据。由于我们按下后是进入了 “自动测量” 模式,因此那当数据获取完后我们得进入 “等待松开” 模式。

1
2
3
4
5
6
7
8
9
10
void Isr_Adc(void) {
int x = ADCDAT0;
int y = ADCDAT1;
if (!(ADCDAT0 & (1<<15))) { /* 在isr_Tc按下后,如果仍然按下才打印 */
x &= 0x3ff;
y &= 0x3ff;
printf("x = %08d, y = %08d\n\r", x, y);
}
enter_wait_pen_up_mode();
}

有可能触摸屏的测量过程非常长,那当ADC转换结束后,它已经松开了,这时不应该进行打印出坐标,所以这里在isr_Tc按下后,如果仍然按下才打印。

5.2.2.2 ADCDLY寄存器#

由于触摸屏采样的转换速率问题,按下后需要过一段电压才能稳定下来,那么数据才能稳定可能需要一定的延迟,所以需要配置ADC delay,让ADC慢一点产生中断,也就是等坐标稳定后在通知用户。

img

ADCDLY就是用来延时ADC启动的时间,让数据稳定后再进行转换。

img

可以看到,进行auto or manual measure 坐标转换的时序要满足:A = Dx,D表示ADCDLY的值。 现在晶振的频率是12Mhz, 那么根据触摸屏规格书我们取A= 5ms,那么D= 0.005s *12*1000000 = 60000,所以ADCDLY配置成60000.

修改前面的adc_ts_reg_init函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void adc_ts_reg_init(void) {
/* [15] : ECFLG, 1 = End of A/D conversion
* [14] : PRSCEN, 1 = A/D converter prescaler enable
* [13:6]: PRSCVL, adc clk = PCLK / (PRSCVL + 1)
* [5:3] : SEL_MUX, 000 = AIN 0
* [2] : STDBM
* [0] : 1 = A/D conversion starts and this bit is cleared after the startup.
*/
ADCCON = (1<<14) | (49<<6) | (0<<3);
/* 按下触摸屏, 延时一会再发出TC中断
* 延时时间 = ADCDLY * 晶振周期 = ADCDLY * 1 / 12000000 = 5ms
*/
ADCDLY = 60000;
}

5.3 TS触摸屏测试#

从左往右依次点击触摸屏,可以看到x坐标没有明显变化,y坐标反而线性变大。

img

同理,从上往下依次按下触摸屏,可以看到y坐标没有明显变化,x坐标反而线性变大。

img

这里是由于硬件上xp与yp接反了,ym与xm接反了,如下图:但这里并不影响我们的时候,这里我们软件上可以进行x,y坐标的转换:

img

我们软件上可以对x,y轴进行flip, mirror, rotaion旋转等一系列操作即可。比如:

Case1:ts与lcd吻合

img

Case2:ts与lcd相反

img

5.4 利用定时器支持屏幕长按和滑动#

5.4.1 改进定时器#

前面s3c2440裸机-异常中断 | Hexo (fuzidage.github.io)有讲到在handle_irq_c()中去区分中断源,执行不同的isr

image-20240501173712329

那现在通过register_timer注册对应的定时器中断服务程序,timer_irq进行执行不同的定时器中断服务程序。

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
#define TIMER_NUM  32
#define NULL ((void *)0)
typedef void(*timer_func)(void);
typedef struct timer_desc {
  char *name;
  timer_func fp;
}timer_desc, *p_timer_desc;

timer_desc timer_array[TIMER_NUM];

int register_timer(char *name, timer_func fp) {
  int i;
  for (i = 0; i < TIMER_NUM; i++) {
    if (!timer_array[i].fp) {
      timer_array[i].name = name;
      timer_array[i].fp = fp;
      return 0;
    }
  }
  return -1;
}
void unregister_timer(char *name) {
  int i;
  for (i = 0; i < TIMER_NUM; i++) {
    if (!strcmp(timer_array[i].name, name)) {
      timer_array[i].name = NULL;
      timer_array[i].fp = NULL;
      return 0;
    }
  }
  return -1;
}
void timer_irq(void) {
  int i;
  for (i = 0; i < TIMER_NUM; i++) {
    if (timer_array[i].fp) {
      timer_array[i].fp();
    }
  }
}

我们想要用timer来进行进行流水灯实验,那么假如点灯函数为:
Isr_timer_led(){}

那么则只需要在led init的时候进行调用register_timer(“led”, Isr_timer_led), 那么当时间到后触发定时器中断,便会执行timer_irq.进入Isr_timer_led

5.4.2 初始化定时器#

前面s3c2440裸机-异常中断 | Hexo (fuzidage.github.io)有具体讲解,这里采用PWM定时器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void timer_init(void) {
  /* 设置TIMER0的时钟 */
  /* Timer clk = PCLK / {prescaler value+1} / {divider value}
= 50000000/(49+1)/16
= 62500
  */
  TCFG0 = 49; /* Prescaler 0 = 49, 用于timer0,1 */
  TCFG1 &= ~0xf;
  TCFG1 |= 3; /* MUX0 : 1/16 */
  /* 设置TIMER0的初值 */
  TCNTB0 = 625; /* 10Ms中断一次 */
  /* 加载初值, 启动timer0 */
  TCON |= (1<<1); /* Update from TCNTB0 & TCMPB0 */
  /* 设置为自动加载并启动 */
  TCON &= ~(1<<1);
  TCON |= (1<<0) | (1<<3); /* bit0: start, bit3: auto reload */
  /* 设置中断 */
  register_irq(10, timer_irq);
}

5.4.2.1 支持长按和滑动#

我们之前是2s timer触发一次中断,那如果是要支持触摸屏,我们必须让定时器10ms就触发一次中断。因此需要修改timer_init中的寄存器参数。

当按下触摸屏会产生TSC中断,然后启动ADC进而产生adc中断的时候,在Isr_Adc函数中进行定时器的设置,检测长按和滑动操作。

5.4.2.1.1 定义touchscreen_timer_irq#
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
static volatile int g_ts_timer_enable = 0;
static void ts_timer_enable(void) {
  g_ts_timer_enable = 1;
}
static void ts_timer_disable(void) {
  g_ts_timer_enable = 0;
}
static int get_status_of_ts_timer(void) {
  return g_ts_timer_enable;
}
/* 每10ms该函数被调用一次
*/
void touchscreen_timer_irq(void) {
  if (get_status_of_ts_timer() == 0)
    return;

  if (ADCDAT0 & (1<<15)) { /* 如果松开 */
    ts_timer_disable();
    enter_wait_pen_down_mode();
    return;
  }
  /* 如果触摸屏仍被按下, 进入"自动测量模式", 启动ADC */
  else { /* 按下状态 */
    /* 进入"自动测量"模式 */
    enter_auto_measure_mode();
    /* 启动ADC */
    ADCCON |= (1<<0);
  }
}

image-20240501174908562

来分析一下这个程序的过程:

1
2
3
4
5
6
7
8
9
10
11
1. 在touchscreen_init的时候我们先注册了一个timer,然后修改了定时器的产生中断的时间间隔为10ms中断一次,所以touchscreen_timer_irq会每间隔10ms调用一次。没有按下,则touchscreen_timer_irq虽然也有走,但是就直接return.

2. 然后如果按下触摸屏,产生tsc中断,启动adc,产生adc中断。

如果产生了adc中断,但是读取状态发现已经松开了,则进入”等待按下状态“,并且让touchscreen_timer_irq失效。那么要是状态是被按下,则开启ts_timer_enable。

3. 当使能touchscreen_timer_irq这个定时器中断服务程序后,并且10ms到了touchscreen_timer_irq函数执行生效。

如果松开了,则进入”等待按下状态“,并且让touchscreen_timer_irq失效,表示没有长按或者滑动。

如果任然按下,输出长按或者滑动后的坐标结果。

s3c2440裸机编程-LDC

1 LCD硬件原理#

1.1 LCD像素扫描#

里面的每个点就是一个像素点。

它里面有一个电子枪,一边移动,一边发出各种颜色的光。用动态图表示如下:

  1. 电子枪是如何移动的?

     有一条CLK时钟线与LCD相连,每发出一次CLK(高低电平),电子枪就移动一个像素。
    
  2. 颜色如何确定?

     由连接LCD的三组线RGB三原色混合而成:R(Red)、G(Green)、B(Blue)确定。
    
  3. 电子枪如何得知应跳到下一行?

     有一条HSYNC信号线与LCD相连,每发出一次脉冲(高低电平),电子枪就跳到下一行,该信号叫做行同步信号。
    
  4. 电子枪如何得知应跳到原点?

     有一条VSYNC信号线与LCD相连,每发出一次脉冲(高低电平),电子枪就跳到原点,该信号叫做帧同步信号。
    
  5. RGB线上的数据从何而来?

     内存里面划分一块显存(FrameBuffer),里面存放了要显示的数据,LCD控制器从里面将数据读出来,通过RGB三组线传给电子枪,电子枪再依次打到显示屏上。
    
  6. 前面的信号由谁发给LCD?

     有S3C2440里面的LCD控制器来控制发出信号。
    

1.2 LCD硬件原理图#

①是时钟信号,每来一个CLK,电子枪就移动一个像素;

②是用来传输颜色数据;

③是垂直方向同步信号,FRAME(帧);

④是水平方向同步信号,LINE(行);

⑤LED+、LED-背光灯电源。

⑥TSYP、TSXP、TSYM、TSXM是触摸屏信号,暂时不用。

⑦VM接DE是数据使能

1.2.1 RGB LCD模式#

HV模式: HS与VS来控制刷新。比如对于分辨率为1024x600RGB的LCD,LCD控制器发出HS信号后,就会发出1024个DCLK,在每个DCLK上传输像素数据;当发出600个HS信号后,就会发出一个VS信号
DE模式:DE信号来控制刷新,比如对于分辨率为1024x600RGB的LCD,LCD控制器发出DE信号后,就要发出1024个DCLK,在每个DCLK上传输像素数据;当发出600个DE信号,刷新完一帧数据

1.2.2 LCD时序分析#

image

①从一行最开始的像素开始分析,如上图标号①,DE信号开始有效,电子枪每次在CLK下降沿时从数据线Dn0-Dn7上得到数据(Dn0-Dn7上的数据来源于FrameBuffer,后面会讲),然后发射到显示屏上,然后移动到下一个位置。从1 st pixellast pixel,就这样从一行的最左边,一直移动到一行的最右边,完成了一行的显示,假设一行有x个pixel。可以看到每发送一个pixel,需要1个时钟周期(1/tc)。

②当打完一行的最后一个数据后,会收到Hsync行同步信号,那么电子枪会跳到下一行,如上图标号②,根据时序图,一个Hsync周期,也就是一行数据刷新时间th, 可以大致分为五部分组成:thp、thb、1/tc、thd、thf。
thp:称为脉冲宽度,这个时间不能太短,太短电子枪可能识别不到。
thb:电子枪正确识别到thp后,会从最右端移动最左端,这个移动的时间就是thb,称之为移动时间。
thd:表示显示一行数据的时间
thf:表示显示完最右像素,再过多久Hsync才来。

③同理,当电子枪移动到最后一行时,就会发送一个Vsync垂直同步信号,让电子枪移动回最上边。如上图标号③,根据时序图,一个Vsync周期,也就是一帧数据刷新时间tv, 可以大致分为:tvp、tvb、tvd、tvf。
tvp:Vsync信号的脉冲宽度
tvb:电子枪从最后一行移动到第一行的移动时间
tvf:表示显示完最后一行像素,再过多久Vsync才来。

假设一共有y行,那么LCD的分辨率就是x*y。

下面是LCD显示配置示意图:

从左往右看,可以看到Total width = HSYNC width + HBP + Active width + HFP。当发出一个HSYNC信号后,电子枪就会从最右边花费HBP时长移动到最左边,等到了最右边后,等待HFP时长后下一轮HSYNC信号才会发出。因此,HBP和HFP分别决定了左边和右边的黑框。

 HSYNC是行同步信号的脉冲宽度(低电平有效)
 HBP表示屏幕左边黑框的宽度(电子枪要花多久才能从最右边移动到最左边)行后肩
 Active width表示有效数据宽度
 HFP表示屏幕右边黑框的宽度(再过多久HSYNC才会发出)行前肩

同理从上往下看,Total height = Vsync width + VBP + Active width + VFP。当发出一个VSYNC信号后,电子枪就会从最下边花费VBP时长移动到最上边,等到了最下边后,等待VFP时长后下一轮VSYNC信号才会发出。因此,VBP和VFP分别决定了上边和下边的黑框。 中间灰色区域才是有效显示区域。

VSYNC是帧同步信号的脉冲宽度(低电平有效)
VBP表示屏幕上边黑框的宽度(电子枪要花多久才能从最后一行移动到最上面一行)帧后肩
Active height表示有效数据高度
VFP表示屏幕下边黑框的宽度(再过多久VSYNC才会发出) 帧前肩

总结:

1.2.2.1 行时序#

image

HSPW:有些地方也叫做 thp,是 HSYNC 信号宽度,也就是 HSYNC 信号持续时间。HSYNC信号不是一个脉冲,而是需要持续一段时间才是有效的,单位为 CLK。
HOZVAL:有些地方叫做 thd,显示一行数据所需的时间,假如屏幕分辨率为 1024*600,那么 HOZVAL 就是 1024,单位为 CLK。

1.2.2.2 帧时序#

image

VSPW:有些地方也叫做 tvp,是 VSYNC 信号宽度,也就是 VSYNC 信号持续时间,单位为 1 行的时间
LINE:有些地方叫做 tvd,显示一帧有效数据所需的时间,假如屏幕分辨率为 1024*600,那么 LINE 就是 600 行的时间。

1.2 FrameBuffer和BPP概念#

FrameBuffer是在内存中的一段区域,这段区域专门用来存放颜色数据的。如下图:

BPP(Bits Per Pixels)表示每个像素占据多少位。 前面的LCD引脚功能图里,有R0-R7、G0-G7、B0-B7,那么每个像素是占据38=24位的,*所以硬件上LCD的BPP是确定的.

那么在FrameBuffer中,每个像素在FrameBuffer中,占据多少位BPP(Bits Per Pixels)?

虽然LCD上的引脚是固定的,但我们使用的时候,可以根据实际情况进行取舍,查看我们的硬件原理图,发现我们的LCD硬件上只有R1-R5、G0-G5、B1-B5与SOC相连,5+6+5=16BPP,所以每个像素就只占据16位数据。等效连接图如下:

1.3 LCD种类#

S3C2440芯片手册介绍了LCD控制器支持TFT和STN两种LCD,我们常用的都是TFT材质的,本开发板采用的就是一款TFT材质的LCD.

1.4 LCD访问框架#

如下图,LCD控制器从SDRAM中的FrameBuffer区域取出颜色数据,发送给电子枪,电子枪按照特定的时钟周期将颜色数据显示在LCD上。

2 LCD控制器#

2.1 s3c2440 LCD控制器框图#

S3C2440 LCD控制器用于传输视频数据并且生成必要的控制信号,如VFRAME,VLINE,VCLK,VM等。除了控制信号,S3C2440还有视频数据端口,即VD [23:0]。通过设置REGBANK(寄存器组),LCDCDMA会自动(无需CPU参与)把内存上FrameBuffer里的数据,通过VIDPRCS发送到引脚VD[23:0]数据总线上,再配合VIDEOMUX引脚的控制信号,正确的显示出来。

1
2
3
4
REGBANK:具有17个可编程寄存器组和256x16调色板存储器,用于配置LCD控制器。 
TIMEGEN:产生控制信号,例如 VSYNC、HSYNC、VCLK等信号
LCDCDMA:可以自动从FrameBuff中把数据copy出来。
VIDPRCS:从LCDCDMA接收视频数据,将数据输出到VD[23:0]数据总线上。

总结LCD控制器主要功能如下:

1
2
1. 取:从内存(FrameBuffer)取出某个像素的数据(之后需要把FrameBuffer地址、BPP、分辨率告诉LCD控制器)
2. 发:配合其它信号把FrameBuffer中的数据发给LCD;(那么需要设置LCD控制器时序、设置引脚极性)

2.2 寄存器介绍#

2.2.1 数据存储格式#

可以配置寄存器的BSWP、HWSWP来设置Framebuff中的像素存储格式。

2.2.1.1 BSWP/HWSWP寄存器#

2.2.2.1.1 24BPP#

从图中可以看到24bpp的像素,在lcd控制器的VD[7:0]表示BLUE, VD[15:8]表示GREEN,VD[23:16]表示RED。在内存中的FrameBuffer中每一个像素占据4个字节,当BPP24BL=0时,低24位为颜色数据,当BPP24BL=1时,高24位为颜色数据。

2.2.2.1.2 16BPP#

也可以看到16bpp的像素,在内存中的FrameBuffer中每一个像素占据2个字节,HWSWP用来设置像素数据的存放方式。

再看下LCD控制器的VD引脚输出情况,可以看到16bpp时分5:6:5和5:5:5:i两种数据格式。当5:6:5模式时,VD[7:3]表示BLUE, VD[15:10]表示Green数据,VD[23:19]表示RED。当5:5:5:i模式时,VD[7:3]表示BLUE, VD[15:11]表示Green,VD[23:19]表示RED。其中i表示透明度。

2.2.2.1.3 8BPP#

2.2.2 调色板寄存器#

我们外接的LCD硬件上只有R1-R5、G0-G5、B1-B5与SOC相连,5+6+5=16BPP,所以LCD上每个像素就只占据16位数据。那么当我们的Frame buffer中是8BPP颜色数据时,是如何把颜色数据填充到LCD上的呢?

用调色板

S3C2440A 中的 TFT LCD 控制器支持 1、2、4 或 8bpp调色显示(伪彩色)和16、24bpp无调色显示(真彩色)。S3C2440A 可以支持 256 色调色板给各种色彩映射的选择,以提供灵活操作给用户。

假如是16BPP的数据,LCD控制器从FB取出16bit数据,显示到LCD上,如下图所示:

那么当8BPP的数据时,就需要用到调色板,调色板里存放了256个16bit的数据,FB(frame buffer)只存放每个像素的索引,根据索引去调色板找到对应的数据传给LCD控制器,比如从FB中的第0个元素拿到调色板中的第0个16bit数据,再通过电子枪显示出来,如下图所示:

调色板支持 5:6:5(R:G:B)格式和 5:5:5:I(R:G:B:I)格式。当用户使用5:5:5:I格式时,I表示强度,也就是透明度。I是用作每个RGB 数据的共用 LSB 位,因此 5:5:5:I与R(5+I):G(5+I):B(5+I)格式相同。

2.2.2.1 调色板格式#

0x4D000400为调色板起始地址:

2.2.3 LCD控制寄存器1#

1
2
3
4
5
6
[27:18]为只读数据位,不需要设置;
[17:8]设置CLKVAL(像素时钟频率),我们使用的是TFT屏,因此采用的公式是VCLK = HCLK / [(CLKVAL+1) x 2],其中HCLK为100M。LCD手册里面Clock cycle的要求范围为5-12MHz即可,那么取VCLK=9,根据公式9=100/[(CLKVAL+1)x2],算出CLKVAL≈4.5=5,设置CLKVAL=5
[7]不用管,默认即可;
[6:5]TFT lcd配置为0b11
[4:1]设置bpp模式,用户可选
[0]LCD输出使能,先暂时关闭不输出;

2.2.4 LCD控制寄存器2(垂直方向参数)#

s3c2440 LCD控制器时序图如下:

1
2
3
4
[31:24] : VBPD = tvb - 1 (表示显示完最后一行像素,再过多久Vsync才来,表示上边黑框)
[23:14] : LINEVAL = 每帧有多少行 - 1
[13:6] : VFPD = tvf - 1(下边黑框)
[5:0] : VSPW = tvp - 1 (Vsync信号的脉冲宽度)

2.2.5 LCD控制寄存器3(水平方向参数)#

1
2
3
[25:19] : HBPD = thb - 1(左边黑框)
[18:8] : HOZVAL = 每行有多少列 - 1
[7:0] : HFPD = thf - 1 (右边黑框)

2.2.5 LCD控制寄存器4#

1
[7:0]: HSPW = thp - 1 (Hsync信号的脉冲宽度)

2.2.5 LCD控制寄存器5#

1
2
3
4
5
6
7
8
9
10
11
12
13
[12] : BPP24BL(表示24bpp的数据是大端还是小端)
[11] : FRM565 (数据存放格式)
[10] : INVVCLK(时钟是否反转极性,当配置成0时数据在时钟下降沿被锁存)
[9] : HSYNC是否反转
[8] : VSYNC是否反转
[7] : INVVD, rgb是否反转
[6] : INVVDEN
[5] : INVPWREN
[4] : INVLEND
[3] : PWREN(LCD_PWREN output signal enable/disable)
[2] : ENLEND
[1] : BSWP
[0] : HWSWP

2.2.6 LCDSADDR1寄存器#

frame buffer的起始地址寄存器:

1
2
3
[29:21] : LCDBANK, A[30:22] of fb
[20:0] : LCDBASEU, A[21:1] of fb
即[29:0]表示Frame buffer的起始地址的[30:1]。

2.2.7 LCDSADDR2寄存器#

frame buffer的结束地址寄存器:

1
[20:0] : LCDBASEL,A[21:1] of end addr,即framebuffer的结束地址。

3 LCD裸机编程#

3.1 软件框架#

为了让程序更加好扩展,体现出高内聚、低耦合的特点,能够兼容各种不同型号的lcd,假如有两款尺寸大小的lcd,如何快速的在两个lcd上切换?

首先我们抽象出lcd_3.5.c和lcd_4.3.c的共同点,比如都有初始化函数init(),我们可以新建一个lcd.c,然后定义一个结构体:

1
2
3
struct lcd_opr{
void (*init)(void);
};

用户不接触lcd_3.5.c和lcd_4.3.c,只需要在lcd.c里通过指针访问对应的结构体的函数,也就调用了不同init():

img

我们的目的是在LCD显示屏上画线、画圆(geomentry.c)和写字(font.c)其核心是画点(farmebuffer.c),这些都属于纯软件。此外还需要一个lcd_test.c测试程序提供操作菜单,调用画线、画圆和写字操作。

往下操作的是LCD相关的内容,不同的LCD,其配置的参数也会不一样,通过lcd_3.5.c或lcd_4.3.c来设置属性参数。

根据LCD的特性,来设置LCD控制器,首先编写lcd_controller.c,它向上要接收不同LCD的参数,向下要使用这些参数设置对应具体的某一款LCD控制器。

对于我们开发板,就是s3c2440_lcd_controller.c,假如希望在其它开发板上也实现LCD显示,只需添加相应的代码文件即可。文件自上而下的框架如下:

3.2 数据结构定义#

3.2.1 LCD设备结构体#

我们知道LCD的参数属性有:引脚的极性、时序、数据的格式bpp、分辨率等,使用面向对象的思维方式,将这些封装成结构体放在lcd.h中:

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
enum {
NORMAL = 0,
INVERT = 1,
};

/* NORMAL : 正常极性
* INVERT : 反转极性
*/
typedef struct pins_polarity {
int vclk; /* normal: 在下降沿获取数据 */
int rgb; /* normal: 高电平表示1 */
int hsync; /* normal: 高脉冲 */
int vsync; /* normal: 高脉冲 */
} pins_polarity, *p_pins_polarity;

typedef struct time_sequence {
/* 垂直方向 */
int tvp; /* vysnc脉冲宽度 */
int tvb; /* 上边黑框, Vertical Back porch */
int tvf; /* 下边黑框, Vertical Front porch */
/* 水平方向 */
int thp; /* hsync脉冲宽度 */
int thb; /* 左边黑框, Horizontal Back porch */
int thf; /* 右边黑框, Horizontal Front porch */
int vclk;
} time_sequence, *p_time_sequence;

typedef struct lcd_params {
/* 引脚极性 */
pins_polarity pins_pol;
/* 时序 */
time_sequence time_seq;
/* 分辨率, bpp */
int xres;
int yres;
int bpp;
/* framebuffer的地址 */
unsigned int fb_base;
} lcd_params, *p_lcd_params;

3.3 操作方法定义#

3.3.1 LCD操作方法-lcd_controller.c#

我们知道在c++中是面向对象编程的,那么一个对象就有它的属性和方法,LCD属性我们上面已经定义好了,那么方法我们可以定义一个lcd_controller.c用来控制管理LCD,定义个一个lcd_controller.h, struct lcd_controller结构体放置lcd对象的一些成员函数,即对象的方法,或者称之为对象的行为:

1
2
3
4
5
6
7
typedef struct lcd_controller {
char *name;
void (*init)(p_lcd_params plcdparams);
void (*enable)(void);
void (*disable)(void);
void (*init_palette)(void);
} lcd_controller, *p_lcd_controller;

那么lcd_controller.c相当于一个管理者,会去选择具体型号的LCD对象去执行具体的成员函数,比如管理s3c2440_lcd_controller.c,它向上接受传入的LCD参数,向下传给具体的LCD控制器。

1
2
3
4
void lcd_controller_init(p_lcd_params plcdparams) {
/* 调用2440的LCD控制器的初始化函数,lcd_controller是一个被选中的对象,即s3c2440_lcd_controller*/
lcd_controller.init(plcdparams);
}

这样在s3c2440_lcd_controller.c再构造一个具体的lcd对象:

1
2
3
4
5
6
struct lcd_controller s3c2440_lcd_controller = {
.name = xxx,
.init = xxx,
.enalbe = xxx,
.disable = xxx,
};

lcd_controller.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
#include "lcd_controller.h"
#define LCD_CONTROLLER_NUM 10

static p_lcd_controller p_array_lcd_controller[LCD_CONTROLLER_NUM];
static p_lcd_controller g_p_lcd_controller_selected;

int register_lcd_controller(p_lcd_controller plcdcon) {
int i;
for (i = 0; i < LCD_CONTROLLER_NUM; i++) {
if (!p_array_lcd_controller[i]) {
p_array_lcd_controller[i] = plcdcon;
return i;
}
}
return -1;
}

int select_lcd_controller(char *name) {
int i;
for (i = 0; i < LCD_CONTROLLER_NUM; i++) {
if (p_array_lcd_controller[i] && !strcmp(p_array_lcd_controller[i]->name, name)) {
g_p_lcd_controller_selected = p_array_lcd_controller[i];
return i;
}
}
return -1;
}

/* 向上: 接收不同LCD的参数
* 向下: 使用这些参数设置对应的LCD控制器
*/
int lcd_controller_init(p_lcd_params plcdparams) {
/* 调用所选择的LCD控制器的初始化函数 */
if (g_p_lcd_controller_selected) {
g_p_lcd_controller_selected->init(plcdparams);
return 0;
}
return -1;
}

void lcd_controller_enable(void) {
if (g_p_lcd_controller_selected)
g_p_lcd_controller_selected->enable();
}

void lcd_controller_disable(void) {
if (g_p_lcd_controller_selected)
g_p_lcd_controller_selected->disable();
}

下面详细分析lcd_controller.c框架的含义以及作用:

  1. 开始定义了一个p_array_lcd_controller数组和g_p_lcd_controller_selected,p_array_lcd_controller数组表示lcd控制器的集合,g_p_lcd_controller_selected表示被选中的那一个lcd_controller;
  2. 当我们初始化时要先调用register_lcd_controller,select_lcd_controller选中具体的lcd_controller;
  3. 然后才能调用lcd_controller_init初始化具体的lcd_controller,去控制具体型号的lcd。

同理,也通过lcd.c去管理lcd_4.3.c,思路如下:

1
2
3
a. 有一个数组存放各类lcd的参数;
b. 有一个register_lcd给下面的lcd程序来设置数组;
c. 有一个select_lcd,供上层选择某款LCD;

3.3.2 具体型号LCD管理-ldc.c#

参考前面的lcd_controller.c编辑lcd.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
#define LCD_NUM 10
static p_lcd_params p_array_lcd[LCD_NUM];
static p_lcd_params g_p_lcd_selected;

int register_lcd(p_lcd_params plcd) {
int i;
for (i = 0; i < LCD_NUM; i++) {
if (!p_array_lcd[i]) {
p_array_lcd[i] = plcd;
return i;
}
}
return -1;
}

int select_lcd(char *name)
{
int i;
for (i = 0; i < LCD_NUM; i++) {
if (p_array_lcd[i] && !strcmp(p_array_lcd[i]->name, name)) {
g_p_lcd_selected = p_array_lcd[i];
return i;
}
}
return -1;
}

void get_lcd_params(unsigned int *fb_base, int *xres, int *yres, int *bpp) {
*fb_base = g_p_lcd_selected->fb_base;
*xres = g_p_lcd_selected->xres;
*yres = g_p_lcd_selected->yres;
*bpp = g_p_lcd_selected->bpp;
}

3.4 LCD初始化#

3.4.1 初始化lcd控制器#

3.4.1.1 初始化引脚#

3.4.1.1.1 背光引脚#

我们配置LCD的背光引脚成输出模式:

1
2
GPBCON &= ~0x3;
GPBCON |= 0x01;
3.4.1.1.2 控制引脚和数据引脚#

然后再配置LCD的控制引脚和数据引脚,LCD控制引脚和数据引脚分别复用了GPC和GPD,如下图所示:


设置GPC, GPD均为0xaaaa,aaaa。

1
2
3
/* LCD专用引脚 */
GPCCON = 0xaaaaaaaa;
GPDCON = 0xaaaaaaaa;
3.4.1.1.3 PWREN引脚#

设置GPG4成PWREN引脚


1
GPGCON |= (3<<8);

3.4.1.2 初始化LCD控制寄存器、地址寄存器#

前面介绍了LCDCON1,LCDCON2,LCDCON3…LCDSADDR1等寄存器,代码如下:

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
void s3c2440_lcd_controller_init(p_lcd_params plcdparams) {
/* [17:8]: CLKVAL, vclk = HCLK / [(CLKVAL+1) x 2]
* 如:9 = 100M /[(CLKVAL+1) x 2], 所以CLKVAL = 4.5 = 5
* CLKVAL = 100/vclk/2-1
* [6:5]: 0b11, tft lcd
* [4:1]: bpp mode
* [0] : LCD video output and the logic enable/disable
*/
int clkval = (double)HCLK/plcdparams->time_seq.vclk/2-1+0.5;
int bppmode = plcdparams->bpp == 8 ? 0xb :\
plcdparams->bpp == 16 ? 0xc :\
0xd; /* 0xd: 24bpp */
LCDCON1 = (clkval<<8) | (3<<5) | (bppmode<<1) ;

/* [31:24] : VBPD = tvb - 1
* [23:14] : LINEVAL = line - 1
* [13:6] : VFPD = tvf - 1
* [5:0] : VSPW = tvp - 1
*/
LCDCON2 = ((plcdparams->time_seq.tvb - 1)<<24) | \
((plcdparams->yres - 1)<<14) | \
((plcdparams->time_seq.tvf - 1)<<6) | \
((plcdparams->time_seq.tvp - 1)<<0);

/* [25:19] : HBPD = thb - 1
* [18:8] : HOZVAL = 列 - 1
* [7:0] : HFPD = thf - 1
*/
LCDCON3 = ((plcdparams->time_seq.thb - 1)<<19) | \
((plcdparams->xres - 1)<<8) | \
((plcdparams->time_seq.thf - 1)<<0);

/*
* [7:0] : HSPW = thp - 1
*/
LCDCON4 = ((plcdparams->time_seq.thp - 1)<<0);

/* 用来设置引脚极性, 设置16bpp, 设置内存中象素存放的格式
* [12] : BPP24BL
* [11] : FRM565, 1-565
* [10] : INVVCLK, 0 = The video data is fetched at VCLK falling edge
* [9] : HSYNC是否反转
* [8] : VSYNC是否反转
* [7] : INVVD, rgb是否反转
* [6] : INVVDEN
* [5] : INVPWREN
* [4] : INVLEND
* [3] : PWREN, LCD_PWREN output signal enable/disable
* [2] : ENLEND
* [1] : BSWP
* [0] : HWSWP
*/

pixelplace = plcdparams->bpp == 24 ? (0) : |\
plcdparams->bpp == 16 ? (1) : |\
(1<<1); /* 8bpp */
LCDCON5 = (plcdparams->pins_pol.vclk<<10) |\
(plcdparams->pins_pol.rgb<<7) |\
(plcdparams->pins_pol.hsync<<9) |\
(plcdparams->pins_pol.vsync<<8) |\
(plcdparams->pins_pol.de<<6) |\
(plcdparams->pins_pol.pwren<<5) |\
(1<<11) | pixelplace;

/* framebuffer地址 */
/*
* [29:21] : LCDBANK, A[30:22] of fb
* [20:0] : LCDBASEU, A[21:1] of fb
*/
addr = plcdparams->fb_base & ~(1<<31);
LCDSADDR1 = (addr >> 1);

/*
* [20:0] : LCDBASEL, A[21:1] of end addr
*/
addr = plcdparams->fb_base + plcdparams->xres*plcdparams->yres*plcdparams->bpp/8;
addr >>=1;
addr &= 0x1fffff;
LCDSADDR2 = addr;
}

3.4.1.3 使能、禁用背光引脚#

根据背光电路背光引脚是GPB0,那么配置GPBDAT[0]置1,使能背光引脚,设置LCDCON5和
LCDCON1使能power enable和LCD输出,反之。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void s3c2440_lcd_controller_enalbe(void) {
/* 背光引脚 : GPB0 */
GPBDAT |= (1<<0);
/* pwren : 给LCD提供AVDD */
LCDCON5 |= (1<<3);
/* LCDCON1'BIT 0 : 设置LCD控制器是否输出信号 */
LCDCON1 |= (1<<0);
}

void s3c2440_lcd_controller_disable(void) {
/* 背光引脚 : GPB0 */
GPBDAT &= ~(1<<0);
/* pwren : 给LCD提供AVDD */
LCDCON5 &= ~(1<<3);
/* LCDCON1'BIT 0 : 设置LCD控制器是否输出信号 */
LCDCON1 &= ~(1<<0);
}

这样我们的s3c2440的lcd控制器初始化就编写完了,那么用户只要调用s3c2440_lcd_controller_init去设置LCD的属性即可。下面开始介绍如何设置LCD属性,让LCD控制器能够适应具体型号的LCD。

1
2
3
4
5
6
struct lcd_controller s3c2440_lcd_controller = {
.name = "s3c2440",
.init = s3c2440_lcd_controller_init,
.enable = s3c2440_lcd_controller_enalbe,
.disable = s3c2440_lcd_controller_disable,
};

3.4.2 初始化lcd设备#

参考AT043TN24 LCD数据手册上的参数性能,见下表:

配置lcd_params属性如下:

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
#define LCD_FB_BASE 0x33c00000
lcd_params lcd_4_3_params = {
.name = "lcd_4.3"
.pins_polarity = {
.de = NORMAL, /* normal: 高电平时可以传输数据 */
.vclk = NORMAL, /* normal: 在下降沿获取数据 */
.rgb = NORMAL, /* normal: 高电平表示1 */
.hsync = INVERT, /* normal: 高脉冲 */
.vsync = INVERT, /* normal: 高脉冲 */
},
.time_sequence = {
/* 垂直方向 */
.tvp= 10, /* vysnc脉冲宽度 */
.tvb= 2, /* 上边黑框, Vertical Back porch */
.tvf= 2, /* 下边黑框, Vertical Front porch */

/* 水平方向 */
.thp= 41, /* hsync脉冲宽度 */
.thb= 2, /* 左边黑框, Horizontal Back porch */
.thf= 2, /* 右边黑框, Horizontal Front porch */

.vclk= 9, /* MHz */
},
.xres = 480,
.yres = 272,
.bpp = 16,
.fb_base = LCD_FB_BASE,
};
1
2
3
4
5
.de表示数据输出使能引脚,高电平有效,所以配置成NORMAL;
.pwren表示LCD_PWREN引脚,高电平有效;
.vclk表示LCD的时钟,从手册的LCD时序图中可以看到下降沿有效,所以配置NORMAL;
.rgb表示颜色数据的引脚极性,高电平表示1,配置成NORMAL;
.hsync表示行同步信号,normal表示高脉冲,参考手册发现该信号低脉冲有效,所以配置成INVERT;

什么是高低脉冲?

1
2
高脉冲:即从逻辑0变化bai到逻辑du1再变化到逻辑0,如此便是一个高脉zhi冲。在单片机中定义高脉冲就是让某个I/O先输出逻辑0,接着保持一定的时间(延时),再输出逻辑1,同样保持一定的时间(延时),最后再转变输出为逻辑0+延时。
低脉冲:反之
1
2
3
4
5
.vsync表示帧同步信号,同.hsync;
.time_sequence时序设置参考上表配置。我们看到thf + thp + thb = 2 + 41 +2 = 45 clk > 44 clk,满足上面的注意事项;
.xres .yres表示分辨率
.bpp表示像素点颜色模式
.fb_base指定frame buffer的基地址

那么最终LCD初始化函数封装如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void lcd_init(void) {
/* 注册LCD,把具体的LCD属性配置下去 */
register_lcd(&lcd_4_3_params);

/* 注册LCD控制器 */
register_lcd_controller(&s3c2440_lcd_controller);

/* 选择某款LCD */
select_lcd("lcd_4.3");
/* 选择某款LCD控制器 */
select_lcd_controller("s3c2440");

/* 使用LCD的参数, 初始化LCD控制器 */
lcd_controller_init(g_p_lcd_selected);
}

总结:我们可以看到,调用的函数都是一些通用型框架型接口,具体的实现本质还得根据硬件本身的特性来配置寄存器来驱动硬件工作。

3.5 实现显示功能#

3.5.1 LCD显示满屏红色#

想要在LCD上显示出数据,所需步骤如下:

1
2
3
4
a. 初始化LCD
b. 使能LCD
c. 获取LCD参数: fb_base, xres, yres, bpp
d. 往framebuffer中写数据

3.5.1.1 初始化LCD#

前面已详细实现。

3.5.1.2 使能LCD#

1
2
3
void lcd_enable() {
lcd_controller_enalbe(); //会间接调用s3c2440_lcd_controller_enalbe
}

3.5.1.3 获取LCD参数#

1
2
3
4
5
6
void get_lcd_params(unsigned int *fb_base, int *xres, int *yres, int *bpp) {
*fb_base = g_p_lcd_selected->fb_base;
*xres = g_p_lcd_selected->xres;
*yres = g_p_lcd_selected->yres;
*bpp = g_p_lcd_selected->bpp;
}

3.5.1.4 往framebuffer中写数据#

假设我们初始化配置了BPP=16,那么如何让全屏显示红色?

就需要从framebuffer基地址开始的整个屏幕的像素点都填充红色值。 对于16BPP,RGB=565,想显示红色,即[15:11]全为1表示红色,[10:5]全为0表示无绿色,[4:0]全为0表示无蓝色,0b1111100000000000=0xF800。
以基地址为起点,分别以xres和yres为边界,依次填充颜色。

1
2
3
4
p = (unsigned short *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
*p++ = 0xf800;

假设我们初始化配置了BPP=24 或者BPP =32,那么如何让全屏显示红色?

其实无论是24bpp还是32bpp,在frame buffer中每个像素点都占4 bytes,对于24BPP or 32 bpp,即RGB:888,每个颜色占8位,一共占据24位。代码如下:

1
2
3
4
p = (unsigned int *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
*p++ = 0xff0000;

当Frame buffer中填满颜色数据时,LCD控制器会参照我们之前的配置将数据填充到LCD显示器上。那前面的24BPP、32BPP是怎样在 只能接收16BPP(硬件上只有16根数据线)的LCD上显示的呢?

1
这是因为在使用24BPP时,发出的8条红色,8条绿色,8条蓝色数据,只用了高5条红色,高6条绿色,高5条蓝色与LCD相连。(前面LCD硬件原理的FrameBuffer和BPP概念有讲)

3.6 实现绘制点线圆函数#

3.6.1 画点#

无论是何种图形,都是基于点来构成的,因此我们需要先实现画点,其他的都是上层的一些数据处理了,像各种图形、甚至色彩鲜艳的图片无非都是一些由点构造出的数据而已。

我们在在farmebuffer.c实现画点,在geomentry.c实现画线、画圆等几何图形,font.c实现画字。

那么一个像素点要显示到lcd上,我们要知道它的位置坐标,然后还要知道它的颜色值,假设该像素点的坐标为(x,y),那么该像素的地址为:

1
(x,y)= fb_base + (xres*(bpp/8))*y +x*bpp/8;

那么所以在画点前需要先获取lcd参数:fb_base、xres、yres、bpp;

1
2
3
4
5
static unsigned int fb_base;
static int xres, yres, bpp;
void fb_get_lcd_params(void) {
get_lcd_params(&fb_base, &xres, &yres, &bpp);
}

然后画点函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static unsigned int fb_base;
static int xres, yres, bpp;
void fb_put_pixel(int x, int y, unsigned int color) {
unsigned char *pc; /* 8bpp */
unsigned short *pw; /* 16bpp */
unsigned int *pdw; /* 32bpp */

unsigned int pixel_base = fb_base + (xres * bpp / 8) * y + x * bpp / 8;

switch (bpp) { //根据像素不同bpp格式,在Frame buffer中存放方式不一样,但对用户来说,不关心颜色格式,通通当做32位色颜色处理,所以这里需要做格式转换
case 8:
pc = (unsigned char *) pixel_base;
*pc = color;
break;
case 16:
pw = (unsigned short *) pixel_base;
*pw = convert32bppto16bpp(color);
break;
case 32:
pdw = (unsigned int *) pixel_base;
*pdw = color;
break;
}
}

用户传入的颜色数据一般都是32bit的,即格式为:0x00RRGGBB。

1
2
3
对于8PP,通过的是调色板索引实现的,这个后续再讲解,直接*pc = color即可(这样只取了高8位,低精度的数据就丢了)。
对于16PP,那么需要进行颜色转换后再存放进frame buffer。
对于32PP,大小刚好对应,直接*pc = color即可。

3.6.2 32bppto16bpp函数#

1
2
3
4
5
6
7
8
9
10
11
//先分别取出RGB,再相应的清除低位数据,实现将RGB888变为RGB565
unsigned short convert32bppto16bpp(unsigned int rgb) {
int r = (rgb >> 16)& 0xff;
int g = (rgb >> 8) & 0xff;
int b = rgb & 0xff;
/* rgb565 */
r = r >> 3;//取低5位
g = g >> 2;//取低6位
b = b >> 3;//取低5位
return ((r<<11) | (g<<5) | (b));
}

3.6.3 画线画圆#

画圆画线的具体原理不是本主题的重点,这些属于研究算法的范畴了,比如这里就有现成的算法可以用,如这篇博客:https://blog.csdn.net/p1126500468/article/details/50428613,里面有画圆画线的函数实现,直接使用就可以了,套用画点的”轮子”就可以了。

3.6.4 测试#

新建一个geometry.c,复制博客中代码,替换里面的描点显示函数即可。

1
2
3
4
5
6
7
8
9
10
11
12
/* 画线 */
draw_line(0, 0, xres - 1, 0, 0xff0000); //(0,0) 到(xres - 1, 0)两点间的线
draw_line(xres - 1, 0, xres - 1, yres - 1, 0xffff00);
draw_line(0, yres - 1, xres - 1, yres - 1, 0xff00aa);
draw_line(0, 0, 0, yres - 1, 0xff00ef);
draw_line(0, 0, xres - 1, yres - 1, 0xff4500);
draw_line(xres - 1, 0, 0, yres - 1, 0xff0780);

delay(1000000);

/* 画圆 */
draw_circle(xres/2, yres/2, yres/4, 0xff00);

3.7 字符库移植#

字符也是由点构成的,一个个点组成的点阵,其实本质上要显示文字就是把字库移植到对应的自己型号相匹配的board上,字库中的每一个字符都是一些点按照对应格式组合成的集合。

从linux内核源码中随便挑选一个字库文件,比如linux-4.18.16/lib/fonts这个目录下就有对应的很多字库文件。在这里我挑选font_8x16.c,如下图:

其中8x16表示每个字符所占的像素点的大小,表示每个字符占的大小为长*宽=8*16个像素点。

我们来看下一个字符’A’是如何显示的?从font_8x16.c我们找到字符’A’的数据,如下图:


那么我们如何让font_8x16.c这个字库的数据显示到lcd上呢?font_8x16.c见附件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
a. 根据要显示的字符的ascii码作为索引,在fontdata_8x16中得到点阵数据
b. 根据点阵来设置对应象素的颜色
c. 根据点阵的某位决定是否描颜色
*/
void fb_print_char(int x, int y, char c, unsigned int color) {
int i, j;
/* 根据c的ascii码作为索引在fontdata_8x16中得到点阵数据(fontdata_8x16是字库的数据集合)*/
unsigned char *dots = &fontdata_8x16[c * 16];
unsigned char data;
int bit;
/* 根据点阵来设置对应象素的颜色 */
for (j = y; j < y+16; j++) {
data = *dots++;
bit = 7;
for (i = x; i < x+8; i++) {
/* 根据点阵的某位决定是否描颜色 */
if (data & (1<<bit))
fb_put_pixel(i, j, color);
bit--;
}
}
}

在font_8x16.c里面,每个字符占据16字节,因此想要根据ascii码找到对应的点阵数据,需要对应的乘16,再取地址,得到该字符的首地址。

在显示之前,还需要获取LCD参数:

1
2
3
4
5
6
7
extern const unsigned char fontdata_8x16[];
/* 获得LCD参数 */
static unsigned int fb_base;
static int xres, yres, bpp;
void font_init(void) {
get_lcd_params(&fb_base, &xres, &yres, &bpp);
}

3.7.1 显示字符串#

如果想显示字符串,那就在每显示完一个字符后,x轴加8即可,同时考虑是否超出屏幕显示范围进行换行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* "abc\n\r123" */
void fb_print_string(int x, int y, char* str, unsigned int color) {
int i = 0, j;
while (str[i]) {
if (str[i] == '\n')
y = y+16;
else if (str[i] == '\r')
x = 0;
else {
fb_print_char(x, y, str[i], color);
x = x+8;
if (x >= xres) {
x = 0;
y = y+16;
}
}
i++;
}
}

s3c2440裸机编程-SPI

1 SPI原理#

1.1 spi概念#

SPI是串行外设接口(Serial Peripheral Interface)的缩写。是 Motorola 公司推出的一种同步串行接口技术,是一种高速的,全双工,同步的通信总线。

特点:

1
2
高速、同步、全双工、非差分、总线式
主从机通信模式

优点:

1
2
支持全双工通信(SPI的数据输入和输出线独立,所以允许同时完成数据的输入和输出)
数据传输速率快(I2c一般只能到100-400Khz, SPI高达上百Mhz)

缺点:

1
没有指定的流控制,没有应答机制确认是否接收到数据,所以跟IIC总线协议比较在数据可靠性上有一定的缺陷

1.2 硬体框架#

img

SCK:提供时钟
DO:作为数据输出
DI:作为数据输入
CS0/CS1:作为片选

同一时刻只能有一个SPI设备处于工作状态。因此cs选中谁,谁就和主控通信。

1.2 数据传输时序#

img

这里是一款SPI flash在SCLK上升延采样数据(D7~D0)的示意图。设现在s3c2440传输一个0x56数据给SPI Flash,时序如下:

img

CS0低选中SPI Flash,配置成模式0, 0x56的二进制就是0b0101 0110,因此在每个SCK时钟周期,DO输出对应的电平。会在每个时钟周期的上升沿采样DO上的电平

1.3 SPI相关的名词缩写#

KPOL: (Clock Polarity)(时钟)极性

CKPHA: (Clock Phase)(时钟)相位

SCK=SCLK:SPI的时钟

Leading edge:前一个边沿

Trailing edge:后一个边沿

1.4 时钟极性相位模式#

CPOL:表示SPI CLK的初始电平(空闲状态时电平),0为低电平,1为高电平

CPHA:表示相位,即第一个还是第二个时钟沿采样数据,0为第一个时钟沿,1为第二个时钟沿

两者组合成4种模式:

SPI模式 CPOL CPHA 空闲状态时钟极性 采样/移位时钟相位
0 0 0 低电平 上升沿采样(锁存)下降沿移位
1 0 1 低电平 上升沿移位下降沿采样(锁存)
2 1 0 高电平 上升沿移位下降沿采样(锁存)
3 1 1 高电平 上升沿采样(锁存)下降沿移位

4个模式波形对比:

img

img

常用的是模式0和模式3,因为它们都是在上升沿采样数据.当配置成模式3时,对于主设备,数据采样在时钟上升沿,数据传送在时钟下降沿****。

主设备SPI时钟和极性的配置应该由外设来决定;二者的配置应该保持一致,即主设备的SDO同从设备的SDO配置一致,主设备的SDI同从设备的SDI配置一致。即因为主从设备是在SCLK的控制下,同时发送和接收数据,并通过2个双向移位寄存器来交换数据 。

举个例子,以 CPOL=0,CPHA=0,模式0为例:空闲CLK为低电平,相位为0,也就是上升延采集数据。由于SPI的全双工可以同时读写,发送MOSI数据为0xD2,接收MISO数据为0x66。

img

2 SPI控制器结构#

img

2.1 SSPSR#

SSPSR:移位寄存器(Shift Register). 根据 SPI 时钟同步信号, 将SSPBUF中的数据一位一位移出去或者收进来。

2.2 SSPBUF#

Master 与 Slave 之间交换的数据其实都是移位寄存器从 SSPBUF 里面拷贝的。通过往 SSPBUF 对应的寄存器 (Tx-Data / Rx-Data register) 里读写数据, 间接地操控 SPI 设备内部的 SSPBUF。

2.3 Controller#

用来发送控制信号的,像CS,SCK等控制信号。

3 SPI裸机示例#

3.1 SPI-OLED显示面板介绍#

img

QG-2864TMBEG01这款OLED为例,可见它支持Parallel/i2c/SPI这3种方式对它进行控制,这里仅对它进行SPI控制。它的product Specification见附件。

3.1.1 并行接口时序#

img

img

3.1.2 SPI串行接口时序#

img

1
2
3
4
5
6
7
Tr/Tf: 表示spi clk上升/下降延不能超过40ns
Tclkl/Tclkh: 表示spi clk低/高电平持续至少20ns
Tcycle: 表示spi clk一个时钟周期至少100ns
Tdsw/Tdhw: 表示spi data的建立/持续时间至少15ms
Tcss:片选建立时间至少20ns
Tcsh:片选持续时间至少10ns
Tas/Tah:地址建立/持续时间至少15ns

3.1.3 power on sequence-上电序列#

img

3.1.4 power down sequence-掉电序列#

img

3.1.5 休眠唤醒#

img

3.2 SPI-OLED面板显示原理#

img

QG-2864TMBEG01这款为例,OLED长有128个像素,宽有64个像素,共128*64=8,192 像素。每个像素用1bit来表示,为1则亮,为0则灭。所以每一个字节数据Data表示8个像素,Data0~Data1023,如上图。 那要怎么在显存里面存放Data数据。

3.2.1 发送地址#

3.2.1.1 页(page)地址模式#

QG-2864TMBEG01 OLED主控有三种地址模式,我们常用的是页地址模式,发送0x20命令,再发送0x02命令,进入页地址模式,如下图:

img

它把显存的64行分为8页,每页对应8行;选中某页后,再选择某列。因此共用页地址,也就是8行都共用同一个页地址,列地址独立,所以page0page7,col0col127。然后就可以往里面写数据了,每写一个数据,列地址就会加1,一直写到最右端的位置,页地址加1,会自动跳到最左端。通过命令来实现发送页地址和列地址,其中列地址分为两次发送,先发送低字节,再发送高字节。如下图,假设每个字符数据大小为8x16像素,假如第一个字符位置为(page,col),相邻的右边就是(page,col+8),写一个字符需要先发8字节,然后跳到下一页坐标就是(page+2,col),发送8字节数据。一个字符需要2个page*8个col,由于一个像素占1个bit, 所以一个Data占1byte, 一个字符占16 byte。

img

3.2.1.1.1 设置page addr#

img

一共就8页,因此X2X0,有3bit足够了。比如选中page0,则x2x0 = 000。

3.2.1.1.2 设置col addr#

img

分两次发送, 先发送列地址低4位,再发送列地址高4位;

3.2.2 发送数据#

如何发送一个字符‘A’,显示到OLED。

  1. 取得字模

这里从网上找了一份8x16的字库。

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
#ifndef __OLEDFONT_H
#define __OLEDFONT_H
const unsigned char oled_asc2_8x16[95][16]= {
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},// 0
{0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x33,0x30,0x00,0x00,0x00},//!1
{0x00,0x10,0x0C,0x06,0x10,0x0C,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},//"2
{0x40,0xC0,0x78,0x40,0xC0,0x78,0x40,0x00,0x04,0x3F,0x04,0x04,0x3F,0x04,0x04,0x00},//#3
{0x00,0x70,0x88,0xFC,0x08,0x30,0x00,0x00,0x00,0x18,0x20,0xFF,0x21,0x1E,0x00,0x00},//$4
{0xF0,0x08,0xF0,0x00,0xE0,0x18,0x00,0x00,0x00,0x21,0x1C,0x03,0x1E,0x21,0x1E,0x00},//%5
{0x00,0xF0,0x08,0x88,0x70,0x00,0x00,0x00,0x1E,0x21,0x23,0x24,0x19,0x27,0x21,0x10},//&6
{0x10,0x16,0x0E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},//'7
{0x00,0x00,0x00,0xE0,0x18,0x04,0x02,0x00,0x00,0x00,0x00,0x07,0x18,0x20,0x40,0x00},//(8
{0x00,0x02,0x04,0x18,0xE0,0x00,0x00,0x00,0x00,0x40,0x20,0x18,0x07,0x00,0x00,0x00},//)9
{0x40,0x40,0x80,0xF0,0x80,0x40,0x40,0x00,0x02,0x02,0x01,0x0F,0x01,0x02,0x02,0x00},//*10
{0x00,0x00,0x00,0xF0,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x1F,0x01,0x01,0x01,0x00},//+11
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0xB0,0x70,0x00,0x00,0x00,0x00,0x00},//,12
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01},//-13
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x30,0x00,0x00,0x00,0x00,0x00},//.14
{0x00,0x00,0x00,0x00,0x80,0x60,0x18,0x04,0x00,0x60,0x18,0x06,0x01,0x00,0x00,0x00},///15
{0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00,0x00,0x0F,0x10,0x20,0x20,0x10,0x0F,0x00},//016
{0x00,0x10,0x10,0xF8,0x00,0x00,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00},//117
{0x00,0x70,0x08,0x08,0x08,0x88,0x70,0x00,0x00,0x30,0x28,0x24,0x22,0x21,0x30,0x00},//218
{0x00,0x30,0x08,0x88,0x88,0x48,0x30,0x00,0x00,0x18,0x20,0x20,0x20,0x11,0x0E,0x00},//319
{0x00,0x00,0xC0,0x20,0x10,0xF8,0x00,0x00,0x00,0x07,0x04,0x24,0x24,0x3F,0x24,0x00},//420
{0x00,0xF8,0x08,0x88,0x88,0x08,0x08,0x00,0x00,0x19,0x21,0x20,0x20,0x11,0x0E,0x00},//521
{0x00,0xE0,0x10,0x88,0x88,0x18,0x00,0x00,0x00,0x0F,0x11,0x20,0x20,0x11,0x0E,0x00},//622
{0x00,0x38,0x08,0x08,0xC8,0x38,0x08,0x00,0x00,0x00,0x00,0x3F,0x00,0x00,0x00,0x00},//723
{0x00,0x70,0x88,0x08,0x08,0x88,0x70,0x00,0x00,0x1C,0x22,0x21,0x21,0x22,0x1C,0x00},//824
{0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00,0x00,0x00,0x31,0x22,0x22,0x11,0x0F,0x00},//925
{0x00,0x00,0x00,0xC0,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x30,0x00,0x00,0x00},//:26
{0x00,0x00,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x60,0x00,0x00,0x00,0x00},//;27
{0x00,0x00,0x80,0x40,0x20,0x10,0x08,0x00,0x00,0x01,0x02,0x04,0x08,0x10,0x20,0x00},//<28
{0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x00,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x00},//=29
{0x00,0x08,0x10,0x20,0x40,0x80,0x00,0x00,0x00,0x20,0x10,0x08,0x04,0x02,0x01,0x00},//>30
{0x00,0x70,0x48,0x08,0x08,0x08,0xF0,0x00,0x00,0x00,0x00,0x30,0x36,0x01,0x00,0x00},//?31
{0xC0,0x30,0xC8,0x28,0xE8,0x10,0xE0,0x00,0x07,0x18,0x27,0x24,0x23,0x14,0x0B,0x00},//@32
{0x00,0x00,0xC0,0x38,0xE0,0x00,0x00,0x00,0x20,0x3C,0x23,0x02,0x02,0x27,0x38,0x20},//A33
{0x08,0xF8,0x88,0x88,0x88,0x70,0x00,0x00,0x20,0x3F,0x20,0x20,0x20,0x11,0x0E,0x00},//B34
{0xC0,0x30,0x08,0x08,0x08,0x08,0x38,0x00,0x07,0x18,0x20,0x20,0x20,0x10,0x08,0x00},//C35
{0x08,0xF8,0x08,0x08,0x08,0x10,0xE0,0x00,0x20,0x3F,0x20,0x20,0x20,0x10,0x0F,0x00},//D36
{0x08,0xF8,0x88,0x88,0xE8,0x08,0x10,0x00,0x20,0x3F,0x20,0x20,0x23,0x20,0x18,0x00},//E37
{0x08,0xF8,0x88,0x88,0xE8,0x08,0x10,0x00,0x20,0x3F,0x20,0x00,0x03,0x00,0x00,0x00},//F38
{0xC0,0x30,0x08,0x08,0x08,0x38,0x00,0x00,0x07,0x18,0x20,0x20,0x22,0x1E,0x02,0x00},//G39
{0x08,0xF8,0x08,0x00,0x00,0x08,0xF8,0x08,0x20,0x3F,0x21,0x01,0x01,0x21,0x3F,0x20},//H40
{0x00,0x08,0x08,0xF8,0x08,0x08,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00},//I41
{0x00,0x00,0x08,0x08,0xF8,0x08,0x08,0x00,0xC0,0x80,0x80,0x80,0x7F,0x00,0x00,0x00},//J42
{0x08,0xF8,0x88,0xC0,0x28,0x18,0x08,0x00,0x20,0x3F,0x20,0x01,0x26,0x38,0x20,0x00},//K43
{0x08,0xF8,0x08,0x00,0x00,0x00,0x00,0x00,0x20,0x3F,0x20,0x20,0x20,0x20,0x30,0x00},//L44
{0x08,0xF8,0xF8,0x00,0xF8,0xF8,0x08,0x00,0x20,0x3F,0x00,0x3F,0x00,0x3F,0x20,0x00},//M45
{0x08,0xF8,0x30,0xC0,0x00,0x08,0xF8,0x08,0x20,0x3F,0x20,0x00,0x07,0x18,0x3F,0x00},//N46
{0xE0,0x10,0x08,0x08,0x08,0x10,0xE0,0x00,0x0F,0x10,0x20,0x20,0x20,0x10,0x0F,0x00},//O47
{0x08,0xF8,0x08,0x08,0x08,0x08,0xF0,0x00,0x20,0x3F,0x21,0x01,0x01,0x01,0x00,0x00},//P48
{0xE0,0x10,0x08,0x08,0x08,0x10,0xE0,0x00,0x0F,0x18,0x24,0x24,0x38,0x50,0x4F,0x00},//Q49
{0x08,0xF8,0x88,0x88,0x88,0x88,0x70,0x00,0x20,0x3F,0x20,0x00,0x03,0x0C,0x30,0x20},//R50
{0x00,0x70,0x88,0x08,0x08,0x08,0x38,0x00,0x00,0x38,0x20,0x21,0x21,0x22,0x1C,0x00},//S51
{0x18,0x08,0x08,0xF8,0x08,0x08,0x18,0x00,0x00,0x00,0x20,0x3F,0x20,0x00,0x00,0x00},//T52
{0x08,0xF8,0x08,0x00,0x00,0x08,0xF8,0x08,0x00,0x1F,0x20,0x20,0x20,0x20,0x1F,0x00},//U53
{0x08,0x78,0x88,0x00,0x00,0xC8,0x38,0x08,0x00,0x00,0x07,0x38,0x0E,0x01,0x00,0x00},//V54
{0xF8,0x08,0x00,0xF8,0x00,0x08,0xF8,0x00,0x03,0x3C,0x07,0x00,0x07,0x3C,0x03,0x00},//W55
{0x08,0x18,0x68,0x80,0x80,0x68,0x18,0x08,0x20,0x30,0x2C,0x03,0x03,0x2C,0x30,0x20},//X56
{0x08,0x38,0xC8,0x00,0xC8,0x38,0x08,0x00,0x00,0x00,0x20,0x3F,0x20,0x00,0x00,0x00},//Y57
{0x10,0x08,0x08,0x08,0xC8,0x38,0x08,0x00,0x20,0x38,0x26,0x21,0x20,0x20,0x18,0x00},//Z58
{0x00,0x00,0x00,0xFE,0x02,0x02,0x02,0x00,0x00,0x00,0x00,0x7F,0x40,0x40,0x40,0x00},//[59
{0x00,0x0C,0x30,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x06,0x38,0xC0,0x00},//\60
{0x00,0x02,0x02,0x02,0xFE,0x00,0x00,0x00,0x00,0x40,0x40,0x40,0x7F,0x00,0x00,0x00},//]61
{0x00,0x00,0x04,0x02,0x02,0x02,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},//^62
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80},//_63
{0x00,0x02,0x02,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},//`64
{0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x00,0x19,0x24,0x22,0x22,0x22,0x3F,0x20},//a65
{0x08,0xF8,0x00,0x80,0x80,0x00,0x00,0x00,0x00,0x3F,0x11,0x20,0x20,0x11,0x0E,0x00},//b66
{0x00,0x00,0x00,0x80,0x80,0x80,0x00,0x00,0x00,0x0E,0x11,0x20,0x20,0x20,0x11,0x00},//c67
{0x00,0x00,0x00,0x80,0x80,0x88,0xF8,0x00,0x00,0x0E,0x11,0x20,0x20,0x10,0x3F,0x20},//d68
{0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x00,0x1F,0x22,0x22,0x22,0x22,0x13,0x00},//e69
{0x00,0x80,0x80,0xF0,0x88,0x88,0x88,0x18,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00},//f70
{0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x00,0x00,0x6B,0x94,0x94,0x94,0x93,0x60,0x00},//g71
{0x08,0xF8,0x00,0x80,0x80,0x80,0x00,0x00,0x20,0x3F,0x21,0x00,0x00,0x20,0x3F,0x20},//h72
{0x00,0x80,0x98,0x98,0x00,0x00,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00},//i73
{0x00,0x00,0x00,0x80,0x98,0x98,0x00,0x00,0x00,0xC0,0x80,0x80,0x80,0x7F,0x00,0x00},//j74
{0x08,0xF8,0x00,0x00,0x80,0x80,0x80,0x00,0x20,0x3F,0x24,0x02,0x2D,0x30,0x20,0x00},//k75
{0x00,0x08,0x08,0xF8,0x00,0x00,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00},//l76
{0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x00,0x20,0x3F,0x20,0x00,0x3F,0x20,0x00,0x3F},//m77
{0x80,0x80,0x00,0x80,0x80,0x80,0x00,0x00,0x20,0x3F,0x21,0x00,0x00,0x20,0x3F,0x20},//n78
{0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x00,0x1F,0x20,0x20,0x20,0x20,0x1F,0x00},//o79
{0x80,0x80,0x00,0x80,0x80,0x00,0x00,0x00,0x80,0xFF,0xA1,0x20,0x20,0x11,0x0E,0x00},//p80
{0x00,0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x0E,0x11,0x20,0x20,0xA0,0xFF,0x80},//q81
{0x80,0x80,0x80,0x00,0x80,0x80,0x80,0x00,0x20,0x20,0x3F,0x21,0x20,0x00,0x01,0x00},//r82
{0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x00,0x00,0x33,0x24,0x24,0x24,0x24,0x19,0x00},//s83
{0x00,0x80,0x80,0xE0,0x80,0x80,0x00,0x00,0x00,0x00,0x00,0x1F,0x20,0x20,0x00,0x00},//t84
{0x80,0x80,0x00,0x00,0x00,0x80,0x80,0x00,0x00,0x1F,0x20,0x20,0x20,0x10,0x3F,0x20},//u85
{0x80,0x80,0x80,0x00,0x00,0x80,0x80,0x80,0x00,0x01,0x0E,0x30,0x08,0x06,0x01,0x00},//v86
{0x80,0x80,0x00,0x80,0x00,0x80,0x80,0x80,0x0F,0x30,0x0C,0x03,0x0C,0x30,0x0F,0x00},//w87
{0x00,0x80,0x80,0x00,0x80,0x80,0x80,0x00,0x00,0x20,0x31,0x2E,0x0E,0x31,0x20,0x00},//x88
{0x80,0x80,0x80,0x00,0x00,0x80,0x80,0x80,0x80,0x81,0x8E,0x70,0x18,0x06,0x01,0x00},//y89
{0x00,0x80,0x80,0x80,0x80,0x80,0x80,0x00,0x00,0x21,0x30,0x2C,0x22,0x21,0x30,0x00},//z90
{0x00,0x00,0x00,0x00,0x80,0x7C,0x02,0x02,0x00,0x00,0x00,0x00,0x00,0x3F,0x40,0x40},//{91
{0x00,0x00,0x00,0x00,0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x00,0x00,0x00},//|92
{0x00,0x02,0x02,0x7C,0x80,0x00,0x00,0x00,0x00,0x40,0x40,0x3F,0x00,0x00,0x00,0x00},//}93
{0x00,0x06,0x01,0x01,0x02,0x02,0x04,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},//~94
};
#endif
  1. 发送页/列地址

  2. 发送数据

3.3 SPI-OLED驱动-GPIO模拟SPI方式#

3.3.1 软件层次#

操作OLED,通过三条线(SCK、DO、CS)与OLED相连,这里没有DI是因为s3c2440只会向OLED传数据而不用接收数据。

1
2
gpio_spi.c来实现gpio模拟spi,负责spi通讯。对于OLED,有专门的指令和数据格式,要传输的数据内容。
oled.c这一层来实现,负责组织数据。

3.3.2 gpio_spi.c#

img

3.3.2.1 spi引脚初始化#

上图J3为板子pin2pin到OLED的底座。

1
2
3
4
5
GPF1作为OLED片选引脚,设置为输出;
GPG4作为OLED的数据(Data)/命令(Command)选择引脚,设置为输出;
GPG5作为SPI的MISO,设置为输入(实际用不到);
GPG6作为SPI的MOSI,设置为输出;
GPG7作为SPI的时钟CLK,设置为输出;

img

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void SPIInit(void) {
/* 初始化引脚 */
SPI_GPIO_Init();
}
static void SPI_GPIO_Init(void) {
/* GPF1 as OLED_CSn output */
GPFCON &= ~(3<<(1*2));
GPFCON |= (1<<(1*2));
GPFDAT |= (1<<1);//取消OLED_CSn片选,pull up
/* GPG2 FLASH_CSn output
* GPG4 OLED_DC output
* GPG5 SPIMISO input
* GPG6 SPIMOSI output
* GPG7 SPICLK output
*/
GPGCON &= ~((3<<(2*2)) | (3<<(4*2)) | (3<<(5*2)) | (3<<(6*2)) | (3<<(7*2)));
GPGCON |= ((1<<(2*2)) | (1<<(4*2)) | (1<<(6*2)) | (1<<(7*2)));
GPGDAT |= (1<<2);//取消FLASH_CSn 片选,pull up
}

3.3.2.2 写命令#

D/C即数据(Data)/命令(Command)选择引脚,它为高电平时,OLED即认为收到的是数据;它为低电平时,OLED即认为收到的是命令。先设置为命令模式,再片选OLED,再传输命令,再恢复成原来的模式和取消片选。

img

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
static void SPI_Set_DO(char val) {
if (val)
GPGDAT |= (1<<6);
else
GPGDAT &= ~(1<<6);
}
static void SPI_Set_CLK(char val) {
if (val)
GPGDAT |= (1<<7);
else
GPGDAT &= ~(1<<7);
}
void SPISendByte(unsigned char val) {
int i;
for (i = 0; i < 8; i++){
SPI_Set_CLK(0);
SPI_Set_DO(val & 0x80);//MSB
SPI_Set_CLK(1);
val <<= 1;
}
}
static void OLED_Set_DC(char val) {
if (val)
GPGDAT |= (1<<4);
else
GPGDAT &= ~(1<<4);
}
static void OLED_Set_CS(char val) {
if (val)
GPFDAT |= (1<<1);
else
GPFDAT &= ~(1<<1);
}
static void OLEDWriteCmd(unsigned char cmd) {
OLED_Set_DC(0); /* command */
OLED_Set_CS(0); /* select OLED */
SPISendByte(cmd);
OLED_Set_CS(1); /* de-select OLED */
OLED_Set_DC(1); /* gpio output default is pull up*/
}
  1. 拉低DC引脚表示要发送是命令;

  2. 片选

  3. 发送1byte数据

3.3.2.2.1 SPISendByte#

img
SPISendByte是把一个byte数据从高位往低位依次发送到DO。spi配置模式0, 主控先设置CLK为低,由于是MSB, 先传送高位,然后CLK为高,在CLK这个上升沿,DO的数据被锁存,OLED就读取了一位数据。接着左移一位,传输下一位。通过SPI_Set_CLK()和SPI_Set_DO()配置SCK和DO的时序,用gpio模拟出了spi。至此,SPI初始化和OLED初始化就基本完成了,接下来就是OLED显示部分。

这里gpio模拟spi传送时主控没有加延时控制SCK的频率,那是由于s3c2440本身cpu运行就很慢,这里不延时也是能满足该款外设的spi传输时序,如果cpu很快,那么需要控制spi时序。

1
2
3
4
5
6
7
8
9
10
11
//每隔一个SPI时钟,发送1位数据,MSB-高位先出
//这里的SPI时钟并没有指定周期,这就取决于指令执行的速率,指令执行越快,gpio模拟的SPI时钟越快,如下:
void SPISendByte(unsigned char val) {
int i;
for (i = 0; i < 8; i++) {
SPI_Set_CLK(0);
SPI_Set_DO(val & 0x80);//MSB
SPI_Set_CLK(1);
val <<= 1;
}
}
  1. 取消片选
  2. DC拉高

3.3.2.3 写数据#

与写命令同理:

1
2
3
4
5
6
static void OLEDWriteDat(unsigned char data){
OLED_Set_DC(1); /* data*/
OLED_Set_CS(0); /* select OLED */
SPISendByte(data);
OLED_Set_CS(1); /* de-select OLED */
}

3.2.3 oled.c#

3.2.3.1 初始化OLED#

找到QG-2864TMBEG01 的power on sequence-上电时序。

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 OLEDInit(void){
/* 向OLED发命令以初始化 */
OLEDWriteCmd(0xAE); /*display off*/
OLEDWriteCmd(0x00); /*set lower column address*/
OLEDWriteCmd(0x10); /*set higher column address*/
OLEDWriteCmd(0x40); /*set display start line*/
OLEDWriteCmd(0xB0); /*set page address*/
OLEDWriteCmd(0x81); /*contract control*/
OLEDWriteCmd(0x66); /*128*/
OLEDWriteCmd(0xA1); /*set segment remap*/
OLEDWriteCmd(0xA6); /*normal / reverse*/
OLEDWriteCmd(0xA8); /*multiplex ratio*/
OLEDWriteCmd(0x3F); /*duty = 1/64*/
OLEDWriteCmd(0xC8); /*Com scan direction*/
OLEDWriteCmd(0xD3); /*set display offset*/
OLEDWriteCmd(0x00);
OLEDWriteCmd(0xD5); /*set osc division*/
OLEDWriteCmd(0x80);
OLEDWriteCmd(0xD9); /*set pre-charge period*/
OLEDWriteCmd(0x1f);
OLEDWriteCmd(0xDA); /*set COM pins*/
OLEDWriteCmd(0x12);
OLEDWriteCmd(0xdb); /*set vcomh*/
OLEDWriteCmd(0x30);
OLEDWriteCmd(0x8d); /*set charge pump enable*/
OLEDWriteCmd(0x14);
}

3.2.3.2 驱动显示OLED#

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
static void OLEDSetPos(int page, int col) {
OLEDWriteCmd(0xB0 + page); /* page address */
OLEDWriteCmd(col & 0xf); /* Lower Column Start Address */
OLEDWriteCmd(0x10 + (col >> 4)); /* Lower Higher Start Address */
}
/* page: 0-7
* col : 0-127
* 字符: 8x16象素
*/
void OLEDPutChar(int page, int col, char c) {
int i = 0;
/* 得到字模 */
const unsigned char *dots = oled_asc2_8x16[c - ' '];
/* 发给OLED */
OLEDSetPos(page, col);
/* 发出8字节数据 */
for (i = 0; i < 8; i++)
OLEDWriteDat(dots[i]);
OLEDSetPos(page+1, col);
/* 发出8字节数据 */
for (i = 0; i < 8; i++)
OLEDWriteDat(dots[i+8]);
}
/* page: 0-7
* col : 0-127
* 字符: 8x16象素
*/
void OLEDPrint(int page, int col, char *str) {
int i = 0;
while (str[i]) {
OLEDPutChar(page, col, str[i]);
col += 8;
if (col > 127) {
col = 0;
page += 2;
}
i++;
}
}
static void OLEDSetPos(int page, int col) {
OLEDWriteCmd(0xB0 + page); /* page address */
OLEDWriteCmd(col & 0xf); /* Lower Column Start Address */
OLEDWriteCmd(0x10 + (col >> 4)); /* Lower Higher Start Address */
}
static void OLEDClear(void) {
int page, i;
for (page = 0; page < 8; page ++) {
OLEDSetPos(page, 0);
for (i = 0; i < 128; i++)
OLEDWriteDat(0);
}
}

3.3.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
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
/************************** gpio_spi.c ****************/
#include "s3c24xx.h"
/* 用GPIO模拟SPI */
static void SPI_GPIO_Init(void)
{
/* GPF1 OLED_CSn output */
GPFCON &= ~(3<<(1*2));
GPFCON |= (1<<(1*2));
GPFDAT |= (1<<1);

/* GPG2 FLASH_CSn output
* GPG4 OLED_DC output
* GPG5 SPIMISO input
* GPG6 SPIMOSI output
* GPG7 SPICLK output
*/
GPGCON &= ~((3<<(2*2)) | (3<<(4*2)) | (3<<(5*2)) | (3<<(6*2)) | (3<<(7*2)));
GPGCON |= ((1<<(2*2)) | (1<<(4*2)) | (1<<(6*2)) | (1<<(7*2)));
GPGDAT |= (1<<2);
}

static void SPI_Set_CLK(char val)
{
if (val)
GPGDAT |= (1<<7);
else
GPGDAT &= ~(1<<7);
}

static void SPI_Set_DO(char val)
{
if (val)
GPGDAT |= (1<<6);
else
GPGDAT &= ~(1<<6);
}

void SPISendByte(unsigned char val)
{
int i;
for (i = 0; i < 8; i++)
{
SPI_Set_CLK(0);
SPI_Set_DO(val & 0x80);
SPI_Set_CLK(1);
val <<= 1;
}

}

void SPIInit(void)
{
/* 初始化引脚 */
SPI_GPIO_Init();
}

/******************* oled.c****************/
#include "oledfont.h"
#include "gpio_spi.h"
#include "s3c24xx.h"
static void OLED_Set_DC(char val)
{
if (val)
GPGDAT |= (1<<4);
else
GPGDAT &= ~(1<<4);
}
static void OLED_Set_CS(char val)
{
if (val)
GPFDAT |= (1<<1);
else
GPFDAT &= ~(1<<1);
}
static void OLEDWriteCmd(unsigned char cmd)
{
OLED_Set_DC(0); /* command */
OLED_Set_CS(0); /* select OLED */

SPISendByte(cmd);

OLED_Set_CS(1); /* de-select OLED */
OLED_Set_DC(1); /* */
}
static void OLEDWriteDat(unsigned char dat)
{
OLED_Set_DC(1); /* data */
OLED_Set_CS(0); /* select OLED */

SPISendByte(dat);

OLED_Set_CS(1); /* de-select OLED */
OLED_Set_DC(1); /* */
}
static void OLEDSetPageAddrMode(void)
{
OLEDWriteCmd(0x20);
OLEDWriteCmd(0x02);
}
static void OLEDSetPos(int page, int col)
{
OLEDWriteCmd(0xB0 + page); /* page address */

OLEDWriteCmd(col & 0xf); /* Lower Column Start Address */
OLEDWriteCmd(0x10 + (col >> 4)); /* Lower Higher Start Address */
}
static void OLEDClear(void)
{
int page, i;
for (page = 0; page < 8; page ++)
{
OLEDSetPos(page, 0);
for (i = 0; i < 128; i++)
OLEDWriteDat(0);
}
}
void OLEDInit(void)
{
/* 向OLED发命令以初始化 */
OLEDWriteCmd(0xAE); /*display off*/
OLEDWriteCmd(0x00); /*set lower column address*/
OLEDWriteCmd(0x10); /*set higher column address*/
OLEDWriteCmd(0x40); /*set display start line*/
OLEDWriteCmd(0xB0); /*set page address*/
OLEDWriteCmd(0x81); /*contract control*/
OLEDWriteCmd(0x66); /*128*/
OLEDWriteCmd(0xA1); /*set segment remap*/
OLEDWriteCmd(0xA6); /*normal / reverse*/
OLEDWriteCmd(0xA8); /*multiplex ratio*/
OLEDWriteCmd(0x3F); /*duty = 1/64*/
OLEDWriteCmd(0xC8); /*Com scan direction*/
OLEDWriteCmd(0xD3); /*set display offset*/
OLEDWriteCmd(0x00);
OLEDWriteCmd(0xD5); /*set osc division*/
OLEDWriteCmd(0x80);
OLEDWriteCmd(0xD9); /*set pre-charge period*/
OLEDWriteCmd(0x1f);
OLEDWriteCmd(0xDA); /*set COM pins*/
OLEDWriteCmd(0x12);
OLEDWriteCmd(0xdb); /*set vcomh*/
OLEDWriteCmd(0x30);
OLEDWriteCmd(0x8d); /*set charge pump enable*/
OLEDWriteCmd(0x14);

OLEDSetPageAddrMode();

OLEDClear();

OLEDWriteCmd(0xAF); /*display ON*/
}

/* page: 0-7
* col : 0-127
* 字符: 8x16象素
*/
void OLEDPutChar(int page, int col, char c)
{
int i = 0;
/* 得到字模 */
const unsigned char *dots = oled_asc2_8x16[c - ' '];

/* 发给OLED */
OLEDSetPos(page, col);
/* 发出8字节数据 */
for (i = 0; i < 8; i++)
OLEDWriteDat(dots[i]);

OLEDSetPos(page+1, col);
/* 发出8字节数据 */
for (i = 0; i < 8; i++)
OLEDWriteDat(dots[i+8]);

}

/* page: 0-7
* col : 0-127
* 字符: 8x16象素
*/
void OLEDPrint(int page, int col, char *str)
{
int i = 0;
while (str[i])
{
OLEDPutChar(page, col, str[i]);
col += 8;
if (col > 127)
{
col = 0;
page += 2;
}
i++;
}
}

s3c2440裸机编程-I2C

1 I2C原理#

1.1 硬件电路#

I2C总线是由Philips公司开发的一种简单、双向二线制同步串行总线。

img

SDA(串行数据线)和SCL(串行时钟线)都是双向I/O线,需通过上拉电阻接电源VCC.当总线空闲时.两根线都是高电平。

I2C 总线标准模式下速度可以达到 100Kb/S,快速模式下可以达到 400Kb/S。SDA 和 SCL 这两根线必须要接一个上拉电阻,一般是 4.7K。

1.2 i2c协议#

传输过程如下:

img

1
2
3
4
5
6
7
8
1. 主控发送start讯号(S)
2. 主控发送从设备地址(slave dev addr)
3. 主控发送方向(W/R)
4. 从设备应答(ack)
5. 主控(or从设备)发送数据(data)
6. 从设备(or主控)应答(ack)
...
7. 主控发送停止信号(P)

s3c2440 一次i2c读写过程:

img

1.2.1 S/P信号#

img

start信号:SCL是高电平,SDA被主控拉低。
stop信号:SCL是高电平,SDA被主控拉高。

示波器测量出start信号:
img

示波器测量出stop信号:
img

1.2.2 ACK信号#

img

第9个时钟周期,SDA被拉低表示ack讯号。

1.2.3 DATA格式#

img

用 9个clk传输8bit数据(7bit 从设备地址 + 1bit方向 ),MSB高位先出。第9个clk是ack讯号。

1.2.4 数据有效性#

SDA 线上的数据必须在SCL高电平周期保持稳定,在 SCL 低电平时才能允许改变

1.3 一次完整的I2C数据传输举例#

img

  1. 主控发送了S信号;
  2. 发送地址0x34,包含读写位;
  3. 发送数据0x30, 0x00, 0x01共3个字节数据;
  4. 最后SDA被拉高发送P信号。

这里我是用了带I2C解码的示波器,能将I2C协议解码出来方便调试者阅读分析。

1.4 一条SDA上实现双向传输的原理#

电路设计内部结构使用开极电路。如下图:

img

条件

  1. 主设备发送时,从设备不发送(通过SCL控制即可,比如让前8个clk主控发送数据到SDA,让第9个clk从设备发送数据到SDA)

  2. 主设备发送数据时,从设备的“发送引脚”不能影响SDA数据。反之,从设备发送数据时,主设备的”发送引脚”不能影响到SDA数据。那么如何做到?

    1
    SDA内部电路用三极管,开集电路,原理如下图:

    img

    从上图知道:

    1. 当A,B都为低电平时,三极管不导通,SDA的电平取决于外部电路,这里SDA有上拉电阻,所以对应高电平;

    2. 当主控拉高A时,三极管导通,此时SDA接地,电平被拉低

    3. 同理,当从设备拉高B时,三极管导通,此时SDA接地,电平被拉低

那么电平真值表如下:

img

所以,要实现双向传输:

1
2
3
如果要master-> slave进行数据传输,那么让主控驱动三极管,拉低SDA。
如果要slave-> master进行数据传输,那么让从设备驱动三极管,拉低SDA。
否则,都不驱动三极管,SDA一直输出高电平,处于idle状态。

从下面的例子可以看看数据是怎么传的(实现双向传输)。

举例:主设备发送(8bit)给从设备:

1
2
3
4
5
6
8 个 clk
◼ 从设备不要影响 SDA,从设备不驱动三极管
◼ 主设备决定数据,主设备要发送 1 时不驱动三极管,要发送 0 时驱动三极管
9 个 clk,由从设备决定数据
◼ 主设备不驱动三极管
◼ 从设备决定数据,要发出回应信号的话,就驱动三极管让 SDA 变为 0

从这里也可以知道 ACK 信号是低电平从上面的例子,就可以知道怎样在一条线上实现双向传输,这就是 SDA 上要使用上拉电阻的原因。

为何 SCL 也要使用上拉电阻?在第 9 个时钟之后,如果有某一方需要更多的时间来处理数据,它可以一直驱动三极管把 SCL 拉低。

当 SCL 为低电平时候,大家都不应该使用 IIC 总线,只有当 SCL 从低电平变为高电平的时候,IIC 总线才能被使用。当它就绪后,就可以不再驱动三极管,这是上拉电阻把 SCL 变为高电平,其他设备就可以继续使用 I2C 总线了。

1.5 SCL被从设备拉低表示busy状态#

img

第9个clk 后i2c会产生中断,此时SCL被拉低,表示busy状态,表示谁都不允许再使用i2c, 然后等到中断处理结束了,也就是处于idle状态了,此时会释放出SCL,那么主控可以继续发送SCL讯号表示可以继续进行i2c通信了。

2 I2C控制器#

2.1 I2c主从设备关系#

img

对于写操作,主控作为transmitter,从设备作为receiver。
对于读操作,主控作为receiver, 从设备作为transmitter。

2.2 s3c2440 I2C控制器#

2.2.1 控制器框图#

img

1
2
3
4
5
6
7
8
9
10
11
Pclk = 50Mhz, 经过prescaler分频,可以得到SCL。

IICSTAT: 发出S(start)信号或者P(stop)信号。

Data Bus可以把数据写入IICDS寄存器,然后会自动产生SCL,并且会将8位数据从SDA同步给slave dev,

在数据发送出去后,在第9个SCL时钟,会受到slave dev的ack应答,可以通过查询IICSTAT来判断是否有ACK回应。

当slave dev回应ACK后,那么又可以继续发送数据,继续写入据到IICDS。

当主控想结束,设置IICSTAT发出P信号。

2.2.2 寄存器介绍#

2.2.2.1 IICCON-时钟配置#

img

1
2
3
4
5
6
7
8
9
Bit[7]: 对于发送模式,不需要配置ack信号,ack是接收者发送回来的应答。对于接受模式,设置成1,让它在第9个CLK发出ack讯号(拉低sda)。

Bit[6]:SCL时钟源,pclk分频即可

Bit[5]:中断使能,使用i2c时要去enable

Bit[4]:中断状态标识 表示中断有没有结束,当该bit读出来是1时,SCL被拉低表示busy,也就是i2c中断还在处理中。当i2c中断处理结束后,可以将该bit 清0,释放出SCL。

Bit[3:0]:i2c时钟分频系数配置,SCL时钟 = IICCLK/(IICCON[3:0]+1)

2.2.2.2 IICSTAT-模式配置#

img

1
2
3
4
5
6
7
8
9
bit[7:6]:模式选择

Bit[5]:当读的时候,0表示not busy,1表示busy, 当写的时候,0表示写入STOP, 1表示写入START

Bit[4] : 数据输出使能,0:表示disable, 1表示enable

Bit[3]:仲裁flag

Bit[0]:表示i2c总线上的第9个时钟周期有没有ack,1表示有ack, 0表示无ack

2.2.2.3 IICADD-从机地址配置#

img

2.2.2.4 IICDS-数据寄存器#

img

3 I2C读写操作流程#

The following steps must be executed before any IIC Tx/Rx operations.

  1. Write own slave address on IICADD register, if needed.
  2. Set IICCON register.
    1. Enable interrupt
    2. Define SCL period
  3. Set IICSTAT to enable Serial Output

在操作tx,rx前,要先执行以下几步骤:

  1. IICADD写入从设备地址
  2. 设置IICCON,设置时钟,使能中断
  3. 设置IICSTAT,使能传输

3.1 I2C操作模式#

The S3C2440A IIC-bus interface has four operation modes:
— Master transmitter mode
— Master receive mode
— Slave transmitter mode
— Slave receive mode

3.1.1 主发Master/Transmitter Mode#

img

1
2
3
4
5
6
7
8
9
10
11
12
13
1. 配置成master tx mode(也就是IICSTAT[7:6]配置成11)
2. 把从设备地址写入IICDS,(第一次传输地址)
3. IICSTAT写入0xF0(使能传输,发S信号,使能tx/rx)
3. IICDS中配置的数据(从设备地址7bit + 读写位1bit)就被发送出去了(每传输完一个数据将产生一个中断)
5. 判断第9个clk从设备是否有ack
5.1 如果从设备有ack,恢复i2c传输
IICDS = buf
Clear pending bit
数据被发送出去,继续i2c传输
5.2 如果没有ack, stop,返回错误
IICSTAT = 0xd0
Clear pending bit(IICCON[4])
Delay一会儿等待停止条件生效

3.1.2 主收Master/Receiver Mode#

img

1
2
3
4
5
6
7
8
9
10
11
12
13
1. 配置成master rx mode(也就是IICSTAT[7:6]配置成10)
2. 把从设备地址写入IICDS,(第一次传输地址)
3. IICSTAT写入0xB0(使能传输)
4. IICDS中配置的数据(从设备地址7bit + 读写位1bit)就被发送出去了(每传输完一个数据将产生一个中断)
5. 判断第9个clk从设备是否有ack
5.1 如果从设备有ack,恢复i2c传输
Buf = IICDS
Clear pending bit
数据被接受到,继续i2c传输
5.2 如果没有ack, stop,返回错误
IICSTAT = 0x90
Clear pending bit
Delay一会儿

3.1.3 从发Slave/Transmitter Mode#

img

3.1.4 从收Slave/Receiver Mode#

img

4 I2C程序示例#

4.1 I2C从设备介绍#

IIC控制器只提供了传输数据的能力,至于数据有什么含义,IIC控制器并不知道,数据的含义有外部i2c从设备,我们需要阅读芯片手册,才知道IIC控制器应该发出怎样的数据。

4.1.1 AT24CXX EEPROM#

AT24Cxx系列EEPROM是由美国Mcrochip公司出品,1-512K位的支持I2C总线数据传送协议的串行CMOS E2PROM。I2c传输规则如下:

img

4.2 程序框架#

我们的程序应该分为两层(IIC设备层,IIC控制器层),框架如下图所示:

img

1
2
3
4
5
6
7
最上层是i2c_test层,用来对i2c的功能进行测试和验证。

第2层是i2c设备层,用来对具体某一型号的从设备进行i2c读写。

第3层是通用i2c控制器层,用来提供对具体某一型号的i2c主控进行管理操作。

最底层是i2c控制器具体的型号层。

在通用i2c控制层,我们提供一个统一的接口i2c_transfer,不关使用哪个芯片,他最终都会调用i2c_transfer,来选择某一款I2C控制器,把数据发送出去,或者从I2c设备读到数据。这种层次分明的架构是作为软件开发人员必备的素养和技能。这里也是借鉴了linux内核I2C子系统的模型。

4.2.1 i2c_msg结构体#

我们借鉴Linux I2C子系统的数据结构定义。对于每一次传输的数据都可以用一个i2c_msg结构体来表示。但是,读某个地址的数据时,就要用两个i2c_msg结构体来描述它,因为一个i2c_msg结构体只能描述一个传输方向(读/写),我们读取ac24ccxx某个地址上的数据时,要先写出要读取的地址,然后来读取设备地址上的数据。

img

4.2.2 i2c_test.c#

1
2
3
4
void i2c_test(void) {
/* 初始化: 选择I2C控制器 */
/* 提供菜单供测试 */
}

这个菜单会调用到at24cxx.c里面的函数进行i2c外设读写。

4.2.3 at24cxx.c#

定义描述at24cxx外设,并且实现该外设的操作,里面会使用标准的接口i2c_transfer来启动I2C传输。

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
#define AT24CXX_ADDR 0x50
int at24cxx_write(unsigned int addr, unsigned char *data, int len) {
i2c_msg msg;
int i;
int err;
unsigned char buf[2];

for (i = 0; i < len; i++) {
buf[0] = addr++;
buf[1] = data[i];
/* 构造i2c_msg */
msg.addr = AT24CXX_ADDR;
msg.flags = 0; /* write */
msg.len = 2;
msg.buf = buf;
msg.err = 0;
msg.cnt_transferred = -1;
/* 调用i2c_transfer */
err = i2c_transfer(&msg, 1);
if (err)
return err;
}
return 0;
}

int at24cxx_read(unsigned int addr, unsigned char *data, int len) {
i2c_msg msg[2];
int err;
/* 构造i2c_msg */
msg[0].addr = AT24CXX_ADDR;
msg[0].flags = 0; /* write */
msg[0].len = 1;
msg[0].buf = &addr;
msg[0].err = 0;
msg[0].cnt_transferred = -1;
msg[1].addr = AT24CXX_ADDR;
msg[1].lags = 1; /* read */
msg[1].len = len;
msg[1].buf = data;
msg[1].err = 0;
msg[1].cnt_transferred = -1;
/* 调用i2c_transfer */
err = i2c_transfer(&msg, 2);
if (err)
return err;
return 0;
}

4.2.4 i2c_controller.h#

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct i2c_msg {
unsigned int addr; /* 7bits */
int flags; /* 0 - write, 1 - read */
int len;
int cnt_transferred;
unsigned char *buf;
}i2c_msg, *p_i2c_msg;
typedef struct i2c_controller {
int (*int)(void);
int (*master_xfer)(i2c_msg msgs, int num);
char *name;
}i2c_controller, *p_i2c_controller;

构造i2c_msg和i2c_controller结构。

4.2.5 i2c_controller.c#

实现通用i2c控制器管理,用来注册具体i2c控制器,调用具体控制器去做i2c通信。

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
#define I2C_CONTROLLER_NUM 10
/* 有一个i2c_controller数组用来存放各种不同芯片的操作结构体 */
static p_i2c_controller p_i2c_controllers[I2C_CONTROLLER_NUM];
static p_i2c_controller p_i2c_con_selected;

void register_i2c_controller(p_i2c_controller *p) {
int i;
for (i = 0; i < I2C_CONTROLLER_NUM; i++) {
if (!p_i2c_controllers[i]) {
p_i2c_controllers[i] = p;
return;
}
}
}

/* 根据名字来选择某款I2C控制器 */
int select_i2c_controller(char *name) {
int i;
for (i = 0; i < I2C_CONTROLLER_NUM; i++)
{
if (p_i2c_controllers[i] && !strcmp(name, p_i2c_controllers[i]->name))
{
p_i2c_con_selected = p_i2c_controllers[i];
return 0;
}
}
return -1;
}

/* 实现 i2c_transfer 接口函数 */
int i2c_transfer(i2c_msg msgs, int num) {
return p_i2c_con_selected->master_xfer(msgs, num);
}

void i2c_init(void) {
/* 注册下面的I2C控制器 */
s3c2440_i2c_con_add();
/* 选择某款I2C控制器 */
select_i2c_controller("s3c2440");
/* 调用它的init函数 */
p_i2c_con_selected->init();
}

4.2.6 s3c2440_i2c_controller.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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
void i2c_interrupt_func(int irq) {
/* 每传输完一个数据将产生一个中断 */
/* 对于每次传输, 第1个中断是"已经发出了设备地址" */
}

void s3c2440_i2c_con_init(void)
{
/* 配置引脚用于I2C*/
GPECON &= ~((3<<28) | (3<<30));
GPECON |= ((2<<28) | (2<<30));

/* 设置时钟 */
/* [7] : IIC-bus acknowledge enable bit, 1-enable in rx mode
* [6] : 时钟源, 0: IICCLK = fPCLK /16; 1: IICCLK = fPCLK /512
* [5] : 1-enable interrupt
* [4] : 读出为1时表示中断发生了, 写入0来清除并恢复I2C操作
* [3:0] : Tx clock = IICCLK/(IICCON[3:0]+1).
* Tx Clock = 100khz = 50Mhz/16/(IICCON[3:0]+1)
*/
IICCON = (1<<7) | (0<<6) | (1<<5) | (30<<0);

/* 注册中断处理函数 */
register_irq(27, i2c_interrupt_func);
}

int do_master_tx(p_i2c_msg msg)
{
p_cur_msg = msg;
msg->cnt_transferred = -1;
msg->err = 0;

/* 设置寄存器启动传输 */
/* 1. 配置为 master tx mode */
IICCON |= (1<<7); /* TX mode, 在ACK周期释放SDA */
IICSTAT = (1<<4); /*IIC-bus data output enable/disable(1: Enable Rx/Tx)*/

/* 2. 把从设备地址写入IICDS */
IICDS = msg->addr<<1;//[slave addr [7:1], addr[0] is trans dir]

/* 3. IICSTAT = 0xf0 (启动传输), slave addr数据即被发送出去,当到达第9个clk,无论是否有ack, 将导致中断产生 */
IICSTAT = 0xf0;

/* 后续的传输由中断驱动 */
/* 循环等待中断处理完毕 */
while (!msg->err && msg->cnt_transferred != msg->len);
if (msg->err)
return -1;
else
return 0;
}

int do_master_rx(p_i2c_msg msg)
{
p_cur_msg = msg;
msg->cnt_transferred = -1;
msg->err = 0;

/* 设置寄存器启动传输 */
/* 1. 配置为 Master Rx mode */
IICCON |= (1<<7); /* RX mode, 在ACK周期回应ACK */
IICSTAT = (1<<4); /*IIC-bus data output enable/disable*/

/* 2. 把从设备地址写入IICDS */
IICDS = (msg->addr<<1)|(1<<0);

/* 3. IICSTAT = 0xb0 , 从设备地址即被发送出去, 将导致中断产生 */
IICSTAT = 0xb0;
/* 后续的传输由中断驱动 */
/* 循环等待中断处理完毕 */
while (!msg->err && msg->cnt_transferred != msg->len);
if (msg->err)
return -1;
else
return 0;
}

int s3c2440_master_xfer(p_i2c_msg msgs, int num)
{
int i;
int err;
for (i = 0; i < num; i++)
{
if (msgs[i].flags == 0)/* write */
err = do_master_tx(&msgs[i]);
else
err = do_master_rx(&msgs[i]);
if (err)
return err;
}
return 0;
}

void s3c2440_i2c_con_add(void)
{
register_i2c_controller(&s3c2440_i2c_con);
}
static i2c_controller s3c2440_i2c_con = {
.name = "s3c2440",
.init = s3c2440_i2c_con_init,
.master_xfer = s3c2440_master_xfer,
};

s3c2440_i2c_con_add函数:注册 s3c2440的i2c控制器, 当调用i2c_init就会对选中的这款控制器初始化,也就是调用s3c2440_i2c_con_init。

s3c2440_i2c_con_init函数:

1
2
1).IICCON = (0<<6) | (1<<5) | (30<<0); 设置IICCON控制寄存器。选择发送时钟,使能中断。设置ACK应答使能,bit[7]。
2).register_irq(27, i2c_interrupt_func):注册中断处理函数,当发生I2C中断的时候就会调用i2c_interrupt_func中断处理函数。

s3c2440_master_xfer函数:

当发起i2c传输时,调用i2c_transfer,进而调用s3c2440_master_xfer进行数据传输。写的话do_master_tx,读的话do_master_rx。

do_master_rx函数:

1
IICDS = (msg->addr<<1)|(1<<0):把从设备地址写入IICDS,前7位是从机地址,第8位表示传输方向(0表示写操作,1表示读操作)。

do_master_tx函数:

1
2
1. IICDS = msg->addr<<1: 把从机地址(高7位,所以需要向右移一位)写入到IICDS寄存器中。
2. IICSTAT = 0xf0:设置IICSTAT寄存器,将s3c2440设为主机发送器,并发出S信号后,紧接着就发出从机地址。后续的传输工作将在中断服务程序中完成。

4.3 程序框架总结#

对应程序框架的4层架构。

img

4.4 I2C中断服务程序#

Start信号之后,发出设备地址,在第9个时钟就会产生第一个中断,我们根据i2c的流程图来编写中断程序。每传输完一个数据将又产生一个中断,I2C操作的主体在中断服务程序,它可以分为两部分:写操作,读操作。

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
static p_i2c_msg p_cur_msg;

int isLastData(void)
{
if (p_cur_msg->cnt_transferred == p_cur_msg->len - 1)
return 1; /* 正要开始传输最后一个数据 */
else
return 0;
}

void resume_iic_with_ack(void)
{
unsigned int iiccon = IICCON;
iiccon |= (1<<7); /* 回应ACK */
iiccon &= ~(1<<4); /* 恢复IIC操作 */
IICCON = iiccon;
}

void resume_iic_without_ack(void)
{
unsigned int iiccon = IICCON;
iiccon &= ~((1<<7) | (1<<4)); /* 不回应ACK, 恢复IIC操作 */
IICCON = iiccon;
}

void i2c_interrupt_func(int irq)
{
int index;
unsigned int iicstat = IICSTAT;
unsigned int iiccon;

//printf("i2c_interrupt_func! flags = %d\n\r", p_cur_msg->flags);

p_cur_msg->cnt_transferred++;

/* 每传输完一个数据将产生一个中断 */

/* 对于每次传输, 第1个中断是"已经发出了设备地址" */

if (p_cur_msg->flags == 0) {//write
/* 对于第1个中断, 它是发送出设备地址后产生的
* 需要判断是否有ACK
* 有ACK : 设备存在
* 无ACK : 无设备, 出错, 直接结束传输
*/
if (p_cur_msg->cnt_transferred == 0) { /* 第1次中断 */
if (iicstat & (1<<0)) {/*iicstat [0] == 1表示no ack*/
/* no ack */
/* 停止传输 */
IICSTAT = 0xd0;
IICCON &= ~(1<<4); //clear pending bit
p_cur_msg->err = -1;
printf("tx err, no ack\n\r");
delay(1000);
return;
}
}
if (p_cur_msg->cnt_transferred < p_cur_msg->len) {
/* 对于其他中断, 要继续发送下一个数据
*/
IICDS = p_cur_msg->buf[p_cur_msg->cnt_transferred];
IICCON &= ~(1<<4);//clear pending bit
} else {
/* 停止传输 */
IICSTAT = 0xd0;
IICCON &= ~(1<<4);
delay(1000);
}
} else {//read
/* 对于第1个中断, 它是发送出设备地址后产生的
* 需要判断是否有ACK
* 有ACK : 设备存在, 恢复I2C传输, 这样在下一个中断才可以得到第1个数据
* 无ACK : 无设备, 出错, 直接结束传输
*/
if (p_cur_msg->cnt_transferred == 0) {/* 第1次中断 */
if (iicstat & (1<<0)) {/* no ack */
/* 停止传输 */
IICSTAT = 0x90;
IICCON &= ~(1<<4); //clear pending bit
p_cur_msg->err = -1;
printf("rx err, no ack\n\r");
delay(1000);
return;
} else { /* ack */
/* 如果是最后一个数据, 启动传输时要设置为不回应ACK */
/* 恢复I2C传输 */
if (isLastData())
resume_iic_without_ack();
else
resume_iic_with_ack();
return;
}
}

/* 非第1个中断, 表示得到了一个新数据
* 从IICDS读出、保存
*/
if (p_cur_msg->cnt_transferred < p_cur_msg->len) {
index = p_cur_msg->cnt_transferred - 1;
p_cur_msg->buf[index] = IICDS;

/* 如果是最后一个数据, 启动传输时要设置为不回应ACK */
/* 恢复I2C传输 */
if (isLastData())
resume_iic_without_ack();
else
resume_iic_with_ack();
} else {
/* 发出停止信号 */
IICSTAT = 0x90;
IICCON &= ~(1<<4);
delay(1000);
}
}
}

4.4.1 写操作#

1
2
3
4
5
6
7
8
9
10
1. p_cur_msg->cnt_transferred初始值为-1(do_master_tx启动时设置)。
2. p_cur_msg->cnt_transferred == 0表示是第一次传输数据完后产生的中断,即发送从设备地址产生的中断。
3. iicstat & (1<<0)表示主机没有接受到ACK信号(即发出的设备地址不存在),需要停止传输。
4. IICSTAT = 0xd0置IICSTAT寄存器的[5]写为0,产生P信号。但是由于这时IICCON[4]仍为1,P信号没有实际发出,当执行IICCON &= ~(1<<4);清除IICCON[4]后,P信号才真正发出。
5. 等待一段时间,确保P信号已经发送完毕。


1).假如if (p_cur_msg->cnt_transferred < p_cur_msg->len)条件成立,表示数据还没有发送完毕,需要继续发送数据。
2).执行IICDS = p_cur_msg->buf[p_cur_msg->cnt_transferred]把要发送的数据写入到IICDS寄存器中,经过执行IICCON &= ~(1<<4);清除中断标志后后,紧接着就自动把数据发送出去了,这将触发下一个中断。
3).如果条件不成立表示数据传输完毕,发出P信号,停止数据的传输。

4.4.2 读操作#

见注释。

4.5 测试#

4.5.1 i2c_test.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
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
void do_write_at24cxx(void) {
unsigned int addr;
unsigned char str[100];
int err;

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

if (addr > 256) {
printf("address > 256, error!\n\r");
return;
}

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

printf("writing ...\n\r");
err = at24cxx_write(addr, str, strlen(str)+1);
printf("at24cxx_write ret = %d\n\r", err);
}

void do_read_at24cxx(void) {
unsigned int addr;
int i, j;
unsigned char c;
unsigned char data[100];
unsigned char str[16];
int len;
int err;
int cnt = 0;

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

if (addr > 256) {
printf("address > 256, error!\n\r");
return;
}

/* 获得长度 */
printf("Enter the length to read: ");
len = get_int();

err = at24cxx_read(addr, data, len);
printf("at24cxx_read ret = %d\n\r", err);

printf("Data : \n\r");
/* 长度固定为64 */
for (i = 0; i < 4; i++) {
/* 每行打印16个数据 */
for (j = 0; j < 16; j++) {
/* 先打印数值 */
c = data[cnt++];
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 i2c_test(void)
{
char c;

/* 初始化 */
i2c_init();

while (1) {
/* 打印菜单, 供我们选择测试内容 */
printf("[w] Write at24cxx\n\r");
printf("[r] Read at24cxx\n\r");
printf("[q] quit\n\r");
printf("Enter selection: ");

c = getchar();
printf("%c\n\r", c);

/* 测试内容:
* 3. 编写某个地址
* 4. 读某个地址
*/
switch (c) {
case 'q':
case 'Q':
return;
break;

case 'w':
case 'W':
do_write_at24cxx();
break;

case 'r':
case 'R':
do_read_at24cxx();
break;
default:
break;
}
}
}