北京开源芯片研究院-实习笔记

Posted on 2022-08-14 21:03  何乐斋  阅读(915)  评论(0编辑  收藏  举报

 

 一生一芯学习记录

 

2022.7.25

1.所谓-wall是一个编译选项,让编译器对你的代码提出尽可能多的警告

 

2022.7.26

1.cd不是程序,所以是唯一没有man page的指令

2.管道A | B将A进程的标准输出连接到B进程的标准输入

3.宏定义:

#define name stuff

#define name( parament-list ) stuff

4.函数栈帧(不懂)

5.头文件.h与.c分开放便于模块化

6.ISA的存在形式既不是硬件电路, 也不是软件代码, 而是一本规范手册

 

2022.7.27

1.指针是一种保存变量地址的变量

​2.Verilator的作用是将某些Verilog代码翻译成可编译的C++代码,之后再编写合适的驱动代码也就是sim_main.cpp,一同使用C++编译器进行编译,由此得到可执行的程序,来对Verilog代码进行仿真,通过GTKwave得到波形图等仿真结果

3.apt-get install **这样的命令会下载文件放在 /var/cache/apt/archives目录下

4.与大多数C语言编译器相比,lint可以对程序进行更加广泛的错误分析

5.RTL(Register Transfer Level,寄存器传输级)指:不关注寄存器和组合逻辑的细节(如使用了多少逻辑门,逻辑门之间的连接拓扑结构等),通过描述寄存器到寄存器之间的逻辑功能描述电路的HDL层次

6.soc即系统级芯片又称为片上系统(system on chip),即将系统的主要功能集成到一块芯片上。至少包含一个处理器核

 

2022.7.28

1.虚拟机是通过软件模拟完成硬件,运行在一个完全隔离的环境中,不会对原系统产生影响;双系统是将两个系统分别安装在同一台电脑的不同分区内,可以通过其中一个系统访问另外一个系统的文件。

2.WSL主要面向开发人员,特别是web开发人员,因为他们不要经常访问linux内核。

3.git commit前编写#会被忽视的

4.git diff只针对缓存区的文件,commit后无法diff

5.IC前端设计(逻辑设计)和后端设计(物理设计)

6.在FPGA/IC开发流程中,验证主要包括功能验证和时序验证两个部分

7.时序收敛就指的是某一系统电路能按既定的顺序来执行特定功能。系统内不同单元电路的处理速度和走线延迟等是造成时序无法收敛的因素

8.引脚Pin从集成电路(芯片)内部电路引出与外围电路的接线,所有的引脚就构成了这块芯片的接口。

9.API是Application Programming Intererface的缩写,是应用程序(通过编程代码方式调用的)接口。它不特定指向具体的类或者方法函数等,我们可以理解其为一种应用程序调用的管道。

10.ISA指令集架构

11.把支付宝APP看成一个模拟的ATM机,NEMU就是一个模拟出来的计算机系统。例如NEMU中使用数组来模拟内存, 那么对这个数组进行读写则相当于对内存进行读写。

12.最简单的真实计算机需要满足哪些条件:

-结构上, TRM有存储器(内存), 有PC, 有寄存器, 有加法器

-工作方式上, TRM不断地重复以下过程: 从PC指示的存储器位置取出指令, 执行指令, 然后更新PC

13.从状态机模型的视角来理解计算机的工作过程了: 在每个时钟周期到来的时候, 计算机根据当前时序逻辑部件的状态, 在组合逻辑部件的作用下, 计算出并转移到下一时钟周期的新状态.

14.一个是以代码(或指令序列)为表现形式的静态视角,另一个是以状态机的状态转移为运行效果的动态视角

15.linux中.sh文件是脚本文件,一般都是bash脚本

16. NEMU把ISA相关的代码专门放在nemu/src/isa/目录下, 并通过nemu/include/isa.h提供ISA相关API的声明. 这样以后, nemu/src/isa/之外的其它代码就展示了NEMU的基本框架

17.阅读程序当然是从nemu-main.c开始啊!

18. Kconfig用来配置内核,它就是各种配置界面的源文件,内核的配置工具读取各个Kconfig文件,生成配置界面供开发人员配置内核,最后生成配置文件.config

19. 配置是独立于程序的只读变量,伴随应用的整个生命周期,可以有多种加载方式,需要治理

 

2022.7.29

1. 面向对象有三大特性:封装,继承,多态,它将对象拥有的属性变量和操作这些属性变量的函数打包成一个来表示

2.return 0前边加getchar(),有输入输出时可加2个

3. mingw是一个IDE,GCC是编译器 gcc与mingw关系是Mingw调用GCC来编译代码。

4. “线程是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。”

5. vscode就是编辑器,而vs则是IDE。

 

2022.7.30

1. C语言代码要经过编译和链接才能生成可执行程序:

编译是针对单个源文件(.c 文件)的,有多少个源文件就生成多少个目标文件,并且在生成过程中不受其他源文件的影响。也就是说,每个源文件都是独立编译的。

链接器的作用就是将这些目标文件拼装成一个可执行程序,并为代码(函数)和数据(变量、字符串等)分配好虚拟地址,这和搭积木的过程有点类似。

2. 与静态库不同的是,最后生成了可执行文件,动态链接库是不可以删除的,否则,文件执行不了。

3.实际上。?:是唯一的三元运算符

4.do while(出口条件循环)是先执行一次再判断,而while、for(入口条件循环)是先判断,因此可能一次也不执行

5.指针+1指的是增加一个储存单元,以字节为单位,如short是2字节,double是8字节

 

2022.7.31

1.ptr指针指向对象的地址;*ptr指针指向的对象;&ptr指针本身的地址

2.在被调函数中改变主调函数的变量,必须使用指针

3. 结构体是一种数据类型,也就是说可以用它来定义变量;

结构体就像一个“模板”,定义出来的变量都具有相同的性质。可以将结构体比作“图纸”,结构体变量比作“零件”,根据同一张图纸生产出来的零件的特性都是一样的;

结构体是一种数据类型,是创建变量的模板,不占用内存空间;结构体变量才包含了实实在在的数据、需要存储空间。

4.对于声明“int a = 512,*p = &a;”,可等价于:

int a = 512;

int *p = &a;

等价于:

int a = 512;

int *p;

p = &a;

从而*p,即p指向的元素为a

5.同理可以这样写:double *ptr = array;

则ptr的值为array,即array[0]的地址

*ptr的值为array[0]

6. 如果scanf()是把字符串读入字符数组中,不要使用&(因为字符串变量名在 scanf 语句里表示指针/地址)

7.float的转换说明是%f;而double的是%lf

8. MingGW作用:

MinGW 包含了c++需要使用的头文件和库文件

整合了GNU的工具集,如经典gcc, g++, make

9.while只有在测试条件后的语句才是循环体,包括简单语句(后接;)以及复合语句(后接{})

10.由于上条特性,若有后跟分号的,如“while(n++ < 3);”,则循环直至n增到4后才跳出,注意到分号本身就是一条语句

 

2022.8.1

1.函数声明(后加分号),函数调用,函数定义

2.记得声明并初始化所有的变量

3.在main中printf以及scanf等,其他函数只负责return值

4. 返回值scanf函数返回成功读入的数据项数,读入数据时遇到了“文件结束”则返回EOF

5. 在嵌入式领域当中,在配置内核的时候会有很多配置方法,比如说make config(基于文本的为传统的配置界面)、make oldconfig(如何只想在原来内核配置的基础上修改一部分,则会省去很多麻烦)、make xconfig(基于图形窗口模式的配置界面,Xwindow下推荐使用)、make menuconfig(基于文本选择的配置界面,字符终端下推荐使用),以上4种配置方式的目的都是在源码的顶层目录下面生成一个.config文件,那么在我们嵌入式开发当中,经常使用的和推荐使用的是make menuconfig这种配置方法。

6.赋值表达式的值是赋值运算符左侧运算对象的值,如ch = getchar()的值是ch的新值

7. Monitor(监视器)模块是为了方便地监控客户计算机的运行状态而引入的. 它除了负责与GNU/Linux进行交互(例如读入客户程序)之外, 还带有调试器的功能, 为NEMU的调试提供了方便的途径

8. 所以, 目前我们只需要关心配置系统生成的如下文件:

nemu/include/generated/autoconf.h, 阅读C代码时使用

nemu/include/config/auto.conf, 阅读Makefile时使用

9. Makefile会包含项目中的所有filelist.mk文件, 对上述4个变量的追加定义进行汇总, 最终会过滤出在SRCS-y中但不在SRCS-BLACKLIST-y中的源文件, 来作为最终参与编译的源文件的集合.

10. NEMU的框架代码会把riscv32作为默认的ISA, 如果你希望选择其它ISA, 你需要在NEMU的工程目录下执行make menuconfig, 然后在Base ISA一栏中切换到你选择的ISA, 然后保存配置并退出菜单.

11. 预防性的错误检查,在认为不可能的执行到的情况下加一句ASSERT(0),如果运行到此,代码逻辑或条件就可能有问题。

12. NEMU会在cpu_exec()函数的最后打印执行的指令数目和花费的时间, 并计算出指令执行的频率. 但由于内置客户程序太小, 执行很快就结束了, 目前无法计算出有意义的频率, 将来运行一些复杂的程序时, 此处输出的频率可以用于粗略地衡量NEMU的性能.

13.对while和do while循环,执行continue后是对测试表达式求值;对for循环,continue后是更新表达式求值然后对测试表达式求值,然后执行循环体中的代码

 

2022.8.2

1.被调函数使用的只是从主调函数那里拷贝过来的

2.单个“return;”语句会终止被调函数,并把控制返回给主调函数,因此递归的基线条件应该直接返回值

3.函数类型指的是返回值类型,不是函数参数的类型

4.函数声明可以放在主调函数里面

5.int盛不下会出现负号,可用long long

6.每级递归的变量n都属于本级递归私有

7.字符串结尾必须是\0,双引号括起来的”xxx”被视为指向字符串的指针(即首个字符的地址),类似于数组名即是指向数组的指针

8.对于二维数组int arr[2][3] = {{ 1,2,3 },{ 4,5,6 }}

在%p下,arr == arr[0] == *arr ==&**arr == &arr[0][0],皆为arr[0][0]的地址;

在%d下,**arr == arr[0][0] == *arr[0]

另外,arr[1][2] == *(*(arr+1)+2)

9.int(*pz)[2] : pz是一个指向内含两个int型数组的指针

 int*pz[2] == int(*pz[2]): pz是一个内含两个指针的元素的数组,每个指针都指向int型

10.int ar[3][2]

   int **p

   *p = ar[0] //有效,因为都是指向int的指针(后者指向元素ar[0][0])

   p = ar //无效,前者指向一个(指向int的)指针,后者指向一个(内含两个int的)数组

 

2022.8.3

1.字面量是除符号常量外的常量

2.去掉声明中的数组名,留下来的int[2]即为符合字面量的类型名,故完整复合字面量为(int[2]){10,20}

3.把数组名做实参时,传递给函数的不是整个数组,而是数组的地址(因此函数形参对应的是指针)

4.初始化数组:把静态存储区的字符串拷贝到数组中;ar == &ar[0]是地址常量,可以ar + 1,但不可以ar++

  初始化指针:仅为指针预留存储位置,并存储字符串地址;可以ptr++

 

5.getchar读取每个字符,包括空格、制表符、换行符;scanf则会跳过他们

6.重定向 ./prog <filein >fileout

7.让换行不进入缓冲输入区 while(getchar()!=‘\n’)continue;

使得当输入为换行符时,不return值,而是回去getchar

8.  无缓冲输入

缓冲输入  完全缓冲

          行缓冲(按下ENTER键后才被传送给程序)

9.注意按下回车键后传送换行符

10. 类型定义也是一种声明,声明都要以;号结尾,结构体类型定义的}后面少;号是初学者常犯的错误

