## 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.
Split into separate files

Up to now, we developed everything in a single `main.c` file. However,
already a couple of groups of functionality have emerged:

* Timer and all related things.
* Controller-specific things such as `reboot_to_isp`.

Soon we will start adding definitions for all Megawin-specific special
function registers, there are lots of them, and it’s not desirable to
clutter up `main.c` with them. So this seems a good time to split our
program into several compilation units.

* Timer goes into `timer.h` and `timer.c`.
* `reboot_to_isp` goes into `mg84fl54bd.h` and its corresponding `.c`.

The `Makefile` becomes a bit more complex, as it now has to separately
compile each `.c` file and then link the final program from object
files.
Typing out MG84FL54-specific registers

In the next part we will start using the functionality specifically
provided by our microcontroller, the Megawin [MG84FL54BD][]. It has
quite a few special function registers (SFRs) about which SDCC’s
standard headers know nothing. So we will need to list them. They go in
the controller-specific header file.

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

Megawin’s SFRs come in two varieties. There are ordinary SFRs located in
the direct addressable memory at `0x80` through `0xFF`; and there are USB
SFRs at extended memory addresses `0xFF00` through `0xFFFF`.

Many registers are bit masks (with each bit having separate meaning),
and a few are bit-addressable (the controller can access an individual
bit without using bitwise operations).

The syntax for a direct SFR is:

```c
__sfr __at(0xD5) KBPATN;
```

(It is technically a bit mask register, but its bits don’t have
individual names in the manual so we don’t bother.)

For a bit mask register, we want to define symbolic constants for each
significant bit:

```c
__sfr __at(0xAD) AUXIE;
enum { EUSB = 0x80, ETWSI = 0x40, EKB = 0x20, ESPI = 0x01 };
```

For bit-addressable registers, SDCC provides a mechanism of giving names
to each individual bit:

```c
__sfr __at(0xE8) P4;
__sbit __at(0xE8) P4_0;
__sbit __at(0xE9) P4_1;
__sbit __at(0xEA) P4_2;
__sbit __at(0xEB) P4_3;
```

For `xdata` SFRs, there is no special syntax in SDCC. So we need to
declare them as absolute address `xdata` variables, with a `volatile`
qualifier so that the optimizer does not meddle with them:

```c
volatile unsigned char __xdata __at(0xFFC0) DCON;
```

USB SFRs can also be bit mask registers.

But it’s a bit tiring to type out all this boilerplate so let’s employ
some preprocessor magic. We would like the following syntax:

```c
SFR(0xD5, KBPATN); // byte SFR
SFR_(0xAD, AUXIE, EUSB, ETWSI, EKB, _, _, _, _, ESPI); // bitmask SFR
BIT_SFR(0xE8, P4, _, _, _, _, P4_3, P4_2, P4_1, P4_0); // bit-addressable SFR
XSFR(0xFFD8, UADDR); // byte xdata SFR
XSFR_(0xFFC0, DCON, _, _, _, SRWKP, EP3DIR, _, _, _); // bitmask xdata SFR
```

To achieve this, we define a few macros. The easy ones:

```c
#define SFR(address, name) __sfr __at(address) name
#define XSFR(address, name) volatile unsigned char __xdata __at(address) name
```

Bit masks:

```c
#define BITS(b7, b6, b5, b4, b3, b2, b1, b0) \
    enum { b7 = 0x80, b6 = 0x40, b5 = 0x20, b4 = 0x10, \
        b3 = 0x08, b2 = 0x04, b1 = 0x02, b0 = 0x01 }
#define SFR_(address, name, b7, b6, b5, b4, b3, b2, b1, b0) \
    SFR(address, name); \
    BITS(b7, b6, b5, b4, b3, b2, b1, b0)
#define XSFR_(address, name, b7, b6, b5, b4, b3, b2, b1, b0) \
    XSFR(address, name); \
    BITS(b7, b6, b5, b4, b3, b2, b1, b0)
```

Bit-addressable:

```c
#define BIT_SFR(address, name, b7, b6, b5, b4, b3, b2, b1, b0) \
    SFR(address, name); \
    __sbit __at(address) b0; \
    __sbit __at(address + 1) b1; \
    __sbit __at(address + 2) b2; \
    __sbit __at(address + 3) b3; \
    __sbit __at(address + 4) b4; \
    __sbit __at(address + 5) b5; \
    __sbit __at(address + 6) b6; \
    __sbit __at(address + 7) b7
```

Now we only have to define the magic underscore “don’t care” macro. We
want it to expand each time into a unique unused identifier. Luckily,
the SDCC compiler has an extension macro `__COUNTER__` just for this
purpose.

```c
#define CATENATE(a, b) a ## b
#define UNUSED(x) CATENATE(UNUSED_, x)
#define _ UNUSED(__COUNTER__)
```

(Two auxiliary macros are needed to force expansion of `__COUNTER__`
before `##` catenation.)
## Reboot on any key

While trying to implement the basics of USB, I got tired of continuously
toggling the safety DIP (reboot as ISP → reflash → reboot normally → it
doesn’t work as expected → repeat). Moreover, it is said that DIP
switches have a limited resource. Would be much easier if I could reboot
to ISP mode with a press of a key — we don’t have any use for them yet.

This is where the keypad interrupt comes in handy.

First, we set up the column pins as open-drain outputs:

```c
void keypad_init(void)
{
    P1M0 = 0xFF;
    P1M1 = 0xFF;
    P2M0 = 0xFF;
    P2M1 = 0xFF;
    P3M0 = 0x18;
    P3M0 = 0x18;
```

Then we activate all columns:

```c
    P1 = 0;
    P2 = 0;
    P3 &= 0xE7;
```

And set up the keypad interrupt to trigger when any row goes active:

```c
    KBMASK = 0xFF;
    KBPATN = 0xFF;
    KBCON &= ~PATN_SEL;
    AUXIE |= EKBI;
}
```

The interrupt handler will just reboot to ISP mode.

(While coding this change, I stumbled at the SDK manual calling a few
register bits with inconsistent names (`PATN_SEL` vs `PTNS`, `EKBI` vs
`EKB`). I added aliases for these bits.)