Friday, 2 January 2026

MEGAphone: Cellular Modem Voice Circuit Control

Okay, so I have SMS working already, and a nice library to let me send and receive them, complete with emojis and international characters etc.  But what I don't yet have is the voice call control working in the same way.  The reason being that the EC25 module I have, while supporting 4G, lacks VoLTE support for the Optus Network in Australia.

I've poked the support at Quectel who make the modem, and I have a new firmware build for the modem from them which apparently supports VoLTE.  So let's try that.

Except to do that we have to get the modem into Emergency Download Mode (EDL).

I tried tying the BOOT pin to GND (=normal boot) or to 1.8V. Tying it to 1.8V gets some other situation, but I don't know if it's actually EDL mode. At least QFirehose still fails to talk to it.

So it turns out you can't do it over the UART pins -- it has to be via the micro USB port on the evaluation board.

With that, it was easy to do the firmware update. So now let's try to enable VoLTE.

So, the firmware still lacks an Optus VoLTE profile -- but it does have a Telstra one.

After some poke and fiddle, I now have the modem such that it will receive phone calls, but trying to make one still results in NO CARRIER.  This could be because of differences in the VoLTE profiles between Optus and Telstra, but it still strikes me as odd.

To get it to this state I had to use:

at+qmbncfg="AutoSel",0

at+qmbncfg="Select","Telstra-Commercial_VoLTE"

at+CFUN=1,1

<wait for the modem to reboot>

Then check that this command:

at+qcfg="ims" 

Returns +QCFG: "ims",1,1 instead of +QCFG: "ims",1,0.

So there probably isn't any change to the MEGAphone software required for this -- it's just a once-off modem setup.  We could make a MEGA65 native utility to set this all up, though. And perhaps we should use at+qcfg="ims" periodically to get the 3G/4G/VoLTE status and show that on the status bar, so that if this is a problem for others, they can at least see what's going on.


 

Okay, so the solution to that is to put a ; at end of the dial command to tell the Quectel modem that we want a voice call, not a data call.

So I now have working in-bound and out-bound calls :)

Requirements Specification 

So I can finally get back to implementing our call state management library that will meet the following requirements:

1. Provide a mechanism to dial a number to establish a call.

2.  Monitor the modem through +QIND status messages to tell the software what call state we are in.

3. Provide a function to accept a call.

4. Provide a function to hang up or reject a call.

5. Not allow the establishing of a second call while one is already in progress.

6. Periodically issue AT+qcfg="ims" and report current VoLTE status. 

7. Provide a function to query the current microphone and speaker gain for the handset.

8. Provide a function to set the microphone gain.

9. Provide a function to set the speaker gain. 

10. Provide a function to set the side-tone (i.e., "fold back from microphone into headset so you have a sense of being heard by the phone" gain)  

11. Provide a function to mute a call.

12. Provide a function to unmute a call. 

It probably makes sense to expand the existing modem library I've made to include those functions, rather than split the library, when both need to be parsing modem responses etc.

So let's start with the functions to establish, accept and end calls, since they're nice and easy.  As are the mute, unmute and the various set gain functions.  That gets us 1,3-4,7-11.  We'll tackle the remaining ones in turn, starting with parsing for +QIND and other relevant messages.

We need to parse for +QIND messages that indicate call state, as well as +QCFG messages that tell us our VoLTE status.  The place for this to happen is in modem_parse_line() where we already look for ERROR, OK and +CMGL.

The +QIND messages for ccinfo have this format:

+QIND: "ccinfo",<id>,<dir>,<state>,<mode>,<mpty>,<number>,<type>[,<alpha>] 

 Examples I've seen are like this:

RING

+QIND: "ccinfo",4,1,4,0,0,"+61870106565",128
ata
OK

+QIND: "ccinfo",4,1,0,0,0,"+61870106565",128
ath0
OK

+QIND: "ccinfo",4,1,-1,0,0,"+61870106565",128
 

and

 

atdt+61870106565;
OK

+QIND: "ccinfo",3,0,3,0,0,"61870106565",129

+QIND: "ccinfo",3,0,0,0,0,"61870106565",129