11.函数、结构体等的定义可以放在main()外,但若声明在main中,则全局可使用

12.enum typeName { valueName1, valueName2, valueName3, ...... };

关键字enum枚举数据类型

13. C 库函数 char *strtok(char *str, const char *delim) 分解字符串 str 为一组字符串,delim 为分隔符。

 

2022.8.4

1.数组的元素是变量(除非声明const),但数组名arr不是变量,和单独声明的指向数组的指针ptr不同

2.const char *ptr = “string”

等价于 const char *ptr

       ptr = “string” //字符串字面量是地址

3. 修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用

4. atoi(s)函数用于把一个字符串转换成一个整型数据,该函数定义在stdlib.h中

5.如何调试:

-使用assert()设置检查点, 拦截非预期情况

-例如assert(p != NULL)就可以拦截由空指针解引用引起的段错误

-结合对程序执行行为的理解, 使用printf()查看程序执行的情况(注意字符串要换行)

-printf()输出任意信息可以检查代码可达性: 输出了相应信息, 当且仅当相应代码块被执行

-printf()输出变量的值, 可以检查其变化过程与原因

-使用GDB观察程序的任意状态和行为

-打印变量, 断点, 监视点, 函数调用栈...

 

2022.8.8

1.bug与debug

  • Fault: 实现错误的代码, 例如if (p = NULL)
  • Error: 程序执行时不符合预期的状态, 例如p被错误地赋值成NULL
  • Failure: 能直接观测到的错误, 例如程序触发了段错误

