零、前言

本文章为第三方撰写,仅用作教学参考,与课程安排无关。

因盲目 「参考」 本文内容而导致被评判为 「抄袭」 的,本人概不负责。

本文内容可能过时,具体要求以 《计算机原理与嵌入式系统专题实验指导手册》 为准。

这学期开了计算机原理与嵌入式系统的实验。上学期就非常感兴趣,等着这学期做了手痒难耐渴望编程

打算每次做完实验之后就写点心得下来,之后写实验报告也方便。

粗略翻了一遍指导书。什么代码都没有,和之前的数字系统设计实验形成鲜明的对比。给的代码模板也基本是空的,需要自己写。总的来说,嵌入式系统的实验还是比较有挑战性的。

壹、环境配置

我一向不喜欢用别人配好的环境,因为我根本不知道他做了什么。因此,只要是可以自己装软件配置的实验课,我一概在自己的电脑上配好。哪怕花点时间,我也心甘情愿。毕竟,如果问题真的发生,我没有任何借口来辩解,只能督促着自己去解决。

机房用的是Ubuntu 16.04 LTS x32 ,真的是老古董了。我记得我初一用的是15.04,算来也有八年了。32位系统的性能有限制,因此去官网下了64位的镜像本地安装了一遍。不知道咋搞的,Hyper-V既不能卸载也不能安装,搞得虚拟化技术根本没法打开。等Windows 11 24H2的正式镜像出来后覆盖安装一遍吧。唉DevChannel

之后在VMware的网络适配器选项中配置一下虚拟网卡为静态IP,再设置一下DNS,就可以本地连SSH上去操作了。毕竟虚拟机的GUI操作并不是很舒服。此外,还可以用Visual Studio Code远程连接。使用IDE开发可比用文本编辑器一个一个敲舒服多了。

这是一块搭载三星Exynos(猎户座)4412处理器的开发版,三星Note2使用的也是这颗CPU,真是怀念啊。手机主板被我拆出来封存在铁盒之中,不知道接上屏幕总成之后,还能否正常工作?

阅读芯片技术手册后发现,RS232接口对应的串口就是UART0,因此可以直接将串口读取的设备插线接到开发版的UART0排针,一样可以使用minicom查看日志,且不用拖个很大很笨重的转接头了。使用自制的DapLink可以正常传输数据,不过需要看dmesg日志更换一下设备路径,比如我的是/dev/ttyACM0

不过DapLink的供电是3V3,插上去LDO要烧化了。换成CH340G之后正常,且不用改设备路径,依旧是/dev/ttyUSB0。不过供电没有DC口稳,感觉有时候会断。还是用适配器供电吧,实在不行就接个5V的Type-C转DC诱骗头,毕竟配套的插口太松了,接触不良。

对于64位的系统,按照说明配置完后会报找不到arm-linux-gcc的错误。

解决方法:sudo apt install lsb-core

贰、点灯

没啥好说的,实在比较简单。

叁、汇编调用C

当时问了老师,只要代码模板文件含有C文件,就可以使用C写。那不是所有的都可以在汇编里搞个栈指针指进去然后拿C跑了?

指导书中还详细讲了出栈入栈,但是实验中并未用到。

肆、C调用汇编

在汇编文件内定义好函数,之后在C内调用:

1
2
3
4
5
@ start.S
.global delay

delay:
// your function
1
2
3
// main.c

extern void delay(int r0); // 或者不加extern也可

和之前的实验结果进行对比,可以很明显看出:LED的闪烁频率更高。这说明汇编的效率比C高得多!

伍、流水灯

查一下Datasheet。可以知道:设置连接性的地址是0x110002E0,低电平有效;数据地址是0x110002E4,低电平有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int my(void)
{
GPM4CON = 0x00001111;
while (1)
{
GPM4DAT = 0xf ^ 0x1;
delay(0x10000000);
GPM4DAT = 0xf ^ 0x2;
delay(0x10000000);
GPM4DAT = 0xf ^ 0x4;
delay(0x10000000);
GPM4DAT = 0xf ^ 0x8;
delay(0x10000000);
}
}

或者用循环移位的方法也行。下次试试吧。

陆、打开iCache

这个实验当时卡了一段时间。乍一看确实没头绪,不过多读几遍指导书和Datasheet对应的地方就知道要干啥了。

首先是CP15寄存器的结构。只有C1是配置,那就改它。

“系统刚上电时,iCache中的内容是无效的,往CP15协处理器中的C1bit[12]1可以启动iCache,写0可以关闭iCache。”

那就构造指令来开启关闭。辅导书里告诉了我们两个拓展指令:

  • 从协处理器传送到ARM寄存器:MRC {<cond>} p15, <Cop1>, Rd, CRn, CRm{,<Cop2>}
  • 从ARM寄存器传送到协处理器:MCR {<cond>} p15, <Cop1>, Rd, CRn, CRm{,<Cop2>}

为了不改变原来的指令,应当先读取配置,再改动对应的配置位,其他保持不变。因此要用到BIC/ORR这两个按位操作指令。

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
.globl _start
.globl delay
.globl icache_on
.globl icache_off
_start:
//disable watch dog
ldr r0, =0x10060000 // 把地址0x10060000加载到寄存器r0中
mov r1, #0 // 将立即数0移动到寄存器r1中
str r1, [r0] // 将寄存器r1的值0存储到r0指向的内存地址中

//set stack
ldr sp, =0x02050000 // 将sp设置为地址0x02050000
bl main // 调用主函数main, 跳转到该函数并保存返回地址

//turn on icache
icache_on:
// 构造指令
// C 开启整个iCache 优先级高于I
// Z 开启跳转预测
// BITS 4PVI ZFRS BLDP WCAM
// 0000 0xx1 1xxx xxxx x1xx
// 0000 1 8 0 3 // 这是非法立即数 需要拆分成 0x00001800 与 0x00000003
mrc p15, #0x0, r1, c1, c0, 0 // 先读取原来的设置参数存在 r1 内, 之后对相应位重新设置就行
orr r1, r1, #0x00001800 // orr指令按位取1, 剩下的忽略
@ orr r1, r1, #0x00000003 // 本实验不需要对dCache进行操作
mcr p15, #0x0, r1, c1, c0, 0 //再把修改后的 r1 存进去
mov pc, lr

//turn off icache
icache_off:
mrc p15, #0x0, r1, c1, c0, 0 // 先读取原来的设置参数存在 r1 内, 之后对相应位重新设置就行
bic r1, r1, #0x00001800 // orr的逆指令是bic, 按位取0, 剩下的忽略
@ bic r1, r1, #0x00000003
mcr p15, #0x0, r1, c1, c0, 0 //再把修改后的 r1 存进去
mov pc, lr

delay:
sub r0, r0, #1
cmp r0, #0x0
bne delay
mov pc, lr

halt:
b halt
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
// 设置GPM4CON与GPM4DAT
#define GPM4CON (*(volatile unsigned long *)0x110002E0)
#define GPM4DAT (*(volatile unsigned long *)0x110002E4)

void delay(int count);
void icache_on();
void icache_off();

#define delaytime 0x500000

void led_blink_assembly() // 控制右边两个LED的闪烁
{
GPM4CON &= 0xFFFF0000; // 将低四位置零, 高四位不变
GPM4CON |= 0x00001111; // 将低四位置一, 高四位不变

int i = 0;
while (i < 4) // 开始一个无限循环,持续进行LED的闪烁
{
GPM4DAT = 0b1111; // 将GPM4的数据寄存器设置为0xf,点亮LED
delay(16 * delaytime); // 调用delay函数,产生一个大约的延迟,使LED保持点亮状态
GPM4DAT = 0b1100; // 将GPM4的数据寄存器设置为0xc,熄灭LED
delay(16 * delaytime); // 再次调用delay函数,产生延迟,使LED保持熄灭状态
i++;
}
}

void led_blink_c() // 控制左边两个LED的闪烁
{
GPM4CON &= 0xFFFF0000;
GPM4CON |= 0x00001111; // 设置GPM4的控制寄存器,将其配置为输出模式

int j = 0;
while (j < 4) // 开始一个无限循环,持续进行LED的闪烁
{
GPM4DAT = 0b1111; // 将GPM4的数据寄存器设置为0xf,点亮LED
delay(delaytime); // 调用delay函数,产生一个大约的延迟,使LED保持点亮状态
GPM4DAT = 0b0011; // 将GPM4的数据寄存器设置为0x3,熄灭LED
delay(delaytime); // 再次调用delay函数,产生延迟,使LED保持熄灭状态
j++;
}
}

