oz1010's blog

记录生活或技能

参考原文

Chapter 1 系统接口

Unix utilities实验

实验说明

启动系统

sleep

重点:使用user/user.h的sleep接口实现,单位为jiffies(1/10)。

pingpong

重点:使用pip接口通信。

primes

目的:主进程准备好2-35的数字写入管道。

从管道中读取数字n(此数字为素数),创建一个子进程,并将剩余的非n的倍数的数写入子管道中。然后进程等待子进程的退出。子进程会重复父进程的动作,直到读取的数字到达35,则不再创建子进程。

find

重点:熟悉文件属性读取,和路径拼接。

xargs

重点:使用exec接口实现,并需要构建新的参数数组。

Chapter 2 系统结构

RISC-V has three modes in which the CPU can execute instructions: machine mode, supervisor mode, and user mode.

An application can execute only user-mode instructions and is said to be running in user space, while the software in supervisor mode can also execute privileged instructions and is said to be running in kernel space.

CPUs provide a special instruction ( RISC-V provides the ecall instruction ) that switches the CPU from user mode to supervisor mode and enters the kernel at an entry point specified by the kernel.

the entire operating system resides in the kernel, so that the implementations of all system calls run in supervisor mode. This organization is called a monolithic kernel.

OS designers can minimize the amount of operating system code that runs in supervisor mode, and execute the bulk of the operating system in user mode. This kernel organization is called a microkernel.

源码文件功能描述如下

文件名功能描述
bio.cDisk block cache for the file system.
console.cConnect to the user keyboard and screen.
entry.SVery first boot instructions.
exec.cexec() system call.
file.cFile descriptor support.
fs.cFile system.
kalloc.cPhysical page allocator.
kernelvec.SHandle traps from kernel, and timer interrupts.
log.cFile system logging and crash recovery.
main.cControl initialization of other modules during boot.
pipe.cPipes.
plic.cRISC-V interrupt controller.
printf.cFormatted output to the console.
proc.cProcesses and scheduling.
sleeplock.cLocks that yield the CPU.
spinlock.cLocks that don’t yield the CPU.
start.cEarly machine-mode boot code.
string.cC string and byte-array library.
swtch.SThread switching.
syscall.cDispatch system calls to handling function.
sysfile.cFile-related system calls.
sysproc.cProcess-related system calls.
trampoline.SAssembly code to switch between user and kernel.
trap.cC code to handle and return from traps and interrupts.
uart.cSerial-port console device driver.
virtio_disk.cDisk device driver.
vm.cManage page tables and address spaces.

进程虚拟空间分布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
地址    功能域

MAXVA <--------->
trampoline
<--------->
trapframe
<--------->
heap
<--------->
user stack
<--------->
user text
and data
(followed by global variables)
(Instructions come first)
0 <--------->

risc-v指针宽度为64位,但硬件只使用低39位用于在页表中寻找虚拟地址,而xv6系统中只使用了38位。因此最大地址为2^38 - 1=0x3f,ffff,ffff MAXVA(kernel/riscv.h:363)

进程重要信息存储在struct proc(kernel/proc.h:85)

RISC-V ecall instruction raises the hardware privilege level and changes the program counter to a kernel-defined entry point.

When the system call completes, the kernel switches back to the user stack and returns to user space by calling the sret instruction, which lowers the hardware privilege level and resumes executing user instructions just after the system call instruction.

启动流程:

  • boot loader将kernel加载到内存0x8000 0000
  • 在machine mode下,跳转到_entry(kernel/entry.S:7),设置堆栈,并跳转运行C代码
  • 在C代码入口函数start(kernel/start.c:21)中,切换到supervisor mode,配置时钟中断,并跳转到主函数。
  • 在主函数main(kernel/main.c:11)中,初始化设备和子系统,创建第一个进程
  • 在初始化进程userinit(kernel/proc.c:233)中,寄存器a7装载SYS_EXEC(kernel/syscall.h:8)后再次进入内核
  • 在内核系统调用处理函数syscall(kernel/syscall.c:132)中,启动/init进程
  • 系统调用完成后,返回进程init(user/init.c),创建一个新console设备文件,并打开文件描述符0,1,2。

it sets the previous privilege mode to supervisor in the register mstatus, it sets the return address to main by writing main’s address into the register mepc, disables virtual address translation in supervisor mode by writing 0 into the page-table register satp, and delegates all interrupts and exceptions to supervisor mode.

system call实验

System call tracing

新增一个trace系统调用,它接收一个整数参数,它表明哪些系统调用被标记。当被标记的系统调用返回时,需要打印<pid>: syscall <call name> -> <return value>。此标记对子进程和forks都有效,但对其他进程无效。

关键点

  • user/trace.c中设置调用trace(x)后需要再trace(0)清空进程标记;
  • user/user.h中增加用户调用系统函数int trace(int sys_mask);user/usys.pl增加trace生成相关汇编代码;
  • kernel/syscall.h新增宏编号#define SYS_trace 22kernel/sysproc.c新增标记实现函数uint64 sys_trace(void)
  • syscall增加标记打印逻辑,需要注意allocproc中共用struct proc proc[NPROC];,申请后要清空之前的标记;

Sysinfo

新增一个sysinfo系统调用,它会收集系统空闲内存字节大小freemem和正在使用的进程数量nproc。需要提供用户测试程序sysinfotest调用这个接口,若整个调用没有问题,则打印"sysinfotest: OK"

关键点

  • 需要使用接口copyout将内核空间的数据拷贝到用户空间中

练习

  1. 增加一个系统调用,返回系统剩余可用内存大小

Chapter 3 页表

创建地址空间

核心的数据结构pagetable_t kernel_pagetable(kernel/vm.c:1)中,核心的功能函数是walk,用于查找虚拟地址对应的PTE。

main中调用kvminit(kernel/vm.c:54)创建内核页表,再调用kvmmake,最终通过kvmmapmappageswalk完成物理地址虚拟地址映射。

main中调用kvminithart(kernel/vm.c:62)安装内核页表。主要讲根页表的地址设置到satp寄存器中,设置前后需要刷新TLB缓存。

The RISC-V has an instruction sfence.vma that flushes the current CPU’s TLB. Xv6 executes sfence.vma in kvminithart after reloading the satp register

物理内存分配

分配器定义在kalloc.c(kernel/kalloc.c:1)。每个空闲页的列表元素都是一个struct run

main中调用kinit(kernel/kalloc.c:27)来初始化分配器。它将初始化空闲列表kmem->freelist(kernel/kalloc.c:21)用于保存内核结束位置到PHYSTOP区间的每一页。

进程地址空间

每个进程都有独立的页表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
MAXVA   <--------->
trampoline RX--
<--------->
trapframe R-W-
<--------->
unused
<--------->
heap R-WU
<--------->
stack R-WU
<--------->
guard page
<--------->
data R-WU
<--------->
unused
<--------->
text RX-U
0 <--------->

一个进程的用户内存从虚拟地址零开始,可以增长到MAXVA(kernel/riscv.h:360),允许最大使用256GB内存。

零地址放置的text代码,没有写入权限,当异常的程序试图向零地址写入数据,会出发page fault

sbrk

sbrk是进程为调整内存时的系统调用。它由函数growproc(kernel/proc.c:260)实现。

exec

exec是一个系统调用,它可以用从文件读取的数据替换进程用户空间数据,这样的文件被称为二进制或可行性文件。函数exec(kernel/exec.c:23)会读取并解析ELF格式的文件,它包含struct elfhdrELF文件头部和一系列struct proghdr程序区域头部。每个程序区域头部描述程序必须加载到内存的位置。

/init程序区域头部像下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
$ objdump -p user/_init 

user/_init: file format elf64-little

Program Header:
0x70000003 off 0x0000000000006bac vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**0
filesz 0x0000000000000033 memsz 0x0000000000000000 flags r--
LOAD off 0x0000000000001000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**12
filesz 0x0000000000001000 memsz 0x0000000000001000 flags r-x
LOAD off 0x0000000000002000 vaddr 0x0000000000001000 paddr 0x0000000000001000 align 2**12
filesz 0x0000000000000010 memsz 0x0000000000000030 flags rw-
STACK off 0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**4
filesz 0x0000000000000000 memsz 0x0000000000000000 flags rw-

值得注意的是,头部信息中filesz可能会小于memsz,那时因为这些变量值为0,文件中无需存储,但加载时需要申请memsz大小的空间并清零。

然后,函数需要拷贝参数列表,并将堆栈和PC设置好。最后将释放旧页表,使用新页表。

练习

  1. 解析riscv的设备树,找出总共拥有多少物理内存
  2. 写一个用户程序调用sbrk(1),观察调用前后页表的变化。内核申请了多少空间?新内存的PTE包含哪些数据?
  3. 修改源码让内核使用超级页
  4. 在Unix系统中,若exec处理的可执行文件以#!开头,则会使用第一行剩余部分替换程序作为解释文件执行。修改源码让内核支持这个特性。
  5. 实现一个地址空间随机分布的内核。

Chapter 4 traps和系统调用

riscv的trap机制

Each RISC-V CPU has a set of control registers that the kernel writes to tell the CPU how to handle traps, and that the kernel can read to find out about a trap that has occurred.

在文件riscv.h(kernel/riscv.h:1)中包含系统用到的所有描述。这里列举最重要的寄存器

  • stvec: 内核写入trap处理程序的地址
  • sepc: 当trap发生时,处理器会保存pc;当从trap返回时,调用sret会从此寄存器恢复pc
  • scause: 处理器会存放一个数字描述trap的原因
  • sscratch: trap处理程序使用sscratch来避免改写用户寄存器
  • sstatus: SIE位控制设备中断是否使能;SPP位指示trap触发前是user mode还是supervisor mode;

xv6只将它们使用在计时器中断的特殊场景

硬件处理所有类型trap流程:

  • sstatus的SIE位被清零,则后面的步骤略过
  • sstatus的SIE位清零
  • 拷贝pc值到sepc
  • 保存当前模式到sstatus的SPP位
  • 设置scause
  • 模式切换为supervisor mode
  • 拷贝stvecpc
  • 继续从pc处开始执行

Note that the CPU doesn’t switch to the kernel page table, doesn’t switch to a stack in the kernel, and doesn’t save any registers other than the pc.

用户空间的traps流程

用户空间trap路径是:uservec(kernel/trampoline.S:21)->usertrap(kernel/trap.c:37)-return->usertrapret(kernel/trap.c:90)->userret(kernel/trampoline.S:101)

xv6使用trampoline页来存储stvec,它页包含uservec。trampoline页会被每个进程映射到TRAMPOLINE的地址处。

uservec函数会将32个用户寄存器存储到TRAPFRAME地址所在的trapframe结构中,然后将寄存器satp切换为内核页表,再调用usertrap函数。

usertrap(kernel/trap.c:37)函数会检测trap的原因并处理它。首先将设置stveckernelvec,以便处理内核trap。保存sepc寄存器。如果trap是系统调用,则调用syscall处理它;若是设备中断,devintr处理它;否则,是一种异常场景,调用内核终止异常的进程。若是系统调用,则在函数结束时会调用usertrapret

返回用户空间的第一步是调用usertrapret(kernel/trap.c:90)。它会将stvec设置回uservecuservec的映射地址可以通过TRAMPOLINE、trampoline和uservec计算出来(注意内核中这些是物理地址)。然后恢复pc,最后调用userret函数并将a0设置为用户页表。

userret(kernel/trampoline.S:101)函数将切换satp为用户页表,并恢复32个用户寄存器。最后调用sret返回用户空间。

系统调用流程

以initcode.S中第一个系统调用exec为例。

initcode.S在寄存器a0a1存放着exec的参数,并将系统调用编号存放在a7中。根据系统调用编号在syscalls(kernel/syscall.c:107)数组中匹配到处理函数。指令ecall会触发trap切换到内核中并引发uservecusertrapsyscall执行。

系统调用参数列表

内核trap代码将用户寄存器放在当前进程的trap frame上,可以通过内核函数argintargaddrargfd返回第n个参数,它们通过调用argraw实现(kernel/syscall.c:34)。

有些参数通过用户地址传递,fechstr(kernel/syscall.c:25)函数能够拷贝用户传递的字符串。

内核空间的traps流程

内核空间trap路径是:kernelvec(kernel/kernelvec.S:12)->kerneltrap(kernel/trap.c:135)->kernelvec(kernel/kernelvec.S:12)

若trap不是设备中断,则异常会直接导致xv6内核出panic。

当处理器遇到trap进入内核空间时,总会禁用中断,直到设置stvec后。

缺页异常

若发生在用户空间,内核将终止相关进程。若发生在内核空间,则会直接panic。

许多内核利用缺页异常来实现写时复制(copy-on-write, COW)机制。COW fork的基本方法是,为父进程和子进程初始化共享所有物理页,但将他们的全部设置为只读。当进程向某页写入数据时,会触发store page faults。此时内核需要重新申请新的一页将数据拷贝过来,并再次进行映射。一个重要的优化项是,对于缺页发生在仅从该进程中引用的,无需进行拷贝。

另一个广泛使用的特性是惰性分配(lazy allocation)。当一个应用调用sbrk请求更多内存,内核只调整其使用大小,但不会申请物理内存和创建PTEs。当这些新地址被访问时,才会申请对应的内存页并完成映射。

还有一个广泛使用的特性是需求分页(demand paging)。当大型程序启动时,内核无需将所有数据加载到内存中,而仅仅配置足够的用户地址空间,并将其设置为无效。当发生页错误时,内核将页的内容读入并映射到用户空间。

为应对程序运行时需要的空间比硬件RAM大的场景,操作系统可以实现磁盘映射(paging to disk)。

练习

  1. 配置内核页表,让内核可以直接使用用户空间地址;
  2. 实现惰性内存分配机制;
  3. 实现COW fork;
  4. 是否有方法消除TRAPFRAME页映射到每个用户地址空间?比如,修改uservec函数将32个用户寄存器存入内核栈,或将其存入proc结构中?
  5. 是否有方法消除TRAMPOLINE页映射?

中断和设备驱动

在xv6中,内核trap会处理和识别设备中断,最终交于devintr(kernel/trap.c:178)。

Many device drivers execute code in two contexts: a top half that runs in a process’s kernel thread, and a bottom half that executes at interrupt time.

终端输入

终端驱动(kernel/console.c)是一个简单的驱动框架的示例。

The UART hardware that the driver talks to is a 16550 chip [13] emulated by QEMU. On a real computer, a 16550 would manage an RS232 serial link connecting to a terminal or other computer. When running QEMU, it’s connected to your keyboard and display.

UART的基地址是0x10000000(UART0, kernel/memlayout.h:21),UART0各寄存器定义在文件(kernel/uart.c:22)中。

xv6的main调用consoleinit(kernel/console.c:182)函数,然后再调用uartinit(kernel/uart.c:53)函数来初始化UART硬件。

在init.c(user/init.c:19)打开的文件描述符,它能够读取xv6的命令。调用read系统调用,通过内核来调用consoleread(kernel/console.c:80)。它会一直等待中断并缓存数据到cons.buf中,直到整个行输入完成,会将缓存中数据拷贝给用户最终返回到用户空间。

当用户输入一个字符,UART硬件设备会想处理器产生一个中断,它会激活xv6的trap处理程序。设备中断最后会调用devintr(kernel/trap.c:178)进行处理。接着,通过PLIC硬件单元来分辨是哪个设备中断,如果是UART设备devintr会调用uartintr

uartintr(kernel/uart.c:176)会从UART硬件读取任意输入字符,并交于consoleintr(kernel/console.c:136)进行处理;consoleintr的任务就是将输入放到cons.buf,直到整行输入完成,立即唤醒consoleread

终端输出

在一个连接到终端的文件描述符的write系统调用,最终会调用uartputc(kernel/uart.c:87)。对于每个字符,会调用uartstart来开启设备发送。

驱动的并发

These calls acquire a lock, which protects the console driver’s data structures from concurrent access.

计时器中断

Xv6 uses timer interrupts to maintain its clock and to enable it to switch among compute-bound processes; the yield calls in usertrap and kerneltrap cause this switching.

RISC-V requires that timer interrupts be taken in machine mode, not supervisor mode.

代码在start.c配置接收计时器中断(kernel/start.c:63)。部分工作的目的是编写CLINT硬件(core-local interruptor),在特定延时后产生一个中断。最终,start配置mtvectimervec并使能计时器中断。

A timer interrupt can occur at any point when user or kernel code is executing; there’s no way for the kernel to disable timer interrupts during critical operations.

The basic strategy is for the handler to ask the RISC-V to raise a “software interrupt” and immediately return.

在machine mode的中断处理程序是timervec(kernel/kernelvec.S:95),它主要配置CLINT的MTIMECMP寄存器,并设置sip为2后立刻返回。

练习

  1. 修改uart.c完全不使用中断,同时也需要修改console.c
  2. 增加一个以太网卡驱动

Chapter 6 锁

Xv6 uses a number of concurrency control techniques, depending on the situation; many more are possible. This chapter focuses on a widely used technique: the lock.

竞争

On the RISC-V this instruction is amoswap r, a. amoswap reads the value at the memory address a, writes the contents of register r to that address, and puts the value it read into r.

It performs this sequence atomically, using special hardware to prevent any other CPU from using the memory address between the read and the write.

Xv6’s acquire (kernel/spinlock.c:22) uses the portable C library call __sync_lock_test_and_set, which boils down to the amoswap instruction; the return value is the old (swapped) contents of lk->locked.

acquire(kernel/spinlock.c:22)利用riscv处理器的指令amoswap.w.aq a0, a0, (s1)实现。当获取锁成功时lk->locked为1。

release(kernel/spinlock.c:47)利用riscv处理器的指令amoswap.w zero, zero, (s1)实现。当释放锁成功时lk->locked为0。

使用锁

A hard part about using locks is deciding how many locks to use and which data and invariants each lock should protect.

再入锁

It might appear that some deadlocks and lock-ordering challenges could be avoided by using re-entrant locks, which are also called recursive locks. The idea is that if the lock is held by a process and if that process attempts to acquire the lock again, then the kernel could just allow this (since the process already has the lock), instead of calling panic, as the xv6 kernel does.

锁和中断处理程序

Some xv6 spinlocks protect data that is used by both threads and interrupt handlers.

To avoid this situation, if a spinlock is used by an interrupt handler, a CPU must never hold that lock with interrupts enabled.

例如,clockintr计时器中断处理会增加ticks(kernel/trap.c:164),同时内核线程sys_sleep(kernel/sysproc.c:59)会读取ticks的值。锁tickslock会让两次访问串行化。

指令和内存顺序

It is natural to think of programs executing in the order in which source code statements appear. That’s a reasonable mental model for single-threaded code, but is incorrect when multiple threads interact through shared memory.

To tell the hardware and compiler not to re-order, xv6 uses __sync_synchronize() in both acquire (kernel/spinlock.c:22) and release (kernel/spinlock.c:47). __sync_synchronize() is a memory barrier: it tells the compiler and CPU to not reorder loads or stores across the barrier.

睡眠锁

Xv6 provides such locks in the form of sleep-locks. acquiresleep (kernel/sleeplock.c:22) yields the CPU while waiting

练习

  1. 若屏蔽kalloc(kernel/kalloc.c:69)acquirerelease的调用,会出现哪些问题?若没有看到问题,原因是什么?
  2. kfree中屏蔽锁(恢复kalloc中的锁),会出现哪些问题?
  3. 修改kalloc.c源码让内存申请支持并发,CPU不用相互等待。
  4. 使用POSIX线程进行编码。例如,实现一个并行哈希表并测试puts/gets数据量是否随着核数量的增加而增加。
  5. 在xv6中实现pthreads的子集。实现一个用户级的线程库,这样一个用户进程就可以有多个线程,并安排这些线程运行在不同的cpu上并行运行。想出一个设计,正确地处理一个线程进行阻塞系统调用,并改变其共享地址空间。

调度

复用

Xv6 multiplexes by switching each CPU from one process to another in two situations. First, xv6’s sleep and wakeup mechanism switches when a process waits for device or pipe I/O to complete, or waits for a child to exit, or waits in the sleep system call. Second, xv6 periodically forces a switch to cope with processes that compute for long periods without sleeping.

上下文切换

it just saves and restores sets of 32 RISC-V registers, called contexts.

当进程想放弃CPU时,内核线程会调用swtch来保存它的上下文并返回调度器上下文。每个上下文都包含在struct context(kernel/proc.h:2),进程的struct proc和CPU的struct cpu都包含上下文。

swtch(kernel/swtch.S:3)只保存被调用者保存的寄存器。C编译器会生成代码保存调用者保存的寄存器到栈上。

When swtch returns, it returns to the instructions pointed to by the restored ra register, that is, the instruction from which the new thread previously called swtch.

调度

当调用函数swtch后,会切换到调度器的栈。调度器会继续在循环中查找可以切换的进程,并再次调用swtch进行切换。

We just saw that xv6 holds p->lock across calls to swtch: the caller of swtch must already hold the lock, and control of the lock passes to the switched-to code.

mycpu和myproc

Xv6 maintains a struct cpu for each CPU (kernel/proc.h:22), which records the process currently running on that CPU (if any), saved registers for the CPU’s scheduler thread, and the count of nested spinlocks needed to manage interrupt disabling.

RISC-V numbers its CPUs, giving each a hartid. Xv6 ensures that each CPU’s hartid is stored in that CPU’s tp register while in the kernel. This allows mycpu to use tp to index an array of cpu structures to find the right one.

It would be more convenient if xv6 could ask the RISC-V hardware for the current hartid whenever needed, but RISC-V allows that only in machine mode, not in supervisor mode.

The return value of myproc is safe to use even if interrupts are enabled: if a timer interrupt moves the calling process to a different CPU, its struct proc pointer will stay the same.

睡眠和唤醒

Sleep and wakeup are often called sequence coordination or conditional synchronization mechanisms.

管道

A more complex example that uses sleep and wakeup to synchronize producers and consumers is xv6’s implementation of pipes. Each pipe is represented by a struct pipe, which contains a lock and a data buffer.

Let’s suppose that calls to piperead and pipewrite happen simultaneously on two different CPUs.

进程锁

The lock associated with each process (p->lock) is the most complex lock in xv6. A simple way to think about p->lock is that it must be held while reading or writing any of the following struct proc fields: p->state, p->chan, p->killed, p->xstate, and p->pid.

练习

文件系统

概述

The xv6 file system implementation is organized in seven layers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<---------------->
File descriptor
<---------------->
Pathname
<---------------->
Directory
<---------------->
Inode
<---------------->
Logging
<---------------->
Buffer cache
<---------------->
Disk
<---------------->

Disk hardware traditionally presents the data on the disk as a numbered sequence of 512-byte blocks (also called sectors): sector 0 is the first 512 bytes, sector 1 is the next, and so on.

The file system does not use block 0 (it holds the boot sector). Block 1 is called the superblock; it contains metadata about the file system (the file system size in blocks, the number of data blocks, the number of inodes, and the number of blocks in the log). Blocks starting at 2 hold the log. After the log are the inodes, with multiple inodes per block. After those come bitmap blocks tracking which data blocks are in use. The remaining blocks are data blocks

xv6文件系统结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0       <---------------->
boot
1 <---------------->
super
2 <---------------->
log

<---------------->
inodes


<---------------->
bit map
<---------------->
data

....

data
<---------------->

缓存层

代码在bio.c中,缓存层有两个任务

  • 同步访问硬盘块block。确保只有一个块的副本在内存中,且同一时间只有一个内核线程在使用它。
  • 缓存热门数据块block。

The main interface exported by the buffer cache consists of bread and bwrite. A kernel thread must release a buffer by calling brelse when it is done with it.

bread (kernel/bio.c:93) calls bget to get a buffer for the given sector (kernel/bio.c:97).

When the caller is done with a buffer, it must call brelse to release it.

日志层

One of the most interesting problems in file system design is crash recovery.

Xv6 solves the problem of crashes during file-system operations with a simple form of logging.

Once the system call has logged all of its writes, it writes a special commit record to the disk indicating that the log contains a complete operation. At that point the system call copies the writes to the on-disk file system data structures. After those writes have completed, the system call erases the log on disk.

block块分配器

File and directory content is stored in disk blocks, which must be allocated from a free pool. Xv6’s block allocator maintains a free bitmap on disk, with one bit per block.

inode层

It might refer to the on-disk data structure containing a file’s size and list of data block numbers. Or “inode” might refer to an in-memory inode

磁盘上数据展现形式

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
dinode
|----------------|
type
|----------------|
major
|----------------|
minor
|----------------|
nlink
|----------------|
size
|----------------|
address 1 --> data0_1
|----------------|
...
|----------------|
address 12 --> data0_12
|----------------|
indirect --> indirect block
|----------------| |----------------|
address 1 --> data1_1
|----------------|
...
|----------------|
address 256 --> data1_256
|----------------|

文件夹层

A directory is implemented internally much like a file.

目录名称

文件描述符层

All the open files in the system are kept in a global file table, the ftable.

The functions sys_link and sys_unlink edit directories, creating or removing references to inodes. They are another good example of the power of using transactions.

练习

附录

CH.1 Unix utilities实验代码

sleep

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

int main(int argc, char *argv[])
{
int seconds;

if (argc <= 1)
{
fprintf(2, "sleep: need one arg\n");
exit(0);
}

// ticks = 1/10 seconds
seconds = 10 * atoi(argv[1]);
if (seconds < 0)
{
seconds = 0;
}

sleep(seconds);
exit(0);
}

pingpong

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
#include "kernel/types.h"
#include "user/user.h"

int main(int argc, char *argv[])
{
int p1[2]; // parent->child
int p2[2]; // child->parent
pipe(p1);
pipe(p2);
if (fork() == 0)
{
char buffer[5];
close(p1[1]);
close(p2[0]);
read(p1[0], buffer, sizeof(buffer));
printf("%d: received %s\n", getpid(), buffer);
write(p2[1], "pong", 5);
close(p1[0]);
close(p2[1]);
}
else
{
char buffer[5];
close(p1[0]);
close(p2[1]);
write(p1[1], "ping", 5);
read(p2[0], buffer, sizeof(buffer));
printf("%d: received %s\n", getpid(), buffer);
close(p1[1]);
close(p2[0]);
}
exit(0);
}

primes

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
#include "kernel/types.h"
#include "user/user.h"

#define MAX_NUMBER 35

int main()
{
int p0[2]; // parent->child
int n, prime;
pipe(p0);

// feeds the numbers 2 through 35
for (int i = 2; i <= MAX_NUMBER; ++i)
write(p0[1], &i, sizeof(i));

while (read(p0[0], &n, sizeof(n)))
{
printf("%d prime %d\n", getpid(), n);
prime = n;
int p1[2]; // child -> grandchild
pipe(p1);

if (fork() == 0)
{
// child
close(p0[0]);
close(p0[1]);
p0[0] = p1[0];
p0[1] = p1[1];
continue;
}
else
{
// parent
while (n < MAX_NUMBER && read(p0[0], &n, sizeof(n)))
if (n % prime != 0)
write(p1[1], &n, sizeof(n));

// close all resources
close(p0[0]);
close(p0[1]);
close(p1[0]);
close(p1[1]);
// wait children
if (n < MAX_NUMBER)
wait(0);
exit(0);
}
}

wait(0);
printf("%d exit\n", getpid());
exit(0);
}

find

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
#include "kernel/types.h"
#include "user/user.h"
#include "kernel/fs.h"
#include "kernel/stat.h"

void cmp_file(char *path, char *name)
{
char *str1, *str2;
for (str1 = path + strlen(path), str2 = name + strlen(name); str1 >= path && str2 >= name && *str1 == *str2; --str1, --str2)
; // printf("%c %c\n", *str1, *str2);

if ((int)(str2 - name) == -1)
printf("%s\n", path);
}

void find(char *path, char *patern)
{
int fd;
struct dirent de;
struct stat st;
char buf[512], *p;

if ((fd = open(path, 0)) < 0)
{
fprintf(2, "find: connot open %s\n", path);
return;
}

if (fstat(fd, &st) < 0)
{
fprintf(2, "find: connot stat %s\n", path);
close(fd);
return;
}

switch (st.type)
{
case T_DEVICE:
case T_FILE:
cmp_file(path, patern);
break;

case T_DIR:
if (strlen(path) + 1 + DIRSIZ + 1 > sizeof(buf))
{
printf("find: path too long\n");
break;
}

strcpy(buf, path);
p = buf + strlen(buf);
if (*(p - 1) != '/')
*p++ = '/';
while (read(fd, &de, sizeof(de)) == sizeof(de))
{
if (de.inum == 0 || strcmp(".", de.name) == 0 || strcmp("..", de.name) == 0)
continue;
memmove(p, de.name, DIRSIZ);
p[DIRSIZ] = 0;
find(buf, patern);
}
break;
}

close(fd);
}

int main(int argc, char *argv[])
{
if (argc <= 2)
{
fprintf(2, "find: need more args\n");
exit(-1);
}
find(argv[1], argv[2]);
exit(0);
}

xargs

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
#include "kernel/types.h"
#include "kernel/param.h"
#include "user/user.h"

int main(int argc, char *argv[])
{
char buffer[512];
char *newarg[MAXARG];
int i;
int new_idx;
for (i = 1, new_idx = 0; i < argc; ++i)
if (argv[i][0] == '-')
i += 1;
else
newarg[new_idx++] = argv[i];
while (*gets(buffer, sizeof(buffer)) != '\0')
{
buffer[strlen(buffer) - 1] = '\0';
newarg[new_idx] = buffer;

if (fork() == 0)
{
exec(newarg[0], newarg);
exit(0);
}
wait(0);
}

exit(0);
}

CH.2 system calls实验代码

GDB-常用命令

打开GDB调试开关命令:set debug remote 1

GDB-远程序列化协议

简介

所有GDB命令和响应都是以包(packet)形式进行送。一个包的基本结构:$包数据#2字节校验和

1
$packet-data#checksum

2字节校验和是计算$#之间的所有字符数值和,并取模256后的两位十六进制数。

GDB 5.0之前的协议需要包含两字节序列编号

1
$sequence-id:packet-data#checksum

当主机或目标机接收到第一个包,预期的一个响应是确认:可以是+(表明包接收正确)或-(请求重传)

1
2
-> $packet-data#checksum
<- +

连接一旦建立,可以禁用+/-确认。

主机(GDB)发送命令,而目标机(调试目标)发送响应。

包数据不能包含非法字符#$

若包数据中可以使用, ;:作为分隔符。除特别说明,所有数字均使用十六进制表示。

GDB 5.0之前的协议不能使用:作为分隔符(与sequence-id的分隔符冲突)。

在许多包中二进制数据都以两位十六进制字符表示。

二进制数据7d(ASCII字符})作为转义字符。任何转义字符传输时需要跟一个与0x20异或后的结果。例如,单字节0x7d在传输时会转为两个字节0x7d 0x5d。常见的转义字符有0x23 # 0x24 $ 0x7d } 0x2a *

响应数据可以使用运行长度编号来节省空间。例如:编号后'0* ' 表示编码字符串'0000'*后面的空格表示重复0字符32-29=3次。

对于可打印字符#$或数值超过126的都不可使用。对于7次重复(字符$)可以使用5次(字符")来分开表示。例如,'00000000'编码后为'0*"00'

对于不支持的命令,会返回空响应$#00

在最小场景中,目标机必须支持?命令来告诉GDB停止的原因,gG命令用于寄存器访问,mM命令用于内存访问。对于单线程目标要实现c命令,并支持单步调试命令s。多线程目标需要支持vCont命令。其他所有命令都是可选的。

下表中,命令中的空格表示语义分隔,但实际数据包不需要发送。

命令说明
!使能扩展模式
?当连接第一次建立后,会被询问目标机停止的原因。回复与step和continue一样。
A arglen,argnum,arg,…初始化argv[]参数数组传递给程序。其中arglen是十六进制编码后的字节流参数arg的长度。
b baud(不推荐使用)改变串行通信速率
B addr,mode(不推荐使用,使用Z和z包替代)设置(mode是S)或清除(mode是C)在addr处的断点。
bc向后继续执行。目标系统反向执行。
bs向后单步执行。反向执行一条执行。
c [addr]从addr处继续执行;addr省略时,从当前地址继续。
C sig[;addr]与c命令一样,额外可携带一个信号量sig(十六进制数)
d(不推荐使用)其他调试标志
D
D;pid
通知远端目标机GDB断开连接。第二种包含进程编号,只对特定的进程生效。
F RC,EE,CF;XX文件I/O扩展协议命令
g读取通用寄存器
回复:‘XX…’
寄存器每个字节由两个十六进制表示。当目标寄存器不可用时,会使用字符x进行占位。
G XX…写通用寄存器
z0,addr,kind
Z0,addr,kind
[;con_list…]
[;cmds:persis,cmd_list]
插入(Z0)或移除(z0)在addr处kind类型的一个软件断点
z1,addr,kind
Z1,addr,kind
[;con_list…]
[;cmds:persis,cmd_list]
插入(Z1)或移除(z1)在addr处kind类型的一个硬件断点
z2,addr,kind
Z2,addr,kind
插入(Z2)或移除(z2)在addr处的一个写监控点。监控的字节数由kind指定
z3,addr,kind
Z3,addr,kind
插入(Z3)或移除(z3)在addr处的一个读监控点。监控的字节数由kind指定
z4,addr,kind
Z4,addr,kind
插入(Z4)或移除(z4)在addr处的一个访问监控点。监控的字节数由kind指定

停止回复包

通用包

q开头的是通用询问包;以Q开头的是通用设置包;

参考

详见官方文档 Appendix E gdb Remote Serial Protocol章节

语言简介

Verilog在线学习HDLBits主页

基本语句

常量

1
2
3
3'b101 	// binary 101
4'hf // binary 1111
4'd10 // binary 1010

线wire

物理线路没有方向,但建模上需要指定方向。

创造一个模型,有三个输入和四个输出

1
2
3
4
a -> w
b -> x
b -> y
c -> z

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module top_module( 
input a,b,c,
output w,x,y,z );

assign w=a;
assign x=b;
assign y=b;
assign z=c;

// If we're certain about the width of each signal, using
// the concatenation operator is equivalent and shorter:
// assign {w,x,y,z} = {a,b,b,c};

endmodule

非门Inverter

电路中产生一个非门

1
2
3
4
5
module top_module( input in, output out );
assign out = !in;
// similar
// assign out = ~in;
endmodule

与门AND gate

电路中产生一个与门

1
2
3
4
5
6
7
8
module top_module( 
input a,
input b,
output out );

assign out = a & b;

endmodule

或非门Nor gate

电路中产生一个或非门

1
2
3
4
5
6
7
8
module top_module( 
input a,
input b,
output out );

assign out = ~(a | b);
endmodule

异或非门XNOR gate

电路中产生一个异或非门

1
2
3
4
5
6
7
8
module top_module( 
input a,
input b,
output out );

assign out = ~(a ^ b);
endmodule

声明线Declaring wires

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
`default_nettype none
module top_module(
input a,
input b,
input c,
input d,
output out,
output out_n );

wire w1, w2, w3;
assign out = w3;
assign out_n = ~w3;

assign w3 = w1 | w2;
assign w1 = a & b;
assign w2 = c & d;
endmodule

7458芯片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module top_module ( 
input p1a, p1b, p1c, p1d, p1e, p1f,
output p1y,
input p2a, p2b, p2c, p2d,
output p2y );

wire wp11,wp12;
assign p1y = wp11 | wp12;
assign wp11 = p1a & p1b & p1c;
assign wp12 = p1f & p1e & p1d;

wire wp21,wp22;
assign p2y = wp21 | wp22;
assign wp21 = p2a & p2b;
assign wp22 = p2c & p2d;
endmodule

向量Vector

基本使用

声明语法:

1
type [upper:lower] vector_name;

类型可以是wire, reg等等

1
2
3
4
5
6
wire [7:0] w;         // 8-bit wire
reg [4:1] x; // 4-bit reg
output reg [0:0] y; // 1-bit reg that is also an output port (this is still a vector)
input wire [3:-2] z; // 6-bit wire input (negative ranges are allowed)
output [3:0] a; // 4-bit output wire. Type is 'wire' unless specified otherwise.
wire [0:7] b; // 8-bit wire where b[0] is the most-significant bit.

隐含声明可能导致意想不到的结果

1
2
3
4
5
6
wire [2:0] a, c;   // Two vectors
assign a = 3'b101; // a = 101
assign b = a; // b = 1 implicitly-created wire
assign c = b; // c = 001 <-- bug
my_module i1 (d,e); // d and e are implicitly one-bit wide if not declared.
// This could be a bug if the port was intended to be a

unpacked数组维度跟在名称后,packed数组维度放在名称前

1
2
reg [7:0] mem [255:0];   // 256 unpacked elements, each of which is a 8-bit packed vector of reg.
reg mem2 [28:0]; // 29 unpacked elements, each of which is a 1-bit reg.

描绘如下电路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module top_module ( 
input wire [2:0] vec,
output wire [2:0] outv,
output wire o2,
output wire o1,
output wire o0 ); // Module body starts after module declaration

assign {o2,o1,o0} = vec[2:0];
assign outv = vec;

// This is ok too
//assign o0 = vec[0];
//assign o1 = vec[1];
//assign o2 = vec[2];
endmodule

将字数据(2字节)按高低位分离

1
2
3
4
5
6
7
8
9
10
11
12
`default_nettype none     // Disable implicit nets. Reduces some types of bugs.
module top_module(
input wire [15:0] in,
output wire [7:0] out_hi,
output wire [7:0] out_lo );

assign out_hi = in[15:8];
assign out_lo = in[7:0];

// Concatenation operator also works: assign {out_hi, out_lo} = in;
endmodule

向量门

按位与&和逻辑与&对于N-bit位输入来说,会产生不同结果。前者会产生N-bit位输出,而后者只产生1-bit位输出(作为布尔值,非0值为true,0值为false)。

描绘如下电路,out_not高5-3位为b的非,其余为a的非。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module top_module( 
input [2:0] a,
input [2:0] b,
output [2:0] out_or_bitwise,
output out_or_logical,
output [5:0] out_not
);

assign out_or_bitwise = a | b;
assign out_or_logical = a || b;
// ! is logical inverse
assign out_not = {~b, ~a};
endmodule

颠倒8位

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
module top_module (
input [7:0] in,
output [7:0] out
);
assign out = {in[0],in[1],in[2],in[3],in[4],in[5],in[6],in[7]};
//assign {out[0],out[1],out[2],out[3],out[4],out[5],out[6],out[7]} = in;

/*
// I know you're dying to know how to use a loop to do this:

// Create a combinational always block. This creates combinational logic that computes the same result
// as sequential code. for-loops describe circuit *behaviour*, not *structure*, so they can only be used
// inside procedural blocks (e.g., always block).
// The circuit created (wires and gates) does NOT do any iteration: It only produces the same result
// AS IF the iteration occurred. In reality, a logic synthesizer will do the iteration at compile time to
// figure out what circuit to produce. (In contrast, a Verilog simulator will execute the loop sequentially
// during simulation.)
always @(*) begin
for (int i=0; i<8; i++) // int is a SystemVerilog type. Use integer for pure Verilog.
out[i] = in[8-i-1];
end


// It is also possible to do this with a generate-for loop. Generate loops look like procedural for loops,
// but are quite different in concept, and not easy to understand. Generate loops are used to make instantiations
// of "things" (Unlike procedural loops, it doesn't describe actions). These "things" are assign statements,
// module instantiations, net/variable declarations, and procedural blocks (things you can create when NOT inside
// a procedure). Generate loops (and genvars) are evaluated entirely at compile time. You can think of generate
// blocks as a form of preprocessing to generate more code, which is then run though the logic synthesizer.
// In the example below, the generate-for loop first creates 8 assign statements at compile time, which is then
// synthesized.
// Note that because of its intended usage (generating code at compile time), there are some restrictions
// on how you use them. Examples: 1. Quartus requires a generate-for loop to have a named begin-end block
// attached (in this example, named "my_block_name"). 2. Inside the loop body, genvars are read only.
generate
genvar i;
for (i=0; i<8; i = i+1) begin: my_block_name
assign out[i] = in[8-i-1];
end
endgenerate
*/

endmodule

重复操作

声明语法:

1
{num{vector}}

示例

1
2
3
4
{5{1'b1}}           // 5'b11111 (or 5'd31 or 5'h1f)
{2{a,b,c}} // The same as {a,b,c,a,b,c}
{3'd5, {2{3'd6}}} // 9'b101_110_110. It's a concatenation of 101 with
// the second vector, which is two copies of 3'b110.

将8位数据扩展为32位,高24位为符号位(bit[7]),余下8位为输入

1
2
3
4
5
6
7
8
9
10
11
module top_module (
input [7:0] in,
output [31:0] out
);

// Concatenate two things together:
// 1: {in[7]} repeated 24 times (24 bits)
// 2: in[7:0] (8 bits)
assign out = { {24{in[7]}}, in };

endmodule

描述电路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module top_module (
input a, b, c, d, e,
output [24:0] out
);

wire [24:0] top, bottom;
assign top = { {5{a}}, {5{b}}, {5{c}}, {5{d}}, {5{e}} };
assign bottom = {5{a,b,c,d,e}};
assign out = ~top ^ bottom; // Bitwise XNOR

// This could be done on one line:
// assign out = ~{ {5{a}}, {5{b}}, {5{c}}, {5{d}}, {5{e}} } ^ {5{a,b,c,d,e}};

endmodule

模块Module

基本使用

基本语法

1
2
3
module mod_a ( input in1, input in2, output out );
// Module body
endmodule

线与模块端口连接有两种方式:通过位置和通过名称

通过位置连接,模块实例instance1三个端口分别连接线wa, wb, wc

1
mod_a instance1 ( wa, wb, wc );

通过名称连接,模块实例instance2三个端口名称与对应线连接:

1
mod_a instance2 ( .out(wc), .in1(wa), .in2(wb) );

描述电路,其中模块mod_a在其他地方描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module top_module (
input a,
input b,
output out
);

// Create an instance of "mod_a" named "inst1", and connect ports by name:
mod_a inst1 (
.in1(a), // Port"in1"connects to wire "a"
.in2(b), // Port "in2" connects to wire "b"
.out(out) // Port "out" connects to wire "out"
// (Note: mod_a's port "out" is not related to top_module's wire "out".
// It is simply coincidence that they have the same name)
);

/*
// Create an instance of "mod_a" named "inst2", and connect ports by position:
mod_a inst2 ( a, b, out ); // The three wires are connected to ports in1, in2, and out, respectively.
*/

endmodule

移位模块

给已经实现的D触发器(D flip-flop)模块module my_dff ( input clk, input d, output q );,按下图描述电路

1
2
3
4
5
6
7
8
module top_module ( input clk, input d, output q );
wire q1,q2;

my_dff d1 (clk, d, q1);
my_dff d2 (clk, q1, q2);
my_dff d3 (clk, q2, q);
endmodule

8位移位器

描绘电路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module top_module (
input clk,
input [7:0] d,
input [1:0] sel,
output reg [7:0] q
);

wire [7:0] o1, o2, o3; // output of each my_dff8

// Instantiate three my_dff8s
my_dff8 d1 ( clk, d, o1 );
my_dff8 d2 ( clk, o1, o2 );
my_dff8 d3 ( clk, o2, o3 );

// This is one way to make a 4-to-1 multiplexer
always @(*) begin // Combinational always block
case(sel)
2'h0: q = d;
2'h1: q = o1;
2'h2: q = o2;
2'h3: q = o3;
endcase
end
endmodule

全加器

add16全加器已经定义module add16 ( input[15:0] **a**, input[15:0] **b**, input **cin**, output[15:0] **sum**, output **cout** );,它包含16个add1全加器,描述电路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module top_module (
input [31:0] a,
input [31:0] b,
output [31:0] sum
);
wire o1,o2;

add16 inst1(a[15:0],b[15:0],0,sum[15:0],o1);
add16 inst2(a[31:16],b[31:16],o1,sum[31:16],o2);
endmodule

module add1 ( input a, input b, input cin, output sum, output cout );
// Full adder module here
assign sum = cin ? ~(a^b) : a^b;
assign cout = cin ? a|b : a&b;
endmodule

加减器

减法器由全加器变化而来,只需将对第二个操作数取补码即可(反码加一)。等效的电路可以有两种效果:a+b+0a+~b+1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module top_module(
input [31:0] a,
input [31:0] b,
input sub,
output [31:0] sum
);

wire o1,o2;
wire [31:0] b1 = b^{32{sub}};

add16 inst1(a[15:0],b1[15:0],sub,sum[15:0],o1);
add16 inst2(a[31:16],b1[31:16],o1,sum[31:16],o2);
endmodule

过程Procedure

对于综合硬件,有两种相关always块:

  • 组合型:always @(*)
  • 时序型:always @(posedge clk)

块语句需要使用beginend标记出来,若语句只有“单句”时,块标记可以省略。

Always块

对于简单的场景,组合型always块等价于assign语句。

1
2
assign out1 = a & b | c ^ d;
always @(*) out2 = a & b | c ^ d;

assign语句左边符号对应的类型是net(例如:wire),而过程赋值对应的类型是variable(例如:reg)。两者的差异不会对综合的电路产生任何影响,只是一种不成文的约定。

在VHDL中有三种赋值类型:

  • 持续赋值assign x = y;
  • 过程阻塞赋值,在过程中使用x = y;
  • 过程非阻塞赋值,在过程中使用x <= y;

在组合alsways块中,使用阻塞赋值。在时序always块中,使用非阻塞赋值。

使用三种方式,用异或门构建电路。值得注意的是,第三种方式产生的结果会因为有触发器而产生延时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// synthesis verilog_input_version verilog_2001
module top_module(
input clk,
input a,
input b,
output wire out_assign,
output reg out_always_comb,
output reg out_always_ff );

assign out_assign = a^b;
always @(*) out_always_comb = a^b;
always @(posedge clk) out_always_ff <= a^b;
endmodule

if语句

描绘一个2-1选择器

对应的值表

sel_b1sel_b2out_assign out_always
00a
01a
10a
11b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// synthesis verilog_input_version verilog_2001
module top_module(
input a,
input b,
input sel_b1,
input sel_b2,
output wire out_assign,
output reg out_always );

assign out_assign = sel_b1&sel_b2 ? b : a;

always @(*) begin
if (sel_b1&sel_b2) begin
out_always = b;
end
else begin
out_always = a;
end
end
endmodule

if语句锁存

修复下图错误,当cpu_overheated时,产生关闭信号(shut_off_computer设置为1)。

修改前,错误的示意图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// synthesis verilog_input_version verilog_2001
module top_module (
input cpu_overheated,
output reg shut_off_computer,
input arrived,
input gas_tank_empty,
output reg keep_driving ); //

always @(*) begin
if (cpu_overheated)
shut_off_computer = 1;
end

always @(*) begin
if (~arrived)
keep_driving = ~gas_tank_empty;
end

endmodule

修改后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// synthesis verilog_input_version verilog_2001
module top_module (
input cpu_overheated,
output reg shut_off_computer,
input arrived,
input gas_tank_empty,
output reg keep_driving ); //

always @(*) begin
if (cpu_overheated)
shut_off_computer = 1;
else
shut_off_computer = 0;
end

always @(*) begin
if (~arrived)
keep_driving = ~gas_tank_empty;
else
keep_driving = 0;
end

endmodule

case语句

基本语法

1
2
3
4
5
6
case (cond)
2'h0: statments0;
2'h1: statments1;
...
default: statmentsx;
endcase

建立一个6-1选择器,否则输出0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// synthesis verilog_input_version verilog_2001
module top_module (
input [2:0] sel,
input [3:0] data0,
input [3:0] data1,
input [3:0] data2,
input [3:0] data3,
input [3:0] data4,
input [3:0] data5,
output reg [3:0] out );//

always@(*) begin // This is a combinational circuit
case(sel)
4'h0: out = data0;
4'h1: out = data1;
4'h2: out = data2;
4'h3: out = data3;
4'h4: out = data4;
4'h5: out = data5;
default: out = 0;
endcase
end

endmodule

构建一个4bits的优先编码器,输出第一个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
module top_module (
input [3:0] in,
output reg [1:0] pos
);

always @(*) begin // Combinational always block
case (in)
4'h0: pos = 2'h0; // I like hexadecimal because it saves typing.
4'h1: pos = 2'h0;
4'h2: pos = 2'h1;
4'h3: pos = 2'h0;
4'h4: pos = 2'h2;
4'h5: pos = 2'h0;
4'h6: pos = 2'h1;
4'h7: pos = 2'h0;
4'h8: pos = 2'h3;
4'h9: pos = 2'h0;
4'ha: pos = 2'h1;
4'hb: pos = 2'h0;
4'hc: pos = 2'h2;
4'hd: pos = 2'h0;
4'he: pos = 2'h1;
4'hf: pos = 2'h0;
default: pos = 2'b0; // Default case is not strictly necessary because all 16 combinations are covered.
endcase
end

// There is an easier way to code this. See the next problem (always_casez).

endmodule

casez

当部分比特位不关心时,可以使用casez。符号z?在一下场景都是等价的。与casez类似的是casex,后者使用符号x

1
2
3
4
5
6
7
8
9
always @(*) begin
casez (in[3:0])
4'bzzz1: out = 0; // in[3:1] can be anything
4'bzz1z: out = 1;
4'b?1??: out = 2;
4'b1???: out = 3;
default: out = 0;
endcase
end

构建8bits优先编码器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// synthesis verilog_input_version verilog_2001
module top_module (
input [7:0] in,
output reg [2:0] pos );

always @(*) begin
casez (in[7:0])
8'h0: pos = 0;
8'bzzzz_???1: pos = 0;
8'bzzzz_zz10: pos = 1;
8'bzzzz_z100: pos = 2;
8'bzzzz_1000: pos = 3;
8'bzzz1_0000: pos = 4;
8'bzz10_0000: pos = 5;
8'bz100_0000: pos = 6;
8'b1000_0000: pos = 7;
default: pos = 3'bzzz;
endcase
end
endmodule

