6502CPU以及NES游戏机系统

一、6502CPU的介绍,以及NES系统的工作原理

 

6502CPU是曾经非常著名的一款8CISC CPU,曾用于Apple1&2FC(NES)等系统,如今在文曲星等电子产品中依然被广泛使用。其操作码长度为8位,指令长度则为13个字节,65023个寄存器,栈指针,标志位(P)和计数器。寄存器是累加器(A)X变址寄存器,和Y变址寄存器,每一个都是8位的,大多数指令把结果留在累加器里;栈指针是一个8位的寄存器用来指示栈(1 页面)的偏移量,当使用pushpull指令时它自动增加和减少,你也可以用TSXTXS指令直接访问和修改它的值。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 indexX变址寄存器比较

X-M-->NZC

NZC

CPY

compare with Y indexY变址寄存器比较

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

TAY

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

注释:

  1. 这个BIT指令拷贝bit 6V标志,bit 7N标志(例外的是在直接寻址模式的时候V & N没有触及)。累加器和操作数进行与操作 ,Z标志将被适当地设置。(的说明:测试数如01…80应先被装入累加器,该指令应该不改变累加器)
  2. 这个BCC & BCS指令:指令分别对应于BLT (大于时跳转) BGE (小于时跳转)

 

 

6502的寻址模式

一共有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]
绝对变址寻址联合XY变址寄存器使用,以绝对, X”绝对, Y”的形式出现。有效地址由XY的内容加上指令的第二或第三字节包含的地址得到。这种模式允许变址寄存器包含变址或计数值而指令包含基址。这种寻址模式允许定位任何位置并且可以用变址修改多个地方,用以减少代码和指令时间。

0页面变址寻址 [0页面,X 0页面,Y] Zero Page Indexed Addressing [Zero Page,X or Zero Page,Y]
0
页面变址寻址联合XY变址寄存器使用,以“0页面, X”“0页面, Y”的形式出现。有效地址由指令的第二个字节加上变址寄存器的内容得到。因为这是一个“0页面寻址,所以第二字节的内容定位于0页面。另外,因为“0页面寻址模式的特性,所以不会有进位加到高位内存,也不会发生跨页边界的问题。

相对寻址 [相对] Relative Addressing [Relative]
相对寻址只用在转移(branch)指令指令中;它指定了条件转移的目标。指令的第二个字节变成一个操作数,作为偏移加到指向下一条指令的指令指针上。偏移范围从-128127字节,相对于下一条指令。

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的介绍(来自百度百科):

 

NESNintendo Entertainment System的缩写,是Nintendo20世纪80年代和20世纪90年代发售的一种家庭主机,俗称红白机。NES是此类游戏机在欧洲发行版本的缩写,在亚洲发行的游戏机型缩写为FCFamily Computer)又写作Famicom。在该游戏平台上比较著名的游戏有《Contra》,《Super Mario》等等。

 

主机性能:

  中央处理器: 6502 CPU

  二进制数值: 8Bits

  运行频率: NTSC1.7897725 Mhz PAL1.773447 Mhz

  内部储存器: 8KB6KB显示存储器+2KB镜像存储器。

  图像处理器: 64种颜色,除去重复的颜色剩下52种颜色,最大显示数:16种颜色。

  声音处理器: 矩形声波处理器两个,三角型声波处理器一个,噪音处理器一个,PCM数字声音发生器一个。

  硬件接口: 游戏卡带接口×1,游戏手柄接口×2。电源接口×1RF视频线接口×1AV视频线接口×1,周边设备接口×1

  游戏载体: Rom卡带。

  游戏卡带容量: 24KB40KB48KB64KB80KB128KB160KB256KB

  周边设备: ZapperRobot

 

 

 

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

中断:
65023个中断IRQ/BRKNMIRESET,每个中断都有一个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

图案表0256x2x8,可能是VROM

$0FFF

$1000

图案表1256x2x8,可能是VROM

$1FFF

$2000

命名表032x30块)(镜像,见命名表镜像)

$23BF

$23C0

属性表0(镜像,见命名表镜像)

