Saturday 13 April 2024

Controlling a EC25-AU cellular modem using a MEGA65

I'm hoping to get some more work done on the handheld "MEGAphone" version of the MEGA65 this year, as well as supporting Wi-Fi and cellular modems on the expansion board bridge.  So it's time to stick my head back into the fun of cellular modems.

I'm using a Quectel evaluation board, because I have it laying around, and because it has all the bits on it already for connecting to a headset/handset, and later, to the PCM digital audio interface of a MEGA65.

First challenge was to power it. Digging around, it turns out it can be powered via the micro USB port on the board, and sliding the power switch to the ON position.  It won't then actually power up until you press the power button. It is possible to hard-wire the PWRKEY jumper next to that switch to make it turn on automatically when power is applied. I should fit that jumper.

For the MEGA65, I don't want to communicate over the USB port, but using the raw UART interface. This is on J806.  I was trying to use a faulty FTDI cable earlier, which caused me some weird problems. Before I realised it was the FTDI cable at fault, I resorted to isolating the UART from the USB interface by removing R407 and R411.  But once I was using a good USB UART adapter, I was able to send and receive AT commands.

The EC25-AU module defaults to echo off, so I normally set ATE1.

The evaluation board has a 4-pin RJ jack that it turns out can take a normal telephone handset, which is super handy! I had a couple in the shed, one of which works and the other of which has a dead speaker it seems, so good thing that I got them both out, or I would have been pulling out my hair!  

But initially neither worked, because the PCM audio master mode on the EC25-AU wasn't working. After poking about for a bit, I had the vague recollection from the earlier prototypes that the original firmware of the EC25-AU had a problem with this -- only PCM slave mode would work.  But I figured that between 2017 and now that there was probably some new firmware out there that fixed the problem.

Getting firmware updates for these modems is a bit weird: Quectel for Reasons Unknown To Any Mortal don't have a public repository with the various firmware versions for their modems. Instead, you have to join their forum and ask randomly for it somewhere, and then someone will send it to you via a direct message. All quite weird.

Anyway, then I had to figure out how to apply the update.  There is a libqmi-utils package that has a tool that should be able to do it, but I don't know how to drive it. Instead, I eventually found that Quectel have made a utility for Linux for doing this:

https://github.com/nippynetworks/qfirehose/blob/main/qfirehose.c

To use this, you have to drop the modem into "emergency boot mode". For the eval board, you do this by shorting the USB_BOOT pin to 1.8V which means jumpering pins 7 and 9 of J805 together and then giving it power. The jumper can then be removed.

Then after unzipping the firmware update zip file, you use a command like this:

sudo ./qfirehose/QFirehose -f directory-where-you-extracted-the-firmware

It takes a few minutes, and then the modem automatically reboots.

With that working, I then had a working audio interface. It was a bit quiet in the earpiece of the telephone handset, so I used AT+QRXGAIN=65535 to maximise the volume. It's not super loud, but loud enough for what I need for now.  Once the MEGA65 is driving the audio path directly, we will be able to make it much louder if we want.

A few other handy AT commands to make life easier:

AT+QURCCFG="urcport","all" -- This puts the RING, NO CARRIER, CONNECTED messages onto all serial ports of the modem, ensuring we see them.

AT+CRC=1 -- use more verbose status messages, e.g., +CRING: VOICE instead of just RING.

AT+CLCC -- Reports on current call state, including data connections. This allows a state rather than event based management of calls, which is nice for warm-starting telephony software.

AT+CPAS -- Reports if there is an active call, an incoming call, or no calls, again for state rather than event-based call management.

So I think that's probably everything I need for now to allow the MEGA65 to make, receive and indicate call status.  For now, I'll connect to the modem UART using the UART break-out header I put on the expansion board connector board, and just hang it out the back of the MEGA65. 

Next steps will be making sure the UART is plumbed through the MEGA65 to the expansion board, and also to add support for number pads via the Grove I2C port, so that I can add a dial-pad. Then it's time to make some more telephony software!

I'm going to use the Nexys4 non-DDR board as the host for this, since I have one laying around, and the design currently doesn't use PMOD A, which means I have 8 IOs available for this.  PCM audio interface will need 4 pins, I2C for keypads etc will need 2, and then the last 2 will be needed for the UART to the 4G cellular modem. I'll track this with Issue #798.

The easy part is connecting the cellular modem to buffered UART #0. 

But first I have to get the nexys4 target building again, as it's got a bit stale. Fortunately I only had to add the missing NEO True Random Number Generator (TRNG) to get it to start synthesising. I'm not sure if the target will try to have too much stuff and over-fill the A100T FPGA.  I guess I'll find out in about 10 minutes when it finishes. 

Yup -- synthesised just fine. So let's try it out, and see if this old board still works. Yes -- it has started fine. It doesn't have an SD card installed, so it has just dropped to the OpenROM built into the bitstream. But that's fine for this, as I can write and compile a simple C program to talk to the buffered UART that I'll connect to the cellular modem.

Here is the body of my little test program that verifies that the buffered UART is plumbed in:

  // Select Buffered UART 0
  POKE(0xD0E0,0x00);


  // Set UART to 115200.
  // We do this by providing the divisor for 40.5MHz
  // 40.5MHz / 115200 = 351 = $00015F
  POKE(0xD0E4,0x5f); POKE(0xD0E5,0x01); POKE(0xD0E6,0x00);

  // Send HI on the UART
  POKE(0xD0E3,0x48);
  POKE(0xD0E3,0x49);
  POKE(0xD0E3,0x0d);
  POKE(0xD0E3,0x0a);
 
  while(1) {
    // Wait for char to be RXd
    while(PEEK(0xD0E1) & 0x40) continue;
    printf("%c",PEEK(0xD0E2));
    POKE(0xD0E2,0); // acknowledge reception of byte
  }


This is really a nice little UART facility that I built a while back -- the interface is as simple as you can see above. It has 8 UARTs, each with 256 bytes each of RX and TX buffer, so it's really convenient to work with, compared with the UART chips that were available for the C64 back in the day.

Anyway, with that working, I can start writing some actual cellular modem control code.  It's just going to be simple for now, so will fit in a single program. I'll start with some routines to send and receive lines of text from the modem, and keep track of the state that the modem is in.

This is more or less working, and the modem gets detected, and I send a configuration command, but then the modem seems to stop responding. But I don't know if that's actually the case, or if it is the buffered UART stops receiving. Bridging the UART pins seems to indicate that the UART is still working properly, so I'm assuming that it must be the cellular modem stopping sending responses for some reason. Maybe because I am sending too many commands? My periodic ticker is based on 1Hz, which at 115200bps should be totally fine.

It looks like the modem responds to the first command I send, and then nothing after.  Which doesn't make much sense, since when I run the program again without powering off the MEGA65 it works again to send a single command.

Am I doing something weird like acknowledging the reading of one char too many? Doesn't look like it.  Using the oscilloscope, I have confirmed that it is the modem that stops sending.  So what is going wrong? We should be sending ATI + a carriage return endlessly.  If I loop the UART pins, that's what I see being returned. 

On the oscilloscope it looks like the middle character of the "ATI" string I am sending is wrong. It should be $41 $54 $49. But it looks like $41 $2B $49 is sent. Why $2B ? The bit pattern for T = $54 should be 01010100. The bits I am seeing look like 00101011. Is there an extra bit being smashed in there somehow?

Actually, no, there is something else going wrong with the buffered UART. The total bit sequence looks like:

1010000011100101011101001001...

We expect to see $41 = 01000001, $54 = 01010100, $49 = 01001001. Let's try to line those up:

10100000111000101011101001001

So maybe the $54 is being sent as $15 instead?  That would be shifted right one bit. It's possible that my hand-reading the oscilloscope display is a bit wonky, though, and that it's fine. But this probably says that I should make a quick VHDL test case sending this sequence like this, and see if it looks ok, or if it has a bit shift in it like this.