键盘扫描码

处理如下四个按键

Scancode [15:0]Arrow key
16'he06bleft arrow
16'he072down arrow
16'he074right arrow
16'he075up arrow
Anything elsenone
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// synthesis verilog_input_version verilog_2001
module top_module (
input [15:0] scancode,
output reg left,
output reg down,
output reg right,
output reg up );

always @(*) begin
left = 0; down = 0; right = 0; up = 0;
case (scancode)
16'he06b: left = 1;
16'he072: down = 1;
16'he074: right = 1;
16'he075: up = 1;
default: ;
endcase
end
endmodule

更多特性

条件

三元条件操作符?:

1
(condition ? if_true : if_false)

逻辑操作简化

若需要对向量所有位进行门操作时,正常书写比较冗余,可以对与、或和异或操作简化

1
2
3
& a[3:0]     // AND: a[3]&a[2]&a[1]&a[0]. Equivalent to (a[3:0] == 4'hf)
| b[3:0] // OR: b[3]|b[2]|b[1]|b[0]. Equivalent to (b[3:0] != 4'h0)
^ c[2:0] // XOR: c[2]^c[1]^c[0]

实现奇偶校验位算法

1
2
3
4
5
6
7
module top_module (
input [7:0] in,
output parity);

assign parity = ^in[7:0];
endmodule

100bits翻转

1
2
3
4
5
6
7
8
9
10
11
module top_module (
input [99:0] in,
output reg [99:0] out
);

always @(*) begin
for (int i=0;i<$bits(out);i++) // $bits() is a system function that returns the width of a signal.
out[i] = in[$bits(out)-i-1]; // $bits(out) is 100 because out is 100 bits wide.
end

endmodule

比特1计数

1
2
3
4
5
6
7
8
9
10
11
module top_module( 
input [254:0] in,
output [7:0] out );

always @(*) begin // Combinational always block
out = 0;
for (int i=0; i<$bits(in); i++)
out += in[i];
end
endmodule

100bits全加器

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
module top_module( 
input [99:0] a, b,
input cin,
output [99:0] cout,
output [99:0] sum );

genvar i;
generate
for (i=0; i<$bits(a); ++i) begin: adder_gen
adder_1bit u_adder (
.a(a[i]),
.b(b[i]),
.cin(i==0?cin:cout[i-1]),
.sum(sum[i]),
.cout(cout[i])
);
end
endgenerate

endmodule

module adder_1bit (
input a,b,
input cin,
output cout,
output sum
);
assign {cout,sum} = a+b+cin;
// assign sum = cin ? ~(a^b) : a^b;
// assign cout = cin ? a|b : a&b;
endmodule

100位BCD码全加器

已经定义1位BCD码全加器

1
2
3
4
5
6
module bcd_fadd (
input [3:0] a,
input [3:0] b,
input cin,
output cout,
output [3:0] sum );
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
module top_module( 
input [399:0] a, b,
input cin,
output cout,
output [399:0] sum );

wire [100:0] c;
assign cout = c[100];
assign c[0] = cin;

genvar i;
generate
for(i=0; i<100; ++i) begin: bcd_gen
bcd_fadd u_bcd (
.a( a[(4*i+3):(4*i)] ),
.b( b[(4*i+3):(4*i)] ),
.cin( c[i] ),
.cout( c[i+1] ),
.sum( sum[(4*i+3):(4*i)] )
);
end
endgenerate

endmodule

仿真

timescale

定义仿真中时间单位的比例。它的作用是指定时序仿真中时间单位的大小,以便仿真器可以正确地模拟设计中的时序行为。

基本语法

1
`timescale unit / precision

其中,unit是时间单位,可以是1ns1ps1us等,表示一个时钟周期的时间长度;precision是时间精度,表示仿真的最小时间单位;

#num

等待num个时间单位后,执行后面的语句。

基本语法

1
#num statement

task

Verilog语言中具有类似C语言函数的结构有task和function,他们可以增加代码可读性和重复使用性。Function用来描述组合逻辑,只能有一个返回值,function的内部不能包含时序控制。Task类似procedure,执行一段verilog代码,task中可以有任意数量的输入和输出,task也可以包含时序控制。

基本语法

1
2
task TASK_NAME;
endtask

PS/2键盘设备仿真

当用户按键或松开时,键盘以每帧11位的格式串行传送数据给主机,同时在PS2_CLK时钟信号上传输对应的时钟(一般为10.0–16.7kHz)。第一位是开始位(逻辑0),后面跟8位数据位(低位在前),一个奇偶校验位(奇校验)和一位停止位(逻辑1)。每位都在时钟的 下降沿 有效,下图显示了键盘传送一字节数据的时序。在下降沿有效的主要原因是下降沿正好在数据位的中间,因此可以让数据位从开始变化到接收采样时能有一段信号建立时间。

键盘输出数据时序图

键盘通过PS2_DAT引脚发送的信息称为扫描码,每个扫描码可以由单个数据帧或连续多个数据帧构成。当按键被按下时送出的扫描码被称为 通码(Make Code) ,当按键被释放时送出的扫描码称为 断码(Break Code) 。以 W 键为例, W 键的通码是1Dh,如果 W 键被按下,则PS2_DAT引脚将输出一帧数据,其中的8位数据位为1Dh,如果 W 键一直没有释放,则不断输出扫描码1Dh 1Dh … 1Dh,直到有其他键按下或者 W 键被放开。某按键的断码是F0h加此按键的通码,如释放 W 键时输出的断码为F0h 1Dh,分两帧传输。

多个键被同时按下时,将逐个输出扫描码,如:先按左 Shift 键(扫描码为12h)、再按 W 键、放开 W 键、再放开左 Shift 键,则此过程送出的全部扫描码为:12h 1Dh F0h 1Dh F0h 12h。

键盘扫描码

每个键都有唯一的通码和断码。键盘所有键的扫描码组成的集合称为扫描码集。共有三套标准的扫描码集,所有现代的键盘默认使用第二套扫描码。下图显示了键盘各键的扫描码(以十六进制表示),如Caps键的扫描码是58h。 下图可以看出,键盘上各按键的扫描码是随机排列的,如果想迅速的将键盘扫描码转换为ASCII码,一个最简单的方法就是利用查找表 LookUp Table, LUT ,扫描码到ASCII码的转换表格请读者自己生成。

键盘扫描码
扩展键盘和数字键盘的扫描码

键盘控制器

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
module ps2_keyboard(clk,clrn,ps2_clk,ps2_data,data,
ready,nextdata_n,overflow);
input clk,clrn,ps2_clk,ps2_data;
input nextdata_n;
output [7:0] data;
output reg ready;
output reg overflow; // fifo overflow
// internal signal, for test
reg [9:0] buffer; // ps2_data bits
reg [7:0] fifo[7:0]; // data fifo
reg [2:0] w_ptr,r_ptr; // fifo write and read pointers
reg [3:0] count; // count ps2_data bits
// detect falling edge of ps2_clk
reg [2:0] ps2_clk_sync;

always @(posedge clk) begin
ps2_clk_sync <= {ps2_clk_sync[1:0],ps2_clk};
end

wire sampling = ps2_clk_sync[2] & ~ps2_clk_sync[1];

always @(posedge clk) begin
if (clrn == 0) begin // reset
count <= 0; w_ptr <= 0; r_ptr <= 0; overflow <= 0; ready<= 0;
end
else begin
if ( ready ) begin // read to output next data
if(nextdata_n == 1'b0) //read next data
begin
r_ptr <= r_ptr + 3'b1;
if(w_ptr==(r_ptr+1'b1)) //empty
ready <= 1'b0;
end
end
if (sampling) begin
if (count == 4'd10) begin
if ((buffer[0] == 0) && // start bit
(ps2_data) && // stop bit
(^buffer[9:1])) begin // odd parity
fifo[w_ptr] <= buffer[8:1]; // kbd scan code
w_ptr <= w_ptr+3'b1;
ready <= 1'b1;
overflow <= overflow | (r_ptr == (w_ptr + 3'b1));
end
count <= 0; // for next
end else begin
buffer[count] <= ps2_data; // store ps2_data
count <= count + 3'b1;
end
end
end
end
assign data = fifo[r_ptr]; //always set output data

endmodule

键盘仿真模型

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
`timescale 1ns / 1ps
module ps2_keyboard_model(
output reg ps2_clk,
output reg ps2_data
);
parameter [31:0] kbd_clk_period = 60;
initial ps2_clk = 1'b1;

task kbd_sendcode;
input [7:0] code; // key to be sent
integer i;

reg[10:0] send_buffer;
begin
send_buffer[0] = 1'b0; // start bit
send_buffer[8:1] = code; // code
send_buffer[9] = ~(^code); // odd parity bit
send_buffer[10] = 1'b1; // stop bit
i = 0;
while( i < 11) begin
// set kbd_data
ps2_data = send_buffer[i];
#(kbd_clk_period/2) ps2_clk = 1'b0;
#(kbd_clk_period/2) ps2_clk = 1'b1;
i = i + 1;
end
end
endtask

endmodule

