dp901 单片机模拟器

Published 2026-08-29 00:00 4226 words 22 min read

This post is not yet available in English. Showing the original.
这篇文章介绍了 Arceus 901 这是一个实验性的开源 dp901 单片机模拟器 可在 Windows 控制台中模拟 LED 指示灯、数码管、按键及 dp901 操作码的运行 能够较真实地还原 dp901 单片机的操作逻辑

dp901 单片机模拟器

Arceus 901 是一个实验性的开源 dp901 单片机模拟器 可以在 Windows 控制台中模拟 LED 指示灯 数码管 按键和 dp901 操作码的运行

并非嵌入式项目 只是一个小软件罢了

效果图
效果图

前言

项目地址

初一的时候在信息课上用这个dp901单片机 然后进了单片机社团还用这玩意

这单片机手动输入代码慢的一批 按钮还特别膈手

程序有错误更是难以调试debug 只能一个地址一个地址去核对 但凡少了一个命令后面的都要重新输入

因此耗费一晚上写了这个模拟器 希望能造福有缘人

主播在初一写的这个垃圾程序 看似跑起来没bug但实际上有很多问题 release里的勉强能跑 实际上跑满所有cpu核心只是小问题 建议sleep全部改成50来修复 还有各种条件冲突什么的问题 建议出bug之后保存代码重启

程序起名方式也是非常的中二

模拟器运行方法

运行环境

Arceus 901 使用 Win32 API 绘制控制台界面和读取键盘 目前仅支持 Windows

从源码编译时需要支持 C11 或更高版本的 C 编译器和 Windows SDK 源码依赖 Head.h 与 lib/Console.h

仓库中没有附带 Visual Studio 工程文件 可以新建一个 Windows 控制台项目并加入 main.cpp 和 main.rc 后进行编译

加载操作码

模拟器会从程序当前工作目录中的 code.txt 读取操作码

code.txt 中的每一个十进制整数都会占用一个地址 数值之间可以使用空格或换行分隔 为了方便查看地址 建议每行只写一个两位数

00
00
02
09
00
01
00
15

上面第 1 行的地址为 00 第 2 行的地址为 01 以此类推 地址从 00 开始计算

00 和 0 读取后的数值相同 前导零只用于让代码更容易阅读

操作码和它的每一个参数都会分别占用一个地址 跳转地址必须指向操作码 不能指向某条指令的参数

如果启动时没有找到 code.txt 模拟器会建立一个空文件 写入代码后需要重新启动模拟器

模拟器按键

D 保存

按键编号为 10

在编辑状态中将当前显示的数据写入当前内存地址并请求保存到 code.txt

当前版本对文件末尾的计算存在偏差 新增的最高地址可能不会被写入文件 需要持久保存新代码时建议直接编辑 code.txt

R +1

按键编号为 11

在数据状态中将当前数据加 1 按住 I 时则将当前地址加 1

F -1

按键编号为 12

在数据状态中将当前数据减 1 按住 I 时则将当前地址减 1

I 数据/地址

按键编号为 13

与 R 或 F 一起使用时切换当前编辑地址

J 执行/加载

按键编号为 14

在编辑状态中进入加载界面 再按一次 J 会从地址 00 开始执行代码 按 K 则返回编辑状态

K 确认

按键编号为 15

保存当前数据并移动到下一个地址

N 复位

复位键编号为 100

停止当前操作并让模拟器回到待机状态 复位键不能作为 08 和 09 的检测参数

操作码格式

本文使用下面的形式表示操作码语法

操作码 <参数1> <参数2>

尖括号中的内容是参数 实际写入 code.txt 时只需要写十进制数值 不需要写尖括号或参数名称

例如 00 <灯号> 写成下面的内容表示点亮编号 03 的 LED

00
03

LED 与数码管操作码

00 <灯号>

点亮指定的 LED 灯

灯号 的有效范围为 00-07

启用 05 02 数码管段操作模式后 这条指令还会同时点亮第一位数码管中相同编号的段

01 <灯号>

熄灭指定的 LED 灯

灯号 的有效范围为 00-07

启用 05 02 数码管段操作模式后 这条指令还会同时熄灭第一位数码管中相同编号的段

05 <模式>

设置数码管的显示模式

05 00

停止更新数码管显示 同时关闭数码管段操作和计数器显示同步

这项操作不会清空 digital 中已经点亮的段 因此数码管可能保持执行前的画面

05 01

开启数值显示

这项操作只会将显示状态设为开启 不会主动关闭之前由 05 02 开启的数码管段操作和计数器显示同步 如果需要从 05 02 完整切换到普通数值显示 应先执行 05 00 再执行 05 01

05 02

开启数码管段操作和计数器显示同步

开启后 00 与 01 会在操作 LED 的同时控制第一位数码管的一个段 04 06 07 会把计数器数值同步显示到右侧两位数码管

第一位数码管的段编号如下

  00
05  01
  06
04  02
  03  07(小数点)

时间与声音操作码

