Set up build environment

You could read this repository as if it were a book, but that would be
pretty boring. You’d get lost at roughly the third significant commit
without following along. So, what do you need if you want to build and
try everything here?

## Hardware

* Of course, a computer capable of working with USB devices.
* A [Truly Ergonomic][] Computer Keyboard, any model. I actually have
  two of them — an older Model 109 to type on, and a newer 209 to
  conduct my horrible evil experiments on. (Muahahahaha.)
* I also find that a USB hub with individual port on/off switches is
  very handy, though rare to find.

I plug my both keyboards into the hub, and the hub into the computer.
When updating the firmware in the test keyboard, I switch off my main
keyboard port, so as to prevent surprises. I also keep the test keyboard
port switched off when not hacking.

## Software

* I work under [Ubuntu][] 14.04 “Trusty Tahr”, amd64 version. This has
  its benefits and drawbacks.
  * The benefit is that I don’t have to fiddle with installation of
    other tools — I install a few packages and it Just Works. On
    Windows, you should be able to install all of the below, but expect
    to spend some time getting everything to work.
  * The drawback is that the Truly Ergonomic DFU (Device Firmware
    Upgrade) tool does not run under Wine. (That’s to be expected as it
    needs direct access to the USB device, which is not yet supported.)
    So I run [VirtualBox][] with a Windows installation inside, and the
    Extension Pack which is required in order to connect USB devices
    directly to the VirtualBox machine. (I will detail VirtualBox setup
    when it comes to flashing our first test program into the TECK.)
* To compile my test programs, I use [SDCC][] 3.3.0.
* The _de facto_ standard way to manage compilation of a software
  project in the Free software world is GNU [Make][]. Ubuntu 14.04
  comes with Make 3.81.
* On the early stages, I will use a disassembler to look at what the
  compiler produces. I use [Dis51][] 0.5.
* And of course you need [Git][] in order to clone this repository and
  navigate around it.

[Truly Ergonomic]: https://www.trulyergonomic.com/
[Ubuntu]: http://ubuntu.com/
[VirtualBox]: https://www.virtualbox.org/
[SDCC]: http://sdcc.sourceforge.net/
[Make]: http://www.gnu.org/software/make/
[Dis51]: http://home.earthlink.net/~davesullins/software/dis51.html
[Git]: http://git-scm.com/

To install all of the required software in Ubuntu 14.04:

```
$ sudo apt-add-repository 'deb http://download.virtualbox.org/virtualbox/debian trusty contrib'
$ sudo apt-get update
$ sudo apt-get install virtualbox-4.3 sdcc make dis51 git
```

## Safety notice

By updating firmware from inside a virtual machine and over a USB hub, I
violate two guidelines that Truly Ergonomic recommends on their site. I
do this because it works for me and is more convenient than hacking
under Windows, rebooting each time I want to try a new version, or
setting up a physical Windows machine as a firmware update station.

If a firmware update process goes wrong, the keyboard might become
“bricked”. I have never managed to screw up so badly as to be unable to
repeat the firmware update; that is, all the code that is required to
enter the DFU mode has remained functional. However, it is said that one
can force a hard reset to DFU mode by opening up the device and shorting
pins 1 and 36 while power is on.
Using Git to move around on this branch

To clone this repository and check out this branch:

```
$ git clone -b sdcc https://github.com/yurivkhan/teck.git
```

I suggest that you then use `gitk sdcc` to navigate around the branch.
For example, find this commit in the list, right-click it and select
“Create new branch”. Then type in a name for your reading branch. Then
right-click the newly created branch and choose “Check out this
branch”.

To move to a different commit, right-click it and select “Reset _name_
branch to here”. When prompted for the reset type, choose Hard reset.
## Minimal program

Let’s try compiling and linking a minimal C program with the SDCC
compiler, and disassemble it to see what happens behind our back.
Dissecting the minimal program

So, here’s our program:

```c
int main(void)
{
	return 0;
}
```

What could be shorter? Let’s see what it turns into.

```
$ make
```

Here’s the disassembly. (Using the map file generated by the linker, I
deobfuscated the `L00xx` labels and annotated with the sources from the
SDCC standard library.)