一些有用的工具:

-Wall, -Werror: 在编译时刻把潜在的fault直接转变成failure. 这种工具的作用很有限, 只能寻找一些在编译时刻也觉得可疑的fault, 例如if (p = NULL). 不过随着编译器版本的增强, 编译器也能发现代码中的一些未定义行为. 这些都是免费的午餐, 不吃就真的白白浪费了.

assert(): 在运行时刻把error直接转变成failure. assert()是一个很简单却又非常强大的工具, 只要在代码中定义好程序应该满足的特征, 就一定能在运行时刻将不满足这些特征的error拦截下来. 例如链表的实现, 我们只需要在代码中插入一些很简单的assert()(例如指针解引用时不为空), 就能够几乎告别段错误. 但是, 编写这些assert()其实需要我们对程序的行为有一定的了解, 同时在程序特征不易表达的时候, assert()的作用也较为有限.

printf(): 通过输出的方式观察潜在的error. 这是用于回溯fault时最常用的工具, 用于观测程序中的变量是否进入了错误的状态. 在NEMU中我们提供了输出更多调试信息的宏Log(), 它实际上封装了printf()的功能. 但由于printf()需要根据输出的结果人工判断是否正确, 在便利程度上相对于assert()的自动判断就逊色了不少.

GDB: 随时随地观测程序的任何状态. 调试器是最强大的工具, 但你需要在程序行为的茫茫大海中观测那些可疑的状态, 因此使用起来的代价也是最大的.