$23FF

$2400

命名表132x30块)(镜像,见命名表镜像)

$27BF

$27C0

属性表1(镜像,见命名表镜像)

$27FF

$2800

命名表232x30块)(镜像,见命名表镜像)

$2BBF

$2BC0

属性表2(镜像,见命名表镜像)

$2BFF

$2C00

命名表332x30块)(镜像,见命名表镜像)

$2FBF

$2FC0

属性表3(镜像,见命名表镜像)

$2FFF

$3000

$2000-$2EFF的镜像

$3EFF

$3F00

背景调色板#1

$3F0F

$3F10

精灵调色板#1

$3F1F

$3F20

镜像,(见调色板镜像)

$3FFF

$4000

$0000-$3FFF的镜像

$7FFF

命名表:
NES的图像通过Tile矩阵来显示,这个网格就叫命名表。一个命名表和字符模式下的屏幕缓冲比较相象,它包含字符的代码,也就是30列的32Byte长度。每个Tile8x8个象素,每个命名表有32x30Tile,也就是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卡带都带有水平和垂直镜像,允许你改变命名表指向PPUVRAM位置。这种方式同时影响两个命名表,你不能单独改变其中的一个。每个卡带都控制着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 VRAM4个命名表物理上独立的

1

1

单屏幕

$X00

$X00

$X00

$X00

所有的命名表指相同的VRAM区域,X=048C

0

0

VROM镜像

Mapper 68#游戏映射VROMPPU VRAM的命名表,这使得命名表是基于VROM的,你不能写它但却可以通过mapper自己来控制是否使用这种特性

 

 

图案表:
图案表储存了实际8x8象素的Tile,同时也储存了用来指向NES调色板全部16种颜色的4位元矩阵的低两位。PPU支持两个图案表在$0000$1000。他们有以下格式:

VRAM地址

图案表内容

颜色效果

$0000
..
..
..
..
..
..
$0007

%00010000 = $10
%00000000 = $00
%01000100 = $44
%00000000 = $00
%11111110 = $FE
%00000000 = $00
%10000010 = $82
%00000000 = $00

0

...1....
..2.2...
.3...3..
2.....2.
1111111.
2.....2.
3.....3.

$0008
..
..
..
..
..
..
$000F

%00000000 = $00
%00101000 = $28
%01000100 = $44
%10000010 = $82 
%00000000 = $00
%10000010 = $82 
%10000010 = $82
%00000000 = $00

1

点表示0号颜色,数字表示实际调色板颜色代号

注意在图案表里存储的是每个点的2个位。其他两个由属性表得到。所以,在屏幕上总体出现的颜色数是16,而每个块里只有4种颜色。

属性表:
每个命名表有它自己的属性表。属性表的每一个字节代表了屏幕上的一组4x4Tile,一共有8x8个字节。有几种方法来描述属性表里一个字节的功能:
    *
保存32x32象素方格的高2位颜色,每16x16象素用2位;
    *
保存168x8 Tile的高2位颜色;
    *
保存44x4 Tile格子的高2位颜色。
看以下两个图表帮助理解:
1
、一个16x16 象素的格子:#0-F 代表了一个8x8 Tile,方块 [x] 代表了48x8 Tile,

方块 0

#0

#1

#2

#3

方块 1

#4

#5

#6

#7

方块 2

#8

#9

#A

#B

方块 3

#C

#D

#E

#F

2、属性表一个字节的实际格式定义如下(对应于上面的例子):

描述

01