int main()
{
while (1)
{
led_blink_assembly();
icache_off();
led_blink_c();
icache_on();
}
}

柒、蜂鸣器

测试代码可以正常让蜂鸣器响起,但是仅进行while循环、无条件判断时蜂鸣器不响起,仅有LED闪烁。原因尚不明确。

地址写错了,我是傻B。只能说细心再细心吧。实验还是很简单的。

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
// BUZZER
#define GPD0CON (*(volatile unsigned long *)0x114000A0)
#define GPD0DAT (*(volatile unsigned long *)0x114000A4)

// LED
#define GPM4CON (*(volatile unsigned long *)0x110002E0)
#define GPM4DAT (*(volatile unsigned long *)0x110002E4)

void delay(int r0)
{
volatile int count = r0;
while (count--);
}

void buzzer_on(void)
{
GPD0DAT = 0xf;
}
void buzzer_off(void)
{
GPD0DAT = 0x0;
}
void buzzer_init(void)
{
GPD0CON = 0x00001111;
buzzer_off();
}

void led_blink()
{
buzzer_init();
GPM4CON = 0x00001111;

while (1)
{
GPM4DAT = 0xf; // 将GPM4的数据寄存器设置为0xf,熄灭LED
buzzer_off(); // 调用buzzer_off函数,使蜂鸣器熄灭
delay(0x100000); // 调用delay函数,产生延迟,使LED保持熄灭状态
GPM4DAT = 0x0; // 将GPM4的数据寄存器设置为0x0,点亮LED
buzzer_on(); // 调用buzzer_on函数,使蜂鸣器响起
delay(0x100000); // 再次调用delay函数,产生延迟,使LED保持点亮状态
}
}

捌、轮询检测按键

上课做了两个小时没做出来。检查了几遍代码,自以为没有逻辑漏洞。等上课去问问老师吧。

Tiny4412裸机开发-按键检测-腾讯云找到了一点灵感。

为什么之前一直检测不到按键?

最开始,我直接用if (GPX3DAT == 0x11110111)这样子的语句进行判断,但会有个问题,即只有中间四位是变化的,其他四位我们不知道也不关心。可能初始有值,但和我们比较的不一样。因此,我们只比较中间四位即可;或者,只判断某一位是否为0即可。

这里也借鉴了一下设置连接性的代码:只将需要的位置1/置0,最大化避免潜在的数据错误,类似上个实验中的BIC/ORR

还是用循环右移了。可能写的更多,但是至少好看,而且还能复用。

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
void delay(int r0)
{
volatile int count = r0;
while (count--);
{
if (!(GPX3DAT == 0xff)) // 这里的判断是为了让每次按下新的按键后取消之前的延时
return;
}
}

/*
* @brief 对4位十六进制整数执行循环移位操作。
* 该函数接受一个4位十六进制整数作为输入,向右移位1位,然后将最高位设置为原最低位的值。
* @param hex_data 要进行循环移位的4位十六进制整数。
* @return 循环移位操作的结果。
*/
int circular_shift(int hex_data)
{
int lowest_bit = hex_data & 1;
hex_data >>= 1;
hex_data |= lowest_bit << 3;
return hex_data;
}

void main(){
...
while(1){
if (!(GPX3DAT & 1 << 2)) // 0b11111011
{
GPM4DAT = 0b1110;
for (i = 0; i < 4; i++)
{
delay(0x100000);
GPM4DAT = circular_shift(GPM4DAT);
}
}
}
}

玖、时钟体系

做的想④,一得阁拉米的。 看Datasheet看的头都大了。

BUILD CIRCUITS. WRITE CODE. RTFM. —— SHENZHEN I/O

参考:第四章、TIny4412 U-BOOT移植四 配置时钟频率源码分析

先看结构图。

图很乱,一点点看。

首先是PLL的设置。查指导书上的表格和Datasheet P580的指令格式可以知道:1.4GHz的APLL的MPS值分别为175 3 0,而APLL_CON0的第16-25、8-13、0-2为分别为MPS。

因此,设置频率的代码应该为APLL_CON0 = (0x1 << 31 | 0xAF << 16 | 0x3 << 8 | 0x0);

