Close

PFC232 as an I2C Slave

A project log for Persistence of Vision POV Display SAO

Accelerometer-based Hand-waved POV display with front and back LEDs, creating a 5 pixel x 12 char display. Message customizable via I2C.

michael-yimMichael Yim 10/07/2024 at 22:090 Comments

To enable user customization of the POV message, my approach is to allow the user to write the message in ASCII characters and send it to the PFC232 microcontroller via I2C. In this setup, the PFC232 will function as an I2C slave, which is a bit unconventional, as microcontrollers typically act as I2C masters, such as when reading data from the ADXL345 sensor. In my case, the microcontroller must act as BOTH a master (for reading the ADXL345), and also a slave for handling the upload of I2C messages from the user.

The Padauk FPPA IDE has a very nice tool to generate boilerplate code for I2C slave, which can handle multiple-byte buffer write and reads.

void    I2C_Slave (void)
{
    $ I2C_SCL_Slave    In;
    $ I2C_SDA_Slave    In;

    .delay    99;        //    Wait for master ready

    
    addr$1    =    0;    //    at the sample code, only access one byte of address.

Stop:    

        .wait1    I2C_SCL_Slave
        if (! I2C_SDA_Slave)    goto Stop;

High:    if (! I2C_SCL_Slave)    goto Stop;
        if (I2C_SDA_Slave)        goto High;

Start:    if (! I2C_SCL_Slave)    goto Chk_Ax;
        if (! I2C_SDA_Slave)    goto Start;
        goto High;

Chk_Ax:
    BYTE    count    =    DEVICE_LEN;
    A    =    I2C_SLAVE_DEVICE;
    do
    {
        sl    A;
        .wait1    I2C_SCL_Slave
        if (I2C_SDA_Slave)
        {
            if (! CF) goto Stop;
            while (1)
            {
                if (! I2C_SCL_Slave)    break;
                if (! I2C_SDA_Slave)    goto Start;
            }
        }
        else
        {
            if (CF) goto Stop;
            while (1)
            {
                if (! I2C_SCL_Slave)    break;
                if (I2C_SDA_Slave)        goto Stop;
            }
        }
    } while (--count);

    if (DEVICE_LEN != 7)
    {
        BYTE    hi_adr    =    0;
        A    =    7 - DEVICE_LEN;
        do
        {
            .wait1    I2C_SCL_Slave
            if (I2C_SDA_Slave)
            {
                CF    =    1;
                slc    hi_adr;
                while (1)
                {
                    if (! I2C_SCL_Slave)    break;
                    if (! I2C_SDA_Slave)    goto Start;
                }
            }
            else
            {
                sl    hi_adr;
                while (1)
                {
                    if (! I2C_SCL_Slave)    break;
                    if (I2C_SDA_Slave)        goto Stop;
                }
            }
        } while (--A);
    }

    .wait1    I2C_SCL_Slave

    BYTE    data;

    if (I2C_SDA_Slave)
    {    //    Read
        while (1)
        {
            if (! I2C_SCL_Slave)    break;
            if (! I2C_SDA_Slave)    goto Start;
        }

        $ I2C_SDA_Slave        Out, Low;
        .wait1    I2C_SCL_Slave

Send:    count    =    8;
        data    =    ~ *addr;
        addr$0++;

        do
        {
            .wait0    I2C_SCL_Slave
            sl        data;
        #if    _SYS(OP:SWAPC IO.N)
            swapc    _PXC(I2C_SDA_Slave);
        #else
            if (!CF)    $ I2C_SDA_Slave    In;
            if (CF)        $ I2C_SDA_Slave    Out;
        #endif
            $ I2C_SDA_Slave    Low;
            .wait1    I2C_SCL_Slave
        } while (--count);

        .wait0    I2C_SCL_Slave
        $ I2C_SDA_Slave        In;

        .wait1    I2C_SCL_Slave

Watch:    if (I2C_SDA_Slave)    goto Stop;
        if (I2C_SCL_Slave)    goto Watch;
        goto Send;
    }
    //    Write
    while (1)
    {
        if (! I2C_SCL_Slave)    break;
        if (I2C_SDA_Slave)        goto Stop;
    }

    $ I2C_SDA_Slave        Out, Low;
    .wait1    I2C_SCL_Slave

    addr$1.0    =    1;
    while (1)
    {
        .wait0    I2C_SCL_Slave
        $ I2C_SDA_Slave        In;

        count    =    8;

        do
        {
            .wait1    I2C_SCL_Slave
            if (I2C_SDA_Slave)
            {
                CF    =    1;
                slc    data;
                while (1)
                {
                    if (! I2C_SCL_Slave)    break;
                    if (! I2C_SDA_Slave)    goto Start;
                }
            }
            else
            {
                sl    data;
                while (1)
                {
                    if (! I2C_SCL_Slave)    break;
                    if (I2C_SDA_Slave)        goto Stop;
                }
            }
        } while (--count);

        $ I2C_SDA_Slave        Out, Low;
        .wait1    I2C_SCL_Slave


        if (addr$1.0)        addr    =    data;
        else
        {
            *addr    =    data;
            addr$0++;

            //Set Message Transfered Flag
            MsgFlag = 1;
        }
    }
}

Initially, I planned to allow users to upload custom messages on the fly, but it turned out to be more challenging than expected with my current hardware setup. The issue arose because both the master and slave I2C devices shared the same SCL and SDA lines, leading to conflicts when they attempted I2C transactions simultaneously. Since the Padauk environment doesn't natively support MUTEX operations, managing these conflicts became difficult.

To resolve this, I simplified the design. Now, the user can only upload a message at the start of the operation. Once the message is uploaded, the LEDs will blink, and the POV display will show the message. No new messages can be uploaded during operation. The only way to change the message is to restart the POV display and upload a new one. 

This made the software implementation much easier. I used a simple state machine in which State 0 waits for the user to upload the message (in I2C Slave mode). Once the message is uploaded, it transitions into State 1, where the I2C Master takes over the SCL and SDA lines. In this state, the ADXL345 is initialized and configured to the right settings. Then comes State 2, where it polls the ADXL345 and synchronizes the scanning of the virtual columns of the POV display.

I also encountered a RAM limitation issue. The PFC232 microcontroller is quite affordable, but it only comes with a small 128-byte data RAM, which is shared between the two FPPA units. Initially, I wanted the POV display to support up to 20 characters, but I discovered that 12 characters is the actual limit. In hindsight, 12 characters turned out to be a practical limit as well. If the message is too long, the user would need to wave the display faster and over a greater distance. Beyond a certain point, the message becomes too long to display properly, given the user's realistic waving speed.

With all these challenges finally resolved, including addressing the PCB's cosmetic issues, I moved forward and sent the design for a new PCB production cycle. Now, it's just a matter of waiting for the new PCBs and parts to arrive so I can begin the process of mass manufacturing. 

In the meantime, I'll be focusing on fine-tuning my LumenPnP Pick and Place machine to get it ready for this production run. It's essential to ensure the machine is calibrated precisely for this particular job, as it will be handling small components with high accuracy and speed. 

Discussions