Tuesday, 7 April 2015

CI20 and an LED Panel

The Application


So, wired up to the 4 GPIO pins from last time is a Sure DE-DP14111 32*16 LED Dot matrix Display. It's a smaller version of this display here, and although the LED's are smaller the physical HW is still very similar to the "3216 Bicolor LED 3mm Dot Matrix Display Information Board" from that page.

Hardware Interface

The interface to this board is a 16 pin IDC cable and two power rails. However the connector is fairly sparsely populated, and the power rails show as continuous.

Although the board is labelled as requiring a 5V supply I'm actually using a (separate) 3V3 supply to the Sure board, with a common ground to the CI20 and driving the inputs directly from the CI20 expansion header. This "works for me"(tm) with a dimmed display, however for a full 5V system I'd want to build a 3v3 to 5V driver board up, which I can skip for now.

The board really has 6 pins it requires:
  • Ground
  • Power - Nominally 5V, but I'm using 3v3 to match the CI20 expansion header levels
  • Two chip select  controls
  • Two Data line controls
The chip selects are worth taking a second to describe.

The front of the board consists of 8 LED panels, each of which contains an 8x8 LED Array, with 64 (bicolour) Red/Green LED's.

Logically speaking the display is actually broken up into four different areas; a pair of 8x8 panels (128 LEDs) is driven by a single HT1632C driver chip.

The two chip select inputs on the header are used to pick which of the HT1632C chips the data lines are communicating with, by controlling the chip select line of the HT1632C devices.

The chip select controls are a clock and a data line; these actually feed a 74HC164 shift register. Each of the device CS lines is fed to a separate delay stage. So when the chip select  line is pulsed and clocked then the shift register moves the "active low"  along the outputs through CS1 to CS2 to CS3 etc.

Using this mechanism the CS can be selected individually by pulsing the CS_IN line low, and clocking along to the specific chip, or multiple devices can be selected by holding the input low and clocking. In addition the boards can be chained together, since the final CS (CS4) is passed through to the output header.

This does make selecting a device slightly more complex however. A simple piece of code to do this, using the GPIO routines from the previous post, can be:
int DisplayInterface::ClockSelect(int val)
{
    _pins[SELECT_ACTIV].SetData(val);
    _pins[SELECT_CLOCK].SetData(1);
    usleep(USLEEP_DELAY);
    _pins[SELECT_CLOCK].SetData(0);

    usleep(USLEEP_DELAY);
return 0;
}

int DisplayInterface::SelectSegment(int which)
{
    for (int i=0 ; i < 4; i++)
    {// Clock all CS lines to high
        ClockSelect(1);
    }
    ClockSelect(0);
    while (which-- > 0)
    {
        ClockSelect(1);
    }
return 0;
}
i.e. we have two pins SELECT_ACTIV (the CS In, pin 1) and SELECT_CLOCK (CS Clock, pin 2). The ClockSelect() method takes the CS value in and makes a clock transition (low to high, then back to low). The usleep() means we can see the display fill as it happens by tuning the delay.


The SelectSegment() simply takes a chip offset then clocks the CS lines through so that all are high, then pushes in a low pulse and clocks this along to the target device.

This gives us a very simple "pick one" solution for the CS mechanism.

The Data

The data control of the device is likewise a serial bus, with a clock and data input. These fan out to all four of the HT1632C devices, and the chip select determines which one is active.

So a single bit write across the data interface would be of the form
int DisplayInterface::ClockWrite(int val)
{
    _pins[WRITE_CLOCK].SetData(0);
    _pins[WRITE_DATA].SetData(val);
usleep(USLEEP_DELAY);
    _pins[WRITE_CLOCK].SetData(1);
usleep(USLEEP_DELAY);
return 0;
}
Where WRITE_CLOCK and WRITE_DATA are pins 5 and 7 of the input header.

To simplify the write I'm only going to deal with simple command sequences here, where we enable the CS for a device, write a single command and then de-select it. There are continuous write modes, but for my application (simple, largely static, display) then I don't care about frequent updates, so I won't bother with that for now.