Nope -- the problem was much simpler: CC65 converting ASCII to PETSCII causing commands in capitals to fail.

Okay, so I am happy that the UART is working now. Let's tackle the I2C interface to the keypads next. But first a brief detour as I plan ahead...

For the first prototype I am using an off-the-shelf EC25 evaluation board. This is great for development, but they are relatively expensive, around AU$600. Once I have this first unit built up, it will be deployed at my Mum's place (she wants a "landline" phone that "just works", rather than the rubbish ones on the market right now).  So I will need more hardware for ongoing development. The EC25-AU 4G modems themselves are quite cheap, under AU$100 (about US$65), and I already have a spare one.  Theoretically I also have a little mPCIe breakout board that I got at the time, but I can't for the life of me find where I put it. So I've looked to see what other options there are for this.

The Osmocom project designed such a board (and it might even be what I bought before, but can't remember).  Anyway, there is a German company who makes a variant of this: https://shop.sysmocom.de/miniPCIe-WWAN-Modem-USB-break-out-board-v3-kit/mpcie-breakout-v3-kit.  So I'll probably order one of those as soon as I deploy this initial prototype to my Mum's place. Anyway, back to the I2C stuff now...

The MEGA65 supports custom I2C device sets for each target, so I will use that facility in iomapper.vhdl to add a new device page for the nexys4 board that supports having up to 8 I2C IO expanders, and just makes all their registers visible in memory.  I can later on make them automatically simulate the MEGA65 keyboard if I want, but initially I can just probe them to detect key presses, and also to control the LEDs for each key. I'm also not going to worry about hardware support for making the bi-colour LEDs do yellow by quickly toggling the polarity of the IO expander pins.  Just red, green or off will be fine for now.

Okay, so let's dig in and prepare the I2C device page. I did write a tool ages ago to automatically generate a lot of the boiler plate for this. I've used that to populate the I2C state machine, and setup the I2C device page.  This synthesises now, but I'm not seeing any traffic on the I2C pins I have assigned. I'm not quite sure why, as I have configured iomapper.vhdl to synthesise the keypad_i2c.vhdl file that I have added for this, and I can see that it gets synthesised by Vivado.  Might be time for simulation to see what is going on...

Simulation is running, but of course is quite slow. So I've also added an extra debug register to the new I2C device page so that I can see for sure if it is being mapped or not. I'm not currently seeing that register responding, so I should probably debug if the accesses to the register page is working.

Found one stupid error that was preventing me from reading from the I2C register page: I had plumbed all the bits and pieces for it into the fast IO interface bus, except for the read data lines :) Now I can read the registers in simulation.  So I'll resynthesise that -- and indeed that fixes that problem -- I can now see the I2C register page in memory at $FFD75xx. It doesn't explain why the I2C lines are not being driven, though.

For the I2C lines, I do have some debug infrastructure I can add that allows manual control of those lines from the I2C register page. I should probably add that back in. That'll need another resynthesis, which I am doing now. While that was running I traced the plumbing through and found where I hadn't connected the I2C lines in machine.vhdl, so that's almost certainly the reason for that problem! Now resynthesising...

Now I can read the I2C registers from the connected board :) There is a wrinkle though, which I had forgotten about with these I2C IO expanders: They read the same 2 addresses repeatedly, so to read all 8 registers I have to schedule 4 separate reads. This is actually a feature of these IO expanders, as it reduces the latency if you are scanning the input lines on a single IO expander, as you can basically just read them continuously, without having to reschedule a new I2C transaction each time. But we want to be able to read multiple IO expanders, and also check the other registers.  So I have rejigged this and am resynthesising.

While that runs I can actually test output, though. This is because while I can't read the registers, I should still be able to write to them.

To do this, I need to write to registers $06 and $07 to set the direction. Zero bits mean output (opposite to on a C64 CIA). Then I can set the output values via registers 2 and 3.  With that I should be able to control the LED I have hooked up on the test board. Because I connected both pins of the LED to output pins I can reverse the polarity of the LED under software control, and thus use a bi-colour LED. And that all works: I can turn the LED on and off :)