键盘测试代码

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
`timescale 1ns / 1ps
module keyboard_sim;

/* parameter */
parameter [31:0] clock_period = 10;

/* ps2_keyboard interface signals */
reg clk,clrn;
wire [7:0] data;
wire ready,overflow;
wire kbd_clk, kbd_data;
reg nextdata_n;

ps2_keyboard_model model(
.ps2_clk(kbd_clk),
.ps2_data(kbd_data)
);

ps2_keyboard inst(
.clk(clk),
.clrn(clrn),
.ps2_clk(kbd_clk),
.ps2_data(kbd_data),
.data(data),
.ready(ready),
.nextdata_n(nextdata_n),
.overflow(overflow)
);

initial begin /* clock driver */
clk = 0;
forever
#(clock_period/2) clk = ~clk;
end

initial begin
clrn = 1'b0; #20;
clrn = 1'b1; #20;
model.kbd_sendcode(8'h1C); // press 'A'
#20 nextdata_n =1'b0; #20 nextdata_n =1'b1;//read data
model.kbd_sendcode(8'hF0); // break code
#20 nextdata_n =1'b0; #20 nextdata_n =1'b1; //read data
model.kbd_sendcode(8'h1C); // release 'A'
#20 nextdata_n =1'b0; #20 nextdata_n =1'b1; //read data
model.kbd_sendcode(8'h1B); // press 'S'
#20 model.kbd_sendcode(8'h1B); // keep pressing 'S'
#20 model.kbd_sendcode(8'h1B); // keep pressing 'S'
model.kbd_sendcode(8'hF0); // break code
model.kbd_sendcode(8'h1B); // release 'S'
#20;
$stop;
end

endmodule

电路

组合逻辑

基本门

GND

1
assign out = 1'b0;

NOR

1
assign out = ~(in1|in2);

多路选择器

9路选择器

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
module top_module( 
input [15:0] a, b, c, d, e, f, g, h, i,
input [3:0] sel,
output [15:0] out );

// Case statements can only be used inside procedural blocks (always block)
// This is a combinational circuit, so use a combinational always @(*) block.
always @(*) begin
out = '1; // '1 is a special literal syntax for a number with all bits set to 1.
// '0, 'x, and 'z are also valid.
// I prefer to assign a default value to 'out' instead of using a
// default case.
case (sel)
4'h0: out = a;
4'h1: out = b;
4'h2: out = c;
4'h3: out = d;
4'h4: out = e;
4'h5: out = f;
4'h6: out = g;
4'h7: out = h;
4'h8: out = i;
endcase
end
endmodule

4位256路选择器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module top_module (
input [1023:0] in,
input [7:0] sel,
output [3:0] out
);

// We can't part-select multiple bits without an error, but we can select one bit at a time,
// four times, then concatenate them together.
assign out = {in[sel*4+3], in[sel*4+2], in[sel*4+1], in[sel*4+0]};

// Alternatively, "indexed vector part select" works better, but has an unfamiliar syntax:
// assign out = in[sel*4 +: 4]; // Select starting at index "sel*4", then select a total width of 4 bits with increasing (+:) index number.
// assign out = in[sel*4+3 -: 4]; // Select starting at index "sel*4+3", then select a total width of 4 bits with decreasing (-:) index number.
// Note: The width (4 in this case) must be constant.

endmodule

使用示例

南京大学电路教学项目-NVboard,可以图形化显示过程状态和数据,同时也支持verilator。

双控开关

Makefile

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
VERILATOR_FLAGS := --build -j 0 -cc --exe -x-assign fast -Wall

C_SRC := $(shell ls csrc/*.cpp)
V_SRC := $(shell ls vsrc/*.v)
OBJ_DIR := obj_dir
TARGET := obj_dir/Vtop
#TARGET_ARGS := +trace

.PHONY: run clean

ifdef TRACE
VERILATOR_FLAGS += --trace
endif

all: ${TARGET}

run: ${TARGET}
@rm -rf logs
@${TARGET} ${TARGET_ARGS}

${TARGET}: ${V_SRC} ${C_SRC}
#@verilator --build -j 0 -cc --exe -x-assign fast -Wall --trace $^
@verilator ${VERILATOR_FLAGS} $^

sim:
$(call git_commit, "sim RTL") # DO NOT REMOVE THIS LINE!!!
@echo "Write this Makefile by your self."

include ../Makefile

clean:
@rm -rf logs
@rm -rf ${OBJ_DIR}

verilog代码文件top.v

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module top
(
input a,
input b,
output f
);

assign f = a ^ b;

initial begin
$display("[%0t] Model running...\n", $time);
end

endmodule

主体仿真文件sim_main.cpp

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
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdint.h>
#include <memory>
#include "Vtop.h"
#include "verilated.h"

#if VM_TRACE_VCD
#include "verilated_vcd_c.h"
#endif

int main(int argc, char** argv)
{
const std::unique_ptr<VerilatedContext> contextp{new VerilatedContext};
contextp->debug(0); // Set debug level, 0 is off, 9 is highest
contextp->randReset(2); // Randomization reset policy
contextp->commandArgs(argc, argv); // Pass arguments so Verilated code can see them

// "TOP" will be hierarchical name of the module.
const std::unique_ptr<Vtop> top{new Vtop{contextp.get(), "TOP"}};

#if VM_TRACE_VCD
Verilated::mkdir("logs");
contextp->traceEverOn(true); // Verilator must compute traced signals
const std::unique_ptr<VerilatedVcdC>tfp{new VerilatedVcdC};
top->trace(tfp.get(), 99); // Trace 99 levels of hierarchy (or see below)
tfp->open("logs/simu_top.vcd");
printf("Start trace ...\n");
#endif

uint64_t sim_time = 10000;
while(contextp->time()<sim_time && !contextp->gotFinish())
{
contextp->timeInc(1);

int a = rand() & 1;
int b = rand() & 1;
top->a = a;
top->b = b;
top->eval();
// printf("a = %d, b = %d, f = %d, time = %lu\n", a, b, top->f, contextp->time());
assert(top->f == (a ^ b));

#if VM_TRACE_VCD
tfp->dump(contextp->time());
#endif
}

top->final();

// Coverage analysis
#if VM_COVERAGE
Verilated::mkdir("logs");
contextp->converagep()->write("logs/coverage.dat");
#endif

// Final simulation summary
contextp->statsPrintSummary();

return 0;
}

Linux命令

常用命令

cd

ls

awk

sed

ps

df

fdisk

wc

find

grep/egrep

top

vi/vim

diff/vimdiff

man

查看内置帮助手册,不仅可以查看命令手册,还有系统调用、库函数、异常码、宏等等信息。

1
2
3
4
5
6
7
8
# 学习如何RTFM
$ man man
# 学习如何使用库函数
$ man 3 getopt
# 检索含有关键词xxx的命令
$ man -k xxx
$ man readline
$ man bash

xargs

strace

system call trace, 记录程序运行过程中的系统调用信息

netstat

ip

ifconfig

ssh

telnet

sort

history

!n再次执行编号n命令

!xxx再次执行以xxx开头的最近一条命令

yes

不断重复输出y

watch

在前台定时执行命令

1
2
3
4
5
6
$ watch -t -n 1 "echo -n '第六期一生一芯 | 周六 15:00~17:00 | '; \
date; echo '课程主页 https://ysyx.oscc.cc/docs/'"
第六期一生一芯 | 周六 15:00~17:00 | Thu Apr 4 16:58:59 CST 2024
课程主页 https://ysyx.oscc.cc/docs/


crontab

在后台定时执行命令,需要编写对应的配置文件

timeout

执行特定时间,返回超时或命令自身的返回值

  • 0:命令成功执行并完成。
  • 124:命令因超时而终止。
  • 其他:命令执行失败
1
$ timeout 6 mkdir -p /mnt/dev0

脚本编写

细节介绍参考man bash

基本语法

简介

脚本的本质就是将众多Linux小工具按照一定的逻辑顺序依次执行处理,脚本文件内容就是这些命令的集合。

程序运行时都会打开3个文件

  • 0号文件,标准输入
  • 1号文件,标准输出
  • 2号文件,标准错误输出

可以通过lsof -p <PID>查看进程打开的文件。

>重定向标准输出,>>追加方式重定向标准输出,2>&1将标准错误重定向到标准输出。

内置变量有

  • $0 $1 ... $n:脚本名称(有时会包含相对或绝对路径)、第1个参数……第n个参数
  • $?:最后一条命令或函数的返回值
  • $@:传递$0之外的所有参数,可以用数组保存其值:ALL_ARGS=("$@")
  • $#:传递给脚本或函数的参数个数

内置命令

shift

基本格式:shift [n]

将参数左移n位,默认值为1

变量用法

基本用法

使用示例

1
2
3
4
# 变量赋值
var="/root/project/abc"
# 变量使用
echo $var ${var}

变量展开

又名shell参数展开,基本格式

1
2
3
4
${变量##模式}
${变量#模式}
${变量%%模式}
${变量%模式}

模式支持的符号:

  • * 匹配任意字符串
  • ? 匹配任意单个字符
  • [...] 匹配字符集,比如:范围[0-9a-z],取反[!abc][^abc]
  • \ 匹配转义字符,比如:*

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ var="/root/project/abc"
# 从左开始,最小匹配模式删除匹配字符串
$ echo ${var#*/}
root/project/abc

# 从左开始,最大匹配模式删除匹配字符串
$ echo ${var##*/}
abc

# 从右开始,最小匹配模式删除匹配字符串
$ echo ${var%/*}
/root/project
# 从右开始,最大匹配模式删除匹配字符串
$ echo ${var%%/*}

交互界面

基本操作

  • Tab键自动补全
  • 上下方向键遍历历史命令

任务管理

  • Ctrl+Z最小化,或运行时命令末尾添加&(但用户退出后后台任务也会退出,可以使用nohup命令避免)
  • jobs任务栏
  • bg后台执行任务,将指定任务2切换到后台执行bg %2
  • fg前台执行任务,将指定任务2切换到前台执行fg %2

shell可以通过快捷键操作光标快速移动,更多快捷键可以阅读man readlineCommands for Moving部分

  • Ctrl+B光标前移一个字符
  • Ctrl+F光标后移一个字符
  • Ctrl+A光标移到首字符处
  • Ctrl+E光标移到首字符处

基本单元

shell都是基于文本进行处理,其中只有数字字符串在特定场景可以进行数学运算。

通配符

  • *任意长度的字符串
  • ?任意一个字符
  • [xxx]集合中任意一个字符

示例

1
2
$ echo Hello-{a,bb,ccc}-{1,2}!
Hello-a-1! Hello-a-2! Hello-bb-1! Hello-bb-2! Hello-ccc-1! Hello-ccc-2!

数组

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 创建一个数组
array=()
declare -a array

# 添加元素
array+=("item0")
array+=("item1")
array+=("item2")

# 遍历一个数组
for item in $array; do echo item is $item; done
for i in {1..${#array[@]}}; do item=${array[$i]}; echo item is $item; done
for item in ${array[@]}; do echo item is $item; done

# 删除指定元素
unset 'array[1]' # 删除item0,但是索引依然保留
# 重新创建一个新数组,通过过滤方式
new_array=()
for item in ${array[@]}; do
if [ $item != "item0" ]; then
new_array+=("$item")
fi
done

# 删除整个数组
unset array
array=() # 清空整个数组

判断逻辑

if

1
$ if mkdir -p /mnt/dev0; then echo ok; else echo error; fi

命令执行成功则打印ok,否则打印失败

case

循环逻辑

while

1
2
3
$ while [[ `seq 1 10 | shuf | head -n 1` != "1" ]]; do echo "retry"; done
retry
retry

for

捕获信号

trap 命令用于捕获和处理信号或错误。当特定信号发生时,trap 可以执行指定的命令。trap 可以捕获多种信号,包括系统信号和错误。

一些常见的信号量包括:

  • SIGINT (2): 中断信号,通常由 Ctrl+C 触发。
  • SIGTERM (15): 终止信号,通常由 kill 命令发送,用于请求进程终止。
  • SIGKILL (9): 强制终止信号,无法被捕获或忽略,通常用于强制停止进程。
  • SIGQUIT (3): 退出信号,通常由 Ctrl+ 触发,用于生成 core dump。
  • SIGSTOP (19): 停止信号,无法被捕获或忽略,通常用于挂起进程。
  • SIGCONT (18): 恢复信号,用于恢复一个被 SIGSTOP 停止的进程。
  • SIGHUP (1): 挂起信号,通常是终端关闭时发送的。
  • SIGUSR1SIGUSR2: 用户自定义信号,可以用来传递特定信息。
  • SIGSEGV (11): 段错误信号,通常在程序访问非法内存时触发。
  • SIGPIPE (13): 管道破裂信号,通常当向一个没有读取者的管道写数据时触发。

常见的错误有:

  • EXIT:捕获脚本或命令退出时的状态(返回码)。例如,你可以捕获脚本退出时的状态码,或者通过 trap 在脚本退出时执行清理工作。
  • ERR:捕获任何命令的非零退出状态(发生错误时触发)。通过 trap 'command' ERR,你可以在发生错误时执行指定的命令。
1
2
3
4
5
6
7
8
#!/bin/bash

exit_handler() {
# 具体处理逻辑
}

trap exit_handler SIGINT SIGTERM EXIT ERR

输入输出

经典示例

输入参数解析

1
2
3
4
5
6
输入的参数可以包含以下几种情况的任意组合,<xxx>表示参数后必须跟对应的值:
-n <num>
--number <num>
-v
--version
-h

case手动解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash

# 手动解析参数
while [ $# -gt 0 ]; do
case "$1" in
-n|--number)
# 确保后面有值
if [ -n "$2" ] && [[ "$2" != -* ]]; then
number="$2"
shift 2
else
echo "错误: -n 或 --number 需要一个参数"
exit 1
fi
;;
-v|--version) show_version=true; shift; ;;
-h|--help) show_help=true; shift; ;;
*)
echo "忽略非选项参数: $1"
shift
;;
esac
done

自制CPU主频监视器

1
$ watch -n 1 "cat /proc/cpuinfo | grep MHz | awk '{print \$1 NR \$3 \$4 \$2}'"

打包特定文件并上传到远端

1
$ find . -name "*.pdf" | xargs tar cj | ssh yzh@192.168.1.1 'cd ysyx; > pdf.tar.bz2'

统计工具类型分布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ echo $PATH | tr -t : '\n' | xargs -I{} find {} -maxdepth 1 -type f -executable | \
xargs file -b -e elf | sort | uniq -c | sort -nr

835 ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV)
129 Perl script text executable
113 POSIX shell script, ASCII text executable
34 Python script, ASCII text executable
24 ELF 64-bit LSB pie executable, x86-64, version 1 (GNU/Linux)
20 Bourne-Again shell script, ASCII text executable
15 setuid ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV)
12 setgid ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV)
10 ELF 64-bit LSB executable, x86-64, version 1 (SYSV)
9 POSIX shell script, Unicode text, UTF-8 text executable
3 ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux)
2 Python script, Unicode text, UTF-8 text executable
2 PHP script, ASCII text executable
1 setuid, setgid ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV)
1 Python script, ISO-8859 text executable
1 POSIX shell script, ASCII text executable, with very long lines (459)
1 PHP phar archive with SHA1 signature
1 Paul Falstad's zsh script, ASCII text executable
1 JavaScript source, ASCII text

基础知识

关键字

static

const

extern

volatile

register

auto

virtual

delete

default

override

final

explict

friend

内存分布

代码区(Text)

编译器编译源码后集中存放机器指令的区域,需要系统加载到指定内存地址(虚拟地址)才能够正常运行。

  • 存储内容:CPU 执行的机器指令。

  • 特点:只读且共享(如果有多个进程运行同一个程序,它们可以共享内存中的指令段)。

栈区(Stack)

栈由编译器自动分配和释放。它遵循“后进先出”(LIFO)的原则。x86一般向低地址增长。

  • 存储内容:局部变量、函数参数、返回地址。

  • 特点:速度极快,空间有限(通常只有几 MB)。

  • 生存期:随着函数调用的结束,栈帧会被弹出,内存自动回收。

堆区(Heap)

堆是由程序员手动控制的区域。在 C++ 中,我们通过 newmalloc 申请,通过 deletefree 释放。x86一般向高地址增长。

  • 存储内容:动态分配的对象或数组。
  • 特点:空间大(受限于虚拟内存),速度比栈慢,容易产生内存碎片。
  • 生存期:由程序员控制。如果你忘记释放,就会导致内存泄漏

常量区(Constant)

  • 存储内容:常量字符串(如 "Hello World")以及被 const 修饰的全局变量。

  • 特点:只读。尝试修改这部分内存会导致运行时的访问冲突。

全局/静态存储区(Global)

这一块区域在程序启动时分配,程序结束时释放。

  • .data 段:存储已初始化的全局变量和静态变量。

  • .bss 段:存储未初始化(或初始化为 0)的全局变量和静态变量。

    小知识:BSS 段不占用可执行文件实际的大小,只记录所需的空间,在加载时由 OS 清零。

示例代码

Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
TARGET ?= main
RUN_ARGS ?=

all: $(TARGET)

$(TARGET): main.cpp
@g++ -g -O0 -o $@ $<
run: all
@./$(TARGET) $(RUN_ARGS)
clean:
@rm -f $(TARGET)

.PHONY: all

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

using namespace std;

const char *str1 = "Hello world1";
const char *str2 = "Hello world2";

int main(int argc, const char *argv[]) {
cout << "str1 " << &str1 << " " << str1 << endl;
cout << "str2 " << &str2 << " " << str2 << endl;
int v1;
cout << "v1 " << &v1 << endl;
int v2;
cout << "v2 " << &v2 << endl;
auto p1 = new int();
cout << "p1 " << p1 << endl;
auto p2 = new int();
cout << "p2 " << p2 << endl;
return 0;
}

输出信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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
$ make run  
str1 0x563ce0a0d010 Hello world1
str2 0x563ce0a0d018 Hello world2
v1 0x7ffe5fff6530
v2 0x7ffe5fff6534
p1 0x563cfced32c0
p2 0x563cfced32e0

$ readelf -S main
There are 37 section headers, starting at offset 0x7b20:

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000000318 00000318
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.pr[...] NOTE 0000000000000338 00000338
0000000000000030 0000000000000000 A 0 0 8
[ 3] .note.gnu.bu[...] NOTE 0000000000000368 00000368
0000000000000024 0000000000000000 A 0 0 4
[ 4] .note.ABI-tag NOTE 000000000000038c 0000038c
0000000000000020 0000000000000000 A 0 0 4
[ 5] .gnu.hash GNU_HASH 00000000000003b0 000003b0
0000000000000028 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 00000000000003d8 000003d8
0000000000000180 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000000558 00000558
000000000000019b 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 00000000000006f4 000006f4
0000000000000020 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 0000000000000718 00000718
0000000000000060 0000000000000000 A 7 2 8
[10] .rela.dyn RELA 0000000000000778 00000778
0000000000000150 0000000000000018 A 6 0 8
[11] .rela.plt RELA 00000000000008c8 000008c8
00000000000000a8 0000000000000018 AI 6 24 8
[12] .init PROGBITS 0000000000001000 00001000
000000000000001b 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 0000000000001020 00001020
0000000000000080 0000000000000010 AX 0 0 16
[14] .plt.got PROGBITS 00000000000010a0 000010a0
0000000000000010 0000000000000010 AX 0 0 16
[15] .plt.sec PROGBITS 00000000000010b0 000010b0
0000000000000070 0000000000000010 AX 0 0 16
[16] .text PROGBITS 0000000000001120 00001120
0000000000000385 0000000000000000 AX 0 0 16
[17] .fini PROGBITS 00000000000014a8 000014a8
000000000000000d 0000000000000000 AX 0 0 4
[18] .rodata PROGBITS 0000000000002000 00002000
000000000000003c 0000000000000000 A 0 0 4
[19] .eh_frame_hdr PROGBITS 000000000000203c 0000203c
0000000000000044 0000000000000000 A 0 0 4
[20] .eh_frame PROGBITS 0000000000002080 00002080
00000000000000ec 0000000000000000 A 0 0 8
[21] .init_array INIT_ARRAY 0000000000003d60 00002d60
0000000000000010 0000000000000008 WA 0 0 8
[22] .fini_array FINI_ARRAY 0000000000003d70 00002d70
0000000000000008 0000000000000008 WA 0 0 8
[23] .dynamic DYNAMIC 0000000000003d78 00002d78
0000000000000200 0000000000000010 WA 7 0 8
[24] .got PROGBITS 0000000000003f78 00002f78
0000000000000088 0000000000000008 WA 0 0 8
[25] .data PROGBITS 0000000000004000 00003000
0000000000000020 0000000000000000 WA 0 0 8
[26] .bss NOBITS 0000000000004040 00003020
0000000000000118 0000000000000000 WA 0 0 64
[27] .comment PROGBITS 0000000000000000 00003020
000000000000002d 0000000000000001 MS 0 0 1
[28] .debug_aranges PROGBITS 0000000000000000 0000304d
0000000000000030 0000000000000000 0 0 1
[29] .debug_info PROGBITS 0000000000000000 0000307d
00000000000024b3 0000000000000000 0 0 1
[30] .debug_abbrev PROGBITS 0000000000000000 00005530
00000000000005d0 0000000000000000 0 0 1
[31] .debug_line PROGBITS 0000000000000000 00005b00
00000000000001ab 0000000000000000 0 0 1
[32] .debug_str PROGBITS 0000000000000000 00005cab
0000000000001242 0000000000000001 MS 0 0 1
[33] .debug_line_str PROGBITS 0000000000000000 00006eed
0000000000000289 0000000000000001 MS 0 0 1
[34] .symtab SYMTAB 0000000000000000 00007178
00000000000004b0 0000000000000018 35 21 8
[35] .strtab STRTAB 0000000000000000 00007628
0000000000000387 0000000000000000 0 0 1
[36] .shstrtab STRTAB 0000000000000000 000079af
000000000000016a 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), l (large), p (processor specific)

指针与引用

右值引用

struct与class

struct内存分布

class内存分布

sizeof与strlen

对象的三大特性

封装

继承

多态

虚函数实现动态多态的原理

虚函数与纯虚函数的区别

类的访问权限

private

protected

public

类的成员函数

构造函数

析构函数

赋值函数

拷贝函数

移动构造函数

拷贝构造函数

深拷贝

浅拷贝

类的运算符函数

成员函数

构造函数 析构函数 拷贝构造函数

移动构造函数 移动赋值运算符

运算符(= & + - * / ++ – += -= [] () << >> …)

静态成员函数 static

只读成员函数 const

虚函数 纯虚函数 virtual =0

不可调用函数 =delete

默认类函数 =default

覆写函数 防止覆写函数 override final

单参数构造函数显示转换 explict

友元函数 friend

枚举

类型转换

static_cast

dynamic_cast

const_cast

reinterpret_cast

静态与多态

重写

重载

模板

标准库

std

std::move函数

智能指针

auto_ptr

unique_ptr

shared_ptr

weak_ptr

STL容器

迭代器

迭代器原理

迭代器失效

vector

list

map

set

map与unordered_map

set与unordered_set

vector与list

容器空间配置器

编写技巧

预处理宏自动生成

字符串化

C语言宏定义可以有一些特殊用法:

  • #: 预处理阶段,将宏参数转化为字符串
  • ##: 预处理阶段,将两个标识符拼接成一个标识符

具体步骤:

  1. 将需要的枚举名放到固定的地方统一管理,使用特别的宏函数ENUM_OR_STRING封装,例如枚举文件signal_list.gen
1
2
3
4
5
// signal_list.gen
ENUM_OR_STRING(LED_OPEN), \
ENUM_OR_STRING(LED_CLOSE), \
ENUM_OR_STRING(MSG_TEST), \
ENUM_OR_STRING(MSG_BUTT) \
  1. 定义宏函数ENUM_OR_STRING,使用枚举文件声明枚举
1
2
3
4
5
6
7
8
9
10
11
// signal_id.h
/* 消息ID转枚举 */
#ifdef ENUM_OR_STRING
#undef ENUM_OR_STRING
#endif
#define ENUM_OR_STRING(x) x

typedef enum
{
#include "signal_list.gen"
} E_MSG_ID;
  1. 定义宏函数ENUM_OR_STRING,实现获取枚举字符串方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifdef ENUM_OR_STRING
#undef ENUM_OR_STRING
#endif
#define ENUM_OR_STRING(x) #x

const int MAX_LENGTH_MSG = 50;
const char msgIdString[][MAX_LENGTH_MSG] = {
#include "signal_list.gen"
};

const char *GetMsgName(int msgID)
{
return msgIdString[msgID];
}

常见问题

变量初始化顺序

1
2
3
4
5
6
7
8
9
10
11
[build] /root/project/prj3/xxxx2c/include/MODE2C/DEV/CtxUtility.h: In constructor ‘MODE2C::DEV::CtxComponent::CtxComponent(MODE2C::DEV::NodeArchitecture&, MODE2C::DEV::ASTBasePtr, MODE2C::DEV::NodeComponent&)’:
[build] /root/project/prj3/xxxx2c/include/MODE2C/DEV/CtxUtility.h:137:33: warning: ‘MODE2C::DEV::CtxComponent::mpnewInstance’ will be initialized after [-Wreorder]
[build] MODE2C::DEV::NodeComponent* mpnewInstance;
[build] ^~~~~~~~~~~~~
[build] /root/project/prj3/xxxx2c/include/MODE2C/DEV/CtxUtility.h:136:33: warning: ‘MODE2C::DEV::NodeComponent* MODE2C::DEV::CtxComponent::mpexistingInstance’ [-Wreorder]
[build] MODE2C::DEV::NodeComponent* mpexistingInstance;
[build] ^~~~~~~~~~~~~~~~~~
[build] /root/project/prj3/xxxx2c/src/DEV/CtxUtility.cpp:49:1: warning: when initialized here [-Wreorder]
[build] CtxComponent::CtxComponent(MODE2C::DEV::NodeArchitecture& instanceRoot, ASTBasePtr component,
[build] ^~~~~~~~~~~~
[build] In file included from /root/project/prj3/xxxx2c/src/DEV/CtxUtility.cpp:3:0:

这几句告警说的是变量顺序问题,英文大意为:

成员变量mpnewInstance将会在mpexistingInstance之后初始化,当调用类构造函数CtxComponent::CtxComponent进行初始化时。

解决方案:调整相关变量初始化顺序

基本语法

缩进

<TAB>缩进符用于构建目标时需要执行的指令的前导符

<SPACE>空格符可以用于函数或判断排版,但不能用于构建目标的前导符

内置函数

wildcard

遍历符合条件的文件,支持通配符。查找NPC_HOME目录下所有cpp后缀的文件,并返回其列表

1
wildcard $(NPC_HOME)/**/*.cpp

error

错误打印,并退出构建

1
$(error NPC_HOME=$(NPC_HOME) is not a NPC repo)

patsubst

字符串处理函数,字符串替换。将目标字符串的替换,替换为目标格式的字符串。

基本格式:$(patsubst <模式>, <替换>, <文本>)

  • <模式>:指定需要匹配的字符串模式,可以包含通配符 %,表示零个或多个字符。
  • <替换>:指定用来替换匹配到的内容的模式,通配符 % 在替换中保留原始匹配内容。
  • <文本>:要进行匹配和替换的目标文本列表。

riscv32替换为'riscv64'

1
2
# CONFIG_ISA="riscv32"
$(patsubst "%32",'%64',$(CONFIG_ISA))

option

直接使用for option将会循环遍历所有参数,配合case完成解析。示例参考nginx项目nginx-1.22.1/auto/options如下:

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
for option; do
opt="$opt `echo $option | sed -e \"s/\(--[^=]*=\)\(.* .*\)/\1'\2'/\"`"

case "$option" in
-*=*) value=`echo "$option" | sed -e 's/[-_a-zA-Z0-9]*=//'` ;;
*) value="" ;;
esac

case "$option" in
--help) help=yes ;;

--prefix=) NGX_PREFIX="!" ;;
--prefix=*) NGX_PREFIX="$value" ;;
--sbin-path=*) NGX_SBIN_PATH="$value" ;;
# ...
--test-build-devpoll) NGX_TEST_BUILD_DEVPOLL=YES ;;
--test-build-eventport) NGX_TEST_BUILD_EVENTPORT=YES ;;
--test-build-epoll) NGX_TEST_BUILD_EPOLL=YES ;;

*)
echo "$0: error: invalid option \"$option\""
exit 1
;;
esac
done

case

直接使用循环,配合$1shift逐一进行解析。

函数

声明自定义函数

  • 使用变量进行声明
  • 使用define关键字定义

调用自定义函数:使用 $(call 函数名, 参数1, 参数2, ...)

  • 自定义函数的参数用 $1, $2, $3, … 表示。
  • $(call ...) 是用于调用自定义函数的 Makefile 内置函数。

示例1:使用变量将内置函数定义为自定义函数

1
2
3
4
5
CONFIG_ISA="riscv32"
# 变量自定义函数
remove_quote = $(patsubst "%32",%64,$(1))
# 调用自定义函数
GUEST_ISA ?= $(call remove_quote,$(CONFIG_ISA))

示例2:使用define定义多参数处理

注意:函数定义中需要使用空格符缩进

1
2
3
4
5
6
7
8
9
define add_prefix_suffix
$(addprefix $1, $(addsuffix $2, $3))
endef

FILES = main utils lib
RESULT = $(call add_prefix_suffix, src/, .c, $(FILES))

all:
echo $(RESULT)

项目实例

嵌入式处理器r5

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
ifneq ($(V),1)
Q := @
else
Q :=
endif

################################以下项目需用户根据需要更改##########################
TARGET := rtos

# 定义路径
TOP_DIR = .
BUILD_DIR := out
OBJ_DIR := $(TOP_DIR)/$(BUILD_DIR)/OBJ
OUT_DIR := $(TOP_DIR)/$(BUILD_DIR)
BUILD := ./out

#链接文件名称和所在路径
LDSCRIPT := ./cortex_r5.ld
LDSCRIPT += -Wl,-Map,$(BUILD)/$(TARGET).map

# libraries
LIBS +=
LIBDIR += $(TOP_DIR)/lib

#启动文件名称和所在路径
START_FILE_SOURCES += $(TOP_DIR)/boot/FreeRTOS_asm.S
START_FILE_SOURCES += $(TOP_DIR)/boot/boot.S
START_FILE_SOURCES += $(TOP_DIR)/os/freertos/portable/portASM.S

# 头文件路径
C_INCLUDES += -D SAFETY_GMAC
C_INCLUDES += -I $(TOP_DIR)/include
C_INCLUDES += -I $(TOP_DIR)/os/freertos/include
C_INCLUDES += -I $(TOP_DIR)/os/freertos/portable

# APP源文件路径
C_SOURCES :=
# C_SOURCES += $(subst ./,,$(shell find ./ -type f -iname "*.c"))
C_SOURCES += $(TOP_DIR)/app/entry/main.c
C_SOURCES += $(TOP_DIR)/app/base/gic_basic_api.c
C_SOURCES += $(TOP_DIR)/app/base/Uart_PBcfg.c
C_SOURCES += $(TOP_DIR)/app/base/isr_cfg.c
C_SOURCES += $(TOP_DIR)/app/base/Uart.c
C_SOURCES += $(TOP_DIR)/app/base/FreeRTOS_tick_config.c
C_SOURCES += $(TOP_DIR)/app/base/Os_Cfg.c
C_SOURCES += $(TOP_DIR)/app/base/libc_syscall.c
C_SOURCES += $(TOP_DIR)/app/base/Port_Port_Ci.c
C_SOURCES += $(TOP_DIR)/app/base/Port_Cfg.c
C_SOURCES += $(TOP_DIR)/app/base/Port.c
C_SOURCES += $(TOP_DIR)/app/base/Port_PBcfg.c
C_SOURCES += $(TOP_DIR)/app/base/Mcu.c
C_SOURCES += $(TOP_DIR)/app/base/utility_lite.c
C_SOURCES += $(TOP_DIR)/app/base/BswInit.c

C_SOURCES += $(TOP_DIR)/os/freertos/portable/portable_port.c
C_SOURCES += $(TOP_DIR)/os/freertos/portable/MemMang/heap_4.c
C_SOURCES += $(TOP_DIR)/os/freertos/src/queue.c
C_SOURCES += $(TOP_DIR)/os/freertos/src/stream_buffer.c
C_SOURCES += $(TOP_DIR)/os/freertos/src/timers.c
C_SOURCES += $(TOP_DIR)/os/freertos/src/croutine.c
C_SOURCES += $(TOP_DIR)/os/freertos/src/tasks.c
C_SOURCES += $(TOP_DIR)/os/freertos/src/event_groups.c
C_SOURCES += $(TOP_DIR)/os/freertos/src/list.c

# 定义编译标志
GCCFLAGS += -D__SINGLE_DISPLAY_DC__=2
GCCFLAGS += -D__DPTX_CONNECTOR__
GCCFLAGS += -mcpu=cortex-r5 -marm -mfloat-abi=hard -mfpu=vfpv3-d16 -O1 -fmessage-length=0 -fsigned-char -ffunction-sections -fdata-sections -g3
ASFLAGS = $(GCCFLAGS) -x assembler-with-cpp
CCFLAGS = $(GCCFLAGS) $(C_INCLUDES)
LDFLAGS = $(GCCFLAGS) -T$(LDSCRIPT) -L$(LIBDIR) $(LIBS) -nostartfiles -Xlinker --gc-sections --specs=nano.specs -u _printf_float --specs=nosys.specs

# 支持双系统编译,故需选当前系统,0为linux,1为windows
SYS := 1

# 若指定了windows系统,则需确认编译器的路径,若安装时以默认路径安装,则正确
ifeq ($(SYS), 1)
GCC_PATH = "C:/eclipse/Arm_Embedded_Toolchain/arm-none-eabi/bin"
JLINK_PATH = "C:/Program Files (x86)/SEGGER/JLink"
endif

###################################用户修改结束###################################
# 编译器定义
PREFIX = arm-none-eabi-
ifdef GCC_PATH
SUFFIX = .exe
GDB := arm-none-eabi-gdb
SZ := arm-none-eabi-size
CC := arm-none-eabi-gcc
AS := arm-none-eabi-as
LD := arm-none-eabi-ld
AR := arm-none-eabi-ar
OBJCOPY := arm-none-eabi-objcopy
else
CC := $(PREFIX)gcc
SZ := $(PREFIX)size
OBJCOPY := $(PREFIX)objcopy
GDB := $(PREFIX)gdb
endif
BIN := $(OBJCOPY) -O binary -S
HEX := $(OBJCOPY) -O ihex

# Jlink定义,用于支持一键下载和gdb仿真
ifdef JLINK_PATH
SUFFIX = .exe
JLINKEXE := $(JLINK_PATH)/JLink$(SUFFIX)
JLINKGDBSERVER := $(JLINK_PATH)/JLinkGDBServer$(SUFFIX)
else
JLINKEXE := JLinkExe
JLINKGDBSERVER := JLinkGDBServer
endif

#################### CFLAGS config Start ##########################
#搜索所有的h文件,并输出携带-I的.h文件路径
#C_INCLUDES := $(addprefix -I,$(subst ./,,$(sort $(dir $(shell find ./ -type f -iname "*.h")))))

#################### CFLAGS config End ##########################
#链接指令集-specs=nosys.specs
#是否开启优化掉未使用的函数和符号

#制作启动文件依赖Obj,输出去掉路径的.o文件,可兼容.s和.S
START_FILE_OBJ = $(addsuffix .o, $(basename $(notdir $(START_FILE_SOURCES))))
OBJECTS = $(addprefix $(BUILD)/obj/, $(START_FILE_OBJ))
#搜索所有的c文件,制作所有的.c文件依赖Obj
#C_SOURCES = $(subst ./,,$(shell find ./ -type f -iname "*.c"))

OBJECTS += $(addprefix $(BUILD)/obj/, $(notdir $(C_SOURCES:%.c=%.o)))
#PS:去掉终极目标的原始路径前缀并添加输出文件夹路径前缀(改变了依赖文件的路径前缀,需要重新指定搜索路径)
#指定makefile搜索文件的路径(假如终极目标的依赖文件不携带.c文件所在的路径,
#且不指定搜索路径,makefile会报错没有规则制定目标)
vpath %.c $(sort $(dir $(C_SOURCES))) #取出路径并去重和排序(以首字母为单位)
vpath %.s $(dir $(START_FILE_SOURCES))
vpath %.S $(dir $(START_FILE_SOURCES))

#指定为伪目标跳过隐含规则搜索,提升makefile的性能,并防止make时携带的参数与实际文件重名的问题
.PHONY:all clean printf JLinkGDBServer debug download reset commit
all : $(BUILD)/$(TARGET).elf $(BUILD)/$(TARGET).bin $(BUILD)/$(TARGET).hex

#编译启动文件 备用参数:#-x assembler-with-cpp
$(BUILD)/obj/%.o : %.S Makefile | $(BUILD)/obj
$(Q)echo "$(CC) $(ASFLAGS) -c $< -o $@"
$(Q)$(CC) $(ASFLAGS) -c $< -o $@

#编译工程
$(BUILD)/obj/%.o : %.c Makefile | $(BUILD)/obj
$(Q)echo "$(CC) -c $(CCFLAGS) $(BUILD)/obj/$(notdir $(<:.c=.lst)) -c $< -o $@"
$(Q)$(CC) -c $(CCFLAGS) -Wa,-a,-ad,-alms=$(BUILD)/obj/$(notdir $(<:.c=.lst)) -c $< -o $@

#链接所有的.o生成.elf文件
$(BUILD)/$(TARGET).elf : $(OBJECTS) Makefile | $(BUILD)
$(Q)echo "$(CC) $(OBJECTS) $(LDFLAGS) -o $@"
$(Q)$(CC) $(OBJECTS) $(LDFLAGS) -o $@
$(Q)echo "make $@: "
$(Q)$(SZ) $@

$(BUILD)/%.hex: $(BUILD)/%.elf | $(BUILD)
$(HEX) $< $@

$(BUILD)/%.bin: $(BUILD)/%.elf | $(BUILD)
$(BIN) $< $@
# arm-none-eabi-objdump -D -S $(BUILD)/${TARGET}.elf > $(BUILD)/${TARGET}.dump

$(BUILD)/obj:
$(Q)mkdir -p $@
$(Q)echo "mkdir $@"

#用于检查链接脚本和启动文件是否存在,不存在则报错误
$(START_FILE_SOURCES):
$(Q)echo ERROR: The startup file does not exist or has the wrong path !;\
exit 1
$(LDSCRIPT):
$(Q)echo ERROR: The link file does not exist or has the wrong path !;\
exit 2

clean:
$(RM) -rf $(BUILD)