There are actually two kinds of data we can send to the devices, one set is simple and one is not.

Configuration

The "not simple" part is the control sequences which set up the HT1632C devices. These configure the output stages, system oscillator, etc. However the good news is that the data sheet contains some simple "good" configurations we can use.

The configuration sequences are 12 bits each, and are shown in the Command Summary section of the user guide; Figure 2-14 (and on p21 of the HT1632C data sheet); the first three bits indicate the Command ID, and the following bits are configuration information.

For our application we can just use the following canned configuration strings

uint32_t sysen = 0x802;
uint32_t ledon = 0x806;
uint32_t nmos = 0x840;
uint32_t rcmaster = 0x830;
uint32_t pwm10 = 0x962;

These are:
  • SYS_EN - Turn the oscillator on
  • LED On - Turn on the LED Duty cycle generator
  • COM Option - N-MOS open drain output and 8 COM option
  • RC Master mode - set clock from on chip oscillator
  • PWM Duty - 10/16 Duty cycle

Setting them is fairly simple: we can push out a 12 bit configuration packet with the function:

int DisplayInterface::Issue12(uint32_t data)
{
int pos = 11;

     while (pos >= 0)
    {
    uint32_t out;
        out = data >> pos;
        ClockWrite(out &0x01);
        pos--;
    }
return 0;
}


Which simply shifts the correct bits into the output position and uses ClockWrite() to issue it to the board.

We can therefore configure a single chip with:

int DisplayInterface::Enable(int which)
{
uint32_t sysen = 0x802;
uint32_t ledon = 0x806;
uint32_t nmos = 0x840;
uint32_t rcmaster = 0x830;
uint32_t pwm10 = 0x962;

    SelectSegment(which);
    Issue12(sysen);
    SelectSegment(which);
    Issue12(ledon);
    SelectSegment(which);
    Issue12(rcmaster);
    SelectSegment(which);
    Issue12(nmos);
    SelectSegment(which);
    Issue12(pwm10);
return 0;
}

Data

Data is much simpler. All data sequences are a three bit ID (101 - write) , and then a 7 bit address and 4 bits of data

The output LEDs are simply memory mapped; each 4 bit data register maps to four LED status settings. The first 32 registers (0x00-0x1F) control the green LEDs - (4 * 32 = 128 LED's from each panel pair) and the next 32 (0x20-0x3F) control the corresponding red LEDs.

So we can write a data sequence with:
int DisplayInterface::Command(int which, unsigned char id, unsigned char addr, unsigned char data)
{
uint32_t wire;
int pos = 13;

    SelectSegment(which);

    wire = id & 0x07;
    wire = wire << 7;
    wire |= addr&0x7F;
    wire = wire << 4;
    wire |= data&0x0F;
    //printf("Write Command 0x%x\n", wire);
    while (pos >= 0)
    {
    uint32_t out;
        out = wire >> pos;
        ClockWrite(out &0x01);
        pos--;
    }
r
eturn 0;
}

(although we form up the id we don't need to do this, since it's always 0x5 for write)

And therefore using the DisplayInterface class we've defined to date we can do something like this:

DisplayInterface display;
...
    display.Enable(0);
    display.Enable(1);
    display.Enable(2);
    display.Enable(3);

    for (int j=0; j < 100; j++ )
    {

        for (int i =0 ; i < 64; i++)
        {
            display.Command(0, 0x5, i, 0x0f);
            display.Command(1, 0x5, i, 0x0f);
            display.Command(2, 0x5, i, 0x0f);
            display.Command(3, 0x5, i, 0x0f);
        }

        for (int i =0 ; i < 64; i++)
        {
            display.Command(0, 0x5, i, 0x00);
            display.Command(1, 0x5, i, 0x00);
            display.Command(2, 0x5, i, 0x00);
            display.Command(3, 0x5, i, 0x00);
        }
    }

Which turns all the LED's on and off again on all the panels in a (more or less) parallel sequence: