Sunday, 14 December 2025

MEGAphone User Interface and Text Messaging Software

This post is really just a summary of the work done for the telephony software user interface and SMS/Text messaging user interface.  Various other posts describe the considerable work done to achieve this, and the requirements of the software. 

This software is one area that I think is fair to say that I underestimated the work required. That said, the result is much better than I had originally anticipated -- we have a full anti-aliased proportional text rendering engine that can even display emoji and other arbitrary unicode characters, and with quite good performance -- no mean feat on a machine with 384KB RAM and a 40MHz 8-bit processor!  

The architecture that it has ended up with for the software is quite sound, and fairly easy to extend now, with a very modular structure, which I exploit since a single ~50KB program file (the practical limit with the C64 memory layout and compiler tool-chain I'm using) can't fit everything.  A 4KB shared state structure is passed between the sub-programs that are loaded practically instantly from the SD card.

Speaking of compilers, a fair bit of work went into switching from CC65 to MOS-LLVM to improve performance and code density, as well as getting LLVM's much better compiler warnings and errors.

The net result is something that looks quite credible and on a similar graphical standing to the Nokia S60-series of phones.  Before we jump into the technicality of what's been done, and mapping that to the milestones for the NLnet grant that has so generously supported this work, let's have a look at some representative screen-shots and a short video that shows the user-interface in action.

 

Let's start with the status bar at the top of the display, where we have both network signal strength and battery charge information in the top-right. Both of these are already "live", and will reflect the real phone state once we hook the cellular modem in: 

Then on the top-left we have the time and phone network information, again live and pending only the cellular modem integration to become fully active:
Between the two of these is space for message and other notifications.

So let's look now at the user-interface itself. Again we can see the use of the nice proportional text. I know I mentioned it before, but I'm still really quite pleased with the result -- remember this computer has so little RAM (intentionally), that a 720x400 pixel frame buffer with 8-bit colour depth would require 288KB, leaving very little RAM left for either code or data.  So instead it is implemented using the VIC-IV's crazy text modes, which allow for variable width characters, hardware anti-aliasing and a variety of other subtle features that make this feat possible.  

This is also why the display draws so quickly: The software doesn't have to render pixels -- it renders characters, and the anti-aliasing and other compositing effects are all done in hardware. 

But back to the UI...

First up we have the default display of the phone, with dialpad on the left and contact list on the right. In terms of core telephony, this is the heart of what matters, so we've made it simple.  The side-by-side layout means that you don't have to swap between the views endlessly.  You can scroll up and down through the contacts, or add new contacts as needed.
 

By hitting F1, we can copy the phone number of a contact into the dial-pad, ready to dial:

Hit F1 a second time, and it will place the call. Now we see the dialer state change, with the call button changing from green to yellow that call is being established. The cross also goes to red to indicate that we can end the call by hitting that. Also I've hit the "mute button", causing that to go red.  
Both the call-establishing and mute activated buttons blink, so that you have a visual indicator to remind you that they are active (see below). Again, the blinking is done in hardware, so the software doesn't have to do anything to maintain the display:

The other main part of the display -- and that can be used during a call as well as when the phone is idle -- is that contact list. Hitting F3 (or tapping on the contact -- I've been listing keyboard short-cuts for things above that I use for ease of development, but all of them map to the touch-interface as well) brings up the SMS thread for that contact, from where we can read and write SMS messages. 

Being able to text back and forth while in a call is one of those things that I personally find quite useful, but is absurdly convoluted to do on most phones.

So that's a quick guide through the user-interface for telephony, contacts and text messages.

But let's now attend to the specific milestones associated with this work:

m. 1.4 Telephony software User-Interface: Requirements Specification 

So let's look at the requirements for the telephony software:

1. Dialpad that allows dialing.

Visible in the demonstration video. 

2. Contact list that allows storage of contacts, including creation and editing of contacts.

Visible in the demonstration video. 

3. Ability to dial based on contacts, including showing the name of a contact.

4. Scrolling through contact list.

Demonstrated in this blog post: https://c65gs.blogspot.com/2025/11/megaphone-contact-list-and-dialer.html 

5. Selecting a contact to view the SMS message thread for. 

6. Implementation of call state management.

7. UI elements update according to call state.

8. Telephone battery status indication.

9. Cellular network status information. 

n. 1.4 Telephony software User-Interface: Implementation
o. 1.4 Telephony software User-Interface: Testing
p. 1.4 Telephony software User-Interface: Revision

The evidence for these three milestones is presented in this post (implementation), and in previous posts where the software has been implemented, tested and revised to reach the point of demonstration in this blog post that satisfies the requirements of this component.

The source code is as usual at https://github.com/MEGA65/megaphone-modular/tree/main/src/telephony

In terms of evidence of progress, the development of the software is covered in multiple blog-posts, e.g.:

https://c65gs.blogspot.com/2025/06/accessing-shared-resources-from-mega65.html

https://c65gs.blogspot.com/2025/07/megaphone-call-sms-and-contacts.html

https://c65gs.blogspot.com/2025/08/megaphone-software-tools-and-laying.html

https://c65gs.blogspot.com/2025/09/stack-backtrace-on-mega65-using-mos-llvm.html

https://c65gs.blogspot.com/2025/10/simple-memory-protection-scheme.html

https://c65gs.blogspot.com/2025/10/sms-thread-display-message-editing-etc.html

https://c65gs.blogspot.com/2025/11/megaphone-contact-list-and-dialer.html

https://c65gs.blogspot.com/2025/12/megaphone-contact-list-and-dialer.html 

u. 1.6 Text Messaging Software: Requirements Specification 

1. Ability to display SMS messages.

2. Ability to draft SMS messages.

3. Ability to request the sending of an SMS via the cellular network. 

4. Logging of a sent SMS message.

5. Differentiation between received and sent SMS messages.

6. Scrolling through SMS messages.

In addition, we had stretch-goals of:

7. Supporting international characters in SMS messages.

8. Supporting emoji in SMS messages.  

v. 1.6 Text Messaging Software: Implementation
w. 1.6 Text Messaging Software: Testing
x. 1.6 Text Messaging Software: Revision 

The evidence for these three milestones is presented in this post (implementation), and in previous posts where the software has been implemented, tested and revised to reach the point of demonstration in this blog post that satisfies the requirements of this component.

The source code is as usual at https://github.com/MEGA65/megaphone-modular/tree/main/src/telephony

The same blog-posts referenced above also cover the process of developing this software.

MEGAphone Power Management FPGA and Software Interface

Okay, so I'm burning through a bunch of these outstanding items that have been mostly implemented in the background, and now I need to finish in a hurry.

This one is the FPGA that controls power switching of the DC:DC modules that allow the MEGAphone to turn on and off the various sub-systems.

It's requirements are quite simple:

1. Provide an API to the low-power FPGA that controls power to all other modules.

2. Program the low-power FPGA so that it interprets these commands and sends signals to the various DC:DC converters to actually control the power.

3. Provide a mechanism to provide the list of sub-systems in a human-readable and machine parseable that the low-power FPGA knows about, to make it easier for user software to manage.

4. Monitor communications on a second UART, so that events from the cellular modem can be detected.

5. Relay communications from that second UART to the main FPGA.

6. Detect RING and +QIND strings from the cellular modem UART and turn on power to the main FPGA. 

7. Log any +QIND strings from the cellular modem UART so that important events (like SMS reception) don't get missed, and the main FPGA knows what to do when it gets turned on.

8. Allow playing back of the log of cellular modem events to the main FPGA.

9. Allow clearing of the log of cellular modem events to the main FPGA.

10. Allow setting the baud rate that the cellular modem monitor uses, to allow setting the cellular modem UART to various speeds.  

And then for the software interface:

11. Provide a C library that wraps the API for use by any software that wants to run on the MEGAphone and be power aware. 

12. Provide functions that enumerate the various sub-systems.

13. Provide a function that maps a named sub-system or circuit to a circuit ID. 

14. Provide functions to query and set the power of each individual sub-system by circuit ID.

15. Provide a function that allows easy retrieval of arbitrary configuration information from the power control FPGA. 

This will consist of VHDL for requirements 1-10 above, and C for requirements 11-15.

These are simplifications of the previous requirements, based on the experience we've gained in progressing the project.

The low-power FPGA is an iCESugar Nano. We will be using one GPIO each for the DC:DC converter enable lines, and two GPIOs to make a UART interface for the main FPGA and software to talk to.  Electrical isolation of the inter-FPGA interfaces is outside of the scope of this work item.

We'll use the open-source toolchain, so let's make sure it's all installed:

sudo apt install yosys ghdl-gcc yosys-plugin-ghdl gnat nextpnr-ice40 fpga-icestorm

Then we can test synthesis using the blinky "hello world" test that I'll use to populate src/power-control-firmware/

That generates blinky.asc, which can then be copied onto the fake mass-storage device that the iCESugar Nano presents via the USB cable.

We'll tie the USB UART in parallel with two pins for the main FPGA UART to ease debugging. Then we'll have cellular UART pass through (So that the low-power FPGA can intercept RING and other commands that should trigger a wake up of the main FPGA.  That's six pins. We'll also have an I2C interface to talk to IO expanders so that we can monitor and control buttons, again, also so that we can trigger waking. But we will also have one button wired directly for robust waking, even if we don't have an I2C IO expander.

Okay, so I've implemented the cellular modem recording whenever a +QIND message occurs (which report SMS arrival among other handy things). The power to the main FPGA is also turned on in that case.  Similarly if it sees RING it will turn on the main FPGA power.  The power button short press to turn on, and long-press to turn off are also implemented.

The tiny FPGA is _really_ tiny, however, and is already >60% full with just those things. 

Fortunately I just need the commands to turn power on and off, which can be done very simply.

The problem now is when I run the design, the ICELink firmware is also listening in on the UART pins and having kittens.  Dropping the baud rate I'm using to 115,200 instead of 2Mbps seems to have fixed that problem. I can probably live with that.

But we still see no output from the FPGA's little program.  So I'll have a bit of a poke at that. 

Okay, fixed that. 

Also simplified the cellular modem to main FPGA UART path: We only need one UART RX from the cellular modem in the FPGA, and for convenience, a relay of the cellular modem UART pin through the low-power FPGA and out to the main FPGA, so that we don't have a double load on the UART pin from the cellular modem.  

That also means that we can have 6 instead of 4 power circuits.

I've also added a '?' command that reports information about the power control module, including which circuits control which devices.

I'm now writing a utility that lets you talk with this power control interface.  This will be compilable on Linux as well as as a native library on the MEGA65 to support development and testing. This utility will support several commands:

config -- report the set of circuits that the module can control.

status -- show the state of each circuit, and whether there have been any cellular modem events since they were last purged

+<n|circuit name> -- turn on circuit n

-<n|circuit name> -- turf circuit n off

celspeed=<baud rate> -- Set the baud rate that the power control system expects the cellular modem UART to be talking at.

celplay -- play back any recorded cellular events

celclear -- clear the cellular recorded events buffer

Okay, so I've got those all implemented -- and the software library behind it to provide a simple API.  This has been one of those situations where the challenge has been turning the general idea of what I want to do into something functional and simple.  The need to keep the code small for the MEGAphone generally has been a really helpful challenge to work out what each sub-system needs to do, and what they don't need to do.

Requirements Verification 

Anyway, it's all there and working now, so let's go back over our set of requirements, show how they are satisfied, and have a bit of a demo at the end.

1. Provide an API to the low-power FPGA that controls power to all other modules.

Okay, so let's tackle this one from a couple of different directions.  First, here's the C header with the API (which we'll come back to in more detail later in this list):

https://github.com/MEGA65/megaphone-modular/blob/main/src/power-control-firmware/powerctl.h 

Then we have documentation of the API here:

https://github.com/MEGA65/megaphone-modular/blob/main/src/power-control-firmware/README.md  

2. Program the low-power FPGA so that it interprets these commands and sends signals to the various DC:DC converters to actually control the power.

Okay, this one we can see in the source for the low-power FPGA:

https://github.com/MEGA65/megaphone-modular/blob/main/src/power-control-firmware/megaphonepwr.vhdl 

Sure, there are some other files involved, but the main code for the FPGA is <500 lines, reflecting the work done to make this interface as simple as possible, while still doing everything we need.  

3. Provide a mechanism to provide the list of sub-systems in a human-readable and machine parseable that the low-power FPGA knows about, to make it easier for user software to manage.  

This is done in a nice simple way in the above VHDL: I have a BRAM which is pre-populated with the contents of a text file, which the VHDL then plays out to the client if they use the '?' command.  This means that the FPGA needs very little logic to implement this very helpful feature.

The file that actually gets sent is here:

https://github.com/MEGA65/megaphone-modular/blob/main/src/power-control-firmware/config_message.txt

The content of that is quite simple, but effective:



MEGAphone power management System
---------------------------------
https://github.com/MEGA65/megaphone-modular/tree/main/src/power-control-firmware
Produced with the support of the NLnet Foundation (nlnet.nl)

Commands:
        ? - Show this message
        . - Report current status
        P - Playback cellular modem event log
        X - Clear cellular modem event log
      0-9 - Power circuit (drive enable pin high)
shift 0-9 - De-power circuit (pull enable pin low)
      A-G - Select UART speed for cellular modem tap
            (2MBPS,1MBPS,230K4,115K2,19K2,9600,2400)

Playback ends with NUL (0x00) character.

Status indication at 0.5Hz (or as requested via '.'):
     bit 7 : Always set to 1 (i.e., status bytes are 0x80 -- 0xff)
     bit 6 : Set if cellular modem events logged
  bits 5-0 : Status of first six power circuits 

VER:1
MINOR:0
CIRCUITS:6
0:30:20:01:Main FPGA,LCD,LED,B6
1:31:21:02:Auxilliary device 1,C6
2:32:22:04:Auxilliary device 2,C5
3:33:23:08:Auxilliary device 3,E2
4:34:24:10:Auxilliary device 4,B5
5:35:25:20:Auxilliary device 5,C2
6:50:58:40:Cellular modem event flag
7:2e:2e:80:Status byte indicate
END

The top part of the file is the human readable part (obviously ;) and then below that we have some nice simple CSV lines that are machine parseable, but also still quite readable for a human.  

First we have the major (VER) and minor (MINOR) version numbers, so that the library can check that it's talking to a supported version.

Then we have the number of circuits it can control (CIRCUITS), followed by a specification line for each circuit.

Those lines have the circuit ID, then the hex values for the characters to be sent to turn a circuit on or off, and then the bit mask in the status byte that corresponds to this circuit.  Then finally we have the list of circuit(s) that it controls.  For completeness, I also list the FPGA pin that this corresponds to.

Then for the two virtual circuits 6 and 7 I report what the other 2 bits in the status byte are, with the hex for the cellular modem event log playback (0x50 = 'P')  and expunge/clear (0x58 = 'X').

Finally we have the END statement, so that the library knows it's reached the end of the specification, if that's required.

Right now the library assumes that for version 1.0 the hex values are fixed to save code size, but it would be quite easy to use the powerctl_getconfig() function to query those and use them. I will likely improve the library to do exactly that when I get the chance. But it works fine as is.

4. Monitor communications on a second UART, so that events from the cellular modem can be detected.

This is implemented in the FPGA by having a pair of pins that accept the UART's TX line, and then echoes it out on another pin:

    -- Cellular modem UART interface
    E3 : in std_logic;
    -- Pass-through of cellular modem UART interface
    E1 : out std_logic;
 

5. Relay communications from that second UART to the main FPGA.

This is one of those things that is so absurdly simple in an FPGA. It just takes a single line to continually cause the UART line to be copied out at 12MHz, which is more than fast enough for any UART:

  -- And simple connection of the rest of the main FPGA to cellular modem
  -- route.
  E1 <= E3;

6. Detect RING and +QIND strings from the cellular modem UART and turn on power to the main FPGA.

Okay, this is the more complex part of this. First up, we need to implement a UART receiver block to monitor communications:

  -- UART that listens to the cellular modem
  cellular_uart_rx: entity work.uart_rx
    port map (
      clk => clk,
      bit_rate_divisor => cel_uart_div,
      data => cel_rx_data,
      data_ready => cel_rx_ready,
      data_acknowledge => cel_rx_ack,
      uart_rx => E3
      );

Then we have to have a way to look for the RING and +QIND strings. It's a bit longer, but in principle very simple once I boiled it down to it's essence:

        case cel_rx_data is
          when x"52" => -- 'R'
            ring_rx_state <= 1;
          when x"49" => -- 'I'
            if ring_rx_state = 1 then
              ring_rx_state <= 2;
            else
              ring_rx_state <= 0;
            end if;
          when x"4E" => -- 'N'
            if ring_rx_state = 2 then
              ring_rx_state <= 3;
            else
              ring_rx_state <= 0;
            end if;
          when x"47" => -- 'G'
            if ring_rx_state = 3 then
              -- Turn on power to main FPGA
              LED <= '1';
              report_power_status <= '1';

              -- Insert that R into the log
              cel_log_waddr <= cel_log_waddr + 1;
              cel_log_we <= '1';
              cel_log_wdata <= x"52"; -- ASCII 'R'
              
            end if;
            ring_rx_state <= 0;
          when others => null;
        end case;
        -- And the +QIND detector
        case cel_rx_data is
          when x"2b" => -- '+'
            qind_rx_state <= 1;
          when x"51" => -- 'Q'
            if qind_rx_state = 1 then
              qind_rx_state <= 2;
            else
              qind_rx_state <= 0;
            end if;
          when x"49" => -- 'I'
            if qind_rx_state = 2 then
              qind_rx_state <= 3;
            else
              qind_rx_state <= 0;
            end if;
          when x"4E" => -- 'N'
            if qind_rx_state = 3 then
              qind_rx_state <= 4;
            else
              qind_rx_state <= 0;
            end if;
          when x"44" => -- 'D'
            if qind_rx_state = 4 then
              -- Turn on power to main FPGA
              LED <= '1';
              report_power_status <= '1';
              -- And begin logging what the cellular modem has to say, so that
              -- the main FPGA can interrogate us for it once they have powered
              -- up. (note that it will skip the +QIND from each line logged, so
              -- we put a 'Q' into the log to mark the cause of logging.

              -- Log until the next CR or LF
              log_cel <= '1';
              -- Insert that Q into the log
              cel_log_waddr <= cel_log_waddr + 1;
              cel_log_we <= '1';
              cel_log_wdata <= x"51"; -- ASCII 'Q'              
              
            end if;
            qind_rx_state <= 0;
          when others => null;
        end case;

7. Log any +QIND strings from the cellular modem UART so that important events (like SMS reception) don't get missed, and the main FPGA knows what to do when it gets turned on.

Once the above detects these strings, it uses this simple block to record the rest of any line in a +QIND. But the above also inserts a R or Q character to indicate the type of event it's seen.

      if cel_rx_ready = '1' and cel_rx_ready_last='0' then
        cel_rx_ack <= '1';

        cel_rx_data_last <= cel_rx_data;
        
        -- Log output from the modem if required.
        -- This continues until the end of a line is encountered
        if log_cel = '1' then
          -- We don't use the last byte in the cellular data log BRAM,
          -- as we need that address free to confirm we have played back to
          -- the end without looping back around.
          if cel_log_waddr /= CEL_LOG_MAX_ADDR then
            cel_log_waddr <= cel_log_waddr + 1;
            cel_log_we <= '1';
            cel_log_wdata <= std_logic_vector(cel_rx_data);
          end if;
          if cel_rx_data = x"0d" or cel_rx_data = x"0a" then
            log_cel <= '0';
          end if;
        end if;

8. Allow playing back of the log of cellular modem events to the main FPGA.

Playing the log back is also a fairly simple affair. We check if the 'P' command is received:

        case pwr_rx_data is
          when x"50" => -- 'P' -- Play back logged cellular data.
            if cel_log_waddr /= to_unsigned(0,CEL_LOG_BITS) then              
              cel_log_playback <= '1';
              cel_log_raddr <= to_unsigned(1,CEL_LOG_BITS);
            else
              -- Nothing in the log, so just indicate that with a NUL byte
              pwr_tx_data <= x"00";
              pwr_tx_trigger <= '1';
            end if;

And if so, that triggers the actual playback, reading from a BRAM where the events were logged:

      elsif cel_log_playback = '1' then
        if pwr_tx_ready = '1' and pwr_tx_trigger='0' then
          pwr_tx_data <= unsigned(cel_log_rdata);
          pwr_tx_trigger <= '1';
          cel_log_raddr <= cel_log_raddr + 1;
          -- If we reached the end of the log, then stop playing back.
          if cel_log_raddr > cel_log_waddr then
            pwr_tx_data <= x"00";
            cel_log_playback <= '0';
            cel_log_raddr <= to_unsigned(1,CEL_LOG_BITS);
          end if;
        else
          -- See if we need to send anything else
          null;
        end if;
      end if;
 

9. Allow clearing of the log of cellular modem events to the main FPGA.

This is even simpler, we just reset the write address into the log if the command is received:

          when x"58" => -- 'X' Expunge cellular data log
            cel_log_waddr <= to_unsigned(0,CEL_LOG_BITS);
            cel_log_playback <= '0';
  

10. Allow setting the baud rate that the cellular modem monitor uses, to allow setting the cellular modem UART to various speeds.  

Okay, this is the last of the VHDL bits, and is again fairly simple: Rather than a full numeric parser, it just supports single-byte commands to select the baud rate from a list of plausible values:


  constant UART_DIV_2MBPS : integer :=  (12_000_000 / 1) / 2_000_000;
  constant UART_DIV_1MBPS : integer :=  (12_000_000 / 1) / 1_000_000;
  constant UART_DIV_230K : integer :=  (12_000_000 / 1) / 230_400;
  constant UART_DIV_115K : integer :=  (12_000_000 / 1) / 115_200;
  constant UART_DIV_19200 : integer :=  (12_000_000 / 1) / 19_200;
  constant UART_DIV_9600 : integer :=  (12_000_000 / 1) / 9_600;
  constant UART_DIV_2400 : integer :=  (12_000_000 / 1) / 2_400;
...

  -- Default to 2Mbps for cellular UART
  signal cel_uart_div : unsigned(23 downto 0) := to_unsigned(UART_DIV_115K,24);
...

            -- Cellular modem tap UART speed set
          when x"41" => cel_uart_div <= to_unsigned(UART_DIV_2MBPS,24);
          when x"42" => cel_uart_div <= to_unsigned(UART_DIV_1MBPS,24);
          when x"43" => cel_uart_div <= to_unsigned(UART_DIV_230K,24);
          when x"44" => cel_uart_div <= to_unsigned(UART_DIV_115K,24);
          when x"45" => cel_uart_div <= to_unsigned(UART_DIV_19200,24);
          when x"46" => cel_uart_div <= to_unsigned(UART_DIV_9600,24);
          when x"47" => cel_uart_div <= to_unsigned(UART_DIV_2400,24);
 

11. Provide a C library that wraps the API for use by any software that wants to run on the MEGAphone and be power aware.

Okay, and now for the software side.  Let's start with the header and the functions it provides:

// Power management API

// Get the number of power circuits this unit controls
char powerctl_get_circuit_count(void);
// Retrieve the indicated field for the specified circuit
// e.g., asking for FIELD_CIRCUITNAME will return a human readable description of the
// circuit being controlled
char powerctl_getconfig(char circuit_id,char field_id,unsigned char *out,uint8_t out_len,
            int mode);
// Switch a circuit on (non-zero) or off (zero)
char powerctl_switch_circuit(uint8_t circuit_id, char on_off);
// Find the first circuit that contains the specified string in its human-readable name
char powerctl_find_circuit_by_name(char *string);

In short, we can find circuits, and set and get their state and read their config. As is the intention, there is no fat here.

// Cellular event log API

// Set the speed of the tap into the cellular modem UART used to capture
// events that should wake the main FPGA.
// Setting to the incorrect baudrate will disable auto-waking of the main FPGA
// on RING or +QIND events.
// Use AT+QIND to tell the cellular modem which events should be reported, and thus
// should wake the main FPGA.
char powerctl_cel_setbaud(uint32_t speed);
// Clear the cellular modem event log
void powerctl_cellog_clear(void);
// Retrieve the log of cellular modem events
// Returns the number of bytes read.  Excess bytes will be discarded.
uint16_t powerctl_cellog_retrieve(uint8_t *out, uint16_t buf_len);

Similarly the cellular event log API is super simple: We can tell it what baud rate the modem is using, and then we can read the event log and clear it. If you read it, and it has nothing in it, then there were no events :) 

// Low-level utility functions:

// Synchronise with power control FPGA and return current status byte
uint8_t powerctl_sync(void);
// Verify that the power control system is using a compatible version to this
// library.
// If major and minor are not NULL, then return the major and minor version of the
// power control FPGA firmware
char powerctl_versioncheck(uint8_t *major, uint8_t *minor);
// Commence reading the configuration message from the power control FPGA
char powerctl_start_read_config(void);

And finally we have some low-level functions, in case the user wants to do something particularly low-level. 

To demonstrate the use of these functions (and help me with testing, debugging and demonstration), if powerctl.c is compiled with the -DSTANDALONE flag, it also includes a simple command line tool that allows use of all of the functions. There's a video below showing this in action, as I go through how it satisfies the various requirement.

12. Provide functions that enumerate the various sub-systems.

See above. The command line tool uses these functions to show a list of circuits/sub-systems it can control:

    else if (!strcmp(argv[i],"config")) {
      // Do a first pass to get circuit count
      char circuit_count = powerctl_get_circuit_count();
      if (!circuit_count) {
    fprintf(stderr,"ERROR: Could not read count of controlled circuits\n");
    exit(-1);
      }
      fprintf(stderr,"INFO: System controls %d circuits\n",circuit_count);
      for(int circuit_id=0;circuit_id<circuit_count;circuit_id++) {
    // Stop when we fail to read info for a circuit
    unsigned char field[128];
    field[0]=0;
    if (powerctl_getconfig(circuit_id,FIELD_CIRCUITNAME,field,sizeof(field),
                   (circuit_id?GETCONFIG_CONTINUE:GETCONFIG_RESTART))) {
      fprintf(stderr,"ERROR: Failed to read information for circuit %d\n",circuit_id);
      exit(-1);
    }
    fprintf(stderr,"INFO: Circuit %d : %s\n",
        circuit_id,field);
      }
    }
  

13. Provide a function that maps a named sub-system or circuit to a circuit ID.

This function uses the powerctl_getconfig() function to allow searching for circuits by strings:

char powerctl_find_circuit_by_name(char *string)
{
  // Do a first pass to get circuit count
  char circuit_count = powerctl_get_circuit_count();
  if (!circuit_count) {
    return 0xff;
  }

  // Allow specifying circuit by number as well
  if (string[0]&&(!string[1])) {
    int circuit_id = string[0]-'0';
    if (circuit_id>=0&&circuit_id<circuit_count) return circuit_id;
  }
  
  for(int circuit_id=0;circuit_id<circuit_count;circuit_id++) {
    // Stop when we fail to read info for a circuit
    unsigned char field[128];
    field[0]=0;
    if (powerctl_getconfig(circuit_id,FIELD_CIRCUITNAME,field,sizeof(field),
               (circuit_id?GETCONFIG_CONTINUE:GETCONFIG_RESTART))) {
      // Failed to read info for this circuit
      return 0xff;
    }
    if (strstr((char *)field,string)) return circuit_id;
  }
  return 0xff;
}
  

14. Provide functions to query and set the power of each individual sub-system by circuit ID.

Querying status of circuits is done by obtaining the status byte. This is retrieved by calling powerctl_sync(), as we can see here in the implementation of the status command:

    else if (!strcmp(argv[i],"status")) {
      uint8_t st = powerctl_sync();
      if (!(st&0x80)) {
    fprintf(stderr,"ERROR: Failed to read status (received 0x%02x)\n",st);
    exit(-1);
      }
      if (st&0x40) fprintf(stderr,"INFO: Cellular Event(s) Recorded\n");
      else fprintf(stderr,"INFO: No Cellular Events Recorded\n");
      for(int i=0;i<6;i++) {
    if (st&(1<<i)) fprintf(stderr,"INFO: Circuit %d ON\n",i);
    else fprintf(stderr,"INFO: Circuit %d OFF\n",i);
      }
    }

For setting, we have powerctl_switch_circuit(). Again, the example of using this from the command line utility is quite simple:

    if (argv[i][0]=='+') {
      int circuit_id = powerctl_find_circuit_by_name(&argv[i][1]);      
      if ((circuit_id<0)||(circuit_id>5)) {
    fprintf(stderr,"ERROR: Could not find requested circuit.\n");
    exit(-1);
      }
      if (powerctl_switch_circuit(circuit_id,1)) {
    fprintf(stderr,"ERROR: Failed to switch circuit on\n");
    exit(-1);
      }
    }
    else if (argv[i][0]=='-') {
      int circuit_id = powerctl_find_circuit_by_name(&argv[i][1]);      
      if ((circuit_id<0)||(circuit_id>5)) {
    fprintf(stderr,"ERROR: Could not find requested circuit.\n");
    exit(-1);
      }
      if (powerctl_switch_circuit(circuit_id,0)) {
    fprintf(stderr,"ERROR: Failed to switch circuit off\n");
    exit(-1);
      }    
    }

15. Provide a function that allows easy retrieval of arbitrary configuration information from the power control FPGA.

We've already seen powerctl_getconfig() above.

Demonstration

The best way to show that this whole thing works is with a demonstration!

The test-rig I am using for this is the low-power IceSugar Nano board (left) connected to a USB UART that is pretending to be the cellular modem (right):

The yellow LED is tied in parallel to the main FPGA power output pin, so if we see that blink we know that it is switching power.

Here's me driving it for a few minutes to show the key features:


 

First up, here's some examples of using the command-line tool:

$ ./powerctl /dev/ttyACM0 115200 config
INFO: System controls 6 circuits
INFO: Circuit 0 : Main FPGA,LCD,LED,B6
INFO: Circuit 1 : Auxilliary device 1,C6
INFO: Circuit 2 : Auxilliary device 2,C5
INFO: Circuit 3 : Auxilliary device 3,E2
INFO: Circuit 4 : Auxilliary device 4,B5
INFO: Circuit 5 : Auxilliary device 5,C2
$ ./powerctl /dev/ttyACM0 115200 status
INFO: No Cellular Events Recorded
INFO: Circuit 0 ON
INFO: Circuit 1 OFF
INFO: Circuit 2 OFF
INFO: Circuit 3 OFF
INFO: Circuit 4 OFF
INFO: Circuit 5 OFF 

We can see that circuit 0 (which controls the main FPGA and LCD) is on. So let's try turning it off, and checking that it worked: 

$ ./powerctl /dev/ttyACM0 115200 -LCD
$ ./powerctl /dev/ttyACM0 115200 status
INFO: No Cellular Events Recorded
INFO: Circuit 0 OFF
INFO: Circuit 1 OFF
INFO: Circuit 2 OFF
INFO: Circuit 3 OFF
INFO: Circuit 4 OFF
INFO: Circuit 5 OFF

And let's now try turning circuit 4 on by number:

$ ./powerctl /dev/ttyACM0 115200 +4
$ ./powerctl /dev/ttyACM0 115200 status
INFO: No Cellular Events Recorded
INFO: Circuit 0 OFF
INFO: Circuit 1 OFF
INFO: Circuit 2 OFF
INFO: Circuit 3 OFF
INFO: Circuit 4 ON
INFO: Circuit 5 OFF

And it told us there's nothing the cellular event log, but let's take a look anyway:

$ ./powerctl /dev/ttyACM0 115200 celplay
INFO: Cellular event log:

INFO: End of cellular event log

I've then connected a UART to the cellular modem pins on this, and typed a couple of RING messages, as well as a +QIND with some text after it.  Let's see if they show up:

First up, we can see that events have been logged:

$ ./powerctl /dev/ttyACM0 115200 status
INFO: Cellular Event(s) Recorded
INFO: Circuit 0 ON
INFO: Circuit 1 OFF
INFO: Circuit 2 OFF
INFO: Circuit 3 OFF
INFO: Circuit 4 ON
INFO: Circuit 5 OFF

And then let's play it back: 

$ ./powerctl /dev/ttyACM0 115200 celplay
INFO: Cellular event log:
RRQ A message from the cellular modem.


INFO: End of cellular event log

We can see RRQ at the start, which says 2x RING + 1x QIND, and then we get the body of the QIND following that, in this case "A message from the cellular modem", which is the literal string I typed in when I was pretending to be the cellular modem. 

Finally, let's clear the log and check that that worked:

$ ./powerctl /dev/ttyACM0 115200 celclear
$ ./powerctl /dev/ttyACM0 115200 status
INFO: No Cellular Events Recorded
INFO: Circuit 0 ON
INFO: Circuit 1 OFF
INFO: Circuit 2 OFF
INFO: Circuit 3 OFF
INFO: Circuit 4 ON
INFO: Circuit 5 OFF
$ ./powerctl /dev/ttyACM0 115200 celplay
INFO: Cellular event log:

INFO: End of cellular event log

 

Mapping to Milestones

So we have six milestones tied to these requirements:

1.1b -Power managment effective control of all sub-systems: Implementation

SATISFIED: This is addressed by the implementation described above. 

1.1c -Power managment effective control of all sub-systems: Testing

SATISFIED: This is addressed by the demonstration of the correct functioning of the power control FPGA as described and demonstrated in the video above. 

1.1d -Power managment effective control of all sub-systems: Revision/Remediation

SATISFIED: This is addressed by the demonstration of the correct functioning of the system as described above. i.e., it is evidence that the necessary revisions and remediation occurred to achieve correct system function.

1.2b -Power managment interface for use by other software: Implementation

SATISFIED: This is addressed by the implementation described above. 

1.2c -Power managment interface for use by other software: Testing

SATISFIED: This is addressed by the demonstration of the correct functioning of the system as described above.

1.2d -Power managment interface for use by other software: Revision/Remediation

SATISFIED: This is addressed by the demonstration of the correct functioning of the command-line utility as described above. i.e., it is evidence that the necessary revisions and remediation occurred to achieve correct system function.

In short, we're done and dusted with this part of the design.

Sunday, 7 December 2025

MEGAphone Contact list and Dialer Integration

Okay, in the last post, I got the dialer and contacts screen and SMS thread handling etc, all largely in place and tied together.

We're on the home-stretch for the user-interface side of the telephony software, but there are a few things left to implement/do:

First up, we need to split the binary, as I keep hitting running out of RAM for the program in the bottom 64KB.

Second, I need to implement the ability to select a contact to call them, i.e., having it copy the name and number across to the dialpad side.

Third, I need to finish implementing the telephony state management, so that the phone knows when it should ring etc.

Fourth, we need a ring-tone of some sort, for testing at least.  There's a whole can of worms as for how I want to manage that down the track, but for now, we'll just stick a SID file at $E000-$EFFF and do a bit of ROM banking.

So let's get started with binary splitting. I think I'm just going to use #defines to conditionally include the various functions, so that it's easier to share some of the stuff.

We do need a way to keep track of state between the various binaries. I'm thinking that I'll define a nice little structure that we can hard-code it's address to, say, $0200, and any state that needs to be shared can go down in there --- e.g., dialpad partially entered number, currently selected contact, telephone state etc.  It shouldn't need to be very much.

I've created that in shstate.h, and made just dialer.c use it for it's shared state initially. But now the program crashes when I type digits into the dialer.  So I must have stuffed something up.  The structure is at 0x0200, and it's outputting asm that targets there.  But the stacktrace is also a bit weird, making it hard to figure out the real cause.

Interestingly it is not the memory write-protection that's triggering it. Rather it thinks an NMI is being triggered. I was accidentally stomping on the $031x vectors, and I'd rather hoped that fixing that would solve the problem. But it hasn't.

The backtrace seems to show that the problem is happening in our __cyg_profile_func_enter() handler that does the backtrace storage. Or rather, it happens on the JSR to that routine -- which feels totally bonkers.  The alleged BRK instruction is at some low ZP address, e.g., $0008, which suggests that it's jumping somewhere silly, or the NMI handler is being called some other way, and it just looks like it's happening there.

Nothing untoward seems to happen in the profile enter function, looking at the assembly language. Found it -- the KERNAL IRQ is doing C64 keyboard scanning type stuff, and didn't take kindly to me stomping all over $02xx.  So now I've restricted myself to the tape buffer area at $33C - $3FB, and it's working better.

So let's continue with the splitting, then... 

The obvious first line to split down, is between the contact list and the SMS thread screens.  We just need to have the two binaries call each other when switching views.

Ideally, they shouldn't try to redraw the entire screen when switching, so we want to suppress the whole video setup stuff, which of course can just go in it's own separate binary that does that setup, then effectively throws away the code. 

Ah, yes, and we need to unplug our IRQ and NMI catchers before switching binaries. We can do that with a JSR $FF8A.  With that, and some general bug-fixery, we now have FONEINIT.PRG doing the screen setup and then launching FONEMAIN.PRG from the SD card :)

Now we just need to work out how we intend to split the programs.  My current thinking is:

FONEINIT -- Screen initialisation etc

FONEMAIN -- Contact list display and telephony state management

FONESMS -- SMS thread display and editing and telephony statemanagement

FONESMSW -- SMS "write" (i.e., store SMS draft into the thread, update indexes etc, then return to FONESMS or whichever caller was indicated in the shared state area)

I'm sure there will be more than this needed, but that should get each small enough to leave for the telephony state management stuff to be included in each.  There may yet need to be a "telephony event" program, but we'll tackle that when we get to it.

I have it somewhat working, but llvm-mos's C64 CRT0 bootstrap routine makes a call to $FFD2 to set lower-case mode.  I have two choices: Find how to change the CRT0 routine, or just spoof the vector at $0326 that $FFD2 jumps through, so that it does nothing :)

That gets it a bit further, but it still bombs out, looking like it hits a BRK or NMI.  So need to figure out what's causing that. 

Putting a trap on $FE66, the default $0316 vector shows that it is being trapped. So apparently a BRK instruction. But weirdly, if I single-step, the problem never occurs -- it only happens when the CPU is free-running. 

Well, it gets weirder. Today it's triggering a RUN/STOP-RESTORE type situation, but it doesn't hit $FE47, which is where the $0318 vector points.

I'm wondering if llvm binaries don't like being loaded the way I'm loading them. Or if BASIC or the KERNAL are winding up in some messed-up state.  

There is still really something cooky going on, though, because what happens depends on whether the CPU is single-stepping or free-running.  t1 and single stepping doesn't crash. tc with free-running single-stepping also doesn't crash. But t0 to set the CPU going loose totally does crash.

So now I'm putting infinite loops in the initial code in the binary so that I can catch where the crash happens.

Disabling IRQs in the helper loader seems to get it to go a lot further, which makes sense, since single-stepping generally disables IRQs.

With that, it looks like it actually loads the next program, but that then crashes with a report from our traceback code:

>>> NMI/BRK triggered.
 A:4A X:00 Y:08 Z:00 P:B0 S:E0
  BRK source @ 2A44 dialpad_draw_button+0x0209
Backtrace (most recent call first):
[03] 0x0C64 main+0x01A0, SP=0x0C26
[02] 0x0A3F lpeek+0x000D, SP=0x0A85
[01] 0xA504 __mulhi3+0x783B, SP=0x2905
[00] 0x850A __mulhi3+0x5841, SP=0x260B
 

But then with another run:

>>> NMI/BRK triggered.
 A:20 X:3E Y:02 Z:00 P:30 S:DE
  BRK source @ 0A18 lpoke+0x0001
Backtrace (most recent call first):
[03] 0x0C64 main+0x01A0, SP=0x0C26
[02] 0x0A3F lpeek+0x000D, SP=0x0A85
[01] 0xA504 __mulhi3+0x783B, SP=0x2905
[00] 0x850A __mulhi3+0x5841, SP=0x260B
 

So it looks like it's not the program, but some interrupt that's happening.  So what on earth could be enabling and NMI? Not many things in the MEGA65 can generate an NMI.

I've added a Makefile target that use mega65_ftp to push the updated files onto the SD card and then resets and runs FONEINIT.PRG, so that I have a simpler work-flow for tracking this down.

Let's find out if it's a BRK or an NMI that's triggering this for starters.  Well, it turns out it was my memory write protection stuff, by the look of things. I have it setup to auto-protect the heap and RODATA of the LLVM compiled binaries -- which is ostensibly a good thing -- except when you switch binaries and the old write protect region is being used while the new binary is trying to set things up...

Okay, with that fixed, the binaries are being loaded now. But the display is being messed up a bit, specifically the status bar and the selected contact / dialed number bit above the dial-pad. It does look like the SMS display executable is being loaded, even though it should be staying in FONEMAIN which has the contact selection loop... Except that it seems that FONESMS is actually the one that actually has the contact list.  So let's rename them, and make this all a bit more logical.

Done. Now, investigating the display glitches, this looks like the problem is that the glyph cache is not in the shared state area. The problem is that it's too big to fit in the cassette buffer where I have stashed it.  The whole thing is 3KB.  A nice solution would be to tell LLVM that it can't use $C000-$CFFF, and then I instantiate that whole area with my complete shared state structure.   That just requires me to mess with the linker script thing.  

So now we're kind of back to where we were before hit the program size issue.  So let's not get it working so that we can select a phone number to dial from the contact list. We also want to be able to edit the dialing number field. 

Done -- we can now press F1 to load a contact's number into the dialer field.  Maybe I should make it so that if you press F1 a second time, it tries to commence the call. I can do this as part of the whole call state mangement, which it's high-time we implemented.

We have the following states defined right now:

#define CALLSTATE_NUMBER_ENTRY 0 
#define CALLSTATE_CONNECTING 1
#define CALLSTATE_RINGING 2
#define CALLSTATE_CONNECTED 3
#define CALLSTATE_DISCONNECTED 4
#define CALLSTATE_IDLE 5

Let's work on a simple state diagram, so that we can make sure that this makes sense.


So we probably don't need IDLE. And DISCONNECTED becomes NUMBER_ENTRY whenever we modify the number to dial.

I've implemented some stub functions for the various modem functions (like call, answer, reject etc), and added a timer that we can use to timeout from attempting to make or receive a call, so that it then moves back to the DISCONNECTED state.

The dialpad buttons are supposed to change as well, when the call state changes, but that doesn't seem to be happening, at least when we request an outbound call.  Okay, fixed that, and implemented F5 to toggle call mute, and F7 to end a call.

Well, I think that's everything except for a quick demo video!


Next stop is the power management stuff, and gettiing it to talk to the cellular modem. 

Tuesday, 2 December 2025

MEGAphone Case Design

I've been thinking about this part of things for a while, because it's clear that the first iteration of the modular MEGAphone is going to be larger than originally planned, and subject to more evolution for a while.  The goal is to make something that is easy to work on software-wise, as well as being easy to fabricate, and then switch out initial larger modules for smaller more integrated modules later.

So instead of going for a custom machined case, it occurred to me in the shower this morning, that I can just use Pelican style tough cases, with some inserts to hold everything in place.

Doing it this way, we can even allow for the inclusion of a full MEGA65 keyboard, which will make the earlier stages of testing easier.  Similarly, a larger screen could be included.

The more I think about this, the more I like the idea.  Basically we will be making a small number of portable MEGA65s, with cellular capability -- so all the existing development tools and work-flows will still work.

The keyboard PCB is ~32x15cm, and a standard MEGA65 mainboard is smaller than that, so there'd even be room to route standard HDMI to a larger screen in the lid.

It looks like the Tactix Large tough cases will be a good size. And they even come in a nice blue that suits an 8-bit device.  So I've gone and grabbed one, and also fished out the 15" LCD panel I had in the shed from a previous iteration of making a laptop MEGA65. I also have some SVGs with the right cut positions for a MEGA65 keyboard, too.  So this is looking like a good and fast way to build a prototype.

I also have a different prototype MEGAphone that was designed as a land-line, but still has integration of dialer and cellular modem onto a MEGA65 core.  I could in principle fit that into this case and make a working luggable MEGAphone.

Let's start with fitting the LCD, since I'll need that, regardless of what hardware I put in the bottom half of the case. I'll mock it all with MDF panels that I can easily crock up here at home. Then once I know the dimensions of the panels and locations of cut-outs, I can order machine cuts ones online from one of the various fabricators.

The LP154WU1 panel I have is a bit old now, but can still be found online for AU$100 - AU$200 in small quantities, which is all we need for now. The only issue is the driver boards seem to vary with the control button placement. But I can work around that by designing my own, as their interface is extremely simple.  So let's work out how we'll hold the panel in place.  

The panel is just over 5mm thick around the edges, so I could order some 5mm thick shims with a selection of screw hole offsets, so that the screen position and fit can be trimmed a bit, without having to vary the position of holes on the front of the panel.  I'll probably go for 2 screws on either side and on the bottom, and four on the top, with one shim on each side and the bottom and two on the top edge. That should hold in firmly enough if I then have panels that sit behind them and go behind the edges of the panel --- and be safer for allowing variance in the exact dimensions of the panels.

I'll also need a rear panel onto which the driver PCBs can be mounted. That can probably sit in the bottom of the lid, and will have to be a bit smaller to allow for the curve radius of the lid. That panel can probably be fixed to the brace I'll put across the rear of the panel.

Now, that's all a lot of words, so I'll start mocking up the pieces for this, and then convert that to a set of pieces. If I can get all clever, it should be possible to make it able to be cut from a single panel, if the diagonal is long enough to accommodate the back brace, or I do some other horrible hack.  

Anyway, let's start by cutting the aperture for the LCD panel and working out where everything needs to go.

Well, that was helpful in some ways. But it got way too fiddly too fast. So I checked and found our local Maker Space is open on Saturdays, signed up, and went in and did an initial MDF laser cut to test the measurements I'd made.  One of the nice volunteers ran it for me, because I haven't done the laser cutter induction yet:

So much nicer than me hacking away with a jigsaw: 
There were of course lots of holes in the wrong places, so I marked up the MDF and will go back during the week with the updated file: 
 
 
 


In the meantime, I can probably start planning the same for the lower-half.

Years ago I started work on making a laptop MEGA65, and did make SVG outlines for the keyboard cut-out, which would save me a bunch of time about now.  The question is whether I can find it... Nope. So I'll just have to start from scratch. I might be able to save some time by using the MK-II keyboard layout to trace the key blocks.    

Okay, so I've done that, and also gone and picked up a pile more MDF panels ready for testing and revising with the laser-cutter.

So let's finish out the design:  The lower case is deeper, and will need to have, keeping it very modular for now:

1. Keyboard

2. Main logic board

3. Cellular and related modules board

4. Power management board

5. Batteries

6. Solar panel(s)

My plan of using a regular MEGA65 mainboard slightly complicates things, in that it needs 12V DC in, which means we'll need another higher output-voltage DC:DC converter that can be switched by the low-power FPGA.  This is really the trickiest part of the whole thing, because I can't find a DC:DC converter that will go from 2.5V (lowest single-cell LiFePO4 cell voltage) to 12V in a single go -- and that can provide ~2A output.  (The 15" LCD uses about 0.8A, and a MEGA65 uses about 0.3A -- so allowing for some in-rush head-room, 2A feels safe).

Now, the MEGA65 can start with >9V, but the LCD panel's backlight probably really needs 12V, so we'll have to stick with 12V for now.

What I'm looking at right now is one of these eval boards: TPS61088Q1EVM-037, which in theory can Just Do It All :tm:. It even has a nice EN line, so we can control the power from the low-power FPGA board.

So I'll order one, and try it out. The only issue with it is that whoever designed it made lots of components big to allow experimentation... except the resistors that set the output voltage. Those are 0402 fly-poop sized. Fortunately there's a couple of test points I can solder a through-hole resistor to, and then use the soldering iron to erase the corresponding piece of fly poop.

Anyway, that board's ordered. 

Thinking through the other bits and pieces, if I use an R3 or R3A board (or an R2 for that matter), then it has on-board speaker drivers and amplifiers that can be used for the ringer.  I'll use an actual hand-set on this unit, because I have a cellular carrier board on-hand that has audio CODEC and handset drivers -- the benefit of modular design strikes again!

So now I just need to make sure that the board stack will physically fit in the depth of the case, and design a simple board stack that everything can be attached to, and that can attach to the shell of the tough case. This will need a few layers:

1. Top layer to retain the keyboard.

2. Second layer to prevent the keyboard shorting on anything below it.

3. Third layer to hold the MEGA65 main board, low-power FPGA board, battery/charge controller.

4. Fourth layer to hold cellular communications board and 12V regulator board.

5. Fifth layer which is the bottom, to hold the batteries in place, and allow fixing to the shell.

The fifth and possibly fourth layers will need to be slightly smaller to accommodate the curvature at the back of the case.  

Now we need to work out the vertical spacing required between each of those layers, and confirm that it doesn't exceed the total case depth. 

Okay, I've been at the laser cutter in the Makerspace iterating away.  I'll save you the 90 minute build montage, and just show the results so far:



So basically we have the screen bezel and related bits and pieces to go in the top of the case.  The last remaining challenge there is working out where to drill the four holes.  This is a bit of a pain, as they have to be located correctly, as well as have the correct spacing, so that the screen bezel is correctly located.

I've also been iterating on the cuts for the lower-half, to hold the keyboard, phone handset, MEGA65 components as well as the power control and other circuits.  As expected, those are being similarly fiddly to get everything right, as we have quite a few boards and other components to fit in.  I've been working from the top down, and have drafts for the top deck to which the keyboard is screwed, then the deck below that that really just has the floppy drive and phone handset mounts, then the deck below that which hosts the MEGA65 mainboard and the 1S battery to 12V adaptor board. There is space on that one for the power management FPGA, the solar/battery charge controller board and a few other bits and pieces.

Below that I'll still need the cellular communications deck, and then the batteries will sit behind that --- and like the screen in the lid, we'll need some way to hold it in place. 

The cellular comms deck might need to be two panels, so that none of the screws or hole-throughs from the cellular communications deck can contact the batteries.

Okay, I'm back in the Adelaide Maker Space to work on this.  In the meantime I have got the upper-half all fitting, and even put enough of the lower-half together to be able to fire up the MEGA65, even if the lower-half doesn't fit in the case yet.

So let's start with the upper half:

Oops! No cut-outs to allow cables to feed from the top to the bottom half.  Fixed that, and adjusted the hole sizes for the LCD control board in the lower-right, and I can now connect it to power and VGA, and thus to a MEGA65 main-board.  It looks pretty nice, if I say so myself. I'm thinking that I'll likely spray the exposed panels white or "C65 beige" at some point, which I think will look really nice with the blue case:

So let's focus on the lower-half now.  This will be a bit of a slide-show, that doesn't quite reflect the number of iterations or the fiddling about to get everything right.

Let's start with the handset recepticle -- since working as a phone is kind of the whole point of this.  A nice 1990s corporate desk phone handset, itself a bit yellowed, works really nicely for this, and does just fit next to the keyboard:

With a bit of fiddling about, we can pseudo-assemble this (even though the MEGA65 main board is sitting outside the box at this point, out of screen-shot:
But we can load something up :) This show below shows more of the layers of the "PCB Torte" assembled -- in the shot below the MEGA65 main-board is actually in the stack. It's just that the stack doesn't yet fit in the box properly. Only power is being provided externally.
And just a short video snippet, showing it running. Oh, yes, and you can see a floppy disk can be inserted via the cut-out in the upper right into the floppy drive that is hiding under the blank area behind the keyboard.  That would be a great spot for a logo and some real or fake ventilation slots for improved visual effect.

 

So let's tackle the speaker for the ringer next. By using a MEGA65 R3 or R3A mainboard, we can use their on-board speaker drivers. They're not great for long-running audio, as the speakers get rather warm due to high-frequency noise (which is why we discontinued them on the R4+ boards), but for 30 seconds of phone ringing from time to time, they will be totally fine.  So time to measure them up: 

 

 Make sure they fit the cut-outs I've designed for them:

  

Speaking of cut-outs, I learned the hard-way that it's a really good idea to put cut-outs in these panels to make it easier to insert and remove cables: 

 Then it was time to do a bunch of further trimming of the board sizes so that the whole stack can fit in the box.  I also reworked the phone handset holder area, so that the handset could sit a few mm lower, thus allowing the lid to close:

 

With all that done, we can see that it's possible to sit everything at a reasonable depth in the case: 


Okay, so now let's work on the cellular modem board and antennae. This was a real pain, as the cellular eval module board uses big chunky antennae (that should get great reception), but were too tall to mount on the board.  So I designed up set of tabs that allow their mounts to be rotated 90 degrees, thus allowing the height between decks to remain withing the ~26mm limit I can fit:

The tabs are held in place from the rear-size using 3mm screws and spacers.  It's a bit clunky, but works quite well:
 
 
 So also on this deck is where we are putting the big LiFePO4 cells.  They need to be held in place, so a fence of 3mm screws and nylon spacers works for those, too. As they are not 26mm thick, I'll put some spacing material above and below them, or simply make a little brace piece that holds the battery pressed against the lower board.
 
 
And that's really the last of the problems that I had to solve with the case, we've successfully done the (1) Case design; (2) Fabricated the case; (3) Managed to assemble one; and (4) Remediated and mitigated a whole pile of fitment issues via various means:
 
 
I do like the cold-war nuclear codes football vibe that this gives off. Already a few folks in the MEGA65 community have expressed interest in building one of their own -- so it's a case of the aesthetic helping to foster interest. 
 
The source code for the laser-cut files are all up at: https://github.com/gardners/mega65-rugged-case. There's a certain elegance to how simple the design has worked out to be -- five laser-cut panels plus one off-the-shelf case, that you can buy from Bunnings here in Australia, or find something equivalent easily enough wherever you are.   
 
And, look, the lid even shuts!
 

So the next steps here are that I've ordered the parts I need to build up 2 or 3 more of these to distribute to the MEGA65 developer community, once all the software is complete.