6502CPU以及NES游戏机系统
一、6502CPU的介绍,以及NES系统的工作原理
6502CPU是曾经非常著名的一款8位CISC CPU,曾用于Apple1&2、FC(NES)等系统,如今在文曲星等电子产品中依然被广泛使用。其操作码长度为8位,指令长度则为1到3个字节,6502有3个寄存器,栈指针,标志位(P)和计数器。寄存器是累加器(A),X变址寄存器,和Y变址寄存器,每一个都是8位的,大多数指令把结果留在累加器里;栈指针是一个8位的寄存器用来指示栈(1 页面)的偏移量,当使用push和pull指令时它自动增加和减少,你也可以用TSX和TXS指令直接访问和修改它的值。P标志包含下面这些占用1个位的标志:
标志 |
名称 |
作用 |
N |
符号Negative |
当操作结果高位被设置(为负),该位被设置,否则被清除 |
V |
溢出Overflow |
当操作发生借位或进位时,该位被设置 |
B |
中断Break |
遇到‘BRK’指令时,该位被设置 |
D |
十进制Decimal |
当该位被设置时,所以的算术操作都是BCD (例如:09+01=10)。当该位被清除时,所有的算术操作都是以二为补码的二进制 (例如: 09+01=0A) |
I |
中断禁止Interrupt Disable |
当该位被设置时,不会发生中断 |
Z |
零Zero |
当操作结果为零时,该位被设置。否则被清除 |
C |
进位Carry |
当发生进位时,该位被设置 |
这些位按这个顺序排列(从7 [最高]到0 [最低]):
NV-BDIZC (另外10进制运算在NES中并没有用到,因此这里设计过程中将之去除)
6502的指令集:
助记符 |
描述 |
表达式 |
标志 |
读取(load)&储存(store)指令 |
|||
LDA |
load accumulator将数据读入累加器 |
A<--M |
NZ |
LDX |
load X index将数据读入变址寄存器X |
X<--M |
NZ |
LDY |
load Y index将数据读入变址寄存器Y |
Y<--M |
NZ |
STA |
store accumulator储存累加器的数据 |
M<--A |
- |
STX |
store X index储存变址寄存器X的数据 |
M<--X |
- |
STY |
store Y index储存变址寄存器Y的数据 |
M<--Y |
- |
STZ |
store zero储存零 |
M<--0 |
- |
堆栈操作 |
|||
PHA |
push accumulator累加器入栈 |
Stack<--A |
- |
PHX |
push X index变址寄存器X入栈 |
Stack<--X |
- |
PHY |
push Y index变址寄存器Y入栈 |
Stack<--Y |
- |
PHP |
push processor flags 标志位P入栈 |
Stack<--P |
- |
PLA |
pull (pop) accumulator累加器出栈 |
A<--Stack |
NZ |
PLX |
pull (pop) X index变址寄存器X出栈 |
X<--Stack |
NZ |
PLY |
pull (pop) Y index变址寄存器Y出栈 |
Y<--Stack |
NZ |
PLP |
pull (pop) processor flags标志位P出栈 |
P<--Stack |
All |
TSX |
transfer stack pointer to X传送栈指针到X |
X<--S |
NZ |
TXS |
transfer X to stack pointer传送X到栈指针 |
S<--X |
- |
递增(increment)&递减(decrement)操作 |
|||
INA |
increment accumulator累加器递增 |
A++ |
NZ |
INX |
increment X index变址寄存器X递增 |
X++ |
NZ |
INY |
increment Y index变址寄存器Y递增 |
Y++ |
NZ |
DEA |
decrement accumulator累加器递减 |
A-- |
NZ |
DEX |
decrement X index变址寄存器X递减 |
X-- |
NZ |
DEY |
decrement Y index变址寄存器Y递减 |
Y-- |
NZ |
INC |
increment memory location指定的内存单元递增 |
M++ |
NZ |
DEC |
decrement memory location指定的内存单元递减 |
M-- |
NZ |
移位操作 |
|||
ASL |
arithmetic shift left, high bit into carry算术左移,高位移入进位标志 |
C<--A7,A<--(A<<1) |
NZC |
LSR |
logical shift right, low bit into carry逻辑右移,低位移入进位标志 |
C<--A0,A<--(A>>1) |
N=0 ZC |
ROL |
rotate left through carry通过进位标志循环左移 |
C<--A7,A<--((A<<1)+C) |
NZC |
ROR |
rotate right through carry通过进位标志循环右移 |
C<--A0,A<--(A7=C+(A>>1)) |
NZC |
逻辑操作 |
|||
AND |
and accumulator与累加器 |
A<--A&M |
NZ |
ORA |
or accumulator或累加器 |
A<--A|M |
NZ |
EOR |
exclusive-or accumulator异或累加器 |
A<--A^M |
NZ |
BIT |
test bits against accumulator测试累加器的某个位 (1) |
Z<--!(A&M),N<--M7,V<--M6 |
N=M7 V=M6 Z |
CMP |
compare with accumulator与累加器比较 |
A-M-->NZC |
NZC |
CPX |
compare with X index与X变址寄存器比较 |
X-M-->NZC |
NZC |
CPY |
compare with Y index与Y变址寄存器比较 |
Y-M-->NZC |
NZC |
TRB |
test and reset bits对位进行测试和清除 |
|
x |
TSB |
test and set bits对位进行测试和设置 |
|
x |
RMB |
reset memory bit对内存位进行清除 |
|
x |
SMB |
set memory bit对内存位进行设置 |
|
x |
算术操作 |
|||
ADC |
add accumulator, with carry加累加器(带进位) |
A<--A+M+C |
NZCV |
SBC |
subtract accumulator, with borrow减累加器(带借位) |
A<--A-M-~C |
NZCV |
流程控制 指令 |
|||
JMP |
unconditional jump无条件跳转 |
PC<--Address |
- |
JSR |
jump Subroutine跳到子程序 |
Stack<--PC,PC<--Address |
- |
RTS |
return from Subroutine由子程序返回 |
PC<--Stack |
- |
RTI |
return from Interrupt由中断返回 |
P<--Stack,PC<--Stack |
From Stack |
BRA |
branch Always转移 |
PC=PC+offset |
- |
BEQ |
branch on equal (zero set)相等(零标志被设置)时转移 |
if Z=1,PC+=offset |
- |
BNE |
branch on not equal (zero clear)不相等(零标志被清除)时转移 |
if Z=0,PC+=offset |
- |
BCC |
branch on carry clear进位标志被清除时转移(2)
|
if C=0,PC+=offset |
- |
BCS |
branch on carry set进位标志被设置时转移(2)
|
if C=1,PC+=offset |
- |
BVC |
branch on overflow clear溢出标志被清除时转移 |
if V=0,PC+=offset |
- |
BVS |
branch on overflow set溢出标志被设置时转移 |
if V=1,PC+=offset |
- |
BMI |
branch on minus负数时转移 |
if N=1,PC+=offset |
- |
BPL |
branch on plus正数时转移 |
if N=0,PC+=offset |
- |
BBR |
branch on bit reset (zero)某位被清除时转移 |
|
- |
BBS |
branch on bit set (one)某位被设置时转移 |
|
- |
处理器状态指令 |
|||
CLC |
clear carry flag清除进位标志 |
C<--0 |
C=0 |
CLD |
clear decimal mode清除十进制模式 |
D<--0 |
D=0 |
CLI |
clear interrupt disable bit清除中断禁用 |
I<--0 |
I=0 |
CLV |
clear overflow flag清除溢出标志 |
V<--0 |
V=0 |
SEC |
set carry flag进位标志 |
C<--1 |
C=1 |
SED |
set decimal mode十进制模式 |
C<--1 |
D=1 |
SEI |
set interrupt disable bit中断禁用 |
I<--1 |
I=1 |
传送指令 |
|||
TAX |
transfer accumulator to X index传送累加器到X变址寄存器 |
X<--A |
NZ |
|
transfer accumulator to Y index传送累加器到Y变址寄存器 |
Y<--A |
NZ |
TXA |
transfer X index to accumulator传送X变址寄存器到累加器 |
A<--X |
NZ |
TYA |
transfer Y index to accumulator传送Y变址寄存器到累加器 |
A<--Y |
NZ |
特殊指令 |
|||
NOP |
no operation空操作 |
|
- |
BRK |
force break强行中断 |
Stack<--PC,PC<--$FFFE |
B=1 |
注释:
一共有15种寻址模式,如下:
隐式寻址 [隐式] Implied Addressing [Implied]
在隐式寻址模式里, 包含操作数的地址被隐含在指令的操作码里。
累加器寻址 [累加器] Accumulator Addressing [Accumulator]
这种寻址模式表现为只有一个操作数,另一个操作数隐含为累加器。
直接寻址 [直接] Immediate Addressing [Immediate]
在直接寻址模式里,操作数被包含在指令的第二个字节里,不需要其他内存寻址。
绝对寻址 [绝对] Absolute Addressing [Absolute]
在绝对寻址模式里,指令的第二个字节指定了有效地址的低八位,第三个字节指定高八位。因此,这种寻址模式允许访问全部的64K可寻址内存。
0页面寻址 [0页面] Zero Page Addressing [Zero Page]
0页面寻址允许只取指令的第二字节并假设高位地址为0,以得到较短的代码和执行时间。小心地使用0页面寻址能有效地增加代码的效率。
绝对变址寻址 [绝对,X 或 绝对,Y] Absolute Indexed Addressing
[Absolute,X or Absolute,Y]
绝对变址寻址联合X或Y变址寄存器使用,以“绝对, X”和“绝对, Y”的形式出现。有效地址由X或Y的内容加上指令的第二或第三字节包含的地址得到。这种模式允许变址寄存器包含变址或计数值而指令包含基址。这种寻址模式允许定位任何位置并且可以用变址修改多个地方,用以减少代码和指令时间。
0页面变址寻址 [0页面,X 或 0页面,Y] Zero Page Indexed Addressing [Zero Page,X or Zero Page,Y]
0页面变址寻址联合X或Y变址寄存器使用,以“0页面, X”和“0页面, Y”的形式出现。有效地址由指令的第二个字节加上变址寄存器的内容得到。因为这是一个“0页面”寻址,所以第二字节的内容定位于0页面。另外,因为“0页面”寻址模式的特性,所以不会有进位加到高位内存,也不会发生跨页边界的问题。
相对寻址 [相对] Relative Addressing [Relative]
相对寻址只用在转移(branch)指令指令中;它指定了条件转移的目标。指令的第二个字节变成一个操作数,作为偏移加到指向下一条指令的指令指针上。偏移范围从-128到127字节,相对于下一条指令。
0页面变址间接寻址 [(0页面,X)] Zero Page Indexed Indirect
Addressing [(Zero Page,X)]
0页面变址间接寻址(通常参考为 间接 X)指令的第二个字节加到X变址寄存器的内容上;进位被舍弃。加法运算的结果指向0页面的一个内存单元,而这个内存单元是有效地址的低8位。下一个0页面单元是有效地址的高8位。指向有效地址的高8位和低8位都必须在0页面内。
绝对变址间接寻址 [(绝对,X)] Absolute Indexed Indirect Addressing [(Absolute,X)](只用在跳转(Jump)指令)
在绝对变址间接寻址中,指令的第二和第三字节的内容被加到X 寄存器。加法运算的结果指向一个内存单元,而这个内存单元是有效地址的低8位。下一个内存单元是有效地址的高8位。
变址间接寻址 [(0页面),Y] Indexed Indirect Addressing [(Zero Page),Y]
这种寻址通常参考为 间接,Y。指令的第二个字节指向0页面的一个内存单元,这个内存单元的内容被加到Y变址寄存器,结果是有效地址的低8位。加法的进位被加到0页面的下一个内存单元,结果是有效地址的高8位。
0页面间接寻址 [(0页面)] Zero Page Indirect Addressing
[(Zero Page)]
在0页面间接寻址模式里,指令的第二个字节指向0页面的一个内存单元,单元的内容是有效地址的低8位,下一个0页面单元的内容是有效地址的高8位。
绝对间接寻址 [(Absolute)] Absolute
Indirect Addressing [(Absolute)](只用在跳转(Jump)指令)
指令的第二个字节包含了一个内存单元的低8位地址,第三个字节包含了同一个内存单元的高8位地址。而这个内存单元是有效地址的低位,下一个内存单元是有效地址的高位,最后有效地址被装入16位的指令指针。
关于NES的介绍(来自百度百科):
NES是Nintendo
Entertainment System的缩写,是Nintendo在20世纪80年代和20世纪90年代发售的一种家庭主机,俗称红白机。NES是此类游戏机在欧洲发行版本的缩写,在亚洲发行的游戏机型缩写为FC(Family Computer)又写作Famicom。在该游戏平台上比较著名的游戏有《Contra》,《Super Mario》等等。
主机性能:
中央处理器: 6502 CPU。
二进制数值: 8Bits。
运行频率: NTSC:1.7897725 Mhz ;PAL:1.773447
Mhz。
内部储存器: 8KB,6KB显示存储器+2KB镜像存储器。
图像处理器: 64种颜色,除去重复的颜色剩下52种颜色,最大显示数:16种颜色。
声音处理器: 矩形声波处理器两个,三角型声波处理器一个,噪音处理器一个,PCM数字声音发生器一个。
硬件接口: 游戏卡带接口×1,游戏手柄接口×2。电源接口×1,RF视频线接口×1,AV视频线接口×1,周边设备接口×1。
游戏载体: Rom卡带。
游戏卡带容量: 24KB,40KB,48KB,64KB,80KB,128KB,160KB,256KB。
周边设备: Zapper,Robot。
NES内部部件:
CPU - 中央处理器: Self-explanitory. NES使用一个标准6502 ( NMOS )
PPU - 图形处理器: 用来控制图形,活动块和其他视频相关特点
pAPU - pseuedo-Audio 处理器: 固化于CPU; 产生 (5) 声音通道的波形:: 四个 (4) 模拟
和一个 (1) 数字. 在NES内部没有处理音频的物理芯片.
MMC - 大量内存控制器: 微型控制器, 用来控制使NES游戏使用6502的64Kbyte以外的存储器.
他们也可以被用来控制使用CHR-ROM,也许被用来产生“特别效果”,比如强制和中断,
以及其他一些.
VRAM - 图形储存器: 这个储存器在PPU内部. NES中安装了16kbits
的VRAM.
SPR-RAM - 子画面储存器: 用来储存子画面,共256
bytes. 虽然他也在PPU内部,但不是VRAM或者
ROM的一部分.
PRG-ROM - 程序只读储存器: 存储程序代码的存储器.
也可以认为是通过MMC控制的扩展存储器中
的代码部分.
PRG-RAM - 程序可写存储器: 于PRG-ROM同义,不过这个是RAM.
CHR-ROM - 角色只读存储器: 在PPU外部的VRAM数据,
通过MMC在PPU内部与外部交换,或者在启动队
列中“读入”VRAM.
VROM - 与CHR-ROM同义.
SRAM - 存档可写存储器: 一般用来保存RPG游戏的进度. 就像最终幻想系列的“水井”,和“塞
尔达传说”.
WRAM - 与SRAM同义.
DMC - 涞髦仆ǖ: APU中处理数字信号的通道. 通常被认为是PCM
(Pulse信号调制器)通道.
EX-RAM - 扩展存储器: 在任天堂的MMC5中使用的,允许游戏扩展VRAM的容量.
CPU内存映像:
开始地址 |
用途 |
结束地址 |
$0000 |
2K字节RAM,做4次镜象(即$0000-$07FF可用) |
$1FFF |
$2000 |
寄存器 |
$2007 |
$2008 |
寄存器($2000-$2008的镜像,每8个字节镜像一次) |
$3FFF |
$4000 |
寄存器 |
$401F |
$4020 |
扩展ROM |
$5FFF |
$6000 |
卡带的SRAM(需要有电池支持) |
$7FFF |
$8000 |
卡带的下层ROM |
$BFFF |
$C000 |
卡带的上层ROM |
$FFFF |
中断:
6502有3个中断IRQ/BRK、NMI和RESET,每个中断都有一个16位的向量,即指针,用来存放该中断发生时中断服务函数的地址。中断发生时CPU都会把状态标志和返回地址压栈,然后调用中断服务程序。
IRQ/BRK中断由一下两种情况产生:一是软件通过BRK指令产生,一是硬件通过IRQ引脚产生。
RESET在开机的时候触发,这是ROM被装入,6502跳到RESET向量指向的地址没有寄存器被修改,没有内存被清空,这些都只在开机是发生。
NMI指不可屏蔽中断,它在VBlank即屏幕刷新时发生,持续时间根据系统(NTSC/PAL)不同而不同。NTSC是每秒60次,而PAL是每秒50次。6502的中断延时是7个时钟周期,也就是说,进入和离开中断都需要7个时钟周期。它产生于PPU的每一帧结束,NMI中断可以由$2000的第7位的1/0控制允许/禁止。
以上中断在ROM内有以下对应的地址:
中断地址 |
中断 |
优先权 |
$FFFA |
NMI |
中 |
$FFFC |
RESET |
高 |
$FFFE |
IRQ/BRK |
低 |
NES图形处理器PPU
PPU时序:
|
NTSC制式 |
PAL制式 |
基频(Base clock) |
21477270.0Hz |
21281364.0Hz |
CPU主频(Cpu clock) |
1789772.5Hz |
1773447.0Hz |
总扫描线数(Total scanlines) |
262 |
312 |
扫描线总周期(Scanline total cycles) |
1364(15.75KHz) |
1362(15.625KHz) |
水平扫描周期(H-Draw cycles) |
1024 |
1024 |
水平空白周期(H-Blank cycles) |
340 |
338 |
结束周期(End cycles) |
4 |
2 |
帧周期(Frame cycles) |
1364*262 |
1362*312 |
帧IRQ周期(FrameIRQ cycles) |
29830 |
35469 |
帧率(Frame rate) |
60(59.94Hz) |
50Hz |
帧时间(Frame period) |
1000.0/60.0(ms) |
1000.0/50.0(ms) |
镜像是指通过硬件映射特殊的内存地址或范围的一个过程。
PPU内存映像:
开始地址 |
用途 |
结束地址 |
$0000 |
图案表0(256x2x8,可能是VROM) |
$0FFF |
$1000 |
图案表1(256x2x8,可能是VROM) |
$1FFF |
$2000 |
命名表0(32x30块)(镜像,见命名表镜像) |
$23BF |
$23C0 |
属性表0(镜像,见命名表镜像) |
$23FF |
$2400 |
命名表1(32x30块)(镜像,见命名表镜像) |
$27BF |
$27C0 |
属性表1(镜像,见命名表镜像) |
$27FF |
$2800 |
命名表2(32x30块)(镜像,见命名表镜像) |
$2BBF |
$2BC0 |
属性表2(镜像,见命名表镜像) |
$2BFF |
$2C00 |
命名表3(32x30块)(镜像,见命名表镜像) |
$2FBF |
$2FC0 |
属性表3(镜像,见命名表镜像) |
$2FFF |
$3000 |
$2000-$2EFF的镜像 |
$3EFF |
$3F00 |
背景调色板#1 |
$3F0F |
$3F10 |
精灵调色板#1 |
$3F1F |
$3F20 |
镜像,(见调色板镜像) |
$3FFF |
$4000 |
$0000-$3FFF的镜像 |
$7FFF |
命名表:
NES的图像通过Tile矩阵来显示,这个网格就叫命名表。一个命名表和字符模式下的屏幕缓冲比较相象,它包含字符的代码,也就是30列的32Byte长度。每个Tile有8x8个象素,每个命名表有32x30个Tile,也就是256x240象素。PPU支持4个命名表,他们在$2000,$2400,$2800,$2C00。在NTSC制式下,上面和下面的8象素通常不显示出来,只有256x224象素;在PAL制式下,屏幕有256x240象素。
需要说的是,虽然PPU支持4个命名表,任天堂主机只支持2个命名表。另外两个被做了镜像。命名表保存了Tile的编号,而Tile存在图案表里。计算命名表里Tile号对应的实际地址的公式是:
(Tile号×16)+由$2000端口指定的图案表地址
命名表镜像:
NES只有2048字节($800)的VRAM给命名表使用,但是如前表所示,NES有能力寻址到4个命名表。缺省情况下,NES卡带都带有水平和垂直镜像,允许你改变命名表指向PPU的VRAM位置。这种方式同时影响两个命名表,你不能单独改变其中的一个。每个卡带都控制着PPU地址线的A10 和A11。它可能将他们设置成以下4种可能的方式的1种。下面这个图表有助于理解NES里的各种镜像,指向PPU VRAM中命名表的12位地址相当于“$2xxxx”:
名字 |
命名 表#0 |
命名 表#1 |
命名 表#2 |
命名 表#3 |
说明 |
地址线A11 |
地址线A10 |
水平 |
$000 |
$000 |
$400 |
$400 |
|
1 |
0 |
垂直 |
$000 |
$800 |
$000 |
$800 |
|
0 |
1 |
4屏幕镜像 |
$000 |
$400 |
$800 |
$C00 |
卡带里有2K VRAM,4个命名表物理上独立的 |
1 |
1 |
单屏幕 |
$X00 |
$X00 |
$X00 |
$X00 |
所有的命名表指相同的VRAM区域,X=0、4、8、C |
0 |
0 |
VROM镜像 |
Mapper 68#游戏映射VROM到PPU VRAM的命名表,这使得命名表是基于VROM的,你不能写它但却可以通过mapper自己来控制是否使用这种特性 |
|
|
图案表:
图案表储存了实际8x8象素的Tile,同时也储存了用来指向NES调色板全部16种颜色的4位元矩阵的低两位。PPU支持两个图案表在$0000和$1000。他们有以下格式:
VRAM地址 |
图案表内容 |
颜色效果 |
|
$0000 |
%00010000 = $10 |
组 0 |
...1.... |
$0008 |
%00000000 = $00 |
组 1 |
点表示0号颜色,数字表示实际调色板颜色代号 |
注意在图案表里存储的是每个点的2个位。其他两个由属性表得到。所以,在屏幕上总体出现的颜色数是16,而每个块里只有4种颜色。
属性表:
每个命名表有它自己的属性表。属性表的每一个字节代表了屏幕上的一组4x4的Tile,一共有8x8个字节。有几种方法来描述属性表里一个字节的功能:
*保存32x32象素方格的高2位颜色,每16x16象素用2位;
*保存16个8x8
Tile的高2位颜色;
*保存4个4x4
Tile格子的高2位颜色。
看以下两个图表帮助理解:
1、一个16x16 象素的格子:#0-F 代表了一个8x8 Tile,方块 [x] 代表了4个8x8 Tile,
|
|
||||||||||||
|
|
2、属性表一个字节的实际格式定义如下(对应于上面的例子):
位 |
描述 |
0、1 |
方块 0的高两位颜色(Tile #0,1,2,3) |
2、3 |
方块 1的高两位颜色(Tile #4,5,6,7) |
4、5 |
方块 2的高两位颜色(Tile #8,9,A,B) |
6、7 |
方块 3的高两位颜色(Tile #C,D,E,F) |
调色板:
NES有两个调色板,背景调色板和精灵调色板。调色板不包含实际的RGB值,它们更象一个索引表。写到$3F00-$3FFF的D6-D7字节被忽略。
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
3A |
3B |
3C |
3D |
3E |
3F |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
2A |
2B |
2C |
2D |
2E |
2F |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
1A |
1B |
1C |
1D |
1E |
1F |
00 |
01 |
02 |
03 |
04 |
05 |
06 |
07 |
08 |
09 |
0A |
0B |
0C |
0D |
0E |
0F |
调色板镜像:
镜像发生在背景调色板和精灵调色板之间,例如所有写到$3F00的数据会被镜像到$3F10,$3F04镜像到$3F14。背景和精灵的最高3个调色板的0号色盘定义为透明,存在那里的颜色不会被画出来。PPU使用放在$3F00里的颜色作为背景色,详细如下:
*$0D被写到$3F00(镜像到$3F10);
*$03被写到$3F08(镜像到$3F18);
*$1A被写到$3F18;
*$3F08被读到累加器。
PPU使用$0D作为背景颜色,尽管$3F08有一个颜色$03(因为0号颜色在所有的调色板里都定义为透明)。最后,累加器上有一个值$1A,这是从$3F18映像过来的。又一次,这个$1A值没有被画出,因为所有的调色板的0号颜色被定义为透明。整个背景和精灵调色板同时也映像到VRAM的其他区域,$3F20-$3FFF全部都是这两个调色板分别的映像。写到$3F00-$3FFF的D6-D7字节被忽略。
背景滚动:
NES可以通过预提取命名表,图案表和属性表来使背景滚动,背景是独立于精灵而位于最下层的。可以水平和垂直滚动。
水平滚动 |
垂直滚动 |
|||||||
|
|
命名表A通过$2000的D1-D0指定,B是跟在后面的一个命名表,根据镜像不同B是动态的。这个不能工作在水平和垂直同时滚动的游戏里。背景会跨越多个命名表,如下所示:
命名表#2 |
命名表#3 |
命名表#0 |
命名表#1 |
在$2005里写到水平滚动的值可以从0-256,写到垂直滚动的值从0-239,239是考虑了负值的结果,例如248代表-8。
屏幕和精灵分层:
下面是NES画图的循序:
前台 |
|
|
|
后台 |
CI |
OBJs 0-63 |
BG |
OBJs 0-63 |
EXT |
|
精灵RAM |
|
精灵RAM |
|
CI代表颜色亮度,相当于$2001的D7-D5;BG是背景;EXT是扩展口的图像信号。BGPRI代表VRAM里背景‘优先权’位,每个精灵都有的,即第二字节的D5位。OBJ数代表了实际精灵的号码,不是Tile索引值。前台高于任何其他层,最后被画上,后台低于任何其他层,最先被画上。
精灵和精灵RAM:
NES用一个页面(256字节)来存放动画,每个精灵4个字节,一共可以有64个动画/精灵,它们可以是8x8或8x16象素。动画/精灵图案被存储在VRAM的图案表其中一个里面。精灵属性,例如翻转和优先权被储存在一个特殊的256字节的精灵RAM,它不是CPU或PPU的地址的一部分。整个精灵RAM可以通过$4014的DMA方式来写,写一个8位的数到$4014就将这个8位数所指定的内存页面整个拷贝到精灵RAM上。也可以通过把开始地址放在$2003然后读/写于$2004(每次存取地址自动加一),它是一个一个字节存取的。动画/精灵的属性格式是:
动画/精灵属性RAM:
| 精灵#0 | 精灵#1 | ... | 精灵#62 | 精灵#63 |
字节 |
位 |
描述 |
0 |
YYYYYYYY |
精灵左上角的Y坐标-1 |
1 |
IIIIIIII |
Tile索引号 |
2 |
vhp000cc |
v=垂直翻转(1=翻转)
|
h=水平翻转(1=翻转)
|
||
p=背景优先权(0=前台1=后台) |
||
c=颜色的高2位 |
||
3 |
XXXXXXXX |
精灵左上角的X坐标 |
Tile索引号就和命名表里的一样。精灵图案可以象Tile图案对于背景图片一样抓取。唯一的不同是在8x16的精灵时,上半部分(偶数号码)的Tile索引由精灵图案表的$0000开始,而下半部分(奇数号码)由$1000开始。$2000寄存器对8x16精灵无效。全部64个精灵有自己内部的优先权,0号最高(最后被画),63号最低(最先被画)。每条扫描线只能显示8个精灵,每个精灵RAM条目都会被检查是否处于其他精灵的水平范围内。注意,这是基于扫描线的,不是基于精灵的,也就是说,会进行256次检查,而不是256/8或256/16次。在一个真实的NES芯片里,如果精灵被禁止($2001的D4是0)很长一段时间,精灵数据会渐渐消失。可以理解为精灵RAM是一个DRAM,D4控制了DRAM的周期刷新信号。
碰撞标志:
碰撞标志是PPU状态寄存器($2002)的第6位(D6)。PPU能够找出0号精灵的位置,然后设置D6,它是这样工作的:PPU扫描出背景图案象素和精灵象素同时不是透明的第一个地方。比如,屏幕的背景是一个非透明颜色(颜色号>0),0号精灵的坐标是(12,34),它只是在第4行的开始才有象素,那么,碰撞标志在屏幕刷新到(12,37)时才被设置。要记住,0号颜色被定义为透明。引起D6被设置的象素必须是已经被画出来的。下面的例子能帮助理解,这是两个Tile,下划线表示透明的0号颜色,*号表示碰撞标志被设置。
精灵 |
+ |
背景 |
= |
结果 |
‘*’会用2号颜色画出 |
__1111__ |
________ |
__1111__ |
这个例子说的是背景+精灵,但是对于靠近背景的精灵也通用,即精灵+精灵,通过设置背景优先权来实现。D6不可以通过读PPU的状态来复位,只在每次VBlank之后被清零。Hit标志可以用在水平或者垂直屏幕分割的时候,还有许多好玩的效果。
水平和垂直消隐:
所有的游戏机都有一个刷新动作,用来重新定位电子枪显示可见的数据。最通用的显示设备是电视机,它分为每秒刷新60次的NTSC制式和50次的PAL制式。电子枪从左到右画出象素,它每次只能画一条扫描线,画下一条之前要先回到左面并且做好准备,这之间有一段时间叫做水平消隐(HBlank)。在画完全部256条扫描线之后它又回到屏幕左上角准备下一次画屏幕(帧),这之间的一段时间就是垂直消隐(VBlank)。电子枪就是一个不断的走‘之’字形的过程。VBlank标志就是$2002的D7,它表明PPU是否在VBlank期间,当VBlank标志存在时,你就可以通过$2006和$2007访问PPU内存。一个程序可以通过读$2002来使D7复位。在屏幕刷新期间,我们不能访问PPU,而PPU会在CPU背后修改VRAM指针,这样我们很容易在写入VRAM时出错,为了让PPU停下来,可以对$2000和$2001写00。
访问PPU RAM:
在一个任天堂主机,访问PPU内存只可以在VBlank期间。当在屏幕刷新时访问会破坏刷新地址寄存器,一般它经常用来做隐含的“分割屏幕”效果(见“在屏幕刷新的时候访问VRAM”)。很多小些的ROM用只读存储体(VROM)用做图案表。在这种情况下,你不可以写PPU地址,只可以读。
写PPU记忆体:
a) 写高位地址字节到$2006
b) 写低位地址字节到$2006
c) 写数据到$2007。每一个写操作后,地址会增加1($2000的第二位是0)或增加32($2000的第二位是1)。
读PPU记忆体:
a) 写高位地址字节到$2006
b) 写低位地址字节到$2006
c) 从$2007读数据。从$2007读到的第一个字节是无效的(见下例)。然后每读一次地址就增加一($2000的第二位是0)或增加
32($2000的第二位是1)。
例子:VRAM的$2000里有$AA $BB $CC $DD。VRAM的增量是1。执行效果如下:
LDA #$20
STA $2006
LDA #$00
STA $2006 ; VRAM 地址设为 $2000
LDA $2007 ; A=?? VRAM 缓冲=$AA
LDA $2007 ; A=$AA VRAM 缓冲=$BB
LDA $2007 ; A=$BB VRAM 缓冲=$CC
LDA #$20
STA $2006
LDA #$00
STA $2006 ; VRAM 地址设为 $2000
LDA $2007 ; A=$CC VRAM 缓冲=$AA
LDA $2007 ; A=$AA VRAM 缓冲=$BB
这个只适用于$0000-$FEFF,访问调色板数据没有这种现象。
在屏幕刷新的时候访问VRAM:
前面说过,在屏幕刷新的时候存取VRAM的地址和数据是不合法的。许多程序存取这些寄存器来制造不同的滚动效果。比如,一些游戏从屏幕底部开始滚动,那么它可能向$2006写第一行的状态来复位屏幕滚动。更好的诡计是PPU在屏幕刷新时用VRAM的地址寄存器来储存当前地址。通过向$2006修改地址以及让PPU从一个不同的地方接着刷新。关于$2007如何影响屏幕刷新仍不了解。
当不知道向$2006中写什么数据时,看下面的图表。
写到$2006的地址
位 |
描述 |
0-4 |
Tiles的水平偏移量(例如1
= 8象素);水平位置=水平偏移量×8 |
5-9 |
Tiles的垂直偏移量(例如1
= 8象素);垂直位置=垂直偏移量×8 |
A、B |
命名表的号码($2000,$2400,$2800,$2C00); |
C、D |
附加的垂直偏移量,单位:象素(0..3);扫描线=垂直偏移量+附加的垂直偏移量 |
E、F |
00 |
I/O端口
端口是预分配好的可访问地址,程序通过端口和PPU或APU交换信息。每个端口都是16位的
项目中还没有实现的IO寄存器部分用红色表示
端口 |
读写/位 |
功能描述 |
||||
$2000 |
读写 |
PPU控制寄存器 1 |
||||
|
0-1 |
命名表地址:
请记住,因为镜像,只有2个真实的命名表,而不是4个; |
||||
2 |
垂直写, 1=VRAM地址以32递增:
|
|||||
3 |
精灵图案表地址, 1=$1000, 0=$0000
|
|||||
4 |
屏幕图案表地址, 1=$1000, 0=$0000
|
|||||
5 |
精灵尺寸, 1=8x16, 0=8x8 |
|||||
6 |
PPU 主/从模式, 没有在NES里使用 |
|||||
7 |
Vblank使能, 1=在Vblank时发生中断 |
|||||
$2001 |
读写 |
PPU控制寄存器2 |
||||
|
0 |
显示模式,0=彩色,1=单色 |
||||
1 |
背景掩码,0=不显示屏幕的左8列 |
|||||
2 |
精灵掩码,0=不在左8列显示精灵 |
|||||
3 |
屏幕使能,1=显示图像,0=黑屏 |
|||||
4 |
精灵使能,1=显示精灵,0=隐藏精灵 |
|||||
5-7 |
当D0=0:背景颜色,% 000=黑,%001=蓝,%010=绿,%100=红;不用混合使用,因为可能会损坏PPU硬件 |
|||||
$2002 |
读 |
PPU状态寄存器:读取后$2005和$2006被复位,下一个写到$2005的数据是水平的,写到$2006的数据是高位 |
||||
|
0-3 |
未知(???) |
||||
4 |
VRAM写标志:0=写有效,1=写忽略 |
|||||
5 |
扫描线精灵计数:0=当前扫描线精灵个数小于等于8个,1=大于8个 |
|||||
6 |
命中标志:1=精灵刷新碰到了#0精灵。在屏幕刷新状态,这个标志被复位为0 |
|||||
7 |
Vblank标志:1= PPU在Vblank状态。当Vblank结束或CPU读$2002时,该标志被复位为0
|
|||||
$2003 |
写 |
精灵RAM地址:用来设置通过$2004访问的256字节精灵RAM地址。每次访问$2004后该地址增加1 |
||||
$2004 |
读写 |
精灵RAM数据:用来读/写精灵内存。地址通过$2003来设置,每次访问后地址增加1 |
||||
$2005 |
写 |
屏幕滚动偏移:第一个写的值会进入垂直滚动寄存器(若>239,被忽略)。第二个值出现在水平滚动寄存器 |
||||
$2006 |
写 |
VRAM地址:设置$2007访问的VRAM地址。第一个写地址的高6位。第二个写低8位。每次访问$2007后地址增加 |
||||
$2007 |
读写 |
VRAM数据:用来访问VRAM数据,通过$2006设置的地址在每次访问之后会增加1或32 |
||||
$4000 |
写 |
APU方波#1控制端口 |
||||
$4001 |
写 |
APU方波#1音量控制端口 |
||||
$4002 |
写 |
APU方波#1细音调(FT)端口 |
||||
$4003 |
写 |
APU方波#1粗音调(CT)端口 |
||||
$4004 |
写 |
APU方波#2控制端口 |
||||
$4005 |
写 |
APU方波#2音量控制端口 |
||||
$4006 |
写 |
APU方波#2细音调(FT)端口 |
||||
$4007 |
写 |
APU方波#2粗音调(CT)端口 |
||||
$4008 |
写 |
APU三角波控制端口#1 |
||||
$4009 |
? |
APU三角波控制端口#2 |
||||
$400A |
写 |
APU三角波频率端口#1 |
||||
$400B |
写 |
APU三角波频率端口#2 |
||||
$400C |
写 |
APU噪声控制端口#1 |
||||
$400D |
|
未使用(???) |
||||
$400E |
写 |
APU噪声频率端口#1 |
||||
$400F |
写 |
APU噪声频率端口#2 |
||||
$4010 |
写 |
APU增量调制控制端口 |
||||
$4011 |
写 |
APU增量调制D/A端口 |
||||
$4012 |
写 |
APU增量调制地址端口 |
||||
$4013 |
写 |
APU增量调制数据长度 |
||||
$4014 |
写 |
DMA访问精灵RAM:通过写一个值xx到这个端口,引起CPU内存地址为$xx00-$xxFF的区域传送到精灵内存 |
||||
$4015 |
读写 |
声音通道切换 |
||||
|
0 |
方波#1通道:1=声音使能, |
||||
1 |
方波#2通道:1=声音使能, |
|||||
2 |
三角波通道:1=声音使能, |
|||||
3 |
噪声通道:1=声音使能, |
|||||
4 |
增量调制:1=声音使能, |
|||||
5、7 |
未使用 (???) |
|||||
6(只读) |
垂直时钟信号IRQ可用:0=1帧存在,因为没有IRQ;1=1帧被IRQ中断 |
|||||
$4016 |
读写 |
手柄1+选通 |
||||
|
0 |
读:手柄1数据;写:选通,1=复位,0=清除;扩展端口:0=写,1=读 |
||||
1 |
手柄1 存在, 0=连接 |
|||||
3 |
光枪精灵检测:1=瞄准 |
|||||
4 |
光枪扳机:0=按下,1=松开 |
|||||
2、5-7 |
未知,6、7设为%10(???) |
|||||
$4017 |
读写 |
手柄2+ 选通 |
||||
|
0 |
读:手柄2数据;写:选通,1=复位,0=清除;扩展端口:0=??,1=读 |
||||
1 |
手柄2 存在, 0=连接 |
|||||
2、5 |
未使用(???) |
|||||
3 |
光枪精灵检测:1=瞄准 |
|||||
4 |
光枪扳机:0=按下,1=松开 |
|||||
6 |
垂直时钟信号(内部),0=存在,$4016的D6受影响;1=不存在$4016的D6不可触及 |
|||||
7 |
垂直时钟信号(外部),0=存在,1=不存在 |
更多关于NES如何工作的资料在都可以在网上找到,这里限于篇幅就不详细叙述了
CPU的设计部分:
CPU的设计参考了Free_6502的设计方式,并在此基础上修改而成
6502 CPU的寻址比较复杂,光是所有的寻址方式就有15种之多。在这里对于地址的输出部分专门添加了一个地址输出控制模块,其VHDL代码如下:
--addr
process(reset,clk)
variable addr_add1 : std_logic_vector(15
downto 0);
variable addr_add2 : std_logic_vector(7
downto 0);
variable addr_add_cin : std_logic;
variable addr_out_low : std_logic_vector(8
downto 0);
variable addr_out_high : std_logic_vector(7
downto 0);
begin
if
reset='1' then
addr<="0000000000000000";
elsif
clk 'event and clk='1' then
addr_add2:="00000000";
addr_add_cin:='0';
case
addr_op is
when
MC_V_NMI1 =>
addr_add1:=vect_nmi1;
when
MC_V_NMI2 =>
addr_add1:=vect_nmi2;
when
MC_V_RESET1 =>
addr_add1:=vect_reset1;
when
MC_V_RESET2 =>
addr_add1:=vect_reset2;
when
MC_V_IRQ1 =>
addr_add1:=vect_irq1;
when
MC_V_IRQ2 =>
addr_add1:=vect_irq2;
when
MC_NOP =>
addr_add1:=pc_reg;
when
MC_PC_P =>
addr_add1:=pc_reg;
addr_add_cin:='1';
when
MC_SPLIT =>
addr_add1:=din&dint1;
when
MC_SPLIT_P =>
addr_add1:=din&dint1;
addr_add_cin:='1';
when
MC_SPLIT_X =>
addr_add1:=din&dint1;
addr_add2:=x_reg;
when
MC_SPLIT_Y =>
addr_add1:=din&dint1;
addr_add2:=y_reg;
when
MC_DIN_Z =>
addr_add1:="00000000"&din;
when
MC_DIN_ZP =>
addr_add1:="00000000"&din;
addr_add_cin:='1';
when
MC_DIN_ZX =>
addr_add1:="00000000"&din;
addr_add2:=x_reg;
when
MC_DIN_ZXP =>
addr_add1:="00000000"&din;
addr_add2:=x_reg;
addr_add_cin:='1';
when
MC_DIN_ZY =>
addr_add1:="00000000"&din;
addr_add2:=y_reg;
when
MC_SP =>
addr_add1:="000000001"&sp_reg;
when
MC_DINT16 =>
addr_add1:=dint2&dint1;
when
MC_DINT16_X =>
addr_add1:=dint2&dint1;
addr_add2:=x_reg;
when
MC_DINT1_Z =>
addr_add1:="00000000"&dint1;
when
MC_DINT1_ZX =>
addr_add1:="00000000"&dint1;
addr_add2:=x_reg;
when
others =>
addr_add1:=pc_reg;
end
case;
addr_out_low:=('0'&addr_add1(7
downto 0))+('0'&addr_add2(7 downto 0))+addr_add_cin;
addr_out_high:=addr_add1(15
downto 8)+addr_out_low(8);
addr<=addr_out_high(7
downto 0)&addr_out_low(7 downto 0);
end
if;
end
process;
通过CPU 控制器对地址输出结构的控制,可以看到15种寻址结构都已经全部包含在里面。然后整个地址输出部分也只在最后才用到一个加法器,有效的节省了FPGA资源
CPU设计过程中大量用到了信号状态机,其定义部分如下:
type
STATE_S is (RESET1,RESET2,FETCH,START_IRQ,START_NMI,RUN);
signal state : STATE_S;
type
DOUT_OP_S is
(MC_A_REG,MC_X_REG,MC_Y_REG,MC_NOP,MC_P_REG,MC_DINT3,MC_PCH,MC_PCL);
signal dout_op : DOUT_OP_S;
type
ALU_OP_S is (MC_ADD,MC_SUB,MC_ADDC,MC_SUBB,MC_BIT_AND,MC_BIT_OR,MC_BIT_XOR,MC_BIT_ASL,MC_BIT_LSR,MC_BIT_ROL,MC_BIT_ROR,MC_BIT_NOP,MC_PASS1,MC_PASS2);
signal alu_op : ALU_OP_S;
type
ALU_IN1_OP_S is (MC_A_REG,MC_X_REG,MC_Y_REG,MC_DIN,MC_NOP);
signal alu_in1_op : ALU_IN1_OP_S;
type
ALU_IN2_OP_S is (MC_ONE,MC_SP_REG,MC_NOP,MC_DIN);
signal alu_in2_op : ALU_IN2_OP_S;
type
FLAG_OP_S is (MC_NOP,MC_NVZC,MC_NZ,MC_NZC,MC_DIN,MC_BIT,
MC_CLEARV,MC_SETB,MC_SETI,MC_CLEARI,
MC_CLEARC,MC_SETC,MC_CLEARD,MC_SETD);
signal flag_op : FLAG_OP_S;
type
A_LE_S is (MC_NOP,MC_LE);
signal a_le : A_LE_S;
type
X_LE_S is (MC_NOP,MC_LE);
signal x_le : X_LE_S;
type
Y_LE_S is (MC_NOP,MC_LE);
signal y_le : Y_LE_S;
type
DINT1_OP_S is (MC_NOP,MC_DIN);
signal dint1_op : DINT1_OP_S;
signal dint2_op : DINT1_OP_S;
type
DINT3_OP_S is (MC_NOP,MC_ALU);
signal dint3_op : DINT3_OP_S;
type
IR_OP_S is (MC_NOP,MC_DIN);
signal ir_op : IR_OP_S;
type
PC_OP_S is (MC_NOP,MC_INC,MC_BCC,MC_BCS,MC_BEQ,MC_BNE,MC_BMI,
MC_BPL,MC_BVC,MC_BVS,MC_SPLIT);
signal pc_op : PC_OP_S;
type
ADDR_OP_S is (MC_NOP,MC_PC_P,MC_SPLIT,MC_SPLIT_P,MC_SPLIT_X,
MC_SPLIT_Y,MC_DIN_Z,MC_DIN_ZP,MC_DIN_ZX,
MC_DIN_ZXP,MC_DIN_ZY,MC_SP,MC_V_NMI1,MC_V_NMI2,
MC_V_RESET1,MC_V_RESET2,MC_V_IRQ1,MC_V_IRQ2,
MC_DINT16,MC_DINT16_X,MC_DINT1_Z,MC_DINT1_ZX);
signal addr_op : ADDR_OP_S;
type
SP_OP_S is (MC_NOP,MC_PUSH,MC_POP,MC_X_REG);
signal sp_op : SP_OP_S;
type
RD_EN_S is (MC_NOP,MC_READ);
signal rd_en : RD_EN_S;
type
DIN_LE_S is (MC_NOP,MC_EN);
signal din_le : DIN_LE_S;
控制器代码形式如下:(这里以dout的控制信号为例)
process(ir,step,dout_op)
variable opcode : std_logic_vector(10
downto 0);
begin
opcode:=ir&step;
case
opcode is
when
"00001110101" => dout_op<=MC_DINT3;
when
"00011110101" => dout_op<=MC_DINT3;
when
"00000110100" => dout_op<=MC_DINT3;
when
"00010110100" => dout_op<=MC_DINT3;
when
"00000000010" => dout_op<=MC_PCH;
when
"00000000011" => dout_op<=MC_PCL;
when
"00000000100" => dout_op<=MC_P_REG;
when
"11001110101" => dout_op<=MC_DINT3;
when
"11011110101" => dout_op<=MC_DINT3;
when
"11000110100" => dout_op<=MC_DINT3;
when
"11010110100" => dout_op<=MC_DINT3;
when
"11101110101" => dout_op<=MC_DINT3;
when
"11111110101" => dout_op<=MC_DINT3;
when
"11100110100" => dout_op<=MC_DINT3;
when
"11110110100" => dout_op<=MC_DINT3;
when
"00100000001" => dout_op<=MC_PCH;
when
"00100000010" => dout_op<=MC_PCL;
when
"01001110101" => dout_op<=MC_DINT3;
when
"01011110101" => dout_op<=MC_DINT3;
when
"01000110100" => dout_op<=MC_DINT3;
when
"01010110100" => dout_op<=MC_DINT3;
when
"01001000000" => dout_op<=MC_A_REG;
when
"00001000000" => dout_op<=MC_P_REG;
when
"00101110101" => dout_op<=MC_DINT3;
when
"00111110101" => dout_op<=MC_DINT3;
when
"00100110100" => dout_op<=MC_DINT3;
when
"00110110100" => dout_op<=MC_DINT3;
when
"01101110101" => dout_op<=MC_DINT3;
when
"01111110101" => dout_op<=MC_DINT3;
when
"01100110100" => dout_op<=MC_DINT3;
when
"01110110100" => dout_op<=MC_DINT3;
when
"10001101010" => dout_op<=MC_A_REG;
when
"10011001010" => dout_op<=MC_A_REG;
when
"10000001100" => dout_op<=MC_A_REG;
when
"10010001100" => dout_op<=MC_A_REG;
when
"10000101001" => dout_op<=MC_A_REG;
when
"10010101001" => dout_op<=MC_A_REG;
when
"10011101010" => dout_op<=MC_A_REG;
when
"01000011010" => dout_op<=MC_PCH;
when
"01000011011" => dout_op<=MC_PCL;
when
"01000011100" => dout_op<=MC_P_REG;
when
"00110011010" => dout_op<=MC_PCH;
when
"00110011011" => dout_op<=MC_PCL;
when
"00110011100" => dout_op<=MC_P_REG;
when
"10001110010" => dout_op<=MC_X_REG;
when
"10000110001" => dout_op<=MC_X_REG;
when
"10010110001" => dout_op<=MC_X_REG;
when
"10001100010" => dout_op<=MC_Y_REG;
when
"10000100001" => dout_op<=MC_Y_REG;
when
"10010100001" => dout_op<=MC_Y_REG;
when
others => dout_op<=MC_NOP;
end case;
end
process;
其中step是每条指令的执行步骤,由于控制器其实就是个组合逻辑,因此无需用到clk指令来进行时序控制
Alu部分也是纯组合逻辑的结构,其相关代码如下:
--alu
process(alu_out,alu_in1,alu_in2,alu_op,c_flag,alu_add)
begin
alu_add_in2<='0'&alu_in2;
alu_add_cin<=c_flag;
case
alu_op is
when
MC_PASS1 =>
alu_out<='0'&alu_in1;
when
MC_PASS2 =>
alu_out<='0'&alu_in2;
when
MC_ADD =>
alu_add_in2<='0'&alu_in2;
alu_add_cin<='0';
alu_out<=alu_add;
when
MC_ADDC =>
alu_add_in2<='0'&alu_in2;
alu_add_cin<=c_flag;
alu_out<=alu_add;
when
MC_SUB =>
alu_add_in2<='1'&(not
alu_in2);
alu_add_cin<='1';
alu_out<=alu_add;
when
MC_SUBB =>
alu_add_in2<='1'&(not
alu_in2);
alu_add_cin<=not
c_flag;
alu_out<=alu_add;
when
MC_BIT_AND =>
alu_out<='0'&(alu_in1
and alu_in2);
when
MC_BIT_OR =>
alu_out<='0'&(alu_in1
or alu_in2);
when
MC_BIT_XOR =>
alu_out<='0'&(alu_in1
xor alu_in2);
when
MC_BIT_ASL =>
alu_out<='0'&alu_in1(6
downto 0)&'0';
when
MC_BIT_LSR =>
alu_out<="00"&alu_in1(7
downto 1);
when
MC_BIT_ROL =>
alu_out<='0'&alu_in1(6
downto 0)&c_flag;
when
MC_BIT_ROR =>
alu_out<='0'&c_flag&alu_in1(7
downto 1);
when
others =>
alu_out<='0'&alu_in1;
end
case;
end
process;
--alu_add
alu_add<=alu_in1+alu_add_in2+alu_add_cin;
可以看到,alu部分也是采用了最后一步相加的方法,有效的减少了系统中加法器的数目
PPU的设计部分:
PPU的编写完全的从0开始,written totally from scratch。
PPU的内存映射部分比较复杂,前面资料中提到的映射方式如下:
PPU内存映像:
开始地址 |
用途 |
结束地址 |
$0000 |
图案表0(256x2x8,可能是VROM) |
$0FFF |
$1000 |
图案表1(256x2x8,可能是VROM) |
$1FFF |
$2000 |
命名表0(32x30块)(镜像,见命名表镜像) |
$23BF |
$23C0 |
属性表0(镜像,见命名表镜像) |
$23FF |
$2400 |
命名表1(32x30块)(镜像,见命名表镜像) |
$27BF |
$27C0 |
属性表1(镜像,见命名表镜像) |
$27FF |
$2800 |
命名表2(32x30块)(镜像,见命名表镜像) |
$2BBF |
$2BC0 |
属性表2(镜像,见命名表镜像) |
$2BFF |
$2C00 |
命名表3(32x30块)(镜像,见命名表镜像) |
$2FBF |
$2FC0 |
属性表3(镜像,见命名表镜像) |
$2FFF |
$3000 |
$2000-$2EFF的镜像 |
$3EFF |
$3F00 |
背景调色板#1 |
$3F0F |
$3F10 |
精灵调色板#1 |
$3F1F |
$3F20 |
镜像,(见调色板镜像) |
$3FFF |
$4000 |
$0000-$3FFF的镜像 |
$7FFF |
精灵所使用的256Byte ram并没有在上面列出,事实上它也应该是属于PPU的一部分
因此这里可以将之划分为几个部分,
图案表vrom 8K Byte
命名表+属性表sram 4K Byte
调色板palette_table 32X8 bit
精灵Spirit_table 256X8 bit
另外由于CPU的内存映像部分也是在PPU中设计的,这里一并列出
CPU内存映像:
开始地址 |
用途 |
结束地址 |
$0000 |
2K字节RAM,做4次镜象(即$0000-$07FF可用) |
$1FFF |
$2000 |
寄存器 |
$2007 |
$2008 |
寄存器($2000-$2008的镜像,每8个字节镜像一次) |
$3FFF |
$4000 |
寄存器 |
$401F |
$4020 |
扩展ROM |
$5FFF |
$6000 |
卡带的SRAM(需要有电池支持) |
$7FFF |
$8000 |
卡带的下层ROM |
$BFFF |
$C000 |
卡带的上层ROM |
$FFFF |
这里同样可以划分为两个部分
2K字节ram 2K Byte
16K字节的程序rom 16K Byte
IO端口寄存器 from 0x2000 to 0x2007
IO端口寄存器 from 0x4000 to 0x4017(这一组寄存器在报告截止时还没有实现,后面不讨论)
此外由于nes调色板中的颜色并不是最终颜色,而是按照下面所示的颜色表
NES有两个调色板,背景调色板和精灵调色板。调色板不包含实际的RGB值,它们更象一个索引表。写到$3F00-$3FFF的D6-D7字节被忽略。
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
3A |
3B |
3C |
3D |
3E |
3F |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
2A |
2B |
2C |
2D |
2E |
2F |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
1A |
1B |
1C |
1D |
1E |
1F |
00 |
01 |
02 |
03 |
04 |
05 |
06 |
07 |
08 |
09 |
0A |
0B |
0C |
0D |
0E |
0F |
于是这里我们从PC上面的NES模拟器源代码中找到颜色表的详细信息,并转化为VHDL可以使用的代码表如下
constant
r_color : color_rom_type := (
X"7",X"2",X"2",X"6",
X"9",X"B",X"A",X"7",
X"4",X"3",X"3",X"3",
X"3",X"0",X"0",X"0",
X"B",X"4",X"4",X"9",
X"D",X"D",X"E",X"C",
X"8",X"5",X"4",X"4",
X"4",X"0",X"0",X"0",
X"F",X"6",X"5",X"A",
X"F",X"F",X"F",X"F",
X"E",X"9",X"7",X"7",
X"6",X"6",X"0",X"0",
X"F",X"9",X"A",X"C",
X"E",X"F",X"F",X"F",
X"F",X"C",X"A",X"A",
X"A",X"A",X"0",X"0"
);
constant
g_color : color_rom_type := (
X"7",X"0",X"0",X"1",
X"2",X"1",X"3",X"4",
X"5",X"6",X"6",X"6",
X"5",X"0",X"0",X"0",
X"B",X"6",X"4",X"4",
X"4",X"4",X"5",X"7",
X"8",X"A",X"A",X"A",
X"9",X"0",X"0",X"0",
X"F",X"A",X"8",X"7",
X"6",X"6",X"7",X"A",
X"D",X"E",X"F",X"E",
X"D",X"6",X"0",X"0",
X"F",X"D",X"B",X"B",
X"B",X"B",X"C",X"D",
X"F",X"F",X"F",X"F",
X"F",X"A",X"0",X"0"
);
constant
b_color : color_rom_type := (
X"7",X"B",X"B",X"A",
X"7",X"3",X"0",X"0",
X"0",X"0",X"0",X"4",
X"8",X"0",X"0",X"0",
X"B",X"F",X"F",X"F",
X"C",X"6",X"0",X"0",
X"0",X"0",X"1",X"6",
X"C",X"0",X"0",X"0",
X"F",X"F",X"F",X"F",
X"F",X"B",X"3",X"0",
X"2",X"0",X"4",X"9",
X"E",X"6",X"0",X"0",
X"F",X"F",X"F",X"F",
X"F",X"E",X"B",X"A",
X"9",X"8",X"A",X"C",
X"F",X"A",X"0",X"0"
);
PPU的显示输出部分,由于这里采用的是VGA的显示输出方式,需要每一个时钟输出一个RGB信号
CPU和PPU时序:
|
NTSC制式 |
PAL制式 |
基频(Base clock) |
21477270.0Hz |
21281364.0Hz |
CPU主频(Cpu clock) |
1789772.5Hz |
1773447.0Hz |
总扫描线数(Total scanlines) |
262 |
312 |
扫描线总周期(Scanline total cycles) |
1364(15.75KHz) |
1362(15.625KHz) |
水平扫描周期(H-Draw cycles) |
1024 |
1024 |
水平空白周期(H-Blank cycles) |
340 |
338 |
结束周期(End cycles) |
4 |
2 |
帧周期(Frame cycles) |
1364*262 |
1362*312 |
帧IRQ周期(FrameIRQ cycles) |
29830 |
35469 |
帧率(Frame rate) |
60(59.94Hz) |
50Hz |
帧时间(Frame period) |
1000.0/60.0(ms) |
1000.0/50.0(ms) |
CPU主频是通过对PPU主频进行12分频得来,而且前后的场消隐时的严格时序在某些游戏中也有用到。
在这个项目中由于PPU是使用的25Mhz的时钟频率,并且考虑到这里的6502CPU并不是严格的遵守NES所用CPU的指令时钟长度一点,我们设计了一个对clk的8分频器,并用这个作为cpu的clk
--divide
the clk to 3Mhz
process(clk,reset)
begin
if
reset='0' then
clk_3<="000";
elsif
clk'event and clk='1' then
clk_3<=clk_3+1;
end
if;
end
process;