6502单片机c语言,四十年前的 6502 CPU 指令翻译成 JS 代码会是怎样

前言

当时看到一本虚拟机相关的书,正好又在想 JS 混淆相关的事,无意中冒出个想法:能不能把某种 CPU 指令翻译成等价的 JS 逻辑?这样就能在浏览器里直接运行。

注意,这里说的是「翻译」,而不是模拟。模拟简单多了,网上甚至连 JS 版的 x86 模拟器都有很多。

翻译原则上应该在运行之前完成的,并且逻辑上也尽可能做到一一对应。

为了尝试这个想法,于是选择了古董级 CPU 6502 摸索。一是简单,二是情怀~(曾经玩红白机时还盼望能做个小游戏,后来发现 6502 不仅麻烦还早就过时了,还不如学 VB~)

273626-20170303120423735-1931639044.jpg

网上 6502 资料很多,比如这里有个 简单教程并自带模拟器,可以方便测试。

顺便再分享几个有趣的:

对于简单的指令,其实是很容易转成 JS 的,比如 STA 100 指令,就是把寄存器 A 写到地址空间 100 的位置。因为 6502 是 8 位 CPU,不用考虑内存对齐这些复杂问题,所以对应的 JS 很简单:mem[100] = A;

由于 6502 没有 IO 指令,而是通过 Memory Mapped IO 实现的,所以理论上「写入空间」不一定就是「写入内存」,也有可能写到屏幕、卡带等设备里。不过暂时先不考虑这个,假设都是写到内存里:var mem = new Uint8Array(65536);

同样的,读取操作也很简单,就是得更新标记位。为了简单,可以把状态寄存器里的每个 bit 定义成单独的变量:// SR: NV-BDIZCvar SR_N = false,

SR_V = false,

SR_B = false,

...

SR_C = false;

比如翻译 LDA 100 这条指令,变成 JS 就是这样:A = mem[100];SR_Z = (A == 0);SR_N = (A > 127);

类似的,数学计算、位运算等都是很容易翻译的。但是,跳转指令却十分棘手。

因为 JS 里没有 goto,流程控制能力只能到语块,比如 for 里面可以用 break 跳出,但不能从外面跳入。

而 6502 的跳转可以精确到字节的级别,跳到半个指令上,甚至跳到指令区外,将数据当指令执行。

这样灵活的特征,光靠「翻译」肯定是无解的。只能将模拟器打包进去,普通情况执行翻译的 JS ,遇到特殊情况用模拟解释执行,才能凑合着跑下去。退一步考虑

不过为了简单,就不考虑特殊情况了,只考虑指令区内跳转,并且没有跳到半个指令中间,也不考虑指令自修改的情况,这样就容易多了。

仔细思考,JS 能通过 break、return、throw 等跳出语块,但没有任何「跳入语块」的能力。所以,要避开跳入的逻辑。

于是想了个方案:把指令中「能被跳入的地方」都切开,分割成好几块:-------------

XXX 1               |  block 0  |

JXX L2  --.         |           |

XXX 2     |         |           |

L1:           | 

XXX 3     |   |     |  block 1  |

XXX 4     |   |     |           |

L2:         

XXX 5         |     |  block 2  |

XXX 6         |     |           |

JXX L1      --|     |           |

XXX 7               -------------

这样每个块里面只剩跳出的,没有跳入的。

然后把每个块变成一个 function,这样就能通过「函数变量」控制跳转了:var nextFn = block_0;   // 通过该变量流程控制function block_0() {

XXX 1

if (...) {          // JXX L2

nextFn = block_2;

return;

}

XXX 2

nextFn = block_1    // 默认下一块}function block_1() {

XXX 3

XXX 4

nextFn = block_2    // 默认下一块}function block_2() {

XXX 5

XXX 6

if (...) {          // JXX L1

nextFn = block_1;

return;

}

XXX 7

nextFn = null       // end}

于是用一个简单的状态机,就能驱动这些指令块:while (nextFn) {

nextFn();}

不过有些程序是无限循环的,例如游戏。这样就会卡死浏览器,而且也无法交互。

所以还需增加个控制 CPU 周期的变量,能让程序按照理想的速度运行:function block_1() {

...    if (...) {

nextFn = ...

cycle_remain -= 8   // 在此跳出,当前 block 消耗 8 周期

return

}

...

cycle_remain -= 12      // 运行到此,当前 block 消耗 12 周期}...// 模拟 1MHz 的速度(如果使用 50FPS,每帧就得跑 20000 周期)setInterval(function() {

cycle_remain = 20000;

while (cycle_remain > 0) {

nextFn();

}}, 20);

虽然函数之间切换会有一定的开销,但总比无法实现好。比起纯模拟,效率还是高一些。借助现成工具实现