+QIND: "ccinfo",3,0,-1,0,0,"61870106565",129

NO CARRIER
 

The meanings of those fields are documented with the AT+CLCC command, e.g., here

But the run-down of the fields in order is:

ID : Internal call number, e.g., for using with AT commands for handling conference calls etc.

DIR : 0 = call from the phone, 1 = call to the phone.

STATE : 0= active, 1= held, 2=dialing (outbound calls), 3=alerting (inbound calls), 4=ringing (inbound calls), 5=waiting (inbound call). There is also the undocumented value "-1", which means "call just ended" as far as I can tell.

MODE: 0=voice, 1=data, 2=fax

MPTY: 0=not part of a multi-party/conference call, 1= it is part of one

NUMBER: The phone number

TYPE: Type of phone number: 129 = unknown type, 145 = international number (beginning with +), 161= national number. Looks like 129 is international number without the +.

ALPHA: Text of contact name if the number matches an entry in the phone's internal contact list (which we're not using).

So the DIR and STATE numbers are important for us to determine the actual call-state of the phone, and NUMBER and TYPE for getting the phone number of inbound calls.  The rest is probably not that relevant for us.

Okay, I have the call state parsing working.  The modem by default doesn't play a "calling tone" when you dial, nor when the call is being connected.  The EC25 has support for playing a single tone, and I may eventually use that to send pulses of tone to indicate ringing. But for now, I'm using a much more fun feature of the modem: It has a built-in text-to-speech reader!

So now when you dial, it literally says "dialing" in your ear. And then it says "ring ring" when the far end starts ringing.

Ironically on the real MEGA65 hardware it will be easier to put timed tone work in here, case we have the video frame counter to keep time, but in the standalone Linux test program, that's obviously not available.

I've also put in some code to make sure that a previous command finishes before we send the next command to the modem, so that no commands get lost, and generally fixed a number of call handling bugs.

But I can now establish and hang up a call.

So now to implement waiting for an incoming call -- which was super simple.

That just leaves checking the VoLTE state periodically.  The easiest way I can think to do that, is to do a randomised check when modem_poll() is being called, but no command response is pending, and with some low probability query the VoLTE status, so that it happens often enough.  

For now, I'll just implement a modem_query_volte() command.  This will work asynchronously -- i.e., you will have to call modem_poll() and then it will parse the response whenever it comes and update the shared.volte_enabled variable.

The question is how to demonstrate this? I'd go for a video, but the only practical ways I can show it working will need to show my phone number.  So let's instead take a look at the API, some of which will be familiar from the SMS API post:

char modem_init(void); 

Initialise the modem for operation.

char modem_poll(void);

Poll the modem for any responses, or to get a line of input from the modem for custom processing.

void modem_parse_line(void);

Automatically called by modem_poll() to process a line from the modem, e.g., a +QIND line.

char modem_place_call(void);

Establish a call, and set the call state accordingly to the CALLSTATE_CALLING state.

void modem_answer_call(void);

Answer an in-coming call and set the call state to CALLSTATE_CONNECTED.

void modem_hangup_call(void);

Terminate (or reject) a call and set the call state to CALLSTATE_DISCONNECTED. Also cancels the mute state.

void modem_mute_call(void);

Mute the audio channel in a call.

void modem_unmute_call(void);

Cancel the muting of the audio channel in a call.

void modem_toggle_mute(void);

Toggle the mute status of a call (convenient for just having a MUTE button that toggles).

char modem_set_mic_gain(uint8_t gain);

Set the microphone gain: 0 - 255, 255 = loudest.

char modem_set_headset_gain(uint8_t gain);

Set the headset gain: 0 - 255, 255 = loudest.

char modem_set_sidetone_gain(uint8_t gain);

Set the side-tone (microphone to speaker "comfort" loop) gain: 0 - 255, 255 = loudest.

void modem_query_volte(void);

Check whether the modem's cellular connection supports VoLTE.

void modem_query_network(void);

Request that the modem provide the name of the cellular network we are connected to.


Okay, so that's the API described. This lets us do things like this:

 $ bin/modem /dev/ttyUSB1 115200 init sidetone=50 headset=100 mic=100 network volte callrx
make: 'bin/modem' is up to date.
INFO: Setting side-tone level to '50'
INFO: Setting headset level to '100'
INFO: Network name is 'YES OPTUS'.
INFO: VoLTE is enabled.

Then it will sit there until a call is incoming, because I asked it to wait for a call with callrx. Then when that call comes we get something like this:

Answer incoming call from '<number redacted>'
INFO: Notifying user of changed call state. 
 

Okay, so that more or less confirms that. 

And here's a short video showing it in action, with numbers redacted via a bit of shell-scripting magic: 

 


 

 


MEGAphone Cellular Modem and Power Control Hook-up for R3 Boards and Phone Call Handling

For the current MEGAphone prototyping, I am using the MEGA65 R3 mainboards. The R3 specifically has on-board speaker drivers that I'm going to use for the ringer.  Those drivers do have a bit of a heat-generating issue with some speakers because the audio is not filtered from digital before it hits them, but for ring indication purposes they should be just fine. 

Anyway, whichever main board revision I use, I need to set it up with two UARTs: One to the cellular modem, and the other to the power control FPGA board.

I'd like to do that in a way that's compatible with the MEGA65 expansion board configuration, which means I can only put one of those UARTs on the PMODs: The one that would otherwise go to an ESP32 WiFi module -- or now, to a cellular modem.

But first, a sneak-peak of where I get to:

 

Looking at the board layout, there are three likely places I can find the extra 2 GPIOs I need:  The keyboard connector, J17 or J21.

J21 has a bunch of GPIOs that are connected to the MAX10 FPGA that's on the R3 boards but not on the R6 boards. So I could use them, but to do that I have to reflash the MAX10, which is just a bit fiddly -- but doable.

J17 is the JTAG connector for the MAX10 FPGA, so similar issues there, except that it's harder to override the use of the JTAG pins.

The keyboard connector has the JTAG interface to the keyboard's FPGA (yes, everything on the MEGA65 is handled by an FPGA of some sort -- but I promise, there's only these 3 ;) But it also has 3 GPIOs for talking directly to the keyboard, and the JTAG pins on it are switchable via a JTAG enable line.  But those pins also route through the MAX10 FPGA.

So it looks like I'll have to use the MAX10 FPGA one way or another. Well, except that for the R3 board, we already have the MAX10 relay those JTAG pins.

Okay, so here's my solution, which is only _mildly_ horrid:  We make a funky keyboard cable that splits off the "JTAG" pins to the UART, and keeps the keyboard protocol pins going to the keyboard.

Importantly, this trick can also work on the MEGA65 R6, where the MAX10 is absent. So that gives me a board revision independent solution, where the only difference is what drives the ringer / internal speaker.

Okay, sounds like a plan.

So I just need to hook up the two UARTs to the MEGA65's buffered UART (I'll do it for R3 -- R6 targets) in a way that's compatible with the expansion board pinout for the UART on the PMOD.  Should be super simple. 

Tracking it with an issue: https://github.com/MEGA65/mega65-core/issues/932.

Let's see if I have it right on the first commit for any or all of the R3-R6 targets.

Now to do this, I'm going to need to go and get some wire and crimpers to make the cables (and also some of the other internal cables in the brief-case MEGAphone prototype). So a trip to Jaycar is in order.

Well, one typo on all targets. R3 builds now. R4 was broken, but for some other reason. Fixed that, and it's synthesising now.  R5 and R6 are effectively the same target, so I'll just build R6. It's building fine, so I'll go and do a couple of things in the meantime.

Okay, one pin placement error for R5/R6 -- fixing that one, too.

While that runs, I might go and get the bits and pieces I need to more easily make the cables I'll need for this and for generally assembling the MEGAphone prototype with full wiring.

I was hoping to get loose dupont connectors and crimpy bits, so that I could just make up cables of the right lengths. But Jaycar only sells the pre-made 15 or 20cm long cables. So I'll have to cut them, solder in extensions and heat shrink them.  It's just extra annoying work.  But it will be manageable.  I should also solder on the headers to the power management FPGA board that I've been studiously dodging by shoving male dupont connectors through the PCB holes and only having them fall out from time to time.

Let's start by looking at the keyboard connector and work out what needs to go where, and then I can pull out the soldering iron.

 

So we need 3.3V, GND and K_IO1 -- K_IO3 for the keyboard, so that's pins 1-2 and 8-10 that have to pass through to the keyboard.  For the UART we need only pins 6 and 7.

So I've mode my extension cable for the keyboard, and now also soldered in extensions to 8 dupont 0.1" header leads.  I'm still a bit annoyed that Jaycar doesn't have the raw parts for those, as it took an hour just to make those.  But such is life.

Now in theory I have a MEGA65 bitstream that has the two UARTs plumbed to the PMOD and keyboard JTAG pins.  The next step is to test them and that the buffered UARTs are working as I expect.  I'll need a simple API for managing the buffered UARTs, so let's start with that, and then make a simple "MEGAcom" program that can work with them. I'll loosely base it on minicom.

The API for our purposes just needs these functions:

uint16_t modem_uart_write(uint8_t *buffer, uint16_t size);

uint16_t modem_uart_read(uint8_t *buffer, uint16_t size);

char modem_setup_serial(uint8t_ port_number, uint32_t baud_rate);

Okay, got those implemented.  Now to work on the simple terminal program. I've hacked up a simple 80x50 text mode driver and some routines to draw boxes and make some simple menus. The actual serial port control code isn't in there yet, but it should be trivial to plumb it in.  As described above, I've gone for making the structure mirror minicom so that muscle memory Just Works :tm:

 



Not bad looking, if I say so myself :)

Okay, so now I've got the serial port control in place, and with a bit of fiddling, I can even send data to a connected UART. Yay!

But for some reason nothing is being received.  And that remains the case when I enable the UART loopback mode, which should cause local echoing of bytes I send... Ah, except that loopback mode is funny -- it connects the buffered UARTs to each other.

Anyway, once I had realised that, I can see that there seems to be something coming in. So it's possible that I have the plumbing for the UARTs wrong on the PMOD.  Let's try the keyboard connector.

Hmm.. No sign of life on there yet. But it's the middle of the night, and I need some sleep.  But it feels like it should be fairly easy going from here to make it work now.

Well, I stayed up some more, and got the MEGAcom working a bit more :)


Anyway, this all proves that the UART on the keyboard connector is working fine. It's just the PMOD one that's not receiving -- which I suspect is because the pin is set to inout, but hasn't been tristated. I'll resynthesise a bitstream with a fix for that while I get some more sleep.

Okay -- found the problem, I think I had the TX line on the wrong PMOD line -- anyway, with that it works now. So that means we know we have two working UARTs available for connecting the cellular modem and power control FPGA module :)

But after being up most of the night working on this, I need a snooze before I tackle the next part, which will be hooking those things all up, and documenting the pin connections on the various connectors to do so.

To summarise what we need for connections:

1. Cellular modem RX from UART0 (Left PMOD on MEGA65, bottom row, 2nd pin from the right) (purple/yellow wire in the photos below).

2. Cellular modem TX to Power Management FPGA pin E3 (short blue lead in the photos below).

3. Cellular modem TX relay from Power Management FPGA pin E1 to UART0 (Left PMOD on MEGA65, bottom row, 3rd pin from the right) (white wire in the photos below).

4. Power Management FPGA UART pins B3 (long blue wire in the photos below) and A3 (green in the photos below) to UART1 on MEGA65 keyboard cable pins 7 (second pin from left on rear row, when looking from front of the MEGA65 towards the back) and 8 (third pin from the left on the front row, when looking from front of the MEGA65 towards the back).  

5. GND from MEGA65 PMOD (second pin from left, either top or bottom, either PMOD) to cellular modem (black wire in the photos below).

6. GND from MEGA65 PMOD (second pin from left, either top or bottom, either PMOD) to Power Management FPGA (brown wire in the photos below). 

7. (In the phone, but not on my test unit) 3.3V from somewhere to the Power Management FPGA. 

I've started with the cellular modem.  And I think the connection is there, but I'm reading gibberish back from the modem, even though I'm set to 115,200bps as is correct.