这里用左移是为了看起来更加直观。同理可以设置MPLL_CON0EPLL_CON0VPLL_CON0。要注意的是表中EPLL只有192MHz,要想整出96MHz的话还需要再除以2。用计算公式带进去就行了。

之后的一个坑是代码模板里的注释写的是ACLK_COREM1 = 188MHz,但是指导书上要求的是ACLK_COREM1 = 175MHz。这个就很容易设置了,拿350MHz二分频就行。

对于MUX的设置,对着上面图片一点一点看吧。以CMU_TOP为例:

  1. 因为ACLK_160=160MHz, ACLK_100=100MHz,可以看出这俩频率都是从800MHz分出来的,因此上游时钟应当选择800MHz的MPLL,则MUXMPLL_CTRL_USER_T应当选择1。
  2. 我们要选择MUXMPLL_CTRL_USER_T的输出给MUXACLK_100,因此MUXACLK_100应当选择0。
  3. 其余同理。

然后还有一个坑是锁定时间。指导书上说是“一般设置为270cycle的两倍”,但实际上要根据PMS的P来设置。

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
// CMU_CPU
#define CLK_SRC_CPU (*(volatile unsigned int *)0x10044200) // Selects clock source for CMU_CPU
#define CLK_DIV_CPU0 (*(volatile unsigned int *)0x10044500) // Sets clock divider ratio for CMU_CPU
#define CLK_DIV_CPU1 (*(volatile unsigned int *)0x10044504) // Sets clock divider ratio for CMU_CPU
// CMU_DMC
#define CLK_SRC_DMC (*(volatile unsigned int *)0x10040200) // Selects clock source for CMU_DMC
#define CLK_DIV_DMC0 (*(volatile unsigned int *)0x10040500) // Sets clock divider ratio for CMU_DMC
#define CLK_DIV_DMC1 (*(volatile unsigned int *)0x10040504) // Sets clock divider ratio for CMU_DMC
// CMU_TOP
#define CLK_SRC_TOP0 (*(volatile unsigned int *)0x1003C210) // Selects clock source for CMU_TOP0
#define CLK_SRC_TOP1 (*(volatile unsigned int *)0x1003C214) // Selects clock source for CMU_TOP1
#define CLK_DIV_TOP (*(volatile unsigned int *)0x1003C510) // Sets clock divider ratio for CMU_TOP
// CMU_LEFTBUS
#define CLK_SRC_LEFTBUS (*(volatile unsigned int *)0x10034200) // Selects clock source for CMU_LEFTBUS
#define CLK_DIV_LEFTBUS (*(volatile unsigned int *)0x10034500) // Sets clock divider ratio for CMU_LEFTBUS
// CMU_RIGHTBUS
#define CLK_SRC_RIGHTBUS (*(volatile unsigned int *)0x10038200) // Selects clock source for CMU_RIGHTBUS
#define CLK_DIV_RIGHTBUS (*(volatile unsigned int *)0x10038500) // Sets clock divider ratio for CMU_RIGHTBUS
// locktime
#define APLL_LOCK (*(volatile unsigned int *)0x10044000) // Control PLL locking period for APLL
#define MPLL_LOCK (*(volatile unsigned int *)0x10044008) // Controls PLL locking period for MPLL
#define EPLL_LOCK (*(volatile unsigned int *)0x1003C010) // Controls PLL locking period for EPLL
#define VPLL_LOCK (*(volatile unsigned int *)0x1003C020) // Controls PLL locking period for VPLL
// APLL
#define APLL_CON1 (*(volatile unsigned int *)0x10044104) // Control PLL AFC
#define APLL_CON0 (*(volatile unsigned int *)0x10044100) // Control PLL output frequency for APLL
// MPLL
#define MPLL_CON0 (*(volatile unsigned int *)0x10040108) // Controls PLL output frequency for MPLL
#define MPLL_CON1 (*(volatile unsigned int *)0x1004010c) // Controls PLL AFC
// EPLL
#define EPLL_CON2 (*(volatile unsigned int *)0x1003C118) // Controls PLL output frequency for EPLL
#define EPLL_CON1 (*(volatile unsigned int *)0x1003C114) // Controls PLL output frequency for EPLL
#define EPLL_CON0 (*(volatile unsigned int *)0x1003C110) // Controls PLL output frequency for EPLL
// VPLL
#define VPLL_CON0 (*(volatile unsigned int *)0x1003C120) // Controls PLL output frequency for VPLL
#define VPLL_CON1 (*(volatile unsigned int *)0x1003C124) // Controls PLL output frequency for VPLL
#define VPLL_CON2 (*(volatile unsigned int *)0x1003C128) // Controls PLL output frequency for VPLL


