起因

系列第二篇:上一篇把 cc65 环境装好了,这一篇跑通第一个程序——在 FC/NES 上显示 Hello World,顺便看清 cl65 一条命令背后做了什么、为什么 NES 程序“不能退出”。

需求

  • 记事本编辑 helloworld.c,编译生成 helloworld.nes 文件
  • 用模拟器打开 .nes,验证屏幕上显示 Hello World

技术实现原理

cl65 一条命令背后做了什么

cc65 是“多目标”编译器套件:-t nes 决定用哪套头文件、启动代码(crt0)和 C 运行库。cl65 是驱动程序,一条命令实际串起三步:cc65 把 C 编译成汇编 → ca65 汇编成目标文件 helloworld.o → ld65 按 nes.cfg 的段映射做链接,最后拼出 .nes 文件(iNES 头 + PRG 程序段 + CHR 图案段)。这就是下文截图里“先生成 helloworld.o、再生成 helloworld.nes 文件”的原因——.o 是中间目标文件,.nes 才是模拟器认的最终镜像。

为什么是 cprintf 而不是 printf

NES 上没有操作系统,也就没有“标准输出”这个设备,printf 依赖的 stdio 文件抽象在这里无处安放。cc65 的 conio(控制台 I/O)库直接面向屏幕:cprintf 把字符写到文本层——具体说就是 PPU 背景命名表(nametable)里光标所在的格子。少一层抽象,更贴近 8 位机的现实。

为什么必须死循环

while(1){} 不是装饰。C 程序里 main 返回后要把控制权交回操作系统,而 NES 没有操作系统可交——启动代码跑完 main 之后,执行流落到未初始化的 ROM 区域,行为未定义,常见结果就是屏幕一片漆黑。这就好像执行一个 bat 最后不加 pause,窗口一闪就没——程序“结束”了,但机器总得跑点什么。for(;;);while(1){} 等价,都是原地循环不退出。

6502 上的类型长度

后面 helloacm 的例子打印了各类型的 sizeof:char 是 8 位、int/short 是 16 位、long 是 32 位——跟 PC 上 int 32 位不一样。C 标准只规定 char ≤ short ≤ int ≤ long 的相对关系,具体长度由编译器按平台定:6502 是 8 位机,寄存器一次只能算 8 位,但 0~255 的计数范围太小(放个地址都不够),int 取 16 位是编译器在效率和可用性之间的折中。

代码实现

记事本编辑文件 helloworld.c

Helloworld.c代码

1
2
3
4
5
6
#include <conio.h>
void main()
{
    cprintf("Hello World!");
    while(1){}
}

代码说明

  • 备注说明:正常的C语言使用的是 printf 但是CC65使用的是 cprintf 用来在屏幕显示字符串——原理见上文“为什么是 cprintf”;
  • while是为了让程序进入死循环不会退出,如果你想看看没有最后的死循环是啥样的,结果就是屏幕一片漆黑,这就好像你执行一个 bat 最后不加 pause 的话都是一闪就没一样的意思;
  • 另外让程序进入死循环的还有 for(;;); 这个也是不停的循环没有退出。

编译

cmd进入上面编辑的文件夹中 执行下面的命令就可以生成helloworld.nes文件

cmd 命令

1
cl65 -t nes -o helloworld.nes helloworld.c  

用模拟器打开生成的nes文件就可以看到结果了

从上面的文件我们可以看到
先生成的文件是 helloworld.o 然后才是生成 helloworld.nes 文件
用过c的这些应该都了解,我这个对c一知半解的就不多说了

下面再放上两段 helloacm.com 看来的代码
https://helloacm.com/category/6502/

C代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include "conio.h"
void main(){
unsigned int s = 0;
int i = 0;
cprintf("6502-C Compiler. \r\nhttp://HelloACM.com\r\n");
cprintf("Size Of int = %d\r\n", sizeof(int));
cprintf("Size Of long = %d\r\n", sizeof(long));
cprintf("Size Of char = %d\r\n", sizeof(char));
cprintf("Size Of float = %d\r\n", sizeof(float));
cprintf("Size Of double = %d\r\n", sizeof(double));
cprintf("Size Of short = %d\r\n", sizeof(short));
for (i = 0; i <= 100; i ++) {
s += i;
}
cprintf("Sum = %d\r\n", s);
for(;;); // loop forever, never ends
}

这里是为了让学习者了解 int char float double short 长度,跟java还有其他语言不一样的地方,也是因为这个 6502的8位芯片的限制
下面是运行截图
补充 long 是4长度 我就不再截图了

C代码

1
2
3
4
5
6
7
8
#include "conio.h"
void main(){
unsigned int i;
for (i = 1; i < 256; i ++) {
cprintf("%c", (char)i);
}
for(;;); // loop forever, never ends
}

上面这个代码会在屏幕显示ASCII 看下面运行截图

通过上面的代码,现在你可以试着通过学过的其他语言试试你想要运行的东西
后面的内容还在整理,摇杆的获取同屏幕显示的互动 上上下下左右左右BABA start

踩坑记录与注意事项

  • 忘了 -t nes:目标错成默认机器的话,链接的是别的机器的启动代码和运行库,模拟器要么不认要么黑屏——-t 参数同时决定头文件、启动代码、运行库三件套;
  • 删掉 while(1){} 会黑屏:main 返回后没有操作系统接手,执行流跑飞(原理见上文),NES 程序永远不能“正常退出”;
  • 换行要用 \r\n:conio 的 \n 只把光标垂直下移、不回到行首,要 “\r\n” 组合才能换到下一行行头(DOS 时代同款约定);
  • 改了 .c 要重新编译:模拟器打开的是 .nes 镜像,不会热加载源码改动;
  • 打印超过 0x7F 的“ASCII”:NES 字符集里那些编码不是 ASCII,会显示成图案块——第二段代码打印 1..255 时后半段看到的就是这些。