02 <时间> <单位>

暂停程序并在普通数值显示模式下显示倒计时

时间 建议使用 00-99 单位 支持下面的数值

00 = 100 毫秒
01 = 1 秒
02 = 1 分钟
03 = 1 小时

当前代码会从 时间 倒数到 0 因此实际会等待 时间 + 1 个单位

例如 02 09 00 会执行 10 次 100 毫秒等待 实际等待约 1 秒

使用 05 02 开启计数器显示同步后 02 仍然会等待 但不会把倒计时写入数码管显示值

03 <参数1> <参数2>

声音操作码的预留位置

当前版本没有实现声音播放 两个参数不会被读取 但执行时仍会跳过两个地址 因此必须保留两个占位参数

03
00
00

17

尝试将当前指令周期补足到约 10 毫秒 用于稳定操作码执行速度

这条操作码没有参数 当前周期已经超过 10 毫秒时减法会得到负值并传给 Sleep 可能造成异常的长时间等待

计数器操作码

模拟器内部有一个计数器 cnt 执行程序时计数器初始值为 0 按正常的两位非负参数使用时范围为 00-99

04 <数值>

将计数器设置为指定数值

数值 的有效范围为 00-99

06 <数值>

将指定数值加到计数器

结果大于 99 时会被限制为 99

07 <数值>

从计数器减去指定数值

使用非负参数时 结果小于 0 会被限制为 0

06 和 07 不校验参数 推荐只使用 00-99 的非负数 如果通过手工编辑 code.txt 写入负数 单向限幅可能使计数器超出 00-99

11 <比较值> <地址>

比较计数器并进行条件跳转

当 cnt >= 比较值 时跳转到指定地址 否则继续执行下一条操作码

比较值 的有效范围为 00-99 地址 必须指向已有的操作码

11
10
30

上面的代码表示当计数器大于或等于 10 时跳转到地址 30

按键判断操作码

按键判断仅支持编号 10-15 分别对应 D R F I J K

08 <按键编号> <地址>

检测指定按键是否处于按下状态

按键没有按下时跳转到指定地址 按键已经按下时继续执行下一条操作码

这条指令可以理解为 按下则继续 否则跳转

08
11
20

上面的代码表示按下 R 时继续执行 没有按下 R 时跳转到地址 20

09 <按键编号> <地址>

检测指定按键是否处于按下状态

按键已经按下时跳转到指定地址 按键没有按下时继续执行下一条操作码

这条指令可以理解为 按下则跳转 否则继续

09
15
40

上面的代码表示按下 K 时跳转到地址 40

按键状态由模拟器实时读取 如果需要等待按键 可以让 08 在按键未按下时跳回自身地址

跳转与子程序操作码

10 <地址>

无条件跳转到指定地址

10
00

上面的代码会让程序从地址 00 重新开始执行

12 <地址>

调用指定地址处的子程序

模拟器会记住 12 参数后面的地址 再跳转到指定子程序 子程序可以使用 13 返回

12
50

当前版本只保存一个返回地址 不支持嵌套调用 在子程序中再次执行 12 会覆盖原来的返回地址

13

从子程序返回到最近一次 12 后面的操作码

这条操作码没有参数 必须先执行过 12 才能正常返回

系统操作码

14 02 00

清除全部操作码

执行时数码管会显示 14 LED 会闪烁 随后模拟器把代码内存清零并停止运行 保存线程会将清零后的内容写回 code.txt

当前版本只实现了参数组合 02 00 其他 14 参数组合没有功能

这项操作会修改 code.txt 使用前请自行保留代码副本

15

停止程序并返回编辑状态

执行时全部 LED 会短暂闪烁并进入编辑状态 code_15 会先把当前地址设为 00 但执行循环随后还会统一递增一次 因此内部 pos 最终为 01

16

当前版本没有实现 16 操作码

执行循环也不会对未知操作码报告错误 未实现或未知的数值会被当作占用一个地址的空操作直接跳过 不应依赖这个行为编写程序

666

开发调试操作码 执行时弹出内容为 OK 的消息框

这不是 dp901 的正式操作码 不建议在程序中使用

操作码示例

LED 闪烁

下面的代码会让 00 号 LED 亮约 1 秒 再熄灭约 1 秒 然后重新开始

地址  数值  说明
00    00    点亮 LED
01    00    LED 编号
02    02    等待
03    09    时间 9 实际执行 10 个单位
04    00    单位 100 毫秒
05    01    熄灭 LED
06    00    LED 编号
07    02    等待
08    09    时间 9 实际执行 10 个单位
09    00    单位 100 毫秒
10    10    无条件跳转
11    00    跳转到地址 00

实际写入 code.txt 的内容为

00
00
02
09
00
01
00
02
09
00
10
00

按键等待

下面的代码会一直等待 K 键 按下后点亮 01 号 LED 并停止运行

地址  数值  说明
00    08    K 没有按下时跳转
01    15    K 的按键编号
02    00    跳回地址 00
03    00    点亮 LED
04    01    LED 编号
05    15    停止运行