The only problem I am seeing now is that when I set any of the other registers, I stop being able to read the input registers that the keys are connected to, and instead end up reading from the register I last wrote to -- but this is with the bitstream from before breaking the I2C transactions down into the register pairs, so it might magically sort out when that is done.

Okay, that bitstream has finished synthesising now, and now I can see all 8 registers, which is good. I can still read the key switches as well. But now it seems that I can't write to the I2C registers. I think I know the cause of that, though: I messed up the state machine number for the writing phase, leaving an undefined state just before it, which causes the state machine to reset.  Resynthesising that now.

While that is running, I am reminded of an issue I had with these I2C IO expanders where funny things happen if you do back-to-back reads and writes. But that's not an issue until I have at least fixed the register writing, which is still broken. I have added a debug register that will let me check if the writes are actually occurring.

... and the writes aren't occurring. My debug register confirms that they aren't being performed.  Going through the state machine management logic for this, I think I might have found the problem: The state machine is extended by a couple of states if there is a write pending, and there were some out-by-one errors in that from when I changed the number of states for the I2C expanders. Let's see if it fixes it. Nope. Writes still seem to not be occurring. Might be time to simulate it. But first I need some more sleep.

I've started writing a VHDL unit test for the keypad_i2c.vhdl, and is often the case, in the process of doing so found at least part of the problem: I was never asserting write_job_pending. I've added in the missing line to set that when we try to write to a register, but that hasn't solved the problem -- so some further investigation is required. But I can now do explorations in about 20 seconds instead of 10+ minutes :)

Much of the problem was the test wasn't allowing time for the I2C write to occur, as this only happens once per pass of all I2C devices. But there is still something really messed up going on, as the test harness reveals that the I2C IO expender thinks that the read address is being written to.  It could be that my simulation of the I2C IO expander is flaky, or it could be that my I2C transaction generator has a bug in it.  I can test the latter by exporting a waveform file of the test run and trying to interpret it as an I2C transaction.

In theory, the sigrok protocol analyser software for Linux should let me do this. To be honest, I have found it a completely frustrating experience trying to get sigrok to actually build and run stably. Maybe the new snap packaged version of it will make this easier? Nope, it still segfaults.

Well, in the meantime the bitstream has built, and it works, so the problem is in my simulation of the IO expander.  That is, I can read the key switches and control the LEDs, so that's all I need for now.

Anyway, now that I have the ability to interface with the I2C keypads, let's get back to implementing the phone control stuff.

I want to implement checking of the cellular signal strength etc, and start updating the display with relevant information. This means I need to probe the modem regularly with a set of AT commands that will return the information that I am looking for. One command has to finish before the next can be sent, so I have to watch for the OK responses, and wait until then, or until a timeout, so that it can't get deadlocked.

I should probably also keep an eye on whether the modem sends anything over a period of time, and if not, then assume the modem has crashed, gone missing or otherwise not responding so that this can be indicated on the display.

I've implemented that, as well as retrieving the time and date from the cellular network, and computing the day of week from that.  So the display is now very uncomplicated, but provides some key information:

Compare this with a typical Android phone or "senior phone" display that clogs the display up with stuff that the person (in this case, my Mum) doesn't want, and then half the time covers it all with inane dialogue messages. This in contrast, just always shows you the key information.

Now to get the missing LEDs and resistors I need to populate the rest of the keypads. I also need more key switches. It looks I got lucky and bought the last bulk kit of 400 TTC Golden Brown switches from MechStock in Australia for 23 cents a switch, including postage!  The good thing about a phone is that you don't need 100 million perfect key presses per phone, unless you really like to play IVR RPGs endlessly.

Anyway... off to go and get those LEDs so that I can start making the context-sensitive illumination of the keys to indicate which are possible at any point in time.

LEDs acquired, and the key pad assembled, and code implemented to write to the I2C IO expander to control the LEDs. That all seems like it should do the right thing, but it's behaving oddly. Digging around, I have found that the IO expanders are writing to two registers at a time, instead of just one. This means that the data direction lines for the LEDs isn't being set correctly. Also it means that if an LED has its control wires on the two different ports of the IO expander that it can't be correctly controlled.

