James Thorpe

PIC Communications

Jul 24, 2020 electronics pic

Status Checks

It's been a while since the last post about my PIC project, and a fair bit has been achieved and changed since, so let's have a quick run through of what those things are!

Firstly, one of the things I left the last post with was that I wanted to:

Add inter-PIC "are you there" health checks, ie send messages back and forth almost constantly - I can then start testing breaking and reconnecting the serial links to make sure they can detect when they're connected or not

I did this a while back. I utilised the existing millisecond timer and messaging schemes I detailed in the last post. First up within the timer ISR, I keep count of how far along we are then set a flag:

if (pingCount++ == 200) {
    checkNeighbour = true;
    pingCount = 0;
};

I use simple flags within the ISRs where possible so that they return quickly, with the actual work being done in the main loop where possible. Within the main loop, I constantly check this flag:

if (checkNeighbour) {
    CheckNeighbour();
    checkNeighbour = false;
}

With the CheckNeighbour function finally doing the work:

uint8_t lastPing = 0;
void CheckNeighbour() {
    if (lastPong != lastPing) {
        Leds.Yellow = 0;
    } else {
        Leds.Yellow = 80;
    }
    MsgPing p;
    lastPing++;// = (uint8_t)((rand() % 200)+20);
    if (lastPing == 200) {
        lastPing = 0;
    }
    p.value = lastPing;
    SendMessage(&port2, MSG_Ping, (uint8_t *)&p, 1);

}

This results in a PING message being sent every 200ms, or 5 times a second - plenty for my needs. Correspondingly, the HandleMessage function has been updated to deal with these new messages:

void HandleMessage(uint8_t msgType, uint8_t msgbytes[], uint8_t msglength) {
    pMsgCommand pCommand;
    pMsgPing pPing;
    pMsgPong pPong;
    switch (msgType) {
        case MSG_Command:
            pCommand = (pMsgCommand)msgbytes;
            Leds.Blue = pCommand->value ? 255 : 0;
            break;
        case MSG_Ping:
            pPing = (pMsgPing)msgbytes;
            ping = pPing->value;
            break;
        case MSG_Pong:
            pPong = (pMsgPong)msgbytes;
            lastPong = pPong->value;
            break;
    }
}

Note that when a ping is received, it doesn't immediately send the returning pong - instead it sets the ping variable, and the main loop takes care of returning the pong message. I think this code could probably change a bit now - the HandleMessage function is no longer called from within an ISR, so there's no need to keep the return message sending outside of it. There's a fair bit of cleanup to do on the codebase with things like this. Back in the main loop, we send the pong:

if (ping != 255) {
    MsgPong p;
    p.value = ping;
    SendMessage(&port2, MSG_Pong, (uint8_t *)&p, 1);
    ping = 255;
}

Going back to the CheckNeighbour function, above, the start of that function concerns itself with comparing the last pong received with the last ping sent - if they are the same, we light an LED, if not, we turn it off. This allows a simple way of telling whether or not the two sides are talking to each other. With the code running, disconnecting and reconnecting either side of the USART connection makes the LEDs on both sides toggle.

The project sort of started languishing here for some time - I got a bit disillusioned with my plan to use the USARTs, and needing to use something like pogo pins to allow connections between different modules. I'd originally hoped for using IR communication, but the IrDA modules to do so seemed very costly for my needs.

Fresh ideas

Skip forward a number of months, and a random question I asked on Twitter led to a new approach:

@Robin_B random question if I may? I’m tinkering with microcontrollers etc, and would ultimately like to make some modules that can communicate - each talking to several others. IrDA transceivers seem to be relatively expensive - £4 or so each - am I missing something?!

— James Thorpe (@jamesethorpe) June 10, 2020

This led to a bit of a conversation and Jon Bobrow very kindly noted that in their Blinks product they use a single LED on each face for communication purposes. It turns out you can drive them in reverse to charge them up, then time how quickly they discharge to find out if they're being illuminated or not. Bit of playing around later, and success!

😮

Really can’t believe that worked so easily! Flashes at the bottom are comms, blue LED being turned on/off when flash detected! Didn’t even need to tweak the random timing numbers I’d thrown in on the first try! pic.twitter.com/nmSFw0F81l

— James Thorpe (@jamesethorpe) June 15, 2020

That led me off down a different approach - rather than using USARTs / IrDA to communicate, what other bit-banging approaches are there? Yes, I probably wouldn't get the reliability or speed of the standardised IrDA, but I need neither for my experiments - a few tens of bytes per second is plenty. I started off with blue LEDs, then found out that a couple of red LEDs I had were more reliable, then found out about phototransistors!

No need for driving phototransistors backwards and timing how long they take to discharge, instead I can simply read a pin input to see if it's high or low. I ordered a couple of IR LEDs and matching phototransistors, and was underway. I came up with a simple protocol to transmit data that's used in other places - hold it on for a defined period to indicate a reset, then turn it off, then turn it on and off for differing periods for 1's and 0's. The receiving side uses a timer and an ISR to keep count of how long the input is high/low for, and reconstructs the bytes on that side. With a couple of buffers, I found I could then use it to directly replace the USART ports in my existing code:

IR comms are go!

The yellow LEDs light up when both sides are successfully pinging each other - they (currently) do this 5 times a second. Blocking the IR path shows them going out.

Pressing the button sends a command to turn the light on at the far side. pic.twitter.com/1dCWxKQhch

— James Thorpe (@jamesethorpe) July 1, 2020

Enter RGB

With this success under my belt, and the fact I'm no longer tied to needing a USART per face, it meant I could switch to a hexagonal design, allowing for more interesting tessellations. I also decided to keep things a bit simpler, and rather than having an extra board on top holding a (fairly expensive) RGB matrix, I've decided to mount RGB LEDs directly to the PCB itself. I've gone with SK6812 RGBW LEDs, and the design looks like this:

matrix blocks pcb

There's 19 RGBW LEDs on there - 12 in an outer ring, 6 on an inner ring and 1 in the center. There's also an LED per face which can be used for indicating communication statuses, and another near the middle for power. There's also space for 6 push buttons for user input.

Having decided on this, I thought I'd best aquire some of the LEDs to experiment with. One order from China and a couple of weeks delivery time later, and 200 of them turned up:

reel of LEDs

A quick bit of soldering later, and two of them were ready for prototyping with - 3.5mm jack for scale:

two LEDs with wires soldered

These being RGBW need 4 bytes of data to control them. It turned out to be fairly straightforward to bit-bang the necessary data out:

#define delay60 asm("nop"); // an instruction takes 62ns to execute, 
                            // which is close enough for this to work
#define delay300 delay60 delay60 delay60 delay60 delay60
#define delay600 delay300 delay300
#define delay900 delay600 delay300

void sendLEDData(void) {
    // prevent other timers or button pushes etc from interrupting this
    // as it needs to be pretty precise
    INTERRUPT_GlobalInterruptDisable();
    uint8_t ledBit;
    for (uint8_t x = 0; x < 4; x++) {
        for (ledBit = 8; ledBit > 0; ) {
            ledBit--;
            if (leds[x] & (1<<ledBit)) {
                IO_RC6_SetHigh();
                delay600
                IO_RC6_SetLow();
                delay600
            } else {
                IO_RC6_SetHigh();
                delay300
                IO_RC6_SetLow();
                delay900
            }
        }
    }
    INTERRUPT_GlobalInterruptEnable();
    IO_RC6_SetLow();
}

What was immediately obvious, however, was that this was completely interfering with the IR communications which need fairly accurate timings to work.

Fixing things

There's a number of things that need fairly accurate timings here that are all interplaying with each other:

I first tried to come up with a scheme whereby the two sides would synchronise with each other, and could then both be either updating LEDs or talking to each other. A fairly complex thing to achieve in the first place, it becomes even more complex when you consider the future direction of this, where other modules could be added and need to be synced, or other entire sets of modules that are already synced with each other being brought in.

I then came up with a plan based around the fact that I still have plenty of bandwidth to spare. Firstly, let's not allow an LED update to interfere with transmitting an individual byte. Every 40ms (to give a target of 25fps) a flag is set saying that an LED update is due:

ledCounter++;
if (ledCounter == 800) {
    txLeds = true;
    ledCounter = 1;
}

This is inside the main IR ISR, which is called every 50us, so a count of 800 translates to 40ms. The IR transmission code then checks this flag whenever it's not sending a byte, and if it's set will perform an LED update. This means that an LED refresh might happen at worst 8*50 = 400us later than planned, which is fine. That takes care of the transmission side of things, but what about receive?

If an LED update happens during a receive, we chew up over 700us, which is 14 IR period counts - plenty to mess up the receipt of two or three bits of information. I finally came up with a simple, if slightly bandwidth-wasteful, plan to sort this out. Transmit every byte 3 times, and throw away any consecutively received duplicates. A quick update to the higher level message handling code made it so that if a message happens to contain two consecutive bytes that are the same, a NOP byte is inserted and subsequently ignored upon receipt. The LED updates are far apart enough that they shouldn't possibly interfere with the receipt of this more than once. So why not just send each byte 2 times? If the LED update happens during the receipt of the final part of one byte, it can easily run into the second retransmission. But it can't hit a third.

All this coupled together means I have nice smooth 25fps LED updates, and a full-duplex bandwidth of just under 80 bytes per second, per face. The current ping/pong mechanism firing every 200ms uses 40 bytes of that (though could probably be optimised lower - eg combine ping/pong into one message of 5 bytes perhaps, rather than two of 4 bytes each), leaving the other 40 for plenty of other things!

In the above video, the buttons are sending messages across the link to tell the far side to toggle the blue LED, and you can see when I interrupt the IR path the yellow "I'm connected" LEDs extinguish. All while the RGB LEDs cycle through the colours nicely.

Now I just need to convince myself that there really is nothing else left to test before I push the button to get some PCBs made and components bought...

Thanks

Again big thanks to Jon Bobrow for the tip about LEDs that has reinvigorated my excitement for this project, and Robin Baumgarten for the introduction. Go check out Robin's projects (the new Wobble Sphere looks fantastic), and Jon's Blinks - think I might have to invest in some of those at some point!

Back to posts