2. 一些调试的建议:

  • 总是使用-Wall和-Werror
  • 尽可能多地在代码中插入assert()
  • 调试时先启用sanitizer
  • assert()无法捕捉到error时, 通过printf()输出可疑的变量, 期望能观测到error
  • printf()不易观测error时, 通过GDB理解程序的精确行为

2022.8.9

1.多层函数调用,将每层函数需要之后跳转的地址,存放于stack中

2.int*类型表示指向整形的指针,这就解答了为什么指针声明是这样的:

int *p = &var  其实就是(int *)p = &var

 

2022.8.10

1.#define call(x,y) x##y

##把x和y连接起来,相当于新变量xy

xy+call(x,y) 就相当于xy+xy=20+20=40;

 

2022.8.11

1. snpc是指代码中的下一条指令, 而dnpc是指程序运行过程中的下一条指令. 对于顺序执行的指令, 它们的snpc和dnpc是一样的; 但对于跳转指令, snpc和dnpc就会有所不同, dnpc应该指向跳转目标的指令. 显然, 我们应该使用s->dnpc来更新PC, 并且在指令执行的过程中正确地维护s->dnpc

2. 通常一条指令包含操作符和操作数,操作数是指令执行的参与者,也就是说操作数是参与某种功能操作的数据

操作数有三种方式提供:立即数(指令要操作的数据以常量的形式出现在指令中,称为立即操作数)、寄存器存放的数据、内存中的数据

