summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDrew DeVault <sir@cmpwn.com>2014-11-30 13:36:26 -0700
committerDrew DeVault <sir@cmpwn.com>2014-11-30 13:36:26 -0700
commit3d7eeb1ecd287aee9179ea6b818f07fbe0b38780 (patch)
treefe498091ad1167b0b5e6b510b6bcce532b62e1b5
parentb1f903b7ffd59d143957848c8ad04e6478f8706d (diff)
Add crazy cool blog post
-rw-r--r--.gitmodules3
m---------OpenTI0
-rw-r--r--_layouts/post_toolchain.html27
-rw-r--r--_posts/2014-11-30-Porting-an-entire-toolchain-to-the-browser-with-emscripten.md326
-rw-r--r--css/base.css8
-rw-r--r--css/toolchain.scss40
-rw-r--r--js/ide_emu.js150
-rw-r--r--js/toolchain.coffee244
-rw-r--r--scas.data533
-rw-r--r--sources/corelib-hello.asm36
-rw-r--r--sources/fileman.asm15
-rw-r--r--sources/helloworld.asm27
-rw-r--r--tools/genkfs.js22
-rw-r--r--tools/genkfs.js.membin0 -> 1088 bytes
-rw-r--r--tools/kpack.js22
-rw-r--r--tools/kpack.js.membin0 -> 4528 bytes
-rw-r--r--tools/scas.js23
-rw-r--r--tools/scas.js.membin0 -> 7608 bytes
-rw-r--r--tools/z80e.js26
-rw-r--r--tools/z80e.js.membin0 -> 11632 bytes
20 files changed, 1502 insertions, 0 deletions
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..e0a0492
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "OpenTI"]
+ path = OpenTI
+ url = git://github.com/KnightOS/OpenTI.git
diff --git a/OpenTI b/OpenTI
new file mode 160000
+Subproject 81e1d4372f5ca66f1becb9bdc7f619048ae549e
diff --git a/_layouts/post_toolchain.html b/_layouts/post_toolchain.html
new file mode 100644
index 0000000..a084d8e
--- /dev/null
+++ b/_layouts/post_toolchain.html
@@ -0,0 +1,27 @@
+---
+layout: base
+showTitle: true
+---
+
+<link rel="stylesheet" type="text/css" href="/css/toolchain.css" />
+<script src="//cdnjs.cloudflare.com/ajax/libs/ace/1.1.3/ace.js"></script>
+<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script>
+<hr />
+{{ content }}
+<div class="calculator-wrapper">
+ <p style="margin: 0; text-align: center; background: #fff"><a href="#" id="hide-toolchain">Hide toolchain output</a></p>
+ <textarea disabled class="form-control" rows=10 placeholder="(toolchain output)" id="tool-log" style="width: 100%;"></textarea>
+ <div class="calculator">
+ <canvas width="385" height="256" id="screen"></canvas>
+ </div>
+</div>
+<div id="disqus_thread"></div>
+<script type="text/javascript">
+var disqus_shortname = 'drewdevaultblog';
+(function() {
+ var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
+ dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
+ (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
+})();
+</script>
+<script data-main="/js/toolchain.js" src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.15/require.min.js"></script>
diff --git a/_posts/2014-11-30-Porting-an-entire-toolchain-to-the-browser-with-emscripten.md b/_posts/2014-11-30-Porting-an-entire-toolchain-to-the-browser-with-emscripten.md
new file mode 100644
index 0000000..39a5048
--- /dev/null
+++ b/_posts/2014-11-30-Porting-an-entire-toolchain-to-the-browser-with-emscripten.md
@@ -0,0 +1,326 @@
+---
+# vim: tw=80
+layout: post_toolchain
+title: Porting an entire desktop toolchain to the browser with Emscripten
+---
+
+Emscripten is pretty damn cool! It lets you write portable C and cross-compile
+it to JavaScript so it'll run in a web browser. As the maintainer of
+[KnightOS](http://www.knightos.org), I looked to emscripten as a potential means
+of reducing the cost of entry for new developers hoping to target the OS.
+
+Side note: apologies to those with narrow displays.
+
+## Rationale for Emscripten
+
+There are several pieces of software in the toolchain that are required to write
+and test software for KnightOS:
+
+* [scas](https://github.com/KnightOS/scas) - a z80 assembler
+* [genkfs](https://github.com/KnightOS/genkfs) - generates KFS filesystem images
+* [kpack](https://github.com/KnightOS/kpack) - packaging tool, like makepkg on Arch Linux
+* [z80e](https://github.com/KnightOS/z80e) - a z80 calculator emulator
+
+You also need a copy of the latest kernel and any of your dependencies from
+[packages.knightos.org](https://packages.knightos.org). Getting all of this is
+not straightforward. On Linux and Mac, there are no official packages for any of
+these tools. On Windows, there are still no official packages, and you have to
+use Cygwin on top of that. The first step to writing KnightOS programs is to
+manually compile and install several tools, which is a lot to ask of someone who
+just wants to experiment.
+
+All of the tools in our toolchain are written in C. We saw Emscripten as an
+opportunity to reduce all of this effort into simply firing up your web browser.
+It works, too! Here's what was involved.
+
+>**Note**: Click the screen on the emulator to the left to give it your
+>keyboard. Click away to take it back. You can use your arrow keys, F1-F5,
+>enter, and escape (as MODE).
+
+## The final product
+
+Let's start by showing you what we've accomplished. It's now possible for
+curious developers to try out KnightOS programming in their web browser. Of
+course, they still have to do it in assembly, but we're [working on
+that](https://github.com/KnightOS/kcc) 😉. Here's a "hello world" you can run in
+your web browser:
+
+<div class="editor" data-source="/sources/helloworld.asm" data-file="main.asm"></div>
+
+We can also install new dependencies on the fly and use them in our programs.
+Here's another program that draws the "hello world" message in a window. You
+should install `core/corelib` first:
+
+<input type="text" id="package-name" value="core/corelib" />
+<button id="install-package">Install</button>
+
+<div class="editor" data-source="/sources/corelib-hello.asm" data-file="main.asm"></div>
+
+You can find more packages to try out on
+[packages.knightos.org](https://packages.knightos.org). Here's another example,
+this one launches the file manager. You'll have to install a few packages for it
+to work:
+
+Install:
+<button class="install-package-button" data-package="extra/fileman">extra/fileman</button>
+<button class="install-package-button" data-package="core/configlib">core/configlib</button>
+<button class="install-package-button" data-package="core/corelib">core/corelib</button>
+
+<div class="editor" data-source="/sources/fileman.asm" data-file="main.asm"></div>
+
+Feel free to edit any of these examples! You can run them again with the Run
+button, of course. These resources might be useful if you want to play with this
+some more:
+
+[z80 instruction set](http://www.z80.info/z80-op.txt) - [z80 assembly tutorial](http://tutorials.eeems.ca/ASMin28Days/lesson/toc.html) - [KnightOS reference documentation](http://www.knightos.org/documentation/reference)
+
+Note: our toolchain has some memory leaks, so eventually emscripten is going to
+run out of memory and then you'll have to refresh. Sorry!
+
+## How all of the pieces fit together
+
+When you
+loaded this page, a bunch of things happened. First, the [latest
+release](https://github.com/KnightOS/kernel/releases) of the [KnightOS
+kernel](https://github.com/KnightOS/kernel) was downloaded. Then all of the
+emscripten ports of the toolchain were downloaded and loaded. The various
+virtual filesystems were set up, and two packages were downloaded and installed:
+`core/init`, and `core/kernel-headers`. Extracting those packages involves
+copying them into kpack's virtual filesystem and running `kpack -e
+path/to/package root/`.
+
+When you click "Run" on one of these text boxes, the contents of the text box is
+written to `/main.asm` in the assembler's virtual filesystem. The package
+installation process extracts headers to `/include/`, and scas itself is run
+with `/main.asm -I/include -o /executable`, which assembles the program and
+writes the output to `/executable`.
+
+Then we copy the executable into the genkfs filesystem (this is the tool that
+generates filesystem images). We also copy the empty kernel into this
+filesystem, as well as any of the packages we've installed. We then run `genkfs
+/kernel.rom /root`, which creates a filesystem image from `/root` and bakes it
+into `kernel.rom`. This produces a ready-to-emulate ROM image that we can load
+into the z80e emulator on the left.
+
+## The emscripten details
+
+Porting all this stuff to emscripten wasn't straightforward. The easiest part
+was cross-compiling all of them to JavaScript:
+
+ cd build
+ emconfigure cmake ..
+ emmake make
+
+The process was basically that simple for each piece of software. There were
+[a](https://github.com/KnightOS/genkfs/commit/c4eefa87a3b5bdbafcc6d971654608c594f779a1)
+[few](https://github.com/KnightOS/scas/commit/d2044e7d7586a946422ce6493cc6dff01127d1c2)
+[changes](https://github.com/KnightOS/scas/commit/8bc31af28e8419a9fa6c421147ea522935bd0df4)
+made to some of the tools to fix a few problems. The hard part
+came when I wanted to run all of them on the same page. Emscripten compiled code
+assumes that it will be the only emscripten module on the page at any given
+time, so this was a bit challenging and involved editing the generated JS.
+
+The first thing I did was wrap all of the modules in isolated AMD loaders. You
+can see how some of this ended up looking by visiting the actual scripts
+(warning, big files):
+
+* [scas.js](http://localhost:4000/tools/scas.js)
+* [kpack.js](http://localhost:4000/tools/kpack.js)
+* [genkfs.js](http://localhost:4000/tools/genkfs.js)
+
+That was enough to make it so that they could all run. These are part of a
+toolchain, though, so somehow they needed to share files. Emscripten's [FS
+object](http://kripken.github.io/emscripten-site/docs/api_reference/Filesystem-API.html)
+cannot be shared between modules, and their API for mounting filesystems is
+pretty crappy. So the solution was to write a little JS:
+
+{% highlight coffeescript %}
+copy_between_systems = (fs1, fs2, from, to, encoding) ->
+ for f in fs1.readdir(from)
+ continue if f in ['.', '..']
+ fs1p = from + '/' + f
+ fs2p = to + '/' + f
+ s = fs1.stat(fs1p)
+ log("Writing #{fs1p} to #{fs2p}")
+ if fs1.isDir(s.mode)
+ try
+ fs2.mkdir(fs2p)
+ catch
+ # pass
+ copy_between_systems(fs1, fs2, fs1p, fs2p, encoding)
+ else
+ fs2.writeFile(fs2p, fs1.readFile(fs1p, { encoding: encoding }), { encoding: encoding })
+{% endhighlight %}
+
+With this, we can extract packages in the kpack filesystem and copy them to the
+genkfs filesystem:
+
+{% highlight coffeescript %}
+install_package = (repo, name, callback) ->
+ full_name = repo + '/' + name
+ log("Downloading " + full_name)
+ xhr = new XMLHttpRequest()
+ xhr.open('GET', "https://packages.knightos.org/" + full_name + "/download")
+ xhr.responseType = 'arraybuffer'
+ xhr.onload = () ->
+ log("Installing " + full_name)
+ file_name = '/packages/' + repo + '-' + name + '.pkg'
+ data = new Uint8Array(xhr.response)
+ toolchain.kpack.FS.writeFile(file_name, data, { encoding: 'binary' })
+ toolchain.kpack.Module.callMain(['-e', file_name, '/pkgroot'])
+ copy_between_systems(toolchain.kpack.FS, toolchain.scas.FS, "/pkgroot/include", "/include", "utf8")
+ copy_between_systems(toolchain.kpack.FS, toolchain.genkfs.FS, "/pkgroot", "/root", "binary")
+ log("Package installed.")
+ callback() if callback?
+ xhr.send()
+{% endhighlight %}
+
+And this puts all the pieces in place for us to actually pass an assembly file
+through our toolchain:
+
+{% highlight coffeescript %}
+run_project = (main) ->
+ # Assemble
+ window.toolchain.scas.FS.writeFile('/main.asm', main)
+ log("Calling assembler...")
+ ret = window.toolchain.scas.Module.callMain(['/main.asm', '-I/include/', '-o', 'executable'])
+ return ret if ret != 0
+ log("Assembly done!")
+ # Build filesystem
+ executable = window.toolchain.scas.FS.readFile("/executable", { encoding: 'binary' })
+ window.toolchain.genkfs.FS.writeFile("/root/bin/executable", executable, { encoding: 'binary' })
+ window.toolchain.genkfs.FS.writeFile("/root/etc/inittab", "/bin/executable")
+ window.toolchain.genkfs.FS.writeFile("/kernel.rom", new Uint8Array(toolchain.kernel_rom), { encoding: 'binary' })
+ window.toolchain.genkfs.Module.callMain(["/kernel.rom", "/root"])
+ rom = window.toolchain.genkfs.FS.readFile("/kernel.rom", { encoding: 'binary' })
+
+ log("Loading your program into the emulator!")
+ if current_emulator != null
+ current_emulator.cleanup()
+ current_emulator = new toolchain.ide_emu(document.getElementById('screen'))
+ current_emulator.load_rom(rom.buffer)
+ return 0
+{% endhighlight %}
+
+This was fairly easy to put together once we got all the tools to cooperate.
+After all, these are all command-line tools. Invoking them is as simple as
+calling `main` and then fiddling with the files that come out. Porting z80e, on
+the other hand, was not nearly as simple.
+
+## Porting z80e to the browser
+
+[z80e](https://github.com/KnightOS/z80e) is our calculator emulator. It's also
+written in C, but needs to interact much more closely with the user. We need to
+be able to render the display to a canvas, and to receive input from the user.
+This isn't nearly as simple as just calling `main` and playing with some files.
+
+To accomplish this, we've put together
+[OpenTI](https://github.com/KnightOS/OpenTI), a set of JavaScript bindings to
+z80e. This is mostly the work of my friend puckipedia, but I can explain a bit
+of what is involved. The short of it is that we needed to map native structs to
+JavaScript objects and pass JavaScript code as function pointers to z80e's
+hooks. So far as I know, the KnightOS team is the only group to have attempted
+something with this deep of integration between emscripten and JavaScript -
+because we had to do a ton of the work ourselves.
+
+OpenTI contains a
+[wrap](https://github.com/KnightOS/OpenTI/blob/master/webui/js/OpenTI/wrap.js)
+module that is capable of wrapping structs and pointers in JavaScript objects.
+This is a tedious procedure, because we have to know the offset and size of each
+field in native code. An example of a wrapped object is given here:
+
+{% highlight javascript %}
+define(["../wrap"], function(Wrap) {
+ var Registers = function(pointer) {
+ if (!pointer) {
+ throw "This object can only be instantiated with a memory region predefined!";
+ }
+ this.pointer = pointer;
+
+ Wrap.UInt16(this, "AF", pointer);
+ Wrap.UInt8(this, "F", pointer);
+ Wrap.UInt8(this, "A", pointer + 1);
+
+ this.flags = {};
+ Wrap.UInt8(this.flags, "C", pointer, 128, 7);
+ Wrap.UInt8(this.flags, "N", pointer, 64, 6);
+ Wrap.UInt8(this.flags, "PV", pointer, 32, 5);
+ Wrap.UInt8(this.flags, "3", pointer, 16, 4);
+ Wrap.UInt8(this.flags, "H", pointer, 8, 3);
+ Wrap.UInt8(this.flags, "5", pointer, 4, 2);
+ Wrap.UInt8(this.flags, "Z", pointer, 2, 1);
+ Wrap.UInt8(this.flags, "S", pointer, 1, 0);
+ pointer += 2;
+
+ Wrap.UInt16(this, "BC", pointer);
+ Wrap.UInt8(this, "C", pointer);
+ Wrap.UInt8(this, "B", pointer + 1);
+ pointer += 2;
+
+ Wrap.UInt16(this, "DE", pointer);
+ Wrap.UInt8(this, "E", pointer);
+ Wrap.UInt8(this, "D", pointer + 1);
+ pointer += 2;
+
+ Wrap.UInt16(this, "HL", pointer);
+ Wrap.UInt8(this, "L", pointer);
+ Wrap.UInt8(this, "H", pointer + 1);
+ pointer += 2;
+
+ Wrap.UInt16(this, "_AF", pointer);
+ Wrap.UInt16(this, "_BC", pointer + 2);
+ Wrap.UInt16(this, "_DE", pointer + 4);
+ Wrap.UInt16(this, "_HL", pointer + 6);
+ pointer += 8;
+
+ Wrap.UInt16(this, "PC", pointer);
+ Wrap.UInt16(this, "SP", pointer + 2);
+ pointer += 4;
+
+ Wrap.UInt16(this, "IX", pointer);
+ Wrap.UInt8(this, "IXL", pointer);
+ Wrap.UInt8(this, "IXH", pointer + 1);
+ pointer += 2;
+
+ Wrap.UInt16(this, "IY", pointer);
+ Wrap.UInt8(this, "IYL", pointer);
+ Wrap.UInt8(this, "IYH", pointer + 1);
+ pointer += 2;
+
+ Wrap.UInt8(this, "I", pointer++);
+ Wrap.UInt8(this, "R", pointer++);
+
+ // 2 dummy bytes needed for 4-byte alignment
+ }
+
+ Registers.sizeOf = function() {
+ return 26;
+ }
+
+ return Registers;
+});
+{% endhighlight %}
+
+The result of that effort is that you can find out what the current value of a
+register is from some nice clean JavaScript: `asic.cpu.registers.PC` (it's <code
+id="register-pc"></code>, by the way).
+
+## Conclusions
+
+I've put all of this together on [try.knightos.org](http://try.knightos.org).
+The source is available on
+[GitHub](https://github.com/KnightOS/try.knightos.org). It's entirely
+client-side, so it can be hosted on GitHub Pages. I'm hopeful that this will
+make it easier for people to get interested in KnightOS development, but it'll
+be a lot better once I can get more documentation and tutorials written. It'd be
+pretty cool if we could have interactive tutorials like this!
+
+It was a lot of effort to make this happen, but it was worth it. This is some
+pretty cool shit we've got as a result.
+
+If you, reader, are interested in working on some pretty cool shit, there's a
+place for you! We have things to do in Assembly, C, JavaScript, Python, and a
+handful of other things. Did you notice how bad try.knightos.org looks? Maybe
+you have a knack for design and want to help improve it. Whatever the case may
+be, if you have interest in this stuff, come hang out with us on IRC: [#knightos
+on irc.freenode.net](http://webchat.freenode.net/?channels=knightos&uio=d4).
diff --git a/css/base.css b/css/base.css
index e4996ab..b26c10f 100644
--- a/css/base.css
+++ b/css/base.css
@@ -34,3 +34,11 @@ pre {
.footnotes a {
color: #444;
}
+
+blockquote {
+ padding-left: 10px;
+ padding-top: 3px;
+ padding-bottom: 3px;
+ margin-left: 0;
+ border-left: 4px #aaa solid;
+}
diff --git a/css/toolchain.scss b/css/toolchain.scss
new file mode 100644
index 0000000..13ed29e
--- /dev/null
+++ b/css/toolchain.scss
@@ -0,0 +1,40 @@
+---
+---
+
+.editor {
+ height: 400px;
+ margin-bottom: 10px;
+ border: 1px solid #666;
+}
+
+.calculator-wrapper {
+ border-bottom: 1px solid #888;
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ z-index: 99999;
+
+ .calculator {
+ background: url('http://try.knightos.org/static/skin.png') no-repeat;
+ background-size: 500px;
+ width: 500px;
+ height: 330px;
+ margin: 0 auto;
+ position: relative;
+
+ canvas {
+ position: absolute;
+ left: 100px;
+ top: 105px;
+ width: 300px;
+ }
+ }
+}
+
+.run-button {
+ position: absolute;
+ width: 100px;
+ top: 5px;
+ right: 15px;
+ z-index: 9999;
+}
diff --git a/js/ide_emu.js b/js/ide_emu.js
new file mode 100644
index 0000000..252e28e
--- /dev/null
+++ b/js/ide_emu.js
@@ -0,0 +1,150 @@
+define(['z80e', '../OpenTI/webui/js/OpenTI/OpenTI'], function(z80e, OpenTI) {
+ var lcd_ctx;
+
+ var update_lcd;
+ function do_update_lcd(lcd) {
+ update_lcd = lcd;
+ }
+
+ var lcd_data = [];
+ var lcd_colors = [[0x99, 0xB1, 0x99], [0x00, 0x00, 0x00]];
+ function gen_pixeldata() {
+ for (var i = 0; i <= 0xff; i++) {
+ var arr = new Uint8Array(8 * 4 * 4);
+ for (var j = 0; j < 8; j++) {
+ var set = (i & (1 << j)) ? 1 : 0;
+ for (var k = 0; k < 4; k++) {
+ var view = (j * 16) + k * 4;
+ arr[view + 0] = lcd_colors[set][0];
+ arr[view + 1] = lcd_colors[set][1];
+ arr[view + 2] = lcd_colors[set][2];
+ arr[view + 3] = 0xFF;
+ }
+ }
+ lcd_data.push(arr);
+ }
+ }
+ gen_pixeldata();
+
+ function print_lcd(lcd) {
+ var data = lcd_ctx.getImageData(0, 0, 384, 256);
+ var ram = lcd.ram;
+ for (var x = 0; x < (120 * 64) / 8; x++) {
+ var octet = x % 15;
+ if (octet > 11) {
+ continue;
+ }
+ var line = Math.floor(x / 15) * 4;
+ var tocopy = lcd_data[ram[x]];
+ data.data.set(tocopy, ((line++) * 12 + octet) * (4 * 8 * 4));
+ data.data.set(tocopy, ((line++) * 12 + octet) * (4 * 8 * 4));
+ data.data.set(tocopy, ((line++) * 12 + octet) * (4 * 8 * 4));
+ data.data.set(tocopy, ((line++) * 12 + octet) * (4 * 8 * 4));
+ }
+ lcd_ctx.putImageData(data, 0, 0);
+ update_lcd = null;
+ }
+
+ var key_mappings = Array.apply(null, new Array(100)).map(Number.prototype.valueOf, -1);
+ key_mappings[40] = 0x00; // Down
+ key_mappings[37] = 0x01; // Left
+ key_mappings[39] = 0x02; // Right
+ key_mappings[38] = 0x03; // Up
+ key_mappings[16] = 0x65; // 2nd / Shift
+ key_mappings[13] = 0x10; // Enter
+ key_mappings[27] = 0x66; // MODE / Esc
+ key_mappings[112] = 0x64; // F1
+ key_mappings[113] = 0x63; // F2
+ key_mappings[114] = 0x62; // F3
+ key_mappings[115] = 0x61; // F4
+ key_mappings[116] = 0x60; // F5
+ key_mappings[48] = 0x40; // 0
+ key_mappings[49] = 0x41; // 1
+ key_mappings[50] = 0x31; // 2
+ key_mappings[51] = 0x21; // 3
+ key_mappings[52] = 0x42; // 4
+ key_mappings[53] = 0x32; // 5
+ key_mappings[54] = 0x22; // 6
+ key_mappings[55] = 0x43; // 7
+ key_mappings[56] = 0x33; // 8
+ key_mappings[57] = 0x23; // 9
+
+ return function(canvas, ide_log) {
+ var self = this;
+ lcd_ctx = canvas.getContext('2d');
+ this.asic = new OpenTI.TI.ASIC(OpenTI.TI.DeviceType.TI84pSE);
+ this.asic.debugger = new OpenTI.Debugger.Debugger(this.asic);
+ this.asic.hook.addLCDUpdate(do_update_lcd);
+ this.keysEnabled = false;
+ window.current_asic = this.asic;
+ window.addEventListener('click', function(e) {
+ self.keysEnabled = e.target.tagName == 'CANVAS';
+ });
+ window.addEventListener('keydown', function(e) {
+ if (!self.keysEnabled) return;
+ if (e.keyCode <= key_mappings.length && key_mappings[e.keyCode] !== -1) {
+ e.preventDefault();
+ self.asic.hardware.Keyboard.press(key_mappings[e.keyCode]);
+ }
+ });
+ window.addEventListener('keyup', function(e) {
+ if (!self.keysEnabled) return;
+ if (e.keyCode <= key_mappings.length && key_mappings[e.keyCode] !== -1) {
+ e.preventDefault();
+ self.asic.hardware.Keyboard.release(key_mappings[e.keyCode]);
+ }
+ });
+
+ var asic_tick, lcd_tick;
+
+ this.exec = function exec(str) {
+ ide_log("z80e > " + str + "\n");
+
+ if (str.length == 0) {
+ str = prev_command;
+ }
+ prev_command = str;
+
+ var state = new oti.Debugger.Debugger.State(asic.debugger,
+ {
+ print: function(str) { ide_log(str); },
+ new_state: function() { return this; },
+ closed: function() { }
+ });
+
+ state.exec(str);
+ }
+
+ this.cleanup = function cleanup() {
+ clearTimeout(asic_tick);
+ clearTimeout(lcd_tick);
+ lcd_ctx.clearRect(0, 0, 385, 256);
+ return;
+ /* TODO: this causes assertion errors */
+ self.asic.free();
+ };
+
+ this.load_rom = function load_rom(arrayBuffer) {
+ var byteArray = new Uint8Array(arrayBuffer);
+ var pointer = z80e.Module.allocate(byteArray, 'i8', z80e.Module.ALLOC_STACK);
+ z80e.Module.HEAPU32[this.asic.mmu._flashPointer] = pointer;
+
+ this.asic.runloop.tick(1000);
+ this.asic.cpu.halted = 0;
+
+ asic_tick = setTimeout(function tick() {
+ if (!self.asic.stopped || self.asic.cpu.interrupt) {
+ self.asic.runloop.tick(self.asic.clock_rate / 20);
+ }
+ setTimeout(tick, 0);
+ }, 1000 / 60);
+
+ lcd_tick = setTimeout(function tick() {
+ if (update_lcd) {
+ print_lcd(update_lcd);
+ }
+ setTimeout(tick, 1000 / 60);
+ }, 1000 / 60);
+ }
+ }
+})
diff --git a/js/toolchain.coffee b/js/toolchain.coffee
new file mode 100644
index 0000000..8f5a8a1
--- /dev/null
+++ b/js/toolchain.coffee
@@ -0,0 +1,244 @@
+---
+---
+
+require.config({
+ paths: {
+ 'z80e': '../tools/z80e'
+ },
+ shim: {
+ '../tools/kpack': {
+ exports: 'exports'
+ },
+ '../tools/genkfs': {
+ exports: 'exports'
+ },
+ '../tools/scas': {
+ exports: 'exports'
+ },
+ 'z80e': {
+ exports: 'exports'
+ }
+ }
+})
+
+window.toolchain = {
+ kpack: null,
+ genkfs: null,
+ scas: null,
+ z80e: null,
+ ide_emu: null,
+ kernel_rom: null,
+}
+
+files = []
+
+log_el = document.getElementById('tool-log')
+log = (text) ->
+ console.log(text)
+ if log_el.innerHTML == ''
+ log_el.innerHTML += text
+ else
+ log_el.innerHTML += '\n' + text
+ log_el.scrollTop = log_el.scrollHeight
+window.ide_log = log
+
+copy_between_systems = (fs1, fs2, from, to, encoding) ->
+ for f in fs1.readdir(from)
+ continue if f in ['.', '..']
+ fs1p = from + '/' + f
+ fs2p = to + '/' + f
+ s = fs1.stat(fs1p)
+ log("Writing #{fs1p} to #{fs2p}")
+ if fs1.isDir(s.mode)
+ try
+ fs2.mkdir(fs2p)
+ catch
+ # pass
+ copy_between_systems(fs1, fs2, fs1p, fs2p, encoding)
+ else
+ fs2.writeFile(fs2p, fs1.readFile(fs1p, { encoding: encoding }), { encoding: encoding })
+
+install_package = (repo, name, callback) ->
+ full_name = repo + '/' + name
+ log("Downloading " + full_name)
+ xhr = new XMLHttpRequest()
+ xhr.open('GET', "https://packages.knightos.org/" + full_name + "/download")
+ xhr.responseType = 'arraybuffer'
+ xhr.onload = () ->
+ log("Installing " + full_name)
+ file_name = '/packages/' + repo + '-' + name + '.pkg'
+ data = new Uint8Array(xhr.response)
+ toolchain.kpack.FS.writeFile(file_name, data, { encoding: 'binary' })
+ toolchain.kpack.Module.callMain(['-e', file_name, '/pkgroot'])
+ copy_between_systems(toolchain.kpack.FS, toolchain.scas.FS, "/pkgroot/include", "/include", "utf8")
+ copy_between_systems(toolchain.kpack.FS, toolchain.genkfs.FS, "/pkgroot", "/root", "binary")
+ log("Package installed.")
+ callback() if callback?
+ xhr.send()
+
+current_emulator = null
+
+load_environment = ->
+ toolchain.genkfs.FS.writeFile("/kernel.rom", toolchain.kernel_rom, { encoding: 'binary' })
+ toolchain.genkfs.FS.mkdir("/root")
+ toolchain.genkfs.FS.mkdir("/root/bin")
+ toolchain.genkfs.FS.mkdir("/root/etc")
+ toolchain.genkfs.FS.mkdir("/root/home")
+ toolchain.genkfs.FS.mkdir("/root/lib")
+ toolchain.genkfs.FS.mkdir("/root/share")
+ toolchain.genkfs.FS.mkdir("/root/var")
+ toolchain.kpack.FS.mkdir("/packages")
+ toolchain.kpack.FS.mkdir("/pkgroot")
+ toolchain.kpack.FS.mkdir("/pkgroot/include")
+ toolchain.scas.FS.mkdir("/include")
+ packages = 0
+ callback = () ->
+ packages++
+ log("Ready to go!") if packages == 2
+ install_package('core', 'init', callback)
+ install_package('core', 'kernel-headers', callback)
+
+run_project = (main) ->
+ # Assemble
+ window.toolchain.scas.FS.writeFile('/main.asm', main)
+ log("Calling assembler...")
+ ret = window.toolchain.scas.Module.callMain(['/main.asm', '-I/include/', '-o', 'executable'])
+ return ret if ret != 0
+ log("Assembly done!")
+ # Build filesystem
+ executable = window.toolchain.scas.FS.readFile("/executable", { encoding: 'binary' })
+ window.toolchain.genkfs.FS.writeFile("/root/bin/executable", executable, { encoding: 'binary' })
+ window.toolchain.genkfs.FS.writeFile("/root/etc/inittab", "/bin/executable")
+ window.toolchain.genkfs.FS.writeFile("/kernel.rom", new Uint8Array(toolchain.kernel_rom), { encoding: 'binary' })
+ window.toolchain.genkfs.Module.callMain(["/kernel.rom", "/root"])
+ rom = window.toolchain.genkfs.FS.readFile("/kernel.rom", { encoding: 'binary' })
+
+ log("Loading your program into the emulator!")
+ if current_emulator != null
+ current_emulator.cleanup()
+ current_emulator = new toolchain.ide_emu(document.getElementById('screen'))
+ current_emulator.load_rom(rom.buffer)
+ return 0
+
+check_resources = ->
+ for prop in Object.keys(window.toolchain)
+ if window.toolchain[prop] == null
+ return
+ log("Ready.")
+ load_environment()
+
+downloadKernel = ->
+ log("Finding latest kernel on GitHub...")
+ xhr = new XMLHttpRequest()
+ xhr.open('GET', 'https://api.github.com/repos/KnightOS/kernel/releases')
+ xhr.onload = ->
+ json = JSON.parse(xhr.responseText)
+ release = json[0]
+ rom = new XMLHttpRequest()
+ if release?
+ log("Downloading kernel #{ release.tag_name }...")
+ rom.open('GET', _.find(release.assets, (a) -> a.name == 'kernel-TI84pSE.rom').url)
+ else
+ # fallback
+ log("Downloading kernel")
+ rom.open('GET', 'http://builds.knightos.org/latest-TI84pSE.rom')
+ rom.setRequestHeader("Accept", "application/octet-stream")
+ rom.responseType = 'arraybuffer'
+ rom.onload = () ->
+ window.toolchain.kernel_rom = rom.response
+ log("Loaded kernel ROM.")
+ check_resources()
+ rom.send()
+ xhr.onerror = ->
+ xhr.send()
+
+downloadKernel()
+
+log("Downloading scas...")
+require(['../tools/scas'], (scas) ->
+ log("Loaded scas.")
+ window.toolchain.scas = scas
+ window.toolchain.scas.Module.preRun.pop()()
+ check_resources()
+)
+
+log("Downloading kpack...")
+require(['../tools/kpack'], (kpack) ->
+ log("Loaded kpack.")
+ window.toolchain.kpack = kpack
+ check_resources()
+)
+
+log("Downloading genkfs...")
+require(['../tools/genkfs'], (genkfs) ->
+ log("Loaded genkfs.")
+ window.toolchain.genkfs = genkfs
+ check_resources()
+)
+
+log("Downloading emulator bindings...")
+require(['ide_emu'], (ide_emu) ->
+ log("Loaded emulator bindings.")
+ window.toolchain.ide_emu = ide_emu
+ window.toolchain.z80e = require("z80e")
+ check_resources()
+)
+
+((el) ->
+ # Set up default editors
+ editor = ace.edit(el)
+ editor.setTheme("ace/theme/github")
+ if el.dataset.file.indexOf('.asm') == el.dataset.file.length - 4
+ editor.getSession().setMode("ace/mode/assembly_x86")
+ files.push({
+ name: el.dataset.file,
+ editor: editor
+ })
+ xhr = new XMLHttpRequest()
+ xhr.open('GET', el.dataset.source)
+ xhr.onload = () ->
+ editor.setValue(this.responseText)
+ editor.navigateFileStart()
+ xhr.send()
+ button = document.createElement('button')
+ button.className = 'run-button'
+ button.addEventListener('click', (e) ->
+ e.preventDefault()
+ ret = run_project(editor.getValue())
+ if ret != 0
+ alert("Assembler returned nonzero exit status, see log to the left")
+ )
+ button.textContent = 'Run'
+ el.appendChild(button)
+)(el) for el in document.querySelectorAll('.editor')
+
+document.getElementById('install-package').addEventListener('click', (e) ->
+ e.preventDefault()
+ p = document.getElementById('package-name').value.split('/')
+ install_package(p[0], p[1])
+)
+
+((el) ->
+ el.addEventListener('click', () ->
+ p = el.dataset.package.split('/')
+ install_package(p[0], p[1])
+ )
+)(el) for el in document.querySelectorAll('.install-package-button')
+
+document.getElementById('hide-toolchain').addEventListener('click', (e) ->
+ e.preventDefault()
+ to = document.getElementById('tool-log')
+ if e.target.textContent == 'Hide toolchain output'
+ to.style.display = 'none'
+ e.target.textContent = 'Show toolchain output'
+ else
+ to.style.display = 'block'
+ e.target.textContent = 'Hide toolchain output'
+)
+
+window.setInterval(() ->
+ if window.current_asic?
+ document.getElementById('register-pc').textContent = '0x' + window.current_asic.cpu.registers.PC.toString(16).toUpperCase()
+ else
+ document.getElementById('register-pc').textContent = '0x0000'
+, 100)
diff --git a/scas.data b/scas.data
new file mode 100644
index 0000000..e2dd484
--- /dev/null
+++ b/scas.data
@@ -0,0 +1,533 @@
+# z80 Instruction Table
+
+#### INSTRUCTION
+# INS [MNOMIC] [VALUE]
+# MNOMIC is any series of case-insenstive characters with support for special
+# characters to define additional functionality. MNOMIC may not have whitespace.
+# Special Characters:
+# '_': Required whitespace
+# '-': Optional whitespace
+# '%#<bits[s]>': Immediate value (# is a character to use to identify later)
+# '^#<bits[s]>': Immediate value relative to PC (# is a character to use to identify later)
+# '@#<group>': Operand (# is a ch