This probably means its time to go back to the simulation and see if I can visualise the I2C transaction to see what I am doing wrong.  In theory it shouldn't be too hard to find and fix -- especially since I have managed to reproduce the bug in simulation of the VHDL :)

It looks like no I2C STOP signal is ever generated, which is a bit weird. Not even when reading registers. Actually, a STOP does get generated at the start of the write, but nowhere else. We should generate a STOP at the end of the write, and that should fix everything up. We do already put a delay in that should cause the STOP to be emitted, but for some reason the i2c_command_en line pulses low only very briefly, and is then reasserted when it should be low due to the delay period. 

The problem turned out to be the lack of an extra dummy state at the end of the state machine to force the I2C transaction to emit a STOP at the end. The tests under simulation now pass, so time to resynthesise.

While that happens I can work on implementing scanning the keys on the I2C devices, as that is not affected by this bug.  I should then be able to tie one to pick up or hang up a call.

The I2C write fix seems to have worked. But something is going funny in my key scanner: It seems to alternate between reading the correct register and reading the adjacent register. I can't see why this would be. I also can't see it happening in the serial monitor when I repeatedly read the I2C registers. It happens many times per second in the code, though.

Looking a bit more, it looks like the problem might be a common problem I have seen with these IO expanders before: Sometimes they read $FF instead of the correct register values. Perhaps I am clocking something a bit fast somewhere or something. I should look into it a bit more at some point. But for now, I can just enforce de-bouncing of the keys -- reading the I2C register 3 times in a row seems to be enough to force clean de-bouncing. It probably needs 3 not 2, so that it takes long enough for another scan of the I2C bus to occur in between.

Now it's back to the LEDs that are still doing weird things. Since controlling the LEDs also uses reading and writing I2C registers, I have to debounce those as well. That has things slightly better, but still things are messed up.

The latency of the I2C controller is proving a bit annoying, because when we want to modify two bits in a single byte, as is required for switching the LEDs (we have a pin for the anode and one for the cathode, so that we can use bi-colour red/green LEDs), if we do it as two writes the 2nd will mess up, because it works out the bit mask based on the previously cached value.  E.g., if we had $42 in the register and decide we need to set bits 0 and 3 we will read $42, set bit 0 = $43, and write that back, but when we go to set bit 3 we will most likely read $42 still, rather than $43, unless we wait long enough for it to propagate.

With that fixed, it's still doing something odd: Sometimes after writing to the output registers for the LEDs, it reads back the write ports instead of the read ports, i.e., registers 2 and 3 appear imaged (and byte swapped) at registers 0 and 1.  This makes me think that it has something to do with the lack of detection of the I2C STOP signal by the IO expander. So I guess I really do need to look into that and figure it out. This probably can only happen if the write is to the address of the first I2C device in the read loop. 

By adding a 50ms delay after each I2C write I can work-around this problem. This works because the I2C bus will read multiple times after the write, and thus get back to normal.  But I should still fix it at the root cause, if nothing else so that I don't need to add big delays into what is supposed to be real-time code.  The simplest approach here is to add a dummy read of a non-existent I2C device after the write slot, so that the first IO expander doesn't stay under attention.  

That solved that problem nicely.

Now I am working more on the call handling, and making the pick up and hang up buttons light up green and red when they are able to be pressed, along with other buttons for quick-dial also lighting up when configured and able to be pressed (i.e., not already in a call).  This is all a bit fiddly to get the logic right, but I'm progressing, and can answer and hang up calls, although answering sometimes fails because the modem hangs up if you try to enter any command after ATA before it has confirmed the connection or the caller has aborted.

I think to debug that and the remaining LED misbehaviour by implementing a log of activity on the serial monitor interface.  For this I'll use the Hypervisor's ability to write to the serial monitor port with this routine from mega65-libc:

unsigned char the_char;
void debug_msg(char *m)
{
  // Write debug message to serial monitor
  while(*m) {
    the_char=*m;
    __asm__ ("LDA %v",the_char);
    __asm__ ("STA $D643");
    __asm__ ("NOP");
    m++;
  }
  __asm__ ("LDA #$0d");
    __asm__ ("STA $D643");
    __asm__ ("NOP");
  __asm__ ("LDA #$0a");
    __asm__ ("STA $D643");
    __asm__ ("NOP");
}

This has already let me confirm that the LED glitches were not from being commanded to do the wrong thing during the incoming call state (state 3):

=STATE 3
>MODEM AT+CSQ
>LED : LED#15 = 2
>LED : LED#14 = 0
<MODEM : OK
>MODEM AT+QSPN
<MODEM : +CLCC: 1,1,0,1,0,"",128
<MODEM : +CLCC: 2,1,4,0,0,"0427679796",
<MODEM : +CSQ: 25,99
<MODEM : +QSPN: "Boost","Boost","BOOST",0,"50501"
<MODEM : OK
>MODEM AT+QLTS=2
<MODEM : +QLTS: "2024/04/07,10:07:23+38,0"
<MODEM : +CLCC: 1,1,0,1,0,"",128
<MODEM : +CLCC: 2,1,4,0,0,"04276

Led #15 (pick up) is the one we expected to switch to green (value 2), and it is being commanded to do so.  LED #14 (hang up) is expected to be turned off, which it is. This points to problems with the latency of an I2C write to show up when reading the I2C registers to apply the mask for setting the next LED. To confirm this and solve the problem I implemented a cache of the I2C register values, which has completely solved that problem. So I think that was almost certainly the problem.

Next bug is that the logic that waits for an OK to be sent gets out of sync sometimes. This causes a variety of problems, but the one that caught my eye is that when there is an incoming call, the call state sometimes drops back from INCOMING CALL to IDLE, because the AT+CLCC command's response hasn't been observed. We can see this in the log below:

>MODM AT+QSPN
<MODM : +CSQ: 24,99
<MODM : OK
>MODM AT+QLTS=2
=STAT 3
>MODM AT+CLCC
>LD : LD#15 = 2
>LD : LD#14 = 0
<MODM : +QSPN: "Boost","Boost","BOOST",0,"50501"
<MODM : OK
...
=STAT 2

We can see that the underlined AT+QSPN command doesn't get its response immediately, but rather we see the response to some previous commands. Likewise when we send the AT+CLCC command (bold), my code expects to get a list of calls followed by OK. But it sees the OK without seeing any calls, so thinks there aren't any, and drops out of the incoming call mode.

I'm suspecting that this is responsible for several other strange effects, including responses from some commands being cut short, because another command has already been dispatched, and the EC25-AU doesn't seem to buffer the output of previous commands to flush them through.

The question is how the communications gets out of sync in the first place. It happens before a call even occurs. Found and fixed: In the periodic state management function, it would try to send the next AT command, without first checking if we were still waiting for an OK from the previous.

A similar thing happens when answering a call if the ATA is sent in the middle of the +CLCC response block. So I probably need to schedule the ATA, instead of sending it immediately. I'll have to do the same for hanging up via AT+CHUP (ATH is recommended only for terminating data calls on the EC25-AU, apparently).

Generally, I'm working through the state machine logic for call handling still, as it seems that when I plug one problem, another remains, or perhaps is caused by that fix.  What seems like a deceptively simple state model for call handling always has a lot of subtlety to it.

Some of this comes from some annoying features of the modem. For example, the ATA command reports NO CARRIER if the call fails, and CONNECT if a data call connects, but it just says OK if a voice call connects, which creates ambiguity with the responses from other commands. 

Anyway, I have the call answering logic now working, and the LEDs doing what they should. Next step is getting the quick dial functionality working. It tries to dial, and seems to send the modem the complete ATDT command with the number, but the modem only echos back the first few digits of the number, rather than the complete number, suggesting that it doesn't receive it all.  The modem also fails to establish the call, which reinforces this possibility.