不过上述都是理论探讨而已,并没有实践尝试。因为想到个更取巧的办法,可以很方便实现。

因为 emscripten 工具可以把 C 程序编译成 JS,所以不如把 6502 翻译成 C 代码,这样就简单多了,毕竟 C 支持 goto。

于是写了个小脚本,把 6502 汇编码转成 C 代码。比如:$0600  LDA #$01$0602  STA $02$0604  JMP $0600

变成这样的 C 代码:L_0600: A = 0x01; ...

L_0602: write(A, 0x02);

L_0604: goto L_0600;

事实上 C 语言有「宏」功能,所以可将指令逻辑隐藏起来。这样只需更少的转换,符合基本 C 语法就行:L_0600: LDA(0x01)

L_0602: STA(0x02)

L_0604: JMP(0600)

对应的宏实现,可参考这个文件:6502.h

对于「动态跳转」的指令,可通过运行时查表实现:jump_map:switch (pc) {    case 0x0600: goto L_0600;    case 0x0608: goto L_0608;    case 0x0620: goto L_0620;

...

}

然后再实现基本的 IO,可通过 emscripten 内置的 SDL 库实现。C 代码的主逻辑大致就是这样:void render() {

cycle_remain = N;

input();        // 获取输入

update();       // 指令逻辑(执行到 cycle_remain <= 0)

output();       // 屏幕输出}// 通过浏览器的 rAF 接口实现emscripten_set_main_loop(render);演示

我们尝试将一个 6502 版的「贪吃蛇」翻译成 JS 代码。20 06 06 20 38 06 20 0d 06 20 2a 06 60 a9 02 85

02 a9 04 85 03 a9 11 85 10 a9 10 85 12 a9 0f 85

14 a9 04 85 11 85 13 85 15 60 a5 fe 85 00 a5 fe

....

ea ca d0 fb 60

通过现成的反编译工具,变成 汇编码:$0600    20 06 06  JSR $0606

$0603    20 38 06  JSR $0638

$0606    20 0d 06  JSR $060d

$0609    20 2a 06  JSR $062a

$060c    60        RTS

$060d    a9 02     LDA #$02

....

$0731    ca        DEX

$0732    d0 fb     BNE $072f

$0734    60        RTS

然后通过小脚本的正则替换,变成符合 C 语法的 代码:L_0600: JSR(0606, 0600)

L_0603: JSR(0638, 0603)

L_0606: JSR(060d, 0606)

L_0609: JSR(062a, 0609)

L_060c: RTS()

L_060d: LDA_IMM(0x02)

....

L_0731: DEX()

L_0732: BNE(072f)

L_0734: RTS()

最后使用 emscripten 将 C 代码编译成 JS 代码:

273626-20160705203652499-1127679266.png

在线演示(ASDW 控制方向,请用 Chrome 浏览器)

当然,这种方式虽然很简单,但生成的 JS 很大。而且所有的 6502 指令对应的 JS 最终都在一个 function 里面,对浏览器优化也不利。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
单片机C语言时钟电路代码的四个按钮功能可以实现以下功能: 1. 按钮一:时间调整 通过按钮一来调整时钟的时间。当按下按钮一时,单片机检测到信号,并将当时间显示在液晶屏幕上。通过按下按钮一后,可以用其他按钮或旋钮进行上下、左右移动,以选择需要调整的时间位(例如小时、分钟等)。再次按下按钮一,则可以增加或减少选定的时间位数值。最后,通过按下按钮一来保存新的时间设置并退出调整模式。 2. 按钮二:时间启动/暂停 通过按钮二来控制时钟的启动和暂停。当按下按钮二时,单片机检测到信号,并切换时钟的运行状态。如果时钟处于停止状态,则按下按钮二可以启动时钟。如果时钟正在运行,则按下按钮二可以暂停时钟。再次按下按钮二,则可以切换时钟的状态。 3. 按钮三:闹钟设置 通过按钮三来设置闹钟的时间。当按下按钮三时,单片机检测到信号,并将当闹钟时间显示在液晶屏幕上。通过按下按钮三后,可以用其他按钮或旋钮进行上下、左右移动,以选择需要调整的闹钟时间位。再次按下按钮三,则可以增加或减少选定的时间位数值。最后,通过按下按钮三来保存新的闹钟时间设置并退出闹钟设置模式。 4. 按钮四:闹钟启动/关闭 通过按钮四来控制闹钟的启动和关闭。当按下按钮四时,单片机检测到信号,并切换闹钟的状态。如果闹钟处于关闭状态,则按下按钮四可以启动闹钟。如果闹钟正在响铃,则按下按钮四可以关闭闹钟。再次按下按钮四,则可以切换闹钟的状态。 以上是单片机C语言时钟电路代码中的四个按钮功能的简要描述,实际的实现细节还需要根据具体的硬件平台和需求进行编写。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值