通俗一点的理解:操作符是加工的方式,操作数是被加工的东西。

3. 用于寄存器-寄存器操作的 R 类型指令

用于短立即数和访存 load 操作的 I 型指令

用于访存 store 操作的 S 型指令

用于条件跳转操 作的 B 类型指令

用于长立即数的 U 型指令

用于无条件跳转的 J 型指令

4. RV32I 有 31 寄存器加上一个值恒为 0 的 x0 寄存器

5. C语言里 a&~1 使数a的最低位为0

 

2022.8.12

6 种基本指令格式具体介绍如下:

 

1、R-typed

R-typed 指令是最常用的运算指令,具有三个寄存器地址,每个都用 5bit 的数表示。指令的操作由 7 位的 opcode、7 位的 funct7 以及 3 位的 funct3 共同决定的。R-typed 是不包含立即数的所有整数计算指令,一般表示寄存器-寄存器操作的指令。

 

2、I-typed

I-typed 具有两个寄存器地址和一个立即数,其中一个是源寄存器 rs1,一个是目的寄存器 rd,指令的高 12 位是立即数。指令的操作仅由 7 位的 opcode 和 3 位的funct3两者决定。值得注意的是,在执行运算时需要先把 12 位立即数扩展到 32 位之后再进行运算。I-typed 指令相当于将 R-typed 指令格式中的一个操作数改为立即数。一般表示短立即数和访存 load 操作的指令。

 

3、S-typed

S-typed 的指令功能由 7 位 opcode 和 3 位 funct3 决定,指令中包含两个源寄存器和指令的imm[31:25]和 imm[11:7]构成的一个12位的立即数,在执行指令运算时需要把12 位立即数扩展到 32 位,然后再进行运算,S-typed 一般表示访存 store 操作指令,如存储字(sw)、半字(sh)、字节(sb)等指令。

 

4、B-typed

B-typed 的指令操作由 7 位 opcode 和 3 位 funct3 决定,指令中具有两个源寄存器和一个 12 位的立即数,该立即数构成是指令的第32位是 imm[12]、第7位是imm[11]、25 到 30 是 imm[10:5]、8 到 11 位是 imm[4:1],同样的,在执行运算时需要把12 位立即数扩展到 32 位,然后再进行运算。B-typed 一般表示条件跳转操作指令,如相等(beq)、不相等(bne)、大于等于(bge)以及小于(blt)等跳转指令。

 

5、U-typed

U-typed 的指令操作仅由 7 位 opcode 决定,指令中包括一个目的寄存器 rd 和高20 位表示的 20 位立即数。U-typed 一般表示长立即数操作指令,例如 lui 指令,将立即数左移 12 位,并将低 12 位置零,结果写回目的寄存器中。

 

6、J-typed

J-typed 的指令操作由 7 位 opcode 决定,与 U-typed 一样只有一个目的寄存器 rd和一个 20 位的立即数,但是 20 位的立即数组成不同,即指令的 31 位是 imm[20]、 12 到 19 位是 imm[19:12]、20 位是 imm[11]、21 到 30 位是 imm[10:1],J-typed 一般表示无条件跳转指令,如 jal 指令。

 

2022.8.12

1.负数的补码(作为无符号数)越大,值越大

 

2022.8.13

1.通过库, 运行程序所需要的公共要素被抽象成API, 不同的架构只需要实现这些API, 也就相当于实现了支撑程序运行的运行时环境, 这提升了程序开发的效率: 需要的时候只要调用这些API, 就能使用运行时环境提供的相应功能.

2. AM(Abstract machine)

AM = TRM + IOE + CTE + VME + MPE