-include $(wildcard $(BUILD)/obj/*.d)

支持NFS启动的qemu

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
# QEMU		:= qemu-system-aarch64
QEMU := QEMU_AUDIO_DRV=none qemu-system-arm
Machine := -M vexpress-a9
# CPU := -cpu cortex-a9
CPU := -smp 1
Memory := -m 512M
Kernel := -kernel linux-6.1.73/arch/arm/boot/zImage
Dtb := -dtb linux-6.1.73/arch/arm/boot/dts/vexpress-v2p-ca9.dtb
SDCard := -sd images/sd_rootfs.ext4
# Network := -net nic -net user
Network := -nic user

# try to generate a unique GDB port
GDBPORT = $(shell expr `id -u` % 5000 + 25000)
# QEMU's gdb stub command line changed in 0.11
QEMUGDB = $(shell if $(QEMU) -help | grep -q '^-gdb'; \
then echo "-gdb tcp::$(GDBPORT)"; \
else echo "-s -p $(GDBPORT)"; fi)

all:
@echo "all done"

qemu-gdb:
@echo "*** Now run 'gdb' in another window." 1>&2
${QEMU} \
${Machine} \
${CPU} \
${Memory} \
${Kernel} \
${Dtb} \
${SDCard} \
${Network} \
-nographic \
-append "root=/dev/mmcblk0 rw console=ttyAMA0" -S -s

run:
@echo "host 10.0.2.2"
@echo "guest 10.0.2.15"
${QEMU} \
${Machine} \
${CPU} \
${Memory} \
${Kernel} \
${Dtb} \
${SDCard} \
${Network} \
-nographic \
-append "root=/dev/mmcblk0 rw console=ttyAMA0"

run-nfs:
@echo "host 10.0.2.2"
@echo "guest 10.0.2.15"
${QEMU} \
${Machine} \
${CPU} \
${Memory} \
${Kernel} \
${Dtb} \
${Network} \
-nographic \
-append "console=ttyAMA0,115200 noinitrd init=/linuxrc loglevel=8 nfsroot=10.0.2.2:/home/johnny/big-proj/os-dev/arm_rootfs/rootfs_v6.x,v3,nolock rw ip=10.0.2.15:10.0.2.2:10.0.2.1:255.255.255.0::eth0:off rootwait"

gui:
@echo "host 10.0.2.2"
@echo "guest 10.0.2.15"
${QEMU} \
${Machine} \
${CPU} \
${Memory} \
${Kernel} \
${Dtb} \
${SDCard} \
${Network} \
-display sdl -serial mon:stdio -append "root=/dev/mmcblk0 rw console=tty0"

# -net nic -net user
# -netdev user,id=network0 -device e1000,netdev=network0,mac=52:54:00:12:34:56
# qemu-system-arm -M vexpress-a9 \
-cpu cortex-a9 -m 256 -kernel ~/share/linux-4.4.157/object/arch/arm/boot/zImage -dtb ~/share/linux-4.4.157/object/arch/arm/boot/dts/vexpress-v2p-ca9.dtb -append "root=/dev/mmcblk0 rw console=tty0" -sd rootfs.ext3

附录

参考文章

跟我一起写Makefile

常用技巧

常用关键词

cmake_minimum_required

1
cmake_minimum_required(VERSION 3.15)

指定最小CMake版本不能低于3.15

project

1
project(emodem C ASM)

指定工程名称,并指定所有源文件可能使用的语言为C或ASM(汇编)。

message

1
message(STATUS "cross compile cortex-r5")

输出消息。

  • STATUS为普通状态消息
  • FATAL_ERROR为致命异常消息,输出消息后配置会立即终止

if

1
2
3
4
5
6
7
if(NOT FREERTOS_CONFIG_FILE_DIRECTORY)
message(FATAL_ERROR "xxx")
elseif(NOT EXISTS xxx.h)
message(FATAL_ERROR "xxx.h")
else
message(status "x")
endif()

判断条件是否满足,并进行相关逻辑处理。

set

1
set(CMAKE_ASM_FLAGS "${COMMON_FLAGS} -x assembler-with-cpp")

指定CMake宏对应的值,子工程会继承父工程的CMake宏。

add_subdirectory

1
add_subdirectory(FreeRTOS/FreeRTOS-Kernel)

增加子工程。

file

1
file(GLOB_RECURSE APP_SRC   "app/**/*.c")

文件处理,GLOB_RECURSE用于递归查找文件,并将结果保存到CMake宏APP_SRC中。

add_executable

1
2
3
add_executable(${MAIN_TARGET}
${APP_SRC}
)

指定可执行文件目标,并指定编译源码${APP_SRC}

add_library

1
2
3
4
add_library(freertos_kernel STATIC
croutine.c
$<IF:$<BOOL:$<FILTER:${FREERTOS_HEAP},EXCLUDE,^[1-5]$>>,${FREERTOS_HEAP},portable/MemMang/heap_${FREERTOS_HEAP}.c>
)

指定静态库目标,并指定编译源码croutine.c portable/MemMang/heap_xxx.c

include

1
include(${CMAKE_TOOLCHAIN_FILE})

包含头文件${CMAKE_TOOLCHAIN_FILE}

include_directories

1
2
3
4
5
include_directories(${MAIN_TARGET}
include
FreeRTOS/FreeRTOS-Kernel/include
FreeRTOS/FreeRTOS-Kernel/portable/GCC/ARM_COTEXT_R5
)

目标增加include文件搜索路径include FreeRTOS/FreeRTOS-Kernel/include FreeRTOS/FreeRTOS-Kernel/portable/GCC/ARM_COTEXT_R5

target_include_directories

1
2
3
4
5
6
target_include_directories(freertos_kernel
PUBLIC
include
portable/GCC/ARM_COTEXT_R5
${FREERTOS_CONFIG_FILE_DIRECTORY}
)

目标增加include文件搜索路径include FreeRTOS/FreeRTOS-Kernel/portable/GCC/ARM_COTEXT_R5 ${FREERTOS_CONFIG_FILE_DIRECTORY}

add_definitions

1
add_definitions(-DDEBUG -DUSE_LOG)

增加编译宏DEBUGUSE_LOG

1
2
3
target_link_directories(${MAIN_TARGET} PRIVATE
${PROJECT_BINARY_DIR}/lib
)

目标增加链接库搜索路径${PROJECT_BINARY_DIR}/lib

1
2
3
4
target_link_libraries(${MAIN_TARGET} PRIVATE
freertos_kernel_port
freertos_kernel
)

目标增加链接库freertos_kernel_port freertos_kernel

常用CMake宏

指定CMake宏使用set关键词进行设置。

名称说明
PROJECT_NAME工程名称
PROJECT_BINARY_DIR工程二进制主路径,即build构建主路径
PROJECT_SOURCE_DIR工程源文件主路径,即主CMakeList.txt文件所在路径
CMAKE_C_COMPILERC语言编译器路径
CMAKE_CXX_COMPILERC++编译器路径
CMAKE_ASM_COMPILER汇编编译器路径
CMAKE_C_FLAGSC语言编译参数
CMAKE_ASM_FLAGS汇编编译参数
CMAKE_EXE_LINKER_FLAGS可执行文件链接参数
LIBRARY_OUTPUT_PATH库输出路径
EXECUTABLE_OUTPUT_PATH可执行文件输出路径

项目实例

嵌入式处理器r5

工程中有三个文件构成,主CMakeLists负责产生elf文件和生成bin文件。FreeRTOS代码在os/freertos中,CMakeLists用于生成内核静态库libfreertos_kernel.aos/freertos/portable下,CMakeLists用于生成处理器相关静态库libfreertos_kernel_port.a

原版CMakeLists内容比较多,这里固定选择GCC_ARM_COTEXT_R5,已将冗余项裁剪,具体可以参考FreeRTOSv202210.01-LTS对应文件。

主CMakeLists

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
cmake_minimum_required(VERSION 3.15)

project(emodem C ASM)

message(STATUS "cross compile cortex-r5")

set(TOOLCHAIN_PATH /usr/local/gcc-arm-none-eabi-8-2018-q4-major)
set(CMAKE_C_COMPILER ${TOOLCHAIN_PATH}/bin/arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PATH}/bin/arm-none-eabi-gcc)
set(CMAKE_ASM_COMPILER ${TOOLCHAIN_PATH}/bin/arm-none-eabi-gcc)
set(COMMON_FLAGS "-mcpu=cortex-r5 -marm -mfloat-abi=hard -mfpu=vfpv3-d16 -O1 -fmessage-length=0 -fsigned-char -ffunction-sections -fdata-sections -g3")
set(CMAKE_C_FLAGS "-D__SINGLE_DISPLAY_DC__=2 -D__DPTX_CONNECTOR__ ${COMMON_FLAGS} -D SAFETY_GMAC")
set(CMAKE_ASM_FLAGS "-D__SINGLE_DISPLAY_DC__=2 -D__DPTX_CONNECTOR__ ${COMMON_FLAGS} -x assembler-with-cpp")
set(CMAKE_EXE_LINKER_FLAGS "-T ${PROJECT_SOURCE_DIR}/cortex_r5.ld -Wl,-Map,${PROJECT_NAME}.map -nostartfiles -Xlinker --gc-sections --specs=nano.specs -u _printf_float --specs=nosys.specs")

set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
# option(MYDEBUG "enable debug compilation" OFF)
# if (MYDEBUG)
# add_definitions(-DMYDEBUG)
# message(STATUS "Currently is in debug mode")
# else()
# message(STATUS "Currently is not in debug mode")
# endif()

set(FREERTOS_CONFIG_FILE_DIRECTORY ${PROJECT_SOURCE_DIR}/include CACHE STRING "Absolute path to the directory with FreeRTOSConfig.h")
add_subdirectory(os/freertos)

file(GLOB_RECURSE APP_SRC "app/**/*.c")
file(GLOB_RECURSE BOOT_SRC "boot/*.S")

# find_library(FREERTOS_LIB testFunc HINTS ${PROJECT_SOURCE_DIR}/testFunc/lib)

set(MAIN_TARGET ${PROJECT_NAME}_freertos.elf)
add_executable(${MAIN_TARGET}
${BOOT_SRC}
${APP_SRC}
)
include_directories(${MAIN_TARGET}
include
os/freertos/include
os/freertos/portable
)
target_link_directories(${MAIN_TARGET} PRIVATE
${PROJECT_BINARY_DIR}/lib
${PROJECT_SOURCE_DIR}/lib
)
target_link_libraries(${MAIN_TARGET} PRIVATE
freertos_kernel_port
freertos_kernel
# mcal
)
set(MAIN_BIN_TARGET ${PROJECT_NAME}_freertos.bin)
add_custom_command(TARGET ${MAIN_TARGET} POST_BUILD
COMMAND ${TOOLCHAIN_PATH}/bin/arm-none-eabi-objcopy -O binary -S ${EXECUTABLE_OUTPUT_PATH}/${MAIN_TARGET} ${EXECUTABLE_OUTPUT_PATH}/${MAIN_BIN_TARGET}
COMMENT "Generate ${EXECUTABLE_OUTPUT_PATH}/${MAIN_BIN_TARGET}"
)


# target_link_options(${MAIN_TARGET} PRIVATE
# -T${PROJECT_SOURCE_DIR}/cortex_r5.ld
# )

# # indicate lib path
# target_link_directories(${MAIN_TARGET}
# PRIVATE
# ${OUT_PATH}/lib
# )



# # on the basic of debug to set CFLAGS
# if (debug STREQUAL "yes")
# set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -O0 -ggdb3")
# else()
# set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -O2")
# endif()

# # on the basic of r5_LITTLE_FLASH to set CFLAGS
# if (r5_LITTLE_FLASH STREQUAL "TRUE")
# # set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DSAMPLE_620U_NAND")
# endif()

os/freertos的MakeLists

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
cmake_minimum_required(VERSION 3.15)

# User is responsible to set two mandatory options:
# FREERTOS_CONFIG_FILE_DIRECTORY
# FREERTOS_PORT
#
# User can choose which heap implementation to use (either the implementations
# included with FreeRTOS [1..5] or a custom implementation ) by providing the
# option FREERTOS_HEAP. If the option is not set, the cmake will default to
# using heap_4.c.

# Absolute path to FreeRTOS config file directory
# set(FREERTOS_CONFIG_FILE_DIRECTORY ${FREERTOS_CONFIG_FILE_PATH} CACHE STRING "Absolute path to the directory with FreeRTOSConfig.h")

if(NOT FREERTOS_CONFIG_FILE_DIRECTORY)
message(FATAL_ERROR " FreeRTOSConfig.h file directory not specified. Please specify absolute path to it from top-level CMake file:\n"
" set(FREERTOS_CONFIG_FILE_DIRECTORY <absolute path to FreeRTOSConfig.h directory> CACHE STRING \"\")\n"
" or from CMake command line option:\n"
" -DFREERTOS_CONFIG_FILE_DIRECTORY='/absolute_path/to/FreeRTOSConfig.h/directory'")
elseif(NOT EXISTS ${FREERTOS_CONFIG_FILE_DIRECTORY}/FreeRTOSConfig.h)
message(FATAL_ERROR " FreeRTOSConfig.h file not found in the directory specified (${FREERTOS_CONFIG_FILE_DIRECTORY})\n"
" Please specify absolute path to it from top-level CMake file:\n"
" set(FREERTOS_CONFIG_FILE_DIRECTORY <absolute path to FreeRTOSConfig.h directory> CACHE STRING \"\")\n"
" or from CMake command line option:\n"
" -DFREERTOS_CONFIG_FILE_DIRECTORY='/absolute_path/to/FreeRTOSConfig.h/directory'")
endif()

# Heap number or absolute path to custom heap implementation provided by user
set(FREERTOS_HEAP "4" CACHE STRING "FreeRTOS heap model number. 1 .. 5. Or absolute path to custom heap source file")

# FreeRTOS port option
set(FREERTOS_PORT "GCC_ARM_COTEXT_R5" CACHE STRING "FreeRTOS port name")

if(NOT FREERTOS_PORT)
message(FATAL_ERROR " FREERTOS_PORT is not set. Please specify it from top-level CMake file (example):\n"
" set(FREERTOS_PORT GCC_ARM_CM4F CACHE STRING \"\")\n"
" or from CMake command line option:\n"
" -DFREERTOS_PORT=GCC_ARM_CM4F\n"
" \n"
" Available port options:\n"
" GCC_ARM_COTEXT_R5 - Compiller: GCC Target: ARM Cortex-R5")
endif()

add_subdirectory(portable)

add_library(freertos_kernel STATIC
src/croutine.c
src/event_groups.c
src/list.c
src/queue.c
src/stream_buffer.c
src/tasks.c
src/timers.c

# If FREERTOS_HEAP is digit between 1 .. 5 - it is heap number, otherwise - it is path to custom heap source file
$<IF:$<BOOL:$<FILTER:${FREERTOS_HEAP},EXCLUDE,^[1-5]$>>,${FREERTOS_HEAP},portable/MemMang/heap_${FREERTOS_HEAP}.c>
)

target_include_directories(freertos_kernel
PUBLIC
include
portable/GCC/ARM_COTEXT_R5
${FREERTOS_CONFIG_FILE_DIRECTORY}
)

target_link_libraries(freertos_kernel freertos_kernel_port)

os/freertos/portable的CMakeLists

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
# FreeRTOS internal cmake file. Do not use it in user top-level project

add_library(freertos_kernel_port STATIC
# ARM-Cortext-R5 port for GCC
$<$<STREQUAL:${FREERTOS_PORT},GCC_ARM_COTEXT_R5>:
portable_port.c
portASM.S>

# 16-Bit DOS ports for BCC
$<$<STREQUAL:${FREERTOS_PORT},BCC_16BIT_DOS_FLSH186>:
BCC/16BitDOS/common/portcomn.c
BCC/16BitDOS/Flsh186/port.c>
)

if( FREERTOS_PORT MATCHES "GCC_ARM_CM(3|4)_MPU" OR
FREERTOS_PORT STREQUAL "IAR_ARM_CM4F_MPU" OR
FREERTOS_PORT STREQUAL "RVDS_ARM_CM4_MPU" OR
FREERTOS_PORT MATCHES "GCC_ARM_CM(23|33|55|85)_NTZ_NONSECURE" OR
FREERTOS_PORT MATCHES "GCC_ARM_CM(23|33|55|85)_NONSECURE" OR
FREERTOS_PORT MATCHES "GCC_ARM_CM(33|55|85)_TFM" OR
FREERTOS_PORT MATCHES "IAR_ARM_CM(23|33|55|85)_NTZ_NONSECURE" OR
FREERTOS_PORT MATCHES "IAR_ARM_CM(23|33|55|85)_NONSECURE"
)
target_sources(freertos_kernel_port PRIVATE Common/mpu_wrappers.c)
endif()

target_include_directories(freertos_kernel_port PUBLIC
# ARM-Cortext-R5 port for GCC
$<$<STREQUAL:${FREERTOS_PORT},GCC_ARM_COTEXT_R5>:${CMAKE_CURRENT_LIST_DIR}>

# 16-Bit DOS ports for BCC
$<$<STREQUAL:${FREERTOS_PORT},BCC_16BIT_DOS_FLSH186>:
${CMAKE_CURRENT_LIST_DIR}/BCC/16BitDOS/common
${CMAKE_CURRENT_LIST_DIR}/BCC/16BitDOS/Flsh186>

$<$<STREQUAL:${FREERTOS_PORT},BCC_16BIT_DOS_PC>:
${CMAKE_CURRENT_LIST_DIR}/BCC/16BitDOS/common
${CMAKE_CURRENT_LIST_DIR}/BCC/16BitDOS/PC>
)

target_link_libraries(freertos_kernel_port
PUBLIC
$<$<STREQUAL:${FREERTOS_PORT},GCC_RP2040>:pico_base_headers>
$<$<STREQUAL:${FREERTOS_PORT},GCC_XTENSA_ESP32>:idf::esp32>
PRIVATE
freertos_kernel
"$<$<STREQUAL:${FREERTOS_PORT},GCC_RP2040>:hardware_clocks;hardware_exception>"
$<$<STREQUAL:${FREERTOS_PORT},MSVC_MINGW>:winmm> # Windows library which implements timers
)

其他说明

基本结构

工程文件CMakeLists.txt

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
# 指定CMake工具的最小版本号
cmake_minimum_required(VERSION 3.10)

# 指定C++标准版本
set(CMAKE_CXX_STANDARD 17)

# 指定工程名称
# CXX可以不指定
project(TEST CXX)

# 判断主机平台,增加编译宏
if (CMAKE_HOST_SYSTEM_NAME MATCHES "Linux")
message("-- Detecting system: Linux")
add_definitions(-DBUILD_IN_LINUX)
else()
message("-- Detecting system: Windows")
endif()
# 判断平台
if(WIN32)
message(STATUS "This is windows")
elseif(UNIX)
message(STATUS "This is linux")
else()
message(STATUS "This is mac")
endif()