/*
system_clock_init

APLL=1.4GHz, MPLL=800MHz, EPLL=96MHz VPLL=350MHz
ARMCLK=1.4GHz, ACLK_COREM1 = 188MHz; ACLK_COREM0=350MHz
SCLK_DMC=400MHz, ACLK_DMCD = 200MHz;
ACLK_160=160MHz, ACLK_100=100MHz
*/

// #define slow
void system_clock_init(void)
{
/* 构造指令
* 31: 开启控制,高有效
* 16-25: M
* 8-13: P
* 0-2: S
*/
// // M P S
#ifndef slow
APLL_CON0 = (0x1 << 31 | 0xAF << 16 | 0x3 << 8 | 0x0); // 175 3 0
#else
APLL_CON0 = (0x1 << 31 | 0x64 << 16 | 0x3 << 8 | 0x1); // 100 3 1
#endif
MPLL_CON0 = (0x1 << 31 | 0x64 << 16 | 0x3 << 8 | 0x0); // 100 3 0
EPLL_CON0 = (0x1 << 31 | 0x40 << 16 | 0x2 << 8 | 0x3); // 64 2 3 // 因192MHz的S=2, 再取一半则S=2+1=3
VPLL_CON0 = (0x1 << 31 | 0xAF << 16 | 0x3 << 8 | 0x2); // 175 3 2


// MUX Settings
CLK_SRC_CPU = (0x1 << 24 | 0x1 << 0); // MUXAPLL选择APLL输出, MUXMPLL选择MPLL输出
CLK_SRC_DMC = (0x1 << 12); // MUXMPLL选择MPLL输出
CLK_SRC_TOP0 = (0x1 << 8 | 0x1 << 4 | // MUXVPLL选择VPLL输出, MUXEPLL选择EPLL输出
0x0 << 20 | 0x0 << 16); // 因为MPLL为800MHz, 给ACLK_160和ACLK_100使用较为方便
CLK_SRC_TOP1 = (0x1 << 12); // MUXMPLL选择MPLL输出


// DIV Settings
CLK_DIV_CPU0 = (0x7 << 8 | 0x3 << 4); // 1400/175=8, 1400/350=4. 还需要各自再减1
CLK_DIV_DMC0 = (0x1 << 16 | 0x1 << 12); // 800/2=400, 400/2=200. 还需要各自再减1
CLK_DIV_TOP = (0x4 << 8 | 0x7 << 4); // 800/160=5, 800/100=8. 还需要各自再减1


// PLL Lock Time // P
APLL_LOCK = (270 * 0x3); // 3
MPLL_LOCK = (270 * 0x3); // 3
EPLL_LOCK = (3000 * 0x2); // 2
VPLL_LOCK = (3000 * 0x3); // 3
}

拾、UART实验

第三恶心的实验。

看手册配置吧。代码模板里也有较为详细的注释。唯一要仔细想一想的就是如何判断数据发送/接收完毕。有专门的标志位用于判断,每次进行完操作之前判断一下就行。

当时死活不出结果,查了一圈发现是UFCON0设置错了。当时直接无脑拉满的,但是应该按照数据包来设置FIFO的缓冲区大小。此处设置为0x111

关于C中的while(){}while();区别:

如果while后无分号,那么默认下一行为循环内容;如果while后跟了分号,那么这是一个空循环。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
while(test){
printf("hello");
}
while(test)
printf("hello");
/*上面两种表示是一个意思*/

/*下面两种表示是一个意思*/
while(test);
printf("hello");

while(test){
};
printf("hello");

当时在这里卡了一下,结果死活不出结果。

拾壹、ADC转换器

当时也卡了很久,后来发现是判断条件的地方有问题。

check_bits = ((0x8 << 12) & ADCCON) && (~(0x1 & ADCCON)); while (check_bits)换成while ((0x8000 & ADCCON) && (~(0x1 & ADCCON)))就好了。

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
#include "init.h"
#include "print.h"

