James Thorpe

PIC Basic Communications

Aug 14, 2019 electronics pic

Once again, this post is a bit overdue. But, progress!

I left the last post with a few things I'd like to get done:

Next up I'd like to test a couple of things. Firstly, getting the button press itself to trigger an interrupt. Then duplicating the entire circuit further along the breadboard, and adding links coupling the UART serial links together. The goal at that stage will be to have the button on one chip cause the flashing to pause on the other chip.

And that's exactly what I've done. First up, using interrupts for the button. As before, the Code Configurator available in MPLAB is a big help here. At the end of the last update, I'd only configured pin B0 as an input with an internal pull-up resistor. With a few more options set, it will now trigger an interrupt on a negative change - ie when the button is pushed and the pin is pulled to ground. I knew I'd want to debounce this, as buttons can be "noisy" when changing state, so wrote some rudimentary code to do so. First up, we use the auto generated code to let the interrupt routines know which function to call on the pin interrupt:

IOCBF0_SetInterruptHandler(ButtonPress);

The ButtonPress function couldn't be simpler - we just set a flag to say we saw a change:

bool ButtonPressed = false;
void ButtonPress(void) {
    ButtonPressed = true;
}

Remember this function is only called when the button goes low, it's not retriggered when the button is released (at least, not intentionally - that's why we're debouncing...). We then utilise this flag within the MillisecondTimer function that we created last time, and is already called by a timer interrupt:

int ButtonCounter = 0;
void MillisecondTimer(void) {
    if (ButtonPressed) {                // The flag has been set by the interrupt
        ButtonCounter++;
        if (ButtonCounter == 10) {      // We're 10 milliseconds in....
            ButtonPressed = false;
            ButtonCounter = 0;
            if (!PORTBbits.RB0) {       //... read the state of the pin - if it's still low, 
                                        // the button is still pressed and the initial flag 
                                        // wasn't just a bit of noise (say during letting go)
                ButtonIsPressed();      // so call our function
            }
        }
    }
}

As I said, fairly rudimentary, but does the job for now. In a future revision I'll likely add some layers of abstraction in here so it's easy to use any pin for a debounced input.

Going back to what I said I'd like to get done, now was the time to get two PICs talking to each other. I duplicated the circuit on the breadboard, and added the serial links. Now I needed to send a message... back to the Code Configurator! Using this, it was trivial to get UART1 ready to talk. It also auto generated a few functions I'd be able to use. The first thing I did was send a byte whenever the button was pressed:

bool on = false;
void ButtonIsPressed() {
    on = !on;
    UART1_Write(on ? 0x15 : 0x20);
}

Simple enough - each time the button is pressed, inverse a flag and send a byte - 0x15 for on, 0x20 for off. Back in my main routine, I then set up the interrupt handler for incoming bytes:

UART1_SetRxInterruptHandler(Message);

With the corresponding Message function:

void Message(void) {
    uint8_t data = U1RXB;
    LATAbits.LA0 = (data == 0x15);
}

This reads the byte directly from the port (U1RXB), and sets the pin with the LED connected on or off depending on what the value of the received byte was. I was pleasantly surprised to see it all worked fine first time!

It was then time to start adding in abstraction layers on the communications - given the ultimate goal is to have modules that are connected and pulled apart, the communications need to be robust against missing/corrupted bytes etc, so I needed to move from sending a single byte to sending a frame. A bit of research led me down the path of having a start byte, a finish byte and an escape byte in case the start or finish bytes (or indeed the escape byte) need to be within the message itself. I also want a way to easily handle different types of messages, so started building the code with that in mind. I also made use of the functions that the Code Configurator generated - it creates a small buffer for reading bytes and the interrupt then reads into that buffer. For now, I then read from that buffer every millisecond. As the frequency and/or size of messages increases, I may have to revise this approach, but it works for now.

All that said, this is what I've ended up with. Firstly, I created a struct to hold all the information a given serial port needs:

#define MAX_MESSAGE_SIZE 2

typedef uint8_t (*pfnDataReady)(void);
typedef uint8_t (*pfnReadData)(void);
typedef void (*pfnWriteData)(uint8_t);
typedef void (*pfnHandleMessage)(uint8_t msgData[], uint8_t msgLength);

typedef struct {
    pfnDataReady IsDataReady;
    pfnReadData ReadData;
    pfnWriteData WriteData;
    pfnHandleMessage HandleMessage;
    uint8_t Buffer[MAX_MESSAGE_SIZE];
    uint8_t BufferPos;
    uint8_t State;
} UartPort, *pUartPort;

The first 3 typedefs / entries on the struct are for function pointers that make use of the auto generated Code Configurator code. To help get one of these structs into a default state, I made a simple helper function:

UartPort InitialisePort(pfnDataReady fnIsDataReady, pfnReadData fnReadData, pfnWriteData fnWriteData, pfnHandleMessage fnHandleMessage)
{
    UartPort p;
    p.IsDataReady = fnIsDataReady;
    p.ReadData = fnReadData;
    p.WriteData = fnWriteData;
    p.HandleMessage = fnHandleMessage;
    p.State = STATE_WAITINGSTART;
    p.BufferPos = 0;
    return p;
}

A port is then defined in the main program:

UartPort port1;

And initialised with function pointers for the autogenerated functions, along with a pointer to a function it should call when a valid message is received:

port1 = InitialisePort(UART1_is_rx_ready, UART1_Read, UART1_Write, HandleMessage);

With that in place, we can now send and receive messages. Messages are defined as a struct for each message type, which is then passed to the SendMessage function as a pointer to a byte array:

typedef struct {
    uint8_t command;
    uint8_t value;
} MsgCommand, *pMsgCommand;

bool on = false;
void ButtonIsPressed() {
    on = !on;
    
    MsgCommand msg;
    msg.command = 0x10;
    msg.value = on ? 1 : 0;
    SendMessage(&port1, (uint8_t *)&msg, 2);   
}

I don't know how robust casting from a struct to a byte array in this way is, but since my target architecture is known, I'm hoping this won't cause me too many issues in the long run. The SendMessage function:

#define FRAMESTART 0xFE
#define FRAMEEND 0xFD
#define FRAMEESCAPE 0xFC

void SendMessage(pUartPort port, uint8_t msg[], uint8_t length) {
    port->WriteData(FRAMESTART);
    for (uint8_t i = 0; i < length; i++) {
        if (msg[i] == FRAMESTART || msg[i] == FRAMEEND || msg[i] == FRAMEESCAPE) {
            port->WriteData(FRAMEESCAPE);
            port->WriteData(msg[i] ^ 0x20);
        } else {
            port->WriteData(msg[i]);
        }
    }
    port->WriteData(FRAMEEND); 
}

This writes the start byte, then loops through the bytes of the message, escaping where necessary. It seems like if you escape a byte in this sort of protocol, the "done thing" is to XOR it with another value - again 0x20 seems popular for this. I'm unsure the exact reasons for this, but it means that the start, escape and end bytes should never appear in a properly constructed message, so perhaps recovery from incomplete frames is cleaner. Finally we write the end byte.

On the other end, we need to receive these messages. Firstly, within the MillisecondTimer function, we now also call:

CheckComms(&port1);

Which, given a port, will then read any outstanding bytes from it and utilises a simple state machine to find complete frames:

#define STATE_WAITINGSTART 1
#define STATE_INMESSAGE 2
#define STATE_AFTERESCAPE 3

void CheckComms(pUartPort port)
{
    uint8_t data;
    while (port->IsDataReady()) {
        data = port->ReadData();
        if (port->State == STATE_WAITINGSTART && data == FRAMESTART) {
            port->State = STATE_INMESSAGE;
        } else if (port->State == STATE_INMESSAGE) {
            if (data == FRAMEESCAPE) {
                port->State = STATE_AFTERESCAPE;
            } else if (data == FRAMESTART) {
                //new frame, not expected - start again
                port->BufferPos = 0;
            } else {
                if (data != FRAMEEND) {
                    port->Buffer[port->BufferPos++] = data;
                } else {
                    port->HandleMessage(port->Buffer, port->BufferPos);
                    port->BufferPos = 0;
                }
            }
        } else if (port->State == STATE_AFTERESCAPE) {
            port->Buffer[port->BufferPos++] = data ^ 0x20;
        }
    }
}

When we see a complete message, it's handed off to the message handler defined earlier. In this case, my message handler function can assume the type of message (there's only one type), and can cast directly to it. In the future, the messages will need a small header defining the type of message. The underlying send/receive code may well also add some sort of frame checksum byte also. Back to the message handler, this is it:

void HandleMessage(uint8_t msgbytes[], uint8_t msglength) {
    pMsgCommand msg = (pMsgCommand)msgbytes;
    if (msg->command == 0x10) {
        LATAbits.LA0 = msg->value;
    }
}

Technically the command part of the message is unnecessary at the moment (of the one type of message I have, there's also only one command), but it's good to know that it works.

Here it all is working - the two circuits are independent and are only connected via the two white wires between the TX & RX pins (and they're also connected via power and ground of course):

Next steps, in no particular order:

Also, running waaay ahead of myself, I've also spent a bit of time playing around on https://easyeda.com - it's a schematic/pcb design tool, and looks to be part of the same group as https://jlcpcb.com which I've also heard good things about. Here's one version of what the PCB may look like:

pcb

It has a nifty 3D view too:

pcb

Thoughts of 3D printed parts and magnetic couplings are all in the back of my mind too...

Back to posts