Okay, so I did an experiment: I can talk to the cellular modem just fine, e.g., by telling it to dial my mobile phone. But the output from the modem is all messed up.

So what's really weird, is that echoing commands as I type them comes back ok. It's just when there's back-to-back character output that it's a problem, I think... And now it's suddenly completely fine (!!):

 

Was it just a bad connection?

Hmm.. Something more is going on here, as it's varying as to how good/messed up it is. 

Slight baud rate mis-match, perhaps?

We use 40.5MHz/X to get baud rate. 40.5M / 115200 = 351.56, so it's possible that by rounding that down to 351 we're slightly too fast, and as a result pick up the end of the stop bit as the start bit of the next byte, which would explain the behaviour.

Well, except now it's all gone to gibberish again -- even command echo is messed up again. This happened after I moved some of the cables around. So maybe one of them has a bad connection, or a wonky solder joint?

And then without the ATI command echoing properly I just got one perfect ATI response !?

So I'm having a poke at the power management FPGA UART connection now.  That seems to be rock solid. I can send the ? command repeatedly and get a solid slab of sensible looking text back, wit no apparent errors.

Switching the cables around, the problem follows the cellular modem, not the cables. So I think it's something specific there.

What I am going to try now is to hook up the UART relay from the cellular modem to the power management FPGA: It's possible that the drivers on the UART TX line of the cellular modem are a bit weak or off spec or something, so having the signal regenerated in the power management FPGA might just solve the problem.

Yup -- it's rock-solid when it goes through the little Lattice FPGA. 

So let's have some images to help guide the wiring.

In the first two images we can see the keyboard cable made from dupont connectors, with two removed to let me sneak the UART connections in (the blue and green wires that don't follow the rest of the cable up):


(Note that the MEGA65 has an unsoldered second keyboard connector that could in theory be leveraged to allow use of the normal MEGA65 keyboard cable, assuming the JTAG lines don't mind actually still being connected to the keyboard's Lattice FPGA.  As I've reasoned above, I believe that this should be fine, but don't go trying it until I confirm it's safe. (Of course, if you're using a DIY "MK-II" MEGA65 keyboard, that doesn't have JTAG at all, so would absolutely be safe. But unless you're one of the two people who've made one, you don't have a MK-II keyboard -- all retail MEGA65s come with the "MK-I" FPGA-based keyboard).

Don't forget that the purple wire in the bottom of the next image turns yellow in the middle! The yellow wire you can see in the top-left of the image is a red-herring -- just ignore it:

The blue lead in the following image is the short one to the power management FPGA board:

Note in the following photo, the long blue wire to the MEGA65 keyboard connector is the one next to the green, while the short blue wire to the cellular modem is the one on the right next to the white wire.


Okay, so in theory we have the connections we need, and we have the UART communications library all ready, so there doesn't feel like any reason why we can't just plumb it all together, and get the phone software talking with the cellular modem, and, say, show the VoLTE and mobile carrier name in the status bar.

Except... it looks like the weird UART gibberish problem from the cellular modem is rearing it's ugly head again.  I think I need to get to the bottom of that next. My spidey-senses still tell me that there's a timing issue in there somewhere, or at least de-bounce.

I think the first thing I should do is make the UART RX sample mid-bit, instead of at the start, as it strikes me that there can be all kinds of problems with the way I've been doing it to date... except it looks like I fixed that ages ago. So it's not that. And it's de-bounced. So I'm really not sure what's going on here. It's only on the RX side as far as I can tell: The modem hears our commands just fine.

Okay, so I think I might have gotten to the bottom of it: The EC25 Eval Board has an on-board level converter that lifts the 1.8V UART lines to 3.3V. But sometimes that level shifter is having problems.  This explains why the serial output is sometimes fine, sometimes marginal and sometimes rubbish.  Here's some oscilloscope captures the contrast. Contributing to my suspicions here, the UART also feeds two other level converters on the eval board: One for the USB UART interface and another for the DB9 RS232 port.  So it's totally possible that 1.8V UART is sagging a bit sometimes, and the 3.3V level converter gets confused as to what it should be doing.

Anyway, let's look at how this looks in practice: 

Here it is mostly fine, but showing slow rising edges. In this state we get some bit-flips from 1 to 0 (I was probing both sides of a 10W 47 Ohm resistor to see if that helped, but it didn't. And, yes, I know that 10W monster resistor will be somewhat inductive, but I saw the same problems without it, I just didn't take pictures):

But then when it goes truly awful it looks like this:
or this:
When it should be looking reliably like this:

 


So my thinking here is to add a 10K pull-up to 3.3V on this line, so that when the level converter is having a bad day, we still get a clean signal.

So time to make up a little jumper lead with 10K pull-up side-line.  This needs to go on the cellular modem to power management FPGA leg, so that we relay the cleaned up signal to the MEGA65.

I've made the jumper up, and connected it, but with the 3.3K Ohm (I couldn't find my 10K resistors) pull-up not connected, and am now waiting for a bad phase with the UART -- which of course isn't happening now that I want it.  So I'll get on with the integration, and then worry about it when it shows up again, and connect the pull-up to see if it fixes it.