```
CSEG AT 0000h
  0000 020006       LJMP __sdcc_gsinit_startup

__sdcc_program_startup:
  0003 020062       LJMP _main

                                                        ; lib/src/mcs51/crtstart.asm line 43
__sdcc_gsinit_startup:                                  __sdcc_gsinit_startup::
  0006 758107       MOV SP, #7h                             mov     sp,#__start__stack - 1
                                                            .area   GSINIT2 (CODE)
  0009 120066       LCALL __sdcc_external_startup           lcall   __sdcc_external_startup
  000C E582         MOV A, DPL                              mov     a,dpl
  000E 6003         JZ __mcs51_genXINIT                     jz      __sdcc_init_data
  0010 020003       LJMP __sdcc_program_startup             ljmp    __sdcc_program_startup
                                                        __sdcc_init_data:
                                                        ; lib/src/mcs51/crtxinit.asm line 76
__mcs51_genXINIT:                                       __mcs51_genXINIT::
  0013 7900         MOV R1, #0h                             mov      r1,#l_XINIT
  0015 E9           MOV A, R1                               mov      a,r1
  0016 4400         ORL A, #0h                              orl      a,#(l_XINIT >> 8)
  0018 601B         JZ __mcs51_genRAMCLEAR                  jz       00003$
  001A 7A00         MOV R2, #0h                             mov      r2,#((l_XINIT+255) >> 8)
  001C 90006A       MOV DPTR, #006Ah                        mov      dptr,#s_XINIT
  001F 7800         MOV R0, #0h                             mov      r0,#s_XISEG
  0021 75A000       MOV P2, #0h                             mov      __XPAGE,#(s_XISEG >> 8)
L0008:                                                  00001$:
  0024 E4           CLR A                                   clr      a
  0025 93           MOVC A, @A+DPTR                         movc     a,@a+dptr
  0026 F2           MOVX @R0, A                             movx     @r0,a
  0027 A3           INC DPTR                                inc      dptr
  0028 08           INC R0                                  inc      r0
  0029 B80002       CJNE R0, #0h, L0007                     cjne     r0,#0,00002$
  002C 05A0         INC P2                                  inc      __XPAGE
L0007:                                                  00002$:
  002E D9F4         DJNZ R1, L0008                          djnz     r1,00001$
  0030 DAF2         DJNZ R2, L0008                          djnz     r2,00001$
  0032 75A0FF       MOV P2, #0FFh                           mov      __XPAGE,#0xFF
                                                        00003$:
                                                        ; lib/src/mcs51/crtclear.asm line 41
__mcs51_genRAMCLEAR:                                    __mcs51_genRAMCLEAR::
  0035 E4           CLR A                                   clr     a
  0036 78FF         MOV R0, #0FFh                           mov     r0,#(l_IRAM-1)  ; mov r0,a will do for most devices
L0009:                                                  00004$:

  0038 F6           MOV @R0, A                              mov     @r0,a
  0039 D8FD         DJNZ R0, L0009                          djnz    r0,00004$
                                                        ; lib/src/mcs51/crtxclear.asm line 41
__mcs51_genXRAMCLEAR:                                   __mcs51_genXRAMCLEAR::
  003B 7800         MOV R0, #0h                             mov     r0,#l_PSEG
  003D E8           MOV A, R0                               mov     a,r0
  003E 4400         ORL A, #0h                              orl     a,#(l_PSEG >> 8)
  0040 600A         JZ L0010                                jz      00006$
  0042 7900         MOV R1, #0h                             mov     r1,#s_PSEG
  0044 75A000       MOV P2, #0h                             mov     __XPAGE,#(s_PSEG >> 8)
  0047 E4           CLR A                                   clr     a
L0011:                                                  00005$:
  0048 F3           MOVX @R1, A                             movx    @r1,a
  0049 09           INC R1                                  inc     r1
  004A D8FC         DJNZ R0, L0011                          djnz    r0,00005$
L0010:                                                  00006$:
  004C 7800         MOV R0, #0h                             mov     r0,#l_XSEG
  004E E8           MOV A, R0                               mov     a,r0
  004F 4400         ORL A, #0h                              orl     a,#(l_XSEG >> 8)
  0051 600C         JZ L0012                                jz      00008$
  0053 7900         MOV R1, #0h                             mov     r1,#((l_XSEG + 255) >> 8)
  0055 900000       MOV DPTR, #0000h                        mov     dptr,#s_XSEG
  0058 E4           CLR A                                   clr     a
L0013:                                                  00007$:
  0059 F0           MOVX @DPTR, A                           movx    @dptr,a
  005A A3           INC DPTR                                inc     dptr
  005B D8FC         DJNZ R0, L0013                          djnz    r0,00007$
  005D D9FA         DJNZ R1, L0013                          djnz    r1,00007$
L0012:                                                  00008$:
  005F 020003       LJMP __sdcc_program_startup

                                                        ; main.c
_main:                                                  int main(void)
                                                        {
  0062 900000       MOV DPTR, #0000h
  0065 22           RET                                     return 0;
                                                        }

                                                        ; lib/src/_startup.c line 45
__sdcc_external_startup:                                unsigned char _sdcc_external_startup (void) __nonbanked
                                                        {
  0066 758200       MOV DPL, #0h
  0069 22           RET                                     return 0;
                                                        }
END
```