方块 0的高两位颜色(Tile #0,1,2,3)

23

方块 1的高两位颜色(Tile #4,5,6,7)

45

方块 2的高两位颜色(Tile #8,9,A,B)

67

方块 3的高两位颜色(Tile #C,D,E,F)

调色板:
NES
有两个调色板,背景调色板和精灵调色板。调色板不包含实际的RGB值,它们更象一个索引表。写到$3F00-$3FFFD6D7字节被忽略。

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-$3FFFD6D7字节被忽略。

背景滚动:
NES可以通过预提取命名表,图案表和属性表来使背景滚动,背景是独立于精灵而位于最下层的。可以水平和垂直滚动。

水平滚动

垂直滚动

0       512

A

B

A

0


480

B

命名表A通过$2000D1-D0指定,B是跟在后面的一个命名表,根据镜像不同B是动态的。这个不能工作在水平和垂直同时滚动的游戏里。背景会跨越多个命名表,如下所示:

命名表#2
($2800)

命名表#3
($2C00)

命名表#0
($2000)

命名表#1
($2400)

$2005里写到水平滚动的值可以从0256,写到垂直滚动的值从0239239是考虑了负值的结果,例如248代表-8

屏幕和精灵分层:
下面是NES画图的循序:

前台

 

 

 

后台

CI

OBJs 0-63

BG

OBJs 0-63

EXT

 

 精灵RAM 
BGPRI==0

 

 精灵RAM
BGPRI==1

 

CI代表颜色亮度,相当于$2001D7-D5BG是背景;EXT是扩展口的图像信号。BGPRI代表VRAM里背景优先权位,每个精灵都有的,即第二字节的D5位。OBJ数代表了实际精灵的号码,不是Tile索引值。前台高于任何其他层,最后被画上,后台低于任何其他层,最先被画上。

精灵和精灵RAM
NES用一个页面(256字节)来存放动画,每个精灵4个字节,一共可以有64个动画/精灵,它们可以是8x88x16象素。动画/精灵图案被存储在VRAM的图案表其中一个里面。精灵属性,例如翻转和优先权被储存在一个特殊的256字节的精灵RAM,它不是CPUPPU的地址的一部分。整个精灵RAM可以通过$4014DMA方式来写,写一个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/8256/16次。在一个真实的NES芯片里,如果精灵被禁止($2001D40)很长一段时间,精灵数据会渐渐消失。可以理解为精灵RAM是一个DRAMD4控制了DRAM的周期刷新信号。

碰撞标志:
碰撞标志是PPU状态寄存器($2002)的第6位(D6)。PPU能够找出0号精灵的位置,然后设置D6,它是这样工作的:PPU扫描出背景图案象素和精灵象素同时不是透明的第一个地方。比如,屏幕的背景是一个非透明颜色(颜色号>0)0号精灵的坐标是(1234),它只是在第4行的开始才有象素,那么,碰撞标志在屏幕刷新到(1237)时才被设置。要记住,0号颜色被定义为透明。引起D6被设置的象素必须是已经被画出来的。下面的例子能帮助理解,这是两个Tile,下划线表示透明的0号颜色,*号表示碰撞标志被设置。

精灵

+

 背景

=

结果

‘*’会用2号颜色画出

 __1111__
_111111_
11222211
112__211
112__211
11222211
_111111_
__1111__

________
_______2
______21
_____211
____2111
___21111
__211111
_2111111

__1111__
_1111112
11222211
112__*11
112_2211
11222211
_1111111
_2111111

这个例子说的是背景+精灵,但是对于靠近背景的精灵也通用,即精灵+精灵,通过设置背景优先权来实现。D6不可以通过读PPU的状态来复位,只在每次VBlank之后被清零。Hit标志可以用在水平或者垂直屏幕分割的时候,还有许多好玩的效果。

水平和垂直消隐:
所有的游戏机都有一个刷新动作,用来重新定位电子枪显示可见的数据。最通用的显示设备是电视机,它分为每秒刷新60次的NTSC制式和50次的PAL制式。电子枪从左到右画出象素,它每次只能画一条扫描线,画下一条之前要先回到左面并且做好准备,这之间有一段时间叫做水平消隐(HBlank)。在画完全部256条扫描线之后它又回到屏幕左上角准备下一次画屏幕(帧),这之间的一段时间就是垂直消隐(VBlank)。电子枪就是一个不断的走字形的过程。VBlank标志就是$2002D7,它表明PPU是否在VBlank期间,当VBlank标志存在时,你就可以通过$2006$2007访问PPU内存。一个程序可以通过读$2002来使D7复位。在屏幕刷新期间,我们不能访问PPU,而PPU会在CPU背后修改VRAM指针,这样我们很容易在写入VRAM时出错,为了让PPU停下来,可以对$2000$200100

访问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 $DDVRAM的增量是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的地址

描述

04

Tiles的水平偏移量(例如1 = 8象素);水平位置=水平偏移量×8

59

Tiles的垂直偏移量(例如1 = 8象素);垂直位置=垂直偏移量×8

AB

命名表的号码($2000,$2400,$2800,$2C00)

CD

附加的垂直偏移量,单位:象素(0..3);扫描线=垂直偏移量+附加的垂直偏移量

EF

00

 

 

 

I/O端口

端口是预分配好的可访问地址,程序通过端口和PPUAPU交换信息。每个端口都是16位的

项目中还没有实现的IO寄存器部分用红色表示

端口

读写/

功能描述

$2000

读写

PPU控制寄存器 1

 

0-1

命名表地址:

%10 ($2800)

%11 ($2C00)

%00 ($2000)

%01 ($2400)

请记住,因为镜像,只有2个真实的命名表,而不是4个;
当滚动超出当前命名表时PPU会自动切换到另一个命名表

2

垂直写, 1VRAM地址以32递增:

命名表, VW0

命名表, VW1

——>  

|
|
V

3

精灵图案表地址, 1$1000, 0$0000

4

屏幕图案表地址, 1$1000, 0$0000

5

精灵尺寸, 18x16, 08x8

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

D00:背景颜色,% 000=黑,%001=蓝,%010=绿,%100=红;不用混合使用,因为可能会损坏PPU硬件
D01:颜色亮度,% 000没效果,%00 1强化蓝色,%010强化绿色,%100强化红色,不要混合使用

$2002 

PPU状态寄存器:读取后$2005$2006被复位,下一个写到$2005的数据是水平的,写到$2006的数据是高位

 

0-3

未知(???)

4

VRAM写标志:0=写有效,1=写忽略

5

扫描线精灵计数:0=当前扫描线精灵个数小于等于8个,1=大于8

6

命中标志:1=精灵刷新碰到了#0精灵。在屏幕刷新状态,这个标志被复位为0

7

Vblank标志:1 PPUVblank状态。当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设置的地址在每次访问之后会增加132

$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=声音使能,

57

未使用 (???)

6(只读)

垂直时钟信号IRQ可用:01帧存在,因为没有IRQ11帧被IRQ中断

$4016

读写

手柄1+选通

 

0

读:手柄1数据;写:选通,1=复位,0=清除;扩展端口:0=写,1=读

1

手柄1 存在, 0=连接

3

光枪精灵检测:1=瞄准

4

光枪扳机:0=按下,1=松开

257

未知,67设为%10(???)

$4017

读写

手柄2+ 选通

 

0

读:手柄2数据;写:选通,1=复位,0=清除;扩展端口:0??1=读

1

手柄2 存在, 0=连接

25

未使用(???)

3

光枪精灵检测:1=瞄准

4

光枪扳机:0=按下,1=松开

6

垂直时钟信号(内部),0=存在,$4016D6受影响;1=不存在$4016D6不可触及

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

图案表0256x2x8,可能是VROM

$0FFF

$1000

图案表1256x2x8,可能是VROM

$1FFF

$2000

命名表032x30块)(镜像,见命名表镜像)

$23BF

$23C0

属性表0(镜像,见命名表镜像)

$23FF

$2400

命名表132x30块)(镜像,见命名表镜像)

$27BF

$27C0

属性表1(镜像,见命名表镜像)

$27FF

$2800

命名表232x30块)(镜像,见命名表镜像)

$2BBF

$2BC0

属性表2(镜像,见命名表镜像)

$2BFF

$2C00

命名表332x30块)(镜像,见命名表镜像)

$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-$3FFFD6D7字节被忽略。

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信号

 

 

 

 

 

 

CPUPPU时序:

 

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的指令时钟长度一点,我们设计了一个对clk8分频器,并用这个作为cpuclk

 

 

--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;