Okay, so back in that world, I'm trying to debug why the phone software is only seeing empty lines from the modem, even though we're clearly receiving, for example, a RING message from the modem:

*** FONEINIT entered
*** FONECLST entered
MODEM CHAR: 0D
Modem line: ''
MODEM CHAR: 0A
MODEM CHAR: 52
MODEM CHAR: 49
MODEM CHAR: 4E
MODEM CHAR: 47
MODEM CHAR: 0D
Modem line: ''

Note that  the modem line is reported as empty ('') the second time, even though clearly we have seen the 52 49 4E 47 bytes = R I N G.  So that's the place for me to start looking.

Well, for whatever reason, it looks like the shared.modem_line buffer ends up with all $00 bytes in it. So why?  They don't get written into the array at all, it seems.

This is all a bit fishy. I've confirmed it's not the write-protection of memory (that triggers a backtrace when violated, anyway).

To make things fishier and more annoying, after receiving the 2nd line, it seems to ignore all serial input, but it hasn't crashed, per se, it continues to update the status bar.  

Okay, weird, it looks like the shared.modem_line thing is at $D0C0... Maybe I've got >4KB in my shared state structure. That could certainly cause problems.

Okay, that's indeed the case: It's almost 5KB! But I can't quite see why.

Ah, the shared resource structures allow for a resource name that's 240 bytes long x 4 fonts, and we have almost 1KB.

I tried increasing the size for the shared memory region, but then FONESMS doesn't fit (there are only about 700 bytes free in BSS, and I need slightly more than that)!

Now, the nuisance is that once a shared resource has been loaded, we don't need the name field anymore, but the shared resource API keeps the whole record around.  It's literally only needed during the shared resource find and open process. 

So I could potentially refactor that, and thus claw back almost 1KB.  It's probably worth it. Done and tracked via this issue: https://github.com/MEGA65/mega65-libc/issues/76

Right, with that fixed, we now have lines being recorded and seen:

...
Modem line: '+QIND: "ccinfo",4,1,-1,0,0,"XXXXXXXXX",128', line len = 0x2B, first bytes = 2B51494E
...
Modem line: 'NO CARRIER', line len = 0x0A, first bytes = 4E4F2043
 

So that's all great. But I'm not seeing call state events be handled. But let's go back a step, and make sure that FONEINIT actually initialises the cellular modem. Yep, that all looks fine. So really what we need now is something to make the modem periodically report things of interest to us -- like network name, network time and signal strength.

Okay, so I have added an automatic polling function that sends the AT commands to the modem every second or so to get network time, the network name etc.  The first of those I've plumbed in with a parser is the network name function... and tada! We can now see the true network name:

Well, I've hit another nuisance here as I implement the rest of the modem response scanning and status bar update stuff: I've run out of program size again in the SMS program.  I'm tempted to just not build that for the moment, and finish getting the modem response stuff in. That way I'll know just how short I am of space, rather than playing whack-a-mole repeatedly.

Okay, so now I have the network time parsing done.  Let's move onto the network signal level. Got that, and VoLTE indication. So now it's onto the call state management stuff.