Now let’s see what’s happening.

* `__sdcc_gsinit_startup` sets up stack bottom at address 0x07. (The
  8051 microcontroller increments the stack pointer before storing a
  byte, so actual stack starts at 0x08.) This is understandable: the
  first 8 bytes of RAM are mapped to the 0th bank of registers R0÷R7,
  and all further space is available for the program variables and
  stack. And we don’t have any variables at this point.
* Next it calls `__sdcc_external_startup`. The intent is that we can
  replace this function if we want to do anything important before any
  other initialization takes place. The default implementation just
  returns 0, meaning “go on with initialization”.
* `__mcs51_genXINIT` looks like it could copy some data from the code
  memory (pseudo-ROM) into XRAM, if the value of `l_XINIT` were
  non-zero. But note that it by default assumes that the controller uses
  the P2 register for the high byte of XRAM address. Ours does not and
  in fact has dual DPTR, so we would need to recompile with
  `-DDUAL_DPTR`.
* `__mcs51_genRAMCLEAR` zeroes out the IRAM data.
* `__mcs51_genXRAMCLEAR` zeroes out the XRAM data (if any). Again it
  assumes P2 is used for accessing XRAM beyond 0xFF, and this time
  doesn’t let us opt out of that.

Luckily, there is no indication that we will need any initialized
variables — in fact, the stock firmware doesn’t have any. Or rather,
there are, but they are all initialized to zero, except for one
variable. We can replicate that with a dynamic initialization routine.
Set up XRAM-related compiler options

Last time we looked at the default startup code and found that it’s not
very suitable for our architecture, because it assumes that access to
high XRAM addresses is done by writing the high byte of the address into
P2.

SDCC has two options that are related to XRAM:

* `--no-xinit-opt` disables the optimization of initialized variables.
  If it is enabled (by default), the initial values are stored in a part
  of code and copied into XRAM on startup. If disabled, the code will
  contain actual MOV instructions to initialize each variable.
* `--xram-size` tells the compiler how much XRAM we have. It can then
  warn us if we define more variables than can fit.
Disassemble again

Now let’s look what those changes won us.

```
$ make
```

Again, I deobfuscated the labels and added source annotations.