当前版本限制

03 声音操作码尚未实现

12 和 13 只有一个返回地址 因此不能嵌套调用子程序

12 的目标校验存在实现错误 代码检查的是返回地址处保存的数值而不是目标地址 因此可能拒绝有效调用 也可能接受不存在的目标地址 编写程序时除了确保目标指向已有操作码 还应确保 12 后面下一条操作码的数值不大于程序末地址

编辑面板只允许在 00-99 范围内输入数据并在 00-199 范围内切换地址 但保存循环使用不包含末尾的 i < all 当前最高地址可能不会写入 code.txt 地址 199 也不能可靠地作为最后一个地址保存 更长或包含其他数值的代码应直接编辑 code.txt

模拟器读取操作码后不会自动检测参数数量 错误的参数会让后续数值被当作操作码执行

程序使用多个线程直接共享显示 按键和运行状态 运行结果以当前源码行为为准

BUG

由于本项目是作者在初一的时候写的 当时技术实力较差 有许多隐藏的BUG 以下由 gpt-5.6-sol 总结

严重问题

  1. 多个线程存在数据竞争

main.cpp

同时启动 6 个线程,但 pos、all、code、key、light、digital、is_run 等共享变量都不是 atomic,也没有使用 mutex。

例如执行线程和编辑线程会同时修改 pos,保存线程可能在编辑线程修改 code 时读取它。按照 C++ 标准,这属于未定义行为,可能表现为随机跳转、保存内容损坏或偶发崩溃。

  1. 读取超长 code.txt 会越界写内存

main.cpp

while (in >> code[i++]);

没有检查 i < NR。当文件包含超过 10010 个整数时,会写出 code 数组边界,造成内存破坏。

  1. 程序长度计算多算了一个地址

main.cpp

中,即使最后一次读取失败,i++ 仍然会执行,因此 all 最终等于元素数量,而不是最后一个有效地址。

main.cpp

又使用:

if (pos > all)

假设文件有 10 个数,有效地址是 0–9,但 all 会是 10,模拟器还会执行不存在的地址 10。由于数组预先清零,这通常会被误认为 00 操作码,并继续读取地址 11 作为灯号。

  1. 跳转地址可能导致负下标或越界

10 和 11 只检查目标是否大于 all,没有检查负数:

- main.cpp:1128
- main.cpp:1198

08 和 09 连上界都没有检查:

- main.cpp:1214
- main.cpp:1229

例如目标地址为 -1 时,后续可能访问 code[-1];目标很大时则可能访问数组外部。

  1. 12 子程序调用检查了错误的数据

main.cpp

if (code[pos + 1] > all)

此时真正的目标地址是 code[pos],但代码检查的是调用指令后面那条操作码的数值。

  1. 结果是:
- 有效的目标地址可能被错误拒绝。
- 不存在的目标地址可能通过检查。
- 12 后面的操作码数值较大时会莫名报错。

6. 17 可能让程序长时间卡死

main.cpp

Sleep(10 - (clock() - begint));

如果当前周期已经超过 10 毫秒,结果会是负数。Sleep 接收无符号整数,负数转换后会变成非常大的等待时间,程序可能看起来永久卡死。

功能与保存问题

  1. 保存代码存在末尾丢失问题

保存循环使用 i < all:

main.cpp

编辑器却只执行:

all = max(all, pos);

main.cpp

当在新的最高地址写入数据时,该地址可能不会被写入 code.txt。此外,保存线程和编辑线程之间存在竞争,保存请求也可能被覆盖。

  1. 多个无限循环会持续占用 CPU

save、run 和 system_key 等线程在空闲状态下仍然不停轮询:

- main.cpp:255
- main.cpp:1001
- main.cpp:1286

特别是 save() 完全没有 Sleep 或条件变量,可能长期占满一个 CPU 核心。显示线程的帧率等待计算在 main.cpp

也缺少 beginClock,基本无法正确限帧。

  1. 执行 15 后地址实际变成 01

code_15() 把 pos 设为 0:

main.cpp

但执行循环之后无条件执行 pos++:

main.cpp

所以停止后内部地址最终是 1,不是预期的 0。

  1. 计数器可以超出 00–99

06 和 07 没有验证参数:

- main.cpp:1159
- main.cpp:1165

例如 06 -1 可以使计数器变成负数,07 -1 可以使计数器超过 99;极端整数还可能产生有符号溢出。

  1. 数码管关闭不会清空已有画面

05 00 只关闭状态标志,没有清空 digital:

main.cpp

因此数码管可能继续显示关闭前的内容。05 01 也不会清除 05 02 设置的 digit 和 show_c,模式之间会残留状态。

  1. 硬编码使用 D: 盘

main.cpp

将首次运行标记写入 D:\Data\Arceus\Run.Ceh。没有 D: 盘或没有写权限时,创建会失败且没有错误处理,首次运行提示可能每次启动都出现。

If you enjoyed this, leave a comment~