I've now plumbed in RETURN to answer or hangup a call -- and that works -- I've now got it so that I can accept a phone call... And make calls. There's still a bunch of rough edges to sort out, but it basically works for phone calls now :)

Here you can see it in action, after I added some fixes for handling calls without caller ID:


 

But let's go over what getting us to being able to place and receive calls achieves in terms of milestones:

Milestone: j. 1.3 Telephony software: Basic Communications With Cellular Modem: Implementation

Demonstrated by the correct display of the cellular network name and time, among other things. 

Milestone: k. 1.3 Telephony software: Basic Communications With Cellular Modem: Testing

Demonstrated by getting the display of the status bar information as well as the placing and receiving of call sequences (private video is available for NLnet Foundation for verifying this). 

Milestone: l. 1.3 Telephony software: Basic Communications With Cellular Modem: Revision 

Documented in this blog post and in the source code, as I've gotten everything working to this point. 

Milestone: y. 1.7 Integration testing and remediation of software: Requirements Specification 
Milestone: z. 1.7 Integration testing and remediation of software: Implementation
Milestone: aa. 1.7 Integration testing and remediation of software: Testing
Milestone: ab. 1.7 Integration testing and remediation of software: Revision

These four milestones have been addressed in this blog post and in the source code, as I've gotten everything working together: The cellular modem interface software, the GUI, the call state management etc.

 

 

 

 

 


Wednesday, 17 December 2025

MEGAphone Cellular Modem Communications: SMS Messaging

In recent blog posts I've got the telephony and text messaging software working.  What isn't there yet, is making it all talk to the cellular modem -- so that's what we're going to finish here.

So let's start by getting some hardware organised that can talk to the cellular modem. We're prototyping using a real MEGA65 mainboard, so we need to tweak a build for that, which has two buffered UARTs exposed on the PMOD ports: One to talk to the low-power FPGA that manages sub-system power, and the other that talks to the UART of the cellular modem.

But even before we get to that point, we can get the communications with the cellular modem in hand, using a USB UART adaptor from Linux.  So let's do that.

The key things we need to do are:

1. Setup basic modem operating parameters 

2. Configure the modem to say RING when the phone rings.

3. Configure the modem to issue a +QIND message whenever an SMS message is received, or the call state changes, e.g., pick-up, hang-up, ringing etc.

4. Retrieve the first queued SMS message if there are SMS messages pending.

5. Send an SMS to an indicated number.

6. Retrieve network time, name and signal strength information.

Let's go through those one by one and implement them.

But first, let's get that modem plugged in and powered up, and confirm that we have the UART relay through the low-power FPGA working:


So what we have here is the cellular modem development board connected to the USB UART (that is pretending to be the MEGA65 right now), with the TX line from the cellular modem (green) relaying through the low-power FPGA (yellow) so that it can monitor messages from the modem to auto-wake the main FPGA when required.  The TX line from the "MEGA65" goes direct to the RX line on the cellular modem's UART (blue). And apart from that, we have GND (orange).

And as we can see in the screenshot above, we can talk to the modem.

I've also bought and installed a pre-paid SIM card for testing, although it seems to be taking a while to activate, which is annoying, as I'd like to make a test call. 

So we can start working on a simple library to do the things we need to do with the modem.

Hmm.. Turns out the firmware on this module needs updating to support VoLTE with Optus. But I can still work on the SMS processing commands in the meantime.

For that we have AT+CMGL=4 that will list all unread SMS messages, AT+GMD=n that will delete a specific message, as well as commands to read a specific message. What is lacking, but would have been nice, is a command that returns the _number_ of SMS messages. I can approximate it with a list, but that's a bit annoying.

Complicating matters is that saving an SMS message takes a second or two if we index it. So we'll still want to AT+CMGR to read a message when we want to store it. 

So let's start by fleshing out modem_poll() so that it indicates if it has returned a line from the modem, and have it count the number of +CGML lines seen, and also if it's seen an OK or ERROR line, so that we know when it's done.  That way a call to get the number of SMS messages yet to be processed can simply call modem_poll() until it sees an OK or ERROR message, or some sensible timeout occurs.