ATDT can take 5 seconds to complete dialling, during which time no other AT commands should be sent to the modem, as otherwise the call may be terminated. Or at least that seems to be what happens. 

But even with that, I'm not convinced that it's working properly to dial. I am using a pre-paid SIM card, but it should be expired until tomorrow. Unfortunately Boost Mobile don't have a way to check balance via SMS, and I don't have an account setup for it. I'd actually like an SMS-based method, because then I could make the phone periodically query to see if it is running low on credit or not, and provide a visual indication.

I think I found the problem with dialling -- I was using ATDT, which for old land line modems indicated tone instead of pulse dialling. But I don't think the EC25-AU supports that. You just have to use ATD only.  Another problem was that I wasn't waiting for the previous command to complete before sending the ATD command.

With that fixed, and some other minor bits and pieces, it now fairly reliably places calls. Sometimes it does still fail, but it does recover from that, and go back to the idle state. I still want to figure out why, and fix the problem, though.

I've also made the behaviour of the LEDs simpler and more uniform for a user to know what options are available at any point in time. I've also made the background of the screen change colour to indicate state, so that it's easy to work out what state the phone is in: Black is default, so that it doesn't cast too much stray light, especially if you used an OLED monitor.  Blue in calls, grey when trying to get the modem configured, orange during call transitions, such as calling or answering.  I'm sure the design language will evolve, but its already a pretty simple and understandable interface.

Next up I've implemented manual dialling and sending DTMF tones during a call. DTMF in a call isn't perfect, because the play back in the local handset and sending via the network to the remote device are serialised, and there is a limit to how fast it can happen. To keep things simple for now, I turn off the number pad LEDs while the DTMF is in progress, so that the user knows that they can't dial another digit yet.  Later when the audio path is via the FPGA it will be possible to play the DTMF locally simultaneously while sending. But we will still need to allow queuing up multiple DTMF characters to be sent as a single command to allow rapid DTMF dialling. But what we have now works. Manually dialling a number and hitting the "pick up/call" button also works now.

While still disconbobulated in multiple pieces, it's probably now possible to guess that it might be supposed to be a phone :)


This also confirms that both PCBs function correctly -- no errata that needs fixing, which is a pleasant change from many of the PCBs I develop.  Admitedly the design of these keyboard PCBs is really very simple, and leverages the past work I did on a similar alternate design for the full keyboard of the MEGA65.

So all that's left for absolute core functionality of a telephone is for it to be able to ring. The FPGA board I am using has a 3.5mm audio output that the MEGA65 core already supports, so in principle I can just play it out that.  I've found an attribution licensed recording of the right vintage of Australian telephone that I want it to sound like: https://freesound.org/people/petaj/sounds/28353/. It really does need 44KHz to sound nice at 8 bit, and once complete ring phase is ~3 seconds. So this means about 128KB of RAM for the sample. That's too big to compile into a C64-mode memory context program (which is how I am writing this for now).  So I'll have to use the mega65-libc functions to ask the hypervisor to load the file into higher RAM banks from the FAT32 file system on the SD card. That would load fastest. Or I could read it from a D81 image, but that would be slower and fiddlier.

Hmm... another problem for now is that the audio DMA facility on the MEGA65 only supports samples upto 64KB. Maybe I will need to trim it to ~22KHz for now. I do plan to implement direct audio streaming from SD card, which will totally solve this problem, and allow me to have a single very long ring tone, instead of repeating the same 3 seconds of audio. But that's not for just yet.

The main impediment right now is that I can't find a microSD card to put in this FPGA board. I know I have a bunch of them lurking around. It's just that I don't know where I put them. I need to organise one for this, anyway, so that I can have the program on it to be loaded by the MEGA65 ROM on boot, anyway. So that's probably the next step. I'll pick one up tomorrow.

What I did do tonight, though, is dig up the code for asking the hypervisor to load files from the FAT32 filesystem of the SDcard. This provides me with the handy function read_file_fromsdcard(char *filename, uint32_t load_address), which will make it trivial for me to load the ringer sample into memory once I put it on that SD card.