// GPIO Settings
// ADC
#define ADCCFG (*(volatile unsigned long *)0x10010118)

#define ADCCON (*(volatile unsigned long *)0x126C0000)
#define ADCDAT (*(volatile unsigned long *)0x126C000C)
#define ADCMUX (*(volatile unsigned long *)0x126C001C)

void delay(int r0);

unsigned char num2char[10] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};

void ADCInit(void)
{
ADCCFG = 0x0000; // 选择通用ADC
ADCMUX = 0x0; // AIN0
ADCCON = (0x1 << 16 | // 选择12位ADC
0x1 << 14 | // 开启缩放
0x41 << 6 | // 65
0x1 << 0); // 开启ADC转换
puts("\r\nADC Initialized.");
}

void read_adc(void)
{
int temp_adc;
unsigned int bit_3, bit_2, bit_1, bit_0;
ADCCON = ADCCON | (0x1 << 2);
temp_adc = ADCDAT & 0xfff;
puts("adc = ");
bit_3 = temp_adc / 1000;
temp_adc -= bit_3 * 1000;
putc(num2char[bit_3]);

bit_2 = temp_adc / 100;
temp_adc -= bit_2 * 100;
putc(num2char[bit_2]);

bit_1 = temp_adc / 10;
temp_adc -= bit_1 * 10;
putc(num2char[bit_1]);

bit_0 = temp_adc;

putc(num2char[bit_0]);
puts("\r\n");
}


int main(void)
{
UartInit();
ADCInit();

while (1)
{
while (
(0x8000 & ADCCON) &&
(~(0x1 & ADCCON))) // 最低位为0, 表示ADC转换完成
{
read_adc();
delay(0x0A000000);
ADCCON = ((1 << 16) | (1 << 14) | (65 << 6) | (1 << 0));
}
}
}

拾贰、中断体系

第二恶心的实验。寄存器量大,很多还要你自己去查手册。

查手册可知:按键对应的中断源为XEINT26,中断号64

  • ICDIPR_CPUSPI32对应0x0440,因此是ICDIPR16_CPU0
  • ICDIPTR_CPUSPI32对应0x0840,因此是ICDIPTR16_CPU0
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
#include "bunfly.h"
#include "init.h"
#include "print.h"

void do_irq();
void led_on();
void led_off();

void delay(int r0)
{
volatile int count = r0;
while (count--);
}
int led_status = 0;


int main()
{
// step 1: 设置GPIO端口
// 配置KEY
GPX3CON &= 0xFF0000FF; // 将中间四位清零
GPX3CON |= 0x00FFFF00; // 将中间四位置F, 设置为中断源
// 配置LED
GPM4CON &= 0xFFFF0000; // 将低四位置零, 高四位不变
GPM4CON |= 0x00001111; // 将低四位置一, 高四位不变

// step 2: 设置端口中断方式
EXT_INT43_CON &= 0xFF0000FF; // 将中间四位清零
EXT_INT43_CON |= 0x00222200; // 将中间四位置2, 设置为下降沿触发
EXT_INT43_MASK = 0x0; // 中断使能

// step 3: GIC中断控制器设置
ICDIPR16_CPU0 = 0x1; // 优先级为1
ICDIPTR16_CPU0 = 0x1; // CPU Interface 0处理
ICDISER2_CPU0 = 0x1; // 开启SPI32的中断响应
ICCICR_CPU0 = 0x1; // 全局使能中断
ICCPMR_CPU0 = 0x4; // 优先级屏蔽寄存器, 优先级低于4的中断不响应
ICDDCR = 0x1; // 开启配置

while (1)
{
if (led_status == 0)
{
led_off();
}
else
{
led_on();
}
do_irq();
}
}

void do_irq()
{
int interrupt_data = ICCIAR_CPU0;
// 初始值为0x3FF, 用于屏蔽高22位
int interrupt_id = interrupt_data & 0x3FF;
int cpu_id = interrupt_data & (0x7 << 10);
// 处理中断
if (interrupt_id == 64)
{
if (led_status == 0)
{
led_on();
led_status = 1;
}
else
{
led_off();
led_status = 0;
}
EXT_INT43_PEND = (0x1 << 2); // 中断已发生
ICDICPR2_CPU0 = (0x1 << 1);
ICCEOIR_CPU0 = interrupt_id;
}
}