Got all that. Now getting ChatGPT to implement a small SMS PDU decoder.  This turns out to be more complicated than I'd like, but not absurdly so. Just pushing ChatGPT with each decoding error I see and getting it to fix them one by one. In theory I can now decode an SMS into a UTF8 string.

Okay, got that working.  Now getting it to write me the opposite that can take a UTF8 string and prepare it for sending as one or more SMS messages.  That seems to be okay, but now the actual sending the SMS is getting a +CMS ERROR: 304, which means the modem isn't in PDU mode -- but I have set it. So maybe it is botching the message construction after all.

The bug turned out to be my fault, not ChatGPT's: I was putting a CR/LF sequence after the PDU before sending CTRL-Z.  With that fixed, and a few other minor things, I can now send short SMS, long SMS, SMS with emojis etc, and it all arrives correctly :)

There is an intermittent protocol lock-up that I'd like to get to the bottom of, but it fundamentally works :)

So SMS deletion is the next frontier, and one that should be super easy in comparison.

But before I can tackle that, I've hit a problem where if there are too many SMS messages, the AT+CGML=4 command seems to truncate output, and doesn't print OK at the end. So I've added a timeout to that.

With that fixed, the good news is that we can even delete SMS messages.

The bad news is that the message numbers don't pack or shuffle down after a lower number message has been deleted.

So we probably need a command that returns the oldest SMS message, including it's number, so that we can then ask for it to be deleted after.

Okay, we have this working now:

$ bin/modem /dev/ttyUSB1 115200 init smsnext
INFO: Decoded SMS message #1:
       Sender: +61434595174
    Send time: 0000/00/00 00:00.00 (TZ+0min)
       text: Honey 
       concat: 0
       concat_ref: 0
       concat_total: 0
       concat_seq: 0
$ bin/modem /dev/ttyUSB1 115200 init smsdel=1
$ make bin/modem && bin/modem /dev/ttyUSB1 115200 init smsnext
INFO: Decoded SMS message #2:
       Sender: 121
    Send time: 0000/00/00 00:00.00 (TZ+0min)
       text: Welcome to Voicemail! You have 3 new message(s). Please dial 121 to access messages.
       concat: 0
       concat_ref: 0
       concat_total: 0
       concat_seq: 0
$

In short, we can receive SMS messages in order, and then delete them.

So let's watch this all working together, sending and receiving SMS.  I'm filming with my mobile phone, so I can't show them being received on my phone directly.


Milestone Checklist

So that completes the SMS part of our API, which satisfies these milestones:

q. 1.5 Text messaging software: Communications With Cellular Modem: Requirements Specifications

The requirements for this module ended up being able to be reduced to the very simple set of routine's we explored above: (1) Get number of unread SMS; (2) Read and SMS message; (3) Delete a received SMS message; and (4) Send an SMS.

We ended up going beyond this, by implementing some stretch goals, in particular: (5) Parse Unicode and Emoji-containing SMS; (6) Construct and send Unicode and Emoji-containing SMS; (7) Send multi-part SMS.

r.  1.5 Text messaging software: Communications With Cellular Modem: Implementation

The implementation is described and shown in the video above, as well as embodied in the source code, in particular in these files:

https://github.com/MEGA65/megaphone-modular/blob/main/src/telephony/README.md

https://github.com/MEGA65/megaphone-modular/blob/main/src/telephony/smsdecode.c

https://github.com/MEGA65/megaphone-modular/blob/main/src/telephony/smsdecode.h

https://github.com/MEGA65/megaphone-modular/blob/main/src/telephony/smsencode.c

https://github.com/MEGA65/megaphone-modular/blob/main/src/telephony/utf.c

https://github.com/MEGA65/megaphone-modular/blob/main/src/telephony/modem.c

https://github.com/MEGA65/megaphone-modular/blob/main/src/telephony/modem.h 

s. 1.5 Text messaging software: Communications With Cellular Modem: Testing
t. 1.5 Text messaging software: Communications With Cellular Modem: Revision 

The blog post text and video above shows the result of testing and iterative revision until a working module was obtained. 

 

Okay, so now it's on to finishing the cellular modem telephony control module... If only Quectel had already responded with the firmware update I need for the modem for VoLTE on Australian networks...

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.