Micro SD card acquired, inserted, formatted using the MEGA65. Also flashed the bitstream to the QSPI flash on the Nexys4 board, and copied on a MEGA65 ROM and the other files needed for the MEGA65 to boot. So now the Nexys4 board boots to BASIC65. Configured it to mount TELEFONE.D81 as the defailt disk image. Also made Makefile rules to build TELEFONE.D81 and push it to the Nexys4 board.

The result is I now have the hardware booting automatically and running the telephone program automatically as well.

So now I can push that WAV file to the SD card, and hook in the code to load it into an upper bank of RAM, probably BANK 4 at $40000. And it all works, except that it sounds like rubbish. I'm not quite sure why, yet.  The test sine wave tone in the MEGA65's audio DMA system plays nicely without static. But the RINGER.WAV file I have created is really bad.  Yet it plays cleanly in VLC.

Found a few problems: I had signed/unsigned wrong, hadn't normalised the sample to maximum volume, and generally had some of the audio DMA registers messed up. It now plays properly. The Nexys4 audio output is still a bit hissy when playing audio, so I may need to add a PMOD audio codec to get the SNR good enough that the hiss isn't annoying when the phone rings loudly.

But this means we have a working ringer -- which completes the bare minimum functionality required for a telephone: We can make and receive calls, including with DTMF tones to navigate voice menus, and the phone rings when there is an incoming call! 

It would be nice to have a ring tone in your ear while waiting for a call to get answered, but that will require me to route the cellular audio through the FPGA, which I will do, but not just yet. I could actually do that by playing it through the ringer speaker at a reduced volume to simulate it. That's probably a good idea.

It's probably time for me to make up a box to fit all this stuff in a hopefully nice looking box. I have a cunning plan for how to make that box look nice, without spending a lot of money. But first, I need to assemble the other four 4-way key pad PCBs, so that I have the complete set of 20 keys in addition to the dial pad.  I've built those up now, and used 510 Ohm resistors for the LEDs instead of 130 Ohm ones, so that they aren't so bright, and don't suck so much current, since with 32 LEDs, it can add up quickly.

Rather annoyingly I have two brands of red-green LEDs, and one of them is green-red, rather than red-green. It's easy enough to sort in software to make it possible to switch the polarity of any given LED.

In the meanwhile, I have mounted it all on a piece of MDF, so that I can start putting it into a case of some sort.  It's all a bit rough and ready for now, but that's okay. The main thing is that it is functional, and easy to use.

I'll probably work on making the key allocations software remappable, as well as the red/green vs green red problem.  I'm thinking I will add some kind of test mode for mapping this out, although it won't be able to save the configuration to SD card yet. I might make it assemble a valid configuration in memory, that I can then easily pull down and copy on to the SD card manually for now. Eventually it would make sense to allow this to all happen on the device.  

One thing I am careful with here, is that I don't want the configuration mode to be able to entered by interacting with the phone. This is to avoid Sod's Law that a user will somehow end up in the configuration mode and make a mess the instant my back is turned. So instead, it will likely require putting a special file on the SD card and rebooting the phone, after which it will operate only in the configuration mode until the file is removed. It will also say that it is in the configuration mode, and that the file has to be removed to restore normal operation.

The key remapping problem could be reduced somewhat if I enforced that the keypad PCBs had to be assembled with the correct order of I2C addresses. But I prefer to allow the build process to be a bit more relaxed and less frustrating.

So that leaves the following three items that I need to explore soon:

1. Speed dial key / LED configuration, so that you can indicate which LEDs are backwards, and which keys correspond to which contacts / phone numbers.

2. When placing an outbound call, make the expected ringing noises in the handset. 

3. When you pick up the handset to make a call, have a dial-tone in the handset.

These last two require the FPGA to interrupt the audio path between the cellular modem and the handset, which we have proven previously with the MEGAphone prototypes, so will require a little more work to build a custom PCB to do that, but its not a massive amount of work.  The configuration management is the obvious low-hanging fruit, though, so I will likely tackle that next.

No comments:

Post a Comment