void led_on()
{
GPM4DAT |= 0xF;
}

void led_off()
{
GPM4DAT &= 0x0;
}

效果应该类似自锁开关:每次按下按键,LED会改变一次状态。但实际上,每次按下按键之后LED有几率闪烁。造成这种现象的原因是按键抖动。加入去抖函数效果会好一点,不过我比较懒,没写。

2024.09.26 还是写了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void do_irq()
{
// ...
if (interrupt_id == 64)
{
delay(0xA0000);
if (interrupt_id == 64)
{
// ...
}
else
{
return;
}
}
}

拾叁、Printf函数

现成的代码,直接自己编译一下就能用。这玩意可比串口一点点打印好多了,毕竟可以格式化输出。

拾肆、RTC

最恶心的实验。做了六个小时。此外,给的代码模板还有错误,真的佛了。

Include\rtc.h中第55行的#define CPU0 (*(volatile cpu0 *)0x1048000)中,0x1048000应修改为0x1048000

本次实验将所有的地址都封装为结构体,增强了代码可读性,并便于调用。

首先是RTC_ALARMRTC_TIC的中断号。查询可知:一个是76,一个是77。之后用中断号去查找对应的ICDISER寄存器,可以知道这俩都对应ICDISER2。此外,还要查询对应的ICDIPTR寄存器,计算一下就知道对应的是ICDIPTR19

rtc.c中对中断的处理仿照之前按键中断,复制一下就行了。

有一个要注意的地方就是对ICDIPTR.ICDIPTR19寄存器的写入。本次实验都对目标字段的第0位使能,即CPU Interface 0。但这俩都要对应0,怎么办?

阅读ICDIPTR寄存器的地址说明,可以看到前面按照Byte Offset分成四组。这个就是说,每个ICDIPTR寄存器可以添加四组不同的中断CPU控制配置。因此,写入时,一个直接写入,一个需要偏移八位到Byte Offset 1。

目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
└─RTC
│ init.c
│ main.c
│ Makefile
│ printf.lds
│ start.S

├─Include
│ bunfly.h
│ init.h
│ print.h
│ rtc.h
│ uart.h

└─Lib
Makefile
print.c
rtc.c
uart.c

main.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
/********** main.c **********/

#include "init.h"
#include "print.h"
#include "rtc.h"

// Key interrupt
#define EXT_INT43_CON (*(volatile unsigned long *)0x11000e0c)
#define EXT_INT43_MASK (*(volatile unsigned long *)0x11000f0c)
#define EXT_INT43_PEND (*(volatile unsigned long *)0x11000f4c)
#define GPX3CON (*(volatile unsigned long *)0x11000c60)

#define ICDIPR16_CPU0 (*(volatile unsigned long *)0x10490440)
#define ICDIPTR16_CPU0 (*(volatile unsigned long *)0x10490840)

void key_init(void)
{
// step 1: 设置GPIO端口
// 配置KEY
GPX3CON &= 0xFF0000FF; // 将中间四位清零
GPX3CON |= 0x00FFFF00; // 将中间四位置F, 设置为中断源

// step 2: 设置端口中断方式
EXT_INT43_CON &= 0xFF0000FF; // 将中间四位清零
EXT_INT43_CON |= 0x00222200; // 将中间四位置2, 设置为下降沿触发
EXT_INT43_MASK = 0x0; // 中断使能

// step 3: GIC中断控制器设置
ICDIPR16_CPU0 = 0x1; // 优先级为1
ICDIPTR16_CPU0 = 0x1; // CPU Interface 0处理
ICDISER.ICDISER2 = 0x1; // 开启SPI32的中断响应
CPU0.ICCICR = 0x1; // 全局使能中断
CPU0.ICCPMR = 0x4; // 优先级屏蔽寄存器, 优先级低于4的中断不响应
ICDDCR = 0x1; // 开启配置
}