# 判断编译模式
if(CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Debug"))
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Wall -O0")
message(STATUS "Build mode: ${CMAKE_BUILD_TYPE} ${CMAKE_CXX_FLAGS_DEBUG}")
else()
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Wall")
message(STATUS "Build mode: ${CMAKE_BUILD_TYPE} ${CMAKE_CXX_FLAGS_RELEASE}")
endif()

# 查找curl包,CMake自带包管理
# 根据依赖添加
find_package(CURL)
if(CURL_FOUND)
include_directories(${CURL_INCLUDE_DIR})
else()
message(FATAL_ERROR "CURL library not found")
endif()
# 自定义LARCH包,REQUIRED必须具备此包
# 根据依赖添加
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_BINARY_DIR}/../cmod)
find_package(LARCH REQUIRED)
if(LARCH_FOUND)
message(STATUS "LARCH library found")
include_directories(${LARCH_INCLUDE_DIR})
else()
message(FATAL_ERROR "LARCH library not found")
endif()

# 配置各子组件,目录中需要包含CMakeLists.txt文件
add_subdirectory(show)
add_subdirectory(lib)

# 配置工程各类组件输出路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
set(LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib)
set(ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/liba)

# 设置环境变量方式
set(ENV{ENV_PRO_PATH} ${PROJECT_BINARY_DIR})

# 递归(GLOB_RECURSE)查找src/AST目录中cpp文件
file(GLOB_RECURSE AST_SOURCE "src/AST/*.cpp")

# 增加可执行文件编译目标
add_executable(testc "src/tools/Generator.cpp"
${AST_SOURCE}
)

# 增加可执行文件编译的头文件目录
target_include_directories(testc PUBLIC include # include
)

# 处理过程执行命令
# 增加命令执行,并将结果输出到变量中,由message打印出来
exec_program(ls ${PROJECT_BINARY_DIR}
OUTPUT_VARIABLE o1
ARGS -l -h
RETURN_VALUE r1
)
message("ls output:\n${o1} \n ls return: ${r1}")

message

基本格式

message( [STATUS|WARNING|AUTHOR_WARNING|SEND_ERROR|FATAL_ERROR] "Message to display" ...)

STATUS

状态消息,自动在消息前增加--以示区别

1
2
3
message(STATUS "Detecting system: Linux")

[cmake] -- Detecting system: Linux

WARFATAL_ERRORNING

告警消息,消息单独显示一行,CMake会继续执行

1
2
3
4
5
6
7
message(WARNING "Detecting system: Linux")

[cmake] CMake Warning at CMakeLists.txt:8 (message):
[cmake] Detecting system: Linux
[cmake]
[cmake]
[cmake] Not searching for unused variables given on the command line.

AUTHOR_WARNING

开发者告警消息,消息单独显示一行,CMake会继续执行

1
2
3
4
5
6
7
message(AUTHOR_WARNING "Detecting system: Linux")

[cmake] CMake Warning (dev) at CMakeLists.txt:8 (message):
[cmake] Detecting system: Linux
[cmake] This warning is for project developers. Use -Wno-dev to suppress it.
[cmake]
[cmake] Not searching for unused variables given on the command line.

SEND_ERROR

错误消息,消息单独显示一行,CMake会继续执行但会跳过生成步骤

1
2
3
4
5
6
7
8
9
message(SEND_ERROR "Detecting system: Linux")

[cmake] CMake Error at CMakeLists.txt:8 (message):
[cmake] Detecting system: Linux
[cmake]
[cmake]
[cmake] Not searching for unused variables given on the command line.
[cmake] -- Configuring incomplete, errors occurred!
[cmake] See also "/root/workspace/tcl3/build/CMakeFiles/CMakeOutput.log".

FATAL_ERROR

错误消息,消息单独显示一行,CMake会终止所有处理

1
2
3
4
5
6
7
8
9
message(FATAL_ERROR "Detecting system: Linux")

[cmake] CMake Error at CMakeLists.txt:8 (message):
[cmake] Detecting system: Linux
[cmake]
[cmake]
[cmake] Not searching for unused variables given on the command line.
[cmake] -- Configuring incomplete, errors occurred!
[cmake] See also "/root/workspace/tcl3/build/CMakeFiles/CMakeOutput.log".

自定义包

自定义LARCH

创建文件cmod/FindLARCH.cmake,文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# set(CMAKE_FIND_LIBRARY_PREFIXES lib)
# set(CMAKE_FIND_LIBRARY_SUFFIXES .so)

find_path(LARCH_INCLUDE_DIR larch.h /tmp/test/p1/include)
find_path(LARCH_LIBRARY liblarch.so /tmp/test/p1/lib)
# find_library(LARCH_LIBRARY NAMES larch PATH /tmp/test/p1/lib)

if(LARCH_INCLUDE_DIR AND LARCH_LIBRARY)
set(LARCH_FOUND TRUE)
endif()

if(LARCH_FOUND)
if(NOT LARCH_FIND_QUIETLY)
message(STATUS "Found LARCH: ${LARCH_LIBRARY}")
endif()
else()
if(LARCH_FIND_REQUIRED)
message(FATAL_ERROR "LARCH library not found")
endif()
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
cmake_minimum_required(VERSION 3.10)

# 配置库源文件
set(LIB_SRC larch.c)

# 配置版本号
set(API_VERSION 1.2)
set(CUR_VERSION 1.2.1)

# 编译动态库;VERSION为当前版本;SOVERSION为API版本,它会链接到当前版本,不带版本库也会链接到API版本;
add_library(larch SHARED ${LIB_SRC})
set_target_properties(larch PROPERTIES VERSION ${CUR_VERSION} SOVERSION ${API_VERSION})

# 编译静态库,目标不能同名,静态库需要用属性来修改
add_library(larch_static STATIC ${LIB_SRC})
set_target_properties(larch_static PROPERTIES OUTPUT_NAME "larch")

# 获取目标属性
get_target_property(OV larch_static OUTPUT_NAME)
# message(STATUS "This is larch_static name:"${OV})

# 配置共享库和头文件安装路径
install(TARGETS larch larch_static
RUNTIME DESTINATION /${PROJECT_NAME}/bin # 执行程序
LIBRARY DESTINATION /${PROJECT_NAME}/lib # 动态库
ARCHIVE DESTINATION /${PROJECT_NAME}/libstatic # 静态库
# PERMISSIONS OWNER_EXECUTE OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ # 配置权限
)
install(FILES larch.h DESTINATION /${PROJECT_NAME}/include)

可执行文件

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
cmake_minimum_required(VERSION 3.10)

# 配置源码和输出路径
# set(DESTDIR /usr/local)
# CMAKE_INSTALL_PREFIX:=/usr/local
# set(SRC_LIST main.c)
aux_source_directory(. SRC_LIST)

# 配置cmake构建打印
message(STATUS "Start build tr")

# 配置编译执行程序
add_executable(tr ${SRC_LIST})

# 配置头文件搜索路径
# 这里应该改为工程包含路径${LARCH_INCLUDE_DIR},见自定义LARCH包
find_path(header_path NAMES larch.h PATHS ../lib)
if(header_path)
include_directories(${header_path})
message(STATUS "find header path: " ${header_path})
endif(header_path)

# 配置动态库或静态库(liblarch.a)
target_link_libraries(tr larch)
add_dependencies(tr larch)

# 配置程序和库安装路径
install(TARGETS tr
RUNTIME DESTINATION /${PROJECT_NAME}/bin # 执行程序
LIBRARY DESTINATION /${PROJECT_NAME}/lib # 动态库
ARCHIVE DESTINATION /${PROJECT_NAME}/libstatic # 静态库
# PERMISSIONS OWNER_EXECUTE OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ # 配置权限
)

# 配置文档或目录安装路径
install(PROGRAMS ReadMe.md DESTINATION /${PROJECT_NAME}/docs
PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ # 配置权限
)
install(DIRECTORY scripts/ DESTINATION /${PROJECT_NAME}/scripts
PATTERN "scripts/*.sh"
PATTERN "scripts/*.conf" EXCLUDE
)

Xmodem

参考Xmodem 协议介绍及应用(基于 ESP-IDF)

数据长度有两种:128字节和1K字节。以下主要以windterm版本进行说明,可能在不同版本中有些许差异。

协议控制符

缩写数值说明
SOH0x01modem 128字节头标识
STX0x02modem 1024字节头标识
EOT0x04结束文件传输
ACK0x06正常响应
NAK0x15非正常响应
ETB0x17传输块结束
CAN0x18取消文件传输
CRC160x43使用CRC16校验标志

Xmodem-1K协议帧格式

windterm版本发送Frame基本格式:

Byte 0Byte 1Byte 2Byte 3~1026Byte 1027
头标识序号序号反码数据区数据总长度

各字段说明:

  • 头标识可以为:SOH和STX
  • 序号范围0~255;
  • 数据区不足部分需要使用0x1a补齐;

Xmodem-1K交互流程

SenderFlowReceiver
<==NAK
Timeout after 0.5 seconds
<==NAK
Frame-01==>Frame OK
<==ACK
Frame-02==>Frame Error
<==NAK
Frame-02==>Frame OK
<==ACK
EOT==>OK
<==ACK

接收方 - 开启xmodem模式,每0.5s发送一次0x15; - 当接收到帧数据时,正确回复ACK响应,异常回复NAK响应; - 当接收到EOT时,传输结束;

发送方 - 开启xmodem模式,等待接收NAK; - 发送帧数据,等待接收响应; - 当所有数据发送完毕时,主动发送EOT,传输结束;

交互示例

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
send COM21 (1)
str:
d

recv COM21 (51)
str:

download safety code
start xModem Receive->
d a d d a 64 6f 77 6e 6c 6f 61 64 20 73 61 66 65 74 79 20 63 6f 64 65 d d a 73 74 61 72 74 20 78 4d 6f 64 65 6d 20 52 65 63 65 69 76 65 2d 3e 15

recv COM21 (1)
str:
15

send COM21 (12)
str: rx data.log
72 78 20 64 61 74 61 2e 6c 6f 67 d

recv COM21 (12)
str:
15 15 15 15 15 15 15 15 15 15 15 15

recv COM21 (1)
str:
15

recv COM21 (1)
str:
15

recv COM21 (1)
str:
15

recv COM21 (1)
str:
15

recv COM21 (1)
str:
15

send COM21 (1024)
str: ?123456789␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦
2 1 fffffffe 30 31 32 33 34 35 36 37 38 39 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a

send COM21 (4)
str: ␦␦␦
1a 1a 1a 9

recv COM21 (1)
str:
15

send COM21 (1024)
str: ?123456789␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦␦
2 1 fffffffe 30 31 32 33 34 35 36 37 38 39 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a

send COM21 (4)
str: ␦␦␦
1a 1a 1a 9

recv COM21 (1)
str:
6

send COM21 (1)
str:
4

计划

  1. 模拟6502、总线和内存RAM,CPU能够从内存读写数据和获取指令
  2. 参考CPU的数据手册,能够对指令进行解析
  3. 解析卡带Cartidge的文件格式
  4. 实现Mapper功能
  5. 构建PPU模块,能够调用SFML输出画面
  6. 增加按键控制功能

CPU-6502

2A03简介

2A03是Ricoh(理光集团)基于6502制造的NMOS处理器。

小端字节处理器。

CPU内存分布

下图展示处理器如何使用总线访问内存。内存分为三个区域:卡带中的ROM、处理器RAM、I/O寄存器。8位控制总线负责通知组件请求是读还是写。8位数据总线用于读写字节。16位地址总线用于选择目标组件。请注意,ROM是只读的,需要通过MMC访问,以便切换bank。I/O寄存器用于和系统其他组件通信,比如PPU和控制器。

image-20230917160740959

16位地址总线可以访问64KB内存地址$0000-$FFFF。具体的分布见下图。RAM区域$0000-$2000,I/O寄存器区域$2000-$4020,扩展ROM区域$4020-$6000,SRAM区域$6000-$8000,程序ROM区域$8000-$10000

零页是指地址范围$0000-$00FF,在特定模式下允许更快的执行。内存地址$0000-$00FF会被镜像三次到$0800-$2000。比如,任何写到$0000的数据也会写到$0800$1000$1800

位置$2000-$2007每8字节会被镜像到$2008-$3FFF,剩余的寄存器都遵循这个镜像。

SRAM(WRAM)是存储内存,这个地址用于访问记忆游戏Cartridges卡带中的RAM。

$8000开始的地址是分配给Cartridges卡带的PRG-ROM。游戏只有一个16KB数据区(bank)的,会被加载到$8000$C000。游戏有两个16KB数据区的,将一个加载到$8000,另一个加载到$C000。游戏拥有超过两个16KB数据区的,使用内存映射器决定哪个数据区加载到内存。内存映射器会监视特定地址(或地址范围)的内存写入,当地址被写入时,它会执行数据区切换。详见附录D。

image-20230917165157967

寄存器

Program Counter (PC)

16位寄存器;

Stack Pointer (SP)

8位寄存器;固定偏移$0100;位置$0100-$01FF;自顶向下增长;不会检测越界;

Accumulator (A)

8位寄存器;从内存取值;

Index Register X (X)

8位寄存器;从内存取值;地址偏移值;读写SP寄存器;

Index Register Y (Y)

8位寄存器;从内存取值;地址偏移值;

Processor Status (P)

image-20230917211324567
  • Carry Flag (C) 上个指令结果越界标记;指令SEC/CLC,设置为1/0;
  • Zero Flag (Z) 上个指令结果为零标记;
  • Interrupt Disable (I) 中断禁用标记;指令SEI/CLI,设置为1/0;
  • Decimal Mode (D) BCD十进制模式,会被忽略;指令SED/CLD,设置为1/0;
  • Break Command (B) 指明BRK指令,会引发IRQ;
  • Overflow Flag (V) 溢出标记;
  • Negative Flag (N) 符号位;0为正,1为负;

中断

NES有三种类型的中断:NMI、IRQ、复位,中断向量表存储在$FFFA-$FFFF

发生中断时,需要进行如下步骤:

  1. 识别中断;
  2. 完成当前指令执行;
  3. PC和P寄存器压栈;
  4. 设置中断禁用位;
  5. 读取中断向量表处理函数赋值给PC;
  6. 执行处理函数;
  7. 执行RTI指令,恢复P和PC寄存器;
  8. 继续执行程序;

中断或可屏蔽中断,是由某些内存映射器生成的。

IRQ可以由软件使用BRK指令触发。当发生IRQ时,系统跳到$FFFE$FFFF所指地址。

NMI(不可屏蔽中断)是当V-Blank出现在每个帧的末尾时,PPU产生的中断类型。它不受中断禁用寄存器影响,所以它发生时,程序总是被中断。当PPU控制寄存器1($2000)被清空时,会阻止NMI的触发。当发生NMI时,系统跳到$FFFA$FFFB所指地址。

复位中断在系统第一次启动和用户按下复位按钮时触发。当发生复位时,系统跳到$FFFC$FFFD所指地址。

中断优先级为复位、NMI和IRQ。NES有7个周期的中断延时,即它会花费7个CPU周期来开始执行中断处理。

寻址模式

在6502上总共有13种不同的寻址模式。详见附录E。

指令

6502有56条不同的指令,尽管有些指令有多种,使用不同的寻址模式,使得在可能得256个操作码中总计151个有效。指令可以是1、2或3字节长,根据它们的寻址模式。第一个字节为操作码,其他字节为操作数。指令可以分为几个功能组:

  • 加载/存储操作
  • 寄存器传输操作
  • 栈操作
  • 逻辑操作
  • 算术操作
  • 递增/递减
  • 移位
  • 跳转/调用
  • 分支
  • 状态寄存器操作
  • 系统函数

PPU-2C02

内存分布

image-20230923162104444

图案表

英文名PatternTable,卡带CHR-ROM映射到此处,也可能是RAM。游戏中使用的一个图案被称为Tile,背景或精灵都是由若干个Tile组成。

image-20230924191909278

每个Tile大小为8x8像素,游戏屏幕大小为256x240像素,由32x30个Tile组成。

一个图案表占用大小4KB,总共有两个占用8KB。对于许多游戏拥有超过两个图案表,需要使用Mapper来控制映射。

简单计算:

一个PatternTable大小4KB,由256个Tile组成。每个Tile大小为4KB/256=16B。一个Tile大小8x8=64像素,每个像素占用16B/64=2bit。

参考

《Nintendo Entertainment System Documentation Version 1.0.pdf》

《任天堂产品系统文件.chm》

“玩”明白计算机底层系列-计组-红白机模拟器 - 知乎 (zhihu.com)

NES专题_金小庭的博客-CSDN博客

0%