TRM(Turing Machine) - 图灵机, 最简单的运行时环境, 为程序提供基本的计算能力

IOE(I/O Extension) - 输入输出扩展, 为程序提供输出输入的能力

CTE(Context Extension) - 上下文扩展, 为程序提供上下文管理的能力

VME(Virtual Memory Extension) - 虚存扩展, 为程序提供虚存管理的能力

MPE(Multi-Processor Extension) - 多处理器扩展, 为程序提供多处理器通信的能力 (MPE超出了ICS课程的范围, 在PA中不会涉及)

3. RV64I是RV32I的超集,RV32I是RV64I的子集。RV64I包括RV32I的所有40条指令,另外增加了12条RV32I中没有的指令,还有三条移位指令(slli, srli,srai)也进行小小的改动。在RV64I中,整数寄存器是64位的,即xlen=64,所以每条指令中的寄存器都是64位运算,立即数符号位扩展也是到64位。

4. volatile 的意思是“易失的,易改变的”。这个限定词的含义是向编译器指明变量的内容可能会由于其他程序的修改而变化。下面是volatile变量的几个例子:
1). 并行设备的硬件寄存器(如:状态寄存器)
2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3). 多线程应用中被几个任务共享的变量

5. 内联汇编的基本格式:

asm("汇编语句"

     : 输出部分

     : 输入部分

     : 会被修改的部分);

6. grep 主要用于返回匹配的项目,支持正则表达式。

$ grep PATTERN filename      ### 返回所有含有 PATTERN 的行

7. 在循环次数比较小时,i++和++i基本没有区别,当循环次数比较大时,两者的运行时间就有了一些差距,使用++i的for循环运行时间相较于使用i++的for循环要快一点

i++由于是在使用当前值之后再+1,所以需要一个临时的变量来转存。

而++i则是在直接+1,省去了对内存的操作的环节,相对而言能够提高性能

8. memmove() 是比 memcpy() 更安全的方法。如果目标区域和源区域有重叠的话,memmove() 能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中,复制后源区域的内容会被更改。如果目标区域与源区域没有重叠,则和 memcpy() 函数功能相同。

 

2022.8.14

1. sprintf函数的格式:int sprintf( char *buffer, const char *format [, argument,...] );

sprintf函数的功能与printf函数的功能基本一样,只是它把结果输出到指定的字符串buffer中了

2. stdarg.h是C语言中C标准函数库的标头档,stdarg是由stdandard(标准) arguments(参数)简化而来,主要目的为让函数能够接收不定量参数。

va_list

宏定义了一个指针类型,这个指针类型指向参数列表中的参数。

 

void va_start(va_list ap, last_arg)

修改了用va_list申明的指针,比如ap,使这个指针指向了不定长参数列表省略号前的参数。

 

type va_arg(va_list, type)

获取参数列表的下一个参数,并以type的类型返回。

 

void va_end(va_list ap)

参数列表访问完以后,参数列表指针与其他指针一样,必须收回,否则出现野指针。一般va_start 和va_end配套使用。

 

_vsnprintf

用于向字符串中打印数据、数据格式用户自定义

3. VA_LIST的用法:

(1)首先在函数里定义一具VA_LIST型的变量,这个变量是指向参数的指针;

(2)然后用VA_START宏初始化变量刚定义的VA_LIST变量;

(3)然后用VA_ARG返回可变的参数,VA_ARG的第二个参数是你要返回的参数的类型(如果函数有多个可变参数的,依次调用VA_ARG获取各个参数);

(4)最后用VA_END宏结束可变参数的获取。

4. unsigned(-1)=1,111...111(共32个1)。表示unsigned的最大值。

5. size_t是一些C/C++标准在stddef.h中定义的,size_t 类型表示C中任何对象所能达到的最大长度,它是无符号整数

6. 在Kconfig文件中:

config A

    depends on B

    select C

 

它的含义是:CONFIG_A配置与否,取决于CONFIG_B是否配置。一旦CONFIG_A配置了,CONFIG_C也自动配置了。