void do_irq(void)
{
int interrupt_data = CPU0.ICCIAR;
// 初始值为0x3FF, 用于屏蔽高22位
int interrupt_id = interrupt_data & 0x3FF;
int cpu_id = interrupt_data & (0x7 << 10);
switch (interrupt_id)
{
case 64:
printf("Key interrupt!\r\n");
EXT_INT43_PEND |= (0x1 << 2); // 中断已发生
ICDICPR.ICDICPR2 |= (0x1 << 1); // 中断已处理
break;

case 76:
printf("\r\nRTC ALARM 0721 interrupt! RTC ALARM 0721 interrupt!\r\n");
RTCINTP |= (0x1 << 1); // 发生ALARM中断
ICDICPR.ICDICPR2 |= (0x1 << 12); // 中断号76对应第13位中断标志位, 即位移12
break;

case 77:
printf("RTC TIC interrupt!\r\n");
RTCINTP |= (0x1 << 0); // 发生TIC中断
ICDICPR.ICDICPR2 |= (0x1 << 13); // 中断号77对应第14位中断标志位, 即位移13
break;

default:
break;
}

CPU0.ICCEOIR &= ~(0x3FF); // 清除中断
CPU0.ICCEOIR = interrupt_id;
}

int main(void)
{
int old_sec, new_sec;


key_init();
rtc_init();
rtc_alarm();
rtc_tic();


while (1)
{
do_irq();
new_sec = RTC.BCDSEC;
if (new_sec != old_sec)
{
printf("20%x年 %x月 %x日 %x:%x:%x\r\n",
RTC.BCDYEAR,
RTC.BCDMON,
RTC.BCDWEEK,
RTC.BCDHOUR,
RTC.BCDMIN,
RTC.BCDSEC);
old_sec = new_sec;
}
}
}

rtc.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
/********** rtc.c **********/

#include "rtc.h"

#include "print.h"

#define RTCALM_ALM (*(volatile unsigned long *)0x10070050)
#define RTCALM_SEC (*(volatile unsigned long *)0x10070054)
#define RTCALM_MIN (*(volatile unsigned long *)0x10070058)
#define RTCALM_HOUR (*(volatile unsigned long *)0x1007005C)

void rtc_init(void)
{
RTCCON = 0x1; // 开启RTC控制
RTC.BCDYEAR = 0x24; // 2024
RTC.BCDMON = 0x7; // 7月
RTC.BCDDAY = 0x1; // 实际上为星期
RTC.BCDHOUR = 0x12; // 12时
RTC.BCDMIN = 0x7; // 0分
RTC.BCDSEC = 0x0; // 0秒
RTC.BCDWEEK = 0x21; // 实际上为日期, 反了
RTCCON = 0x0; // 关闭RTC控制
printf("RTC init done!\r\n");
}

// #define RTC_TIC_ID 77
void rtc_tic(void)
{
RTCCON = RTCCON & (~(0xf << 4)) | (1 << 8); // 滴答定时频率设置为32768Hz, 并使能滴答定时器


TICCNT = 0x7FFF; // 32768-1=32767
ICDDCR = 1; // 使能分配器


ICDISER.ICDISER2 |= (0x1 << 13); // 使能相应中断到分配器
ICDIPTR.ICDIPTR19 = ICDIPTR.ICDIPTR19 &
(~(0xFF << 0)) | // 偏移量清零
(0x1 << 0); // 选择CPU接口

CPU0.ICCPMR = 0xFF; // 中断屏蔽优先级
CPU0.ICCICR = 1; // 使能中断到CPU
}

// #define RTC_ALARM_ID 76
void rtc_alarm(void)
{
RTCALM.ALM = (0x1 << 6 | // 开启全局时钟警报
0x1 << 0 | // 开启分钟警报
0x1 << 1); // 开启秒警报


RTCALM.SEC = 0x21;
RTCALM.MIN = 0x07; // 每小时07:21产生一次中断

ICDDCR = 1; // 使能分配器
// 使能相应中断到分配器
ICDISER.ICDISER2 |= (0x1 << 12);
// 选择CPU接口
ICDIPTR.ICDIPTR19 = ICDIPTR.ICDIPTR19 &
(~(0xFF << 8)) | // 偏移量清零, 这里置为8是因为上面的已经用过了, 需要再偏移8位
(0x1 << 8); // 选择CPU接口

CPU0.ICCPMR = 0xFF; // 中断屏蔽优先级
CPU0.ICCICR = 0x1; // 使能中断到CPU
}

RTC的日期和星期的寄存器地址反了,因此要想正常打印日期应当给RTC.BCDWEEK进行赋值,或是打印其值。