```
CSEG AT 0000h
  0000 020006       LJMP __sdcc_gsinit_startup

__sdcc_program_startup:
  0003 02001C       LJMP _main

                                                        ; lib/src/mcs51/crtstart.asm line 43
__sdcc_gsinit_startup:                                  __sdcc_gsinit_startup::
  0006 758107       MOV SP, #7h                             mov     sp,#__start__stack - 1
                                                            .area   GSINIT2 (CODE)
  0009 120020       LCALL __sdcc_external_startup           lcall   __sdcc_external_startup
  000C E582         MOV A, DPL                              mov     a,dpl
  000E 6003         JZ __mcs51_genRAMCLEAR                  jz      __sdcc_init_data
  0010 020003       LJMP __sdcc_program_startup             ljmp    __sdcc_program_startup
                                                        __sdcc_init_data:
                                                        ; lib/src/mcs51/crtclear.asm line 41
__mcs51_genRAMCLEAR:                                    __mcs51_genRAMCLEAR::
  0013 E4           CLR A                                   clr     a
  0014 78FF         MOV R0, #0FFh                           mov     r0,#(l_IRAM-1)  ; mov r0,a will do for most devices
L0006:                                                  00004$:
  0016 F6           MOV @R0, A                              mov     @r0,a
  0017 D8FD         DJNZ R0, L0006                          djnz    r0,00004$
  0019 020003       LJMP __sdcc_program_startup

                                                        ; main.c
_main:                                                  int main(void)
                                                        {
  001C 900000       MOV DPTR, #0000h
  001F 22           RET                                     return 0;
                                                        }

                                                        ; lib/src/_startup.c line 45
__sdcc_external_startup:                                unsigned char _sdcc_external_startup (void) __nonbanked
                                                        {
  0020 758200       MOV DPL, #0h
  0023 22           RET                                     return 0;
                                                        }
END
```

Now that’s much better. Notice that by opting out of XRAM initialization
we also lost XRAM zeroing. So much for the better for now; if we ever
want XRAM variables, we can always write a custom
`_sdcc_external_startup` routine.
## Safety switch

Now that we are fairly confident that (1) the compiler works, (2) it
produces code for our processor, (3) the resulting binary does not
contain any unwanted code; we’d like to build something that will run on
the actual hardware. But first we want to make sure we can go back to a
working firmware, or to flash a different program.

The stock firmware does this by handling a special USB request, upon
receiving which it reboots in ISP DFU mode. But our experimental
programs will not have USB handling until much later.

So we will establish a rule: In any unfamiliar situation, reboot to ISP.

And, in the beginning, every situation is unfamiliar.

So let’s write a function to reboot to ISP, and call it immediately upon
entering `main`.
Flashing our first program

We now want to test our first program. It should start up and
immediately reboot the device† into the bootloader.

† Which, for the time being, is no longer a keyboard, as it temporarily
loses the ability of reporting key presses to the host system.

To update the firmware code, we are supposed to use either the Truly
Ergonomic DFU tool, or the generic DFU tool from the [Megawin
MG84FL54BD][MG84FL54BD] SDK. They are functionally identical, it’s just
the logo that differs.

[MG84FL54BD]: http://www.megawin.com.tw/megawin_EN/ProductShow.asp?ID=175

Unfortunately, we can’t just run them under Wine, as it does not
emulate/bridge USB access yet. And I don’t feel like setting up a
separate Windows PC just to tinker with a keyboard.

Luckily, [VirtualBox][], with the Extension Pack installed, gives the
guest system access to USB devices. So install VirtualBox and the
Extension Pack, set up Windows inside it, and install Guest
Additions. (Additions, among other features, gives mouse pointer
integration, better video support, and shared folders.)

[VirtualBox]: https://www.virtualbox.org/

Inside the guest Windows, we need to create a directory and unpack the
Truly Ergonomic DFU tool in it. We will also set up a Shared Folder so
that our compiled program is available to the Windows VM. Copy the
`main.ihx` file into the directory with the DFU tool, and rename it
`reboot.hex`.

The DFU tool operates like this:

* You tell it to load a firmware (point to a .hex file) and then to
  Upgrade.
* It tries to find an attached USB device with the IDs of Truly
  Ergonomic Keyboard in DFU mode (VID 0E6A, PID 030B).
* If none such device is attached, it finds a Truly Ergonomic Keyboard
  in its normal mode (PID 030C), sends to it a request to reboot in
  DFU mode and waits until it does.
* In both cases, when a device with PID 030B is found, the DFU tool
  sends it a series of requests transmitting the firmware code, and
  finally directs it to reboot back in application mode.

In order for this to work from within VirtualBox, we need to connect the
keyboard to the virtual machine before clicking Upgrade. We do this by
right-clicking the USB connector icon on the bottom of the VirtualBox
window and selecting Truly Ergonomic Keyboard from the popup menu. This
may be tricky if you have two identical keyboards attached. So I first
disconnect my main keyboard (or just switch off its port on my USB hub),
leaving only the test device attached. Fortunately, the DFU tool can be
operated using only the mouse.

Note that the keyboard has a protection DIP switch on its bottom. If set
to ON, the keyboard will not reboot in DFU mode when requested. So set
it to OFF on your test subject device and leave ON on the workhorse.

When the DFU tool reboots the keyboard in DFU mode, it is no longer
connected to the VM. We could connect it again using the popup menu, but
that gets tedious very quickly. Luckily, VirtualBox lets us set up a
filter that will always connect devices satisfying its conditions.

Open the Machine menu and click Settings. On the USB tab, click the
button with the plus sign. A menu pops up, listing your devices that can
be connected. Select the one that says Unknown device, Vendor ID 0E6A,
Product ID 030B. A filter will be created. Click OK.

Now, the DFU tool probably has already timed out waiting for the device.
Cycle the power to the keyboard. Connect it to the VM. Click Upgrade
again. This time, the filter will kick in and automatically connect the
keyboard (which has rebooted in the DFU mode). The DFU tool will notice
that and send it the new firmware. Then the keyboard will reboot with
the new firmware. Which, as you remember, automatically and immediately
reboots back to the DFU mode. The filter will again kick in and connect
it to the VM. You can check that this happened by right-clicking the USB
connector icon and seeing that the Unknown device (VID 0E6A, PID 030B)
is checked.

If you like to keep your spare in working condition, you can now reflash
the original firmware. This time, the device is already in DFU mode and
connected to the VM, so just load the .hex file and click Upgrade.
## Pin I/O

Our last test program just rebooted to ISP immediately on startup. Next
thing we want to test some pin input/output.

Let’s recall the pin assignments from disassembling the stock firmware.

* P3.1 [in] DIP switch 4
* P3.2 [in] DIP switch 3
* P3.5 [out] LED 3 (Num Lock by default)
* P3.6 [out] LED 1 (Caps Lock)
* P3.7 [out] LED 2 (Scroll Lock)
* P4.1 [in] DIP switch 2
* P4.2 [in] DIP switch 1
* P4.3 [in] DIP switch 5

Also, we have a bunch of pins connected to the key matrix (P1.0÷7,
P2.0÷7, P3.3, P3.4 select a column, then P0.0÷7 give states of the keys
in that column). But that’s for later.

All these I/O pins are negated: a 0 value indicates a LED, a DIP switch
or a matrix line is active.

This time we will first query the DIP switch 4. If it is in the ON
position (0), we proceed to the main program. If OFF, we reboot to the
ISP mode.

The main program will turn on the middle LED and then just do nothing.
## Tapping the timer

Let’s now see how interrupts are handled in SDCC.

The microcontroller has three timers. The original firmware uses Timer 0
in order to implement the delay routine. It programs this timer to work
in Mode 1. In this mode, the TH0:TL0 registers contain a 16-bit counter
which is incremented every 12 ticks of the system clock running at
12MHz (in other words, each microsecond).

Note that if we used the full capability of the 16-bit timer counter and
used the timer interrupt as a completion signal, we could measure delays
of up to 6.5 milliseconds. This is way too short, so we resort to
counting interrupts. For convenience, we arrange it so that a timer
interrupt happens each millisecond while the timer is running.

The delay routine accepts as argument an 8-bit unsigned number of
milliseconds, stores in a variable, sets the timer count to −1000, and
enables the timer. After that, it waits until the delay count becomes 0.
Meanwhile, the timer interrupt service routine gets called whenever the
timer count overflows, decrements the delay count (if not already zero),
and resets the timer to −1000. (The hexadecimal for −1000 in two’s
complement code is `0xFC18`.)

The 8-bit size for the delay count means we are limited to delays of up
to 255 milliseconds, or roughly ¼s. For larger delays, we can call
`delay(250)` two, three or four times.

To illustrate that the timer works, we will blink the middle LED with a
period of 500 milliseconds.