Top‎ > ‎Atmel AVR stuff‎ > ‎

ARK LED MATRIX CONTROLLER.

April 2014 Update:
  I've added two pieces of code to the project.  First is arkd.c which is an X11 application which replicates the physical display on-screen and acts as a bridge between user applications and the controller.  arkd keeps the serial port open with the correct parameters and receives network messages to update the display.  Second is arkc.c, which is the network client for arkd.  Both are optional, the controller still works stand-alone as documented on this page.


I Wanted to build a LED matrix display of arbitrary length, one character high (8 pixels) that would be controllable with a simple interface and be cheap.  For the display I settled on standard multiplexed 8x8 single-color matrices, as I found cute ones at just $2 a piece.  An 8x8 matrix can be driven simply by using a MAX7219 chip but those are crazy expensive at over $10 a piece.  I want the end result to be 48x8 (6 matrices of 8x8) and I don't want to pay $60 just for the drivers.  Besides, the fun in this project is the do-it-yourself part.  So I instead opted for dirt cheap 74HC595 latched shift registers.

To control them I chose a cheap micro-controller whose job is to refresh the display and accept data from a computer.  I tried using a micro-controller with a UART but servicing the serial data produced little hiccups which affected the display.  I also tried USB with even worse results.  I ended up multiplexing the display refresh and a serial bit-banging receiving timer-driven interrupt routine so the display looks very smooth even while tons of data are being processed.

Here are videos, taken with my potato of a WebCam:

 

Video of the ARK LED MATRIX CONTROLLER first prototype.

 

Video of the second prototype showing clock mode 2.


Any blinking or quivering you see in the video are stroboscopic artifacts due to the crappy WebCam. In real life, the display is steady and smooth as silk.

I've written a webcam streaming utility that syncs with the display's scrolling to show how smooth it appears in person.  The video's fine, but not on youtube, so I've put it at the bottom of this page in the files section.

Only three 8x8 matrices are shown in the first breadboard prototype, as I plain ran out of jumper wires.  The second uses 6.  I'm pretty happy with the results so I don't know if I'm going to go higher.

The first demo is made with a simple bash script doing echo "Text and control characters" > /dev/ttyUSB0 statements.

The nice bit about this project is that it can be made with a very small micro-controller.  I chose an ATTINY84, but even an ATTINY85 (8 pins) would work fine.  It only needs 5 GPIO lines (2 for serial, 3 to control the display).

The serial data coming in is fed directly from a standard PC's DB9 connector.  Level conversion is bypassed completely with the current-limiting resistor, the internal over-voltage diodes at the GPIO pin and inversion is done in software.  Can't be simpler than that.  A few EE people will scream that you need level converters like the MAX232 to drive RS232, but these people are weak-kneed pansies.  0-5V is fine as all modern RS232 interfaces trip around +2 volts.

I'm still in the design phase.  Running at 20Mhz,  the µC is able to drive up to 100 8x8 matrices while receiving data at 19200 baud.  Of course, for large number of matrices the RCLK and SRCLK signals would need fanning-out.  A single 74HC04 every 18 matrices would do fine.  Every matrix with all pixels on at full brightness can consume up to 100 milliamperes.  Standard text at reasonable brightness levels is about 15ma.

There's still many tests and some software improvements to be done, but I'm quite satisfied.  I'll add more videos soon. 

Schematic:


Not shown: Bypass caps. This project requires a 0.1µF per chip and a 100µF cap for the power supply.  I also forgot to show the cathode signals going to the bottom module(s).  Use your imagination.  Also note that in reality the modules's pins aren't neatly ordered; too bad, it would have made for a much simpler build.

The firmware is fully documented and self-contained in one source file called "ark.c".  Originally I had hoped it would fit in 4 Kilo-bytes but I busted that by including a second font.  Since I've got a few kilo-bytes of room in 8K, I'll be adding more fonts and functionality as time goes by.

Incoming serial data is interpreted as ASCII text and is automatically scrolled when it hits either edge of the display.  The controller can receive data at full 19200 baud speed and loses no data even while the controller does slow actions like scrolling, by using hardware flow-control (Clear-to-Send).

Special functionality is achieved using control codes:

//  DEC OCT HEX ASC   BIN    CTL Function
//   1    1   1 SOH 00000001  A  Full reset.
//   2    2   2 STX 00000010  B  Shift text vertically.
//   3    3   3 ETX 00000011  C  Brightness up.
//   5    5   5 ENQ 00000101  E  Pause for 0.1 sec.
//   6    6   6 ACK 00000110  F  Scroll forward (right to left).
//   8   10   8 BS  00001000  H  Backspace.
//  10   12   A LF  00001010  J  Cursor reset @ left.
//  11   13   B VT  00001011  K  Brightness down.
//  12   14   C FF  00001100  L  Clear screen, cursor @ left.
//  13   15   C CR  00001101  M  Cursor reset @ left.
//  14   16   E SO  00001110  N  Next font.
//  15   17   F SI  00001111  O  Standard font.
//  16   20  10 DLE 00010000  P  Enter Plot mode (hex triplets).
//  17   21  11 DC1 00010001  Q  Exit Plot or Line mode.
//  18   22  12 DC2 00010010  R  Enter line mode (hex pairs, first sets cursor).
//  21   25  15 NAK 00010101  U  Scroll backward (left to right).
//  22   26  16 SYN 00010110  V  Non-reverse video.
//  23   27  17 ETB 00010111  W  Reverse video.
//  24   30  18 CAN 00011000  X  Faster scroll speed.
//  25   31  19 EM  00011001  Y  Slower scroll speed.
//  26   32  1A SUB 00011010  Z  Default values for all parameters.
//  28   34  1C FS  00011100  4  Scroll down.
//  29   35  1D GS  00011101  5  Scroll up.
//  30   36  1E RS  00011110  6  Scroll left.
//  31   37  1F US  00011111  7  Scroll right.

By the way, I consider the second prototype a failure in design, as I had originally planned on cutting the display modules' anodes short and replacing them with the resistors.  I stupidly placed the components on the board with that in mind but realized very late that the bit with the resistors wouldn't fly.  The leads were too short and wobbly.  I was therefore forced to place all 48 resistors on the back of the board, and had to connect them all with short bits of wire (otherwise they would be too stiff and all jumbled) and I had to heat-shrink every one of them twice to prevent shorts.  That ended up being a LOT of tedious work, but I'm patient...


There you can see the 48 resistors.  I haven't tried making them all ordered neatly.  It's the back of the board, so I don't care.  You can also see a cheap Radio Shack USB-to-RS232 adapter hot-glued to the bottom of the board, with its heat-shrinked splice to steal power from the USB bus.  The USB cable is the only wire required to operate the display.  The large 6-wire ribbon is only there for debugging and programming.  That'll go.

It's ugly but it works perfectly.

If I'm going to build a final, third version, it'll be larger but use a printed PCB.  We'll see.

Danny C.

Here's ark.c for your perusal:

/*


ARK.C : ARK LED MATRIX DISPLAY CONTROLLER
Copyright (C) 2013 Danny Chouinard

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

See the full text of the license in file COPYING.

To reach me, execute this command in a bourne shell:
$ echo qnaal.pubhvaneq@tznvy.pbz | tr "[a-z]" "[n-za-m]"

That's a rot13 of my e-mail address.


*/

#include <avr/io.h>
#include <avr/eeprom.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <avr/wdt.h>
#include "fonts.h"

// These are the only things that need be modified for different hardware
// configurations.

#define MODULES 6
#define BAUDRATE 19200                  // 1200-19200
#define BRIGHTNESSLEVELS 16             // 2-24

#define _SERPORT     A
#define _SERPIN      0
#define _SRCLKPORT   A
#define _SRCLKPIN    1
#define _RCLKPORT    A
#define _RCLKPIN     2
#define _RDPORT      A
#define _RDPIN       3
#define _CTSPORT     A
#define _CTSPIN      7

#define DFLTSPD    3
#define DFLTBRIGHT 2

// End of hardware parameters.
// You probably don't want to change anything past this line, except the
// 16-bit timer code in inithardware() for different MCUs.

#define CAT2(a,b)   CAT2EXP(a, b)
#define CAT2EXP(a,b)    a ## b
#define CAT3(a,b,c) CAT3EXP(a,b,c)
#define CAT3EXP(a,b,c)    a ## b ## c

#define SERPORT   CAT2(PORT,_SERPORT)
#define SERDDR    CAT2(DDR,_SERPORT)
#define SERPIN    CAT2(PIN,_SERPORT)
#define SERBIT    CAT3(P,_SERPORT,_SERPIN)

#define RCLKPORT  CAT2(PORT,_RCLKPORT)
#define RCLKDDR   CAT2(DDR,_RCLKPORT)
#define RCLKPIN   CAT2(PIN,_RCLKPORT)
#define RCLKBIT   CAT3(P,_RCLKPORT,_RCLKPIN)

#define SRCLKPORT CAT2(PORT,_SRCLKPORT)
#define SRCLKDDR  CAT2(DDR,_SRCLKPORT)
#define SRCLKPIN  CAT2(PIN,_SRCLKPORT)
#define SRCLKBIT  CAT3(P,_SRCLKPORT,_SRCLKPIN)

#define RDPORT    CAT2(PORT,_RDPORT)
#define RDDDR     CAT2(DDR,_RDPORT)
#define RDPIN     CAT2(PIN,_RDPORT)
#define RDBIT     CAT3(P,_RDPORT,_RDPIN)

#define CTSPORT   CAT2(PORT,_CTSPORT)
#define CTSDDR    CAT2(DDR,_CTSPORT)
#define CTSPIN    CAT2(PIN,_CTSPORT)
#define CTSBIT    CAT3(P,_CTSPORT,_CTSPIN)

#define HALF         RECBUFFERLEN/2
#define WIDTH        8*MODULES
#define RECBUFFERLEN 8   // Keep it small, otherwise sender overflows.

uint8_t cursor=0;
uint8_t fnt;
uint8_t fntw;
uint8_t fnth;
uint8_t fntmsk=0x7f;
uint8_t reversevideo;
uint8_t plotmode;
uint8_t linemode;
uint8_t direction;
uint8_t vshift;
uint8_t displayinitstring=1;
uint8_t initstringp=0;
uint8_t speed=DFLTSPD;
// static uint32_t rngk=1148543851;
// static uint32_t rngi=1234567891;

volatile uint8_t rec[RECBUFFERLEN];
volatile uint8_t rechead=0;
volatile uint8_t rectail=0;

volatile uint8_t display[WIDTH];
volatile uint8_t brightness;
volatile uint8_t blc=0;
volatile uint8_t bl[BRIGHTNESSLEVELS];
volatile uint8_t mod=0;
volatile uint8_t rs232inverter=0;

// Serial receiver and display controller.
// This seems rather large for an ISR, but any iteration runs in less than
// 130 cycles.
ISR(TIM1_COMPA_vect) {
  static uint8_t bits;
  static uint8_t rmode=0;
  static uint8_t bitwait=0;
  static uint8_t column=0;
  int8_t x,on,off;
  // Set Clear-to-Send (CTS) if buffer half-full.
  x=rechead;
  if(rechead<rectail) x+=RECBUFFERLEN;
  if((x-rectail)>HALF) {
    if(rs232inverter) CTSPORT|=(1<<CTSBIT);
    else CTSPORT&=~(1<<CTSBIT);
  } else {
    if(rs232inverter) CTSPORT&=~(1<<CTSBIT);
    else CTSPORT|=(1<<CTSBIT);
  }
  // RS232 data receiver.  bitwait is used as a countdown timer in-between
  // bits. rmode counts the bits.
  if(bitwait) {
    bitwait--;
  } else {
    if(rmode==0) {
      if((RDPIN&(1<<RDBIT))^rs232inverter) {
        rmode=1; bitwait=5; // Start bit detected, wait 1.25 bits.
      }
    } else {
      bitwait=3; bits>>=1;
      if((RDPIN&(1<<RDBIT))==0) bits|=128;
      rmode++;
      if(rmode>8) {
        x=rechead;
        if(++rechead==RECBUFFERLEN) rechead=0;
        if(rechead==rectail) {
          rechead=x;
        } else {
          rec[rechead]=bits; rmode=0;
        }
        bitwait=2; // Delay until in the stop bit.
      }
    }
  }
  //
  // Display controller.
  //
  // These three loop blocks have been unrolled by hand (X=7;X>=0;X--)
  // so they execute in fewer cycles.
  //
  // Each timer interrupt the 8 bits for one shift register is shifted in
  // serially in the register chain.  'mod' is the counter which keeps
  // track of which 595 we're shifting data to.  Each full cycle the data
  // for all the modules' pixels for one column of eight is shifted in
  // order.
  //
  // Once all column display bits have been shifted in, the 1-of-8 cathode
  // column selection bits are shifted in and all the registers'
  // RCLK is strobed.
  //
  // Thus, all display modules show one column until the next time RCLK is
  // strobed.  After 8 such column groups, the whole display has been flashed.
  //
  // Brightness is achieved by shifting all zeros instead of the display
  // bits depending on the counter 'blc' indexing the 'bl' array, which
  // defines which iterations should be on or off, as set per
  // setbright().
  //
  on=SERPORT|(1<<SERBIT);
  off=SERPORT&~(1<<SERBIT);
  if(mod<MODULES) {
    if(bl[blc]) {
      x=display[((MODULES-mod-1)<<3)+column];
      if(x&(1<<7)) SERPORT=on;
      else SERPORT=off;
      SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
      if(x&(1<<6)) SERPORT=on;
      else SERPORT=off;
      SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
      if(x&(1<<5)) SERPORT=on;
      else SERPORT=off;
      SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
      if(x&(1<<4)) SERPORT=on;
      else SERPORT=off;
      SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
      if(x&(1<<3)) SERPORT=on;
      else SERPORT=off;
      SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
      if(x&(1<<2)) SERPORT=on;
      else SERPORT=off;
      SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
      if(x&(1<<1)) SERPORT=on;
      else SERPORT=off;
      SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
      if(x&(1<<0)) SERPORT=on;
      else SERPORT=off;
      SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
    } else {
      SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
      SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
      SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
      SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
      SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
      SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
      SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
      SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
    }
  }
  if(mod==MODULES) {
    if(column==7) SERPORT=on;
    else SERPORT=off;
    SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
    if(column==6) SERPORT=on;
    else SERPORT=off;
    SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
    if(column==5) SERPORT=on;
    else SERPORT=off;
    SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
    if(column==4) SERPORT=on;
    else SERPORT=off;
    SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
    if(column==3) SERPORT=on;
    else SERPORT=off;
    SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
    if(column==2) SERPORT=on;
    else SERPORT=off;
    SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
    if(column==1) SERPORT=on;
    else SERPORT=off;
    SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
    if(column==0) SERPORT=on;
    else SERPORT=off;
    SRCLKPIN=(1<<SRCLKBIT); SRCLKPIN=(1<<SRCLKBIT);
    RCLKPIN=(1<<RCLKBIT); RCLKPIN=(1<<RCLKBIT);
    SERPORT&=~(1<<SERBIT);
    mod=0-1; column++;
    if(column==8) {
      column=0;
      if(++blc==BRIGHTNESSLEVELS) blc=0;
    }
  }
  mod++;
}

uint8_t eeread(uint16_t p) {
  return(eeprom_read_byte((uint8_t *)p));
}

void eewrite(uint16_t p, uint8_t data) {
  uint8_t oclkpr;
  cli();
  oclkpr=CLKPR;
  CLKPR=0x80; CLKPR=2;
  eeprom_write_byte((uint8_t *)p,data);
  eeprom_busy_wait();
  CLKPR=0x80; CLKPR=oclkpr;
  sei();
}

uint32_t eeread32(uint16_t p) {
  uint32_t r;
  r=((uint32_t) eeread(p)<<24);
  r=r|((uint32_t) eeread(p+1)<<16);
  r=r|((uint32_t) eeread(p+2)<<8);
  r=r|((uint32_t) eeread(p+3)&0xff);
  return(r);
}

void eewrite32(uint16_t p, uint32_t v) {
  eewrite(p,(v>>24)&0xff);
  eewrite(p+1,(v>>16)&0xff);
  eewrite(p+2,(v>>8)&0xff);
  eewrite(p+3,v&0xff);
}

//  Not used right now.
//// Call with zero arg to get 16 random bits.
//// Call four times with non-zero arg to seed.
//// Period is greater than one quintillion.
//// This function must NOT be used with serious real-money applications as it
//// is predictable.
//uint16_t rng16(uint16_t seed) {
//  if(rngk==0) rngk++;
//  if(rngi==0) rngi++;
//  switch(seed) {
//    case 0:
//      rngk=30903*(rngk&65535)+(rngk>>16);
//      rngi=31083*(rngi&65535)+(rngi>>16);
//      return(rngk+rngi);
//      break;
//    default:
//      rngi=(rngi<<16)+((rngk<<16)>>16);
//      rngk=(rngk<<16)+seed;
//      return(0);
//      break;
//  }
//}

// Gobble up one byte of serial data if any.  Zero if not.
uint8_t recbyte(void) {
  if(rectail==rechead) return(0);
  if(++rectail==RECBUFFERLEN) rectail=0;
  return(rec[rectail]);
}

// Wait until whole display is refreshed frames times.
void vsync(uint8_t frames) {
  while(frames--) {
    while(blc);
    while(!blc);
  }
}

// This routine sets an array (bl) with zeros or ones (wasteful but fast) which
// the display controller uses to flash the display on & off in order to
// simulate PWM.  However instead of simply doing 11110000, it
// spreads the display on/off times evenly according to brightness level.
// So instead of 11110000 you get 10101010.  Same brightness, higher
// frequency, less perceived blinking.
//
// Bash equivalent demonstration:
//
// levels=16  # Works cleanly up to 24
// for ((l=0;l<=levels;l++)); do
//   for ((i=0;i<levels;i++)); do bl[i]=" " ; done
//   j=128
//   if [ $l != 0 ]; then
//     while [ $j -lt $((256*levels)) ]; do
//       bl[j/256]="O";
//       j=$((j+256*levels/l))
//     done
//   fi;
//   for ((x=0;x<levels;x++)); do
//     echo -n "${bl[x]}"
//   done
//   printf "   %2d\n" $l
// done
//                     0
// O                   1
// O       O           2
// O    O     O        3
// O   O   O   O       4
// O  O  O   O  O      5
// O  O O  O  O O      6
// O O  O O O O  O     7
// O O O O O O O O     8
// O O OO O O OO O     9
// O OO OO O OO OO    10
// OO OO OO OO OO O   11
// OO OOO OOO OOO O   12
// OOO OOOO OOOO OO   13
// OOOO OOOOOOO OOO   14
// OOOOOOOO OOOOOOO   15
// OOOOOOOOOOOOOOOO   16
//
// Makes an interesting picture, doesn't it?
//
void setbright(uint8_t l) {
  uint8_t i;
  uint16_t j;
  while(!blc);
  while(blc);
  for(i=0;i<BRIGHTNESSLEVELS;i++) {
    bl[i]=0;
  }
  j=128;
  if(l) {
    while(j<(256*BRIGHTNESSLEVELS)) {
      bl[j>>8]=1;
      j+=256*BRIGHTNESSLEVELS/l;
    }
  }
}

void scrollup(void) {
  uint8_t i;
  for(i=0;i<WIDTH-1;i++) {
    display[i]=(display[i]>>1)|(reversevideo&128);
  }
}

void scrolldown(void) {
  uint8_t i;
  for(i=0;i<WIDTH-1;i++) {
    display[i]=(display[i]<<1)|(reversevideo&1);
  }
}

void scrollleft(void) {
  uint8_t i;
  for(i=0;i<WIDTH-1;i++) {
    display[i]=display[i+1];
  }
  display[i]=reversevideo;
}

void scrollright(void) {
  uint8_t i;
  for(i=WIDTH-1;i>0;i--) {
    display[i]=display[i-1];
  }
  display[i]=reversevideo;
}

// Print one line of pixels, at cursor position, scroll if @ end.
// Since pline is used to print any text, vshift processing is done here.
void pline(uint8_t l) {
  uint8_t i,shiftedmsk;
  shiftedmsk=fntmsk;
  for(i=0;i<(vshift&7);i++) {
    shiftedmsk=((shiftedmsk&128)>>7)|(shiftedmsk<<1);
    l=((l&128)>>7)|(l<<1);
  }
  if(direction) {
    if(cursor==0) {
      vsync(speed);
      scrollright();
      cursor++;
    }
    display[cursor]&=~shiftedmsk;
    display[cursor]|=l^reversevideo;
    cursor--;
  } else {
    if(cursor>=WIDTH) {
      vsync(speed);
      scrollleft();
      cursor--;
    }
    display[cursor]&=~shiftedmsk;
    display[cursor]|=l^reversevideo;
    cursor++;
  }
}

// Display ASCII byte, animate if necessary.
// There's a lot of repeats here due to fonts not having the same
// character sets.  Must do a bit of thinking for the next version.
// For now, this works fine.
uint8_t marquee(uint8_t b) {
  int8_t i;
  if(b<32 || b>127) return(0);
  if(fnt==0) {
    if(direction) {
      for(i=4;i>=0;i--) {
        pline(pgm_read_byte(&font0[b-32][i]));
      }
    } else {
      for(i=0;i<5;i++) {
        pline(pgm_read_byte(&font0[b-32][i]));
      }
    }
    pline(0);
  }
  if(fnt==2) {
    if(b>='a' && b<='l') b=b-'a'+'A';
    if (b>='+' && b<='L') {
      if(direction) {
        for(i=2;i>=0;i--) {
          pline(pgm_read_byte(&font2[b-'+'][i]));
        }
      } else {
        for(i=0;i<3;i++) {
          pline(pgm_read_byte(&font2[b-'+'][i]));
        }
      }
    } else {
      pline(0);
      pline(0);
      pline(0);
    }
    pline(0);
  }
  if(fnt==3) {
    if(b>='a' && b<='z') b=b-'a'+'A';
    if (b>='+' && b<='z') {
      if(direction) {
        for(i=2;i>=0;i--) {
          pline(pgm_read_byte(&font3[b-'+'][i]));
        }
      } else {
        for(i=0;i<3;i++) {
          pline(pgm_read_byte(&font3[b-'+'][i]));
        }
      }
    } else {
      pline(0);
      pline(0);
      pline(0);
    }
    pline(0);
  }
  if(fnt==1) {
    if(direction) {
      for(i=7;i>=0;i--) {
        pline(pgm_read_byte(&font1[b-32][i]));
      }
    } else {
      for(i=0;i<8;i++) {
        pline(pgm_read_byte(&font1[b-32][i]));
      }
    }
  }
  return(0);
}

// Clear whole screen, set cursor at extreme left.
void cls(void) {
  uint8_t i;
  for(i=0;i<WIDTH;i++) display[i]=reversevideo;
  cursor=0;
}

// Pause for 1/10th of a second.
void pause(uint16_t l) {
  uint16_t i,j;
  uint8_t c;
  c=mod;
  for(i=0;i<l;i++) {
    for(j=BAUDRATE/10*8;j;j--) {
      while(c==mod);             // mod changes every interrupt.
      c=mod;
    }
  }
}

// Get a byte of data from the initial string stored in EEPROM,
// start over if FF or end of EEPROM found.
uint8_t getinitstring(void) {
  uint8_t r;
  r=eeread(initstringp);
  if(r==0xff) {
    r=0;
    initstringp=0;
  }
  if(++initstringp>=E2END) initstringp=0;
  return(r);
}

// Set font geometries according to number.  Should stuff this data in fonts.h
// Oh well, next version.
void setfnt(uint8_t f) {
  fntmsk=0x7f;
  fntw=5;
  fnth=7;
  if(f==1) {
    fntmsk=0xff;
    fntw=7;
    fnth=8;
  }
  if(f==2) {
    fntmsk=0x1f;
    fnth=5;
    fntw=3;
  }
  if(f==3) {
    fntmsk=0x07;
    fntw=3;
    fnth=3;
  }
  fnt=f;
  vshift=0;
}

void resetdefaults(void) {
  vshift=direction=fnt=cursor=plotmode=linemode=reversevideo=0;
  speed=DFLTSPD;
  setfnt(fnt);
  // setbright(brightness=DFLTBRIGHT);
}

// Prevent wdt_reset() from being called long enough for the watchdog to
// reset the MCU.  Comment this and other watchdog code in inithardware() if
// your MCU doesn't have a watchdog or you don't want to use it.
void fullreset(void) {
  while(1);
}

void backspace(void) {
  uint8_t w;
  w=fntw+1;
  if(direction) {
    if(cursor<WIDTH-w) cursor+=w;
  } else {
    if(cursor>=w) cursor-=w;
  }
}

// Process serial data regarding line and plot modes.
// Pass-through otherwise.  Also has a repeat.  Should clean. DCCD.
uint8_t dolineplot(uint8_t b) {
  static uint8_t bynibbles=0;
  static uint8_t x=0;
  uint8_t oldvshift;
  if(!b) return(0);
  if(plotmode) {
    if(b>='a' && b<='f') b=b-'a'+'A';
    if((b>='0' && b<='9') || (b>='A' && b<='F')) {
      if(b>='A') b=b-'A'+':';
      bynibbles=(bynibbles<<4)|(b&15);
      if(plotmode==2) x=bynibbles;
      if(plotmode==3) {
        if(x<WIDTH) {
          if(bynibbles&8) display[x]&=~(1<<(bynibbles&7));
          else display[x]|=(1<<(bynibbles&7));
        }
        plotmode=0;
      }
      b=0;
      plotmode++;
    }
  }
  if(linemode) {
    if(b>='a' && b<='f') b=b-'a'+'A';
    if((b>='0' && b<='9') || (b>='A' && b<='F')) {
      if(b>='A') b=b-'A'+':';
      bynibbles=(bynibbles<<4)|(b&15);
      if(linemode==2) {
        if(bynibbles<WIDTH) cursor=bynibbles;
      }
      if(linemode==4) {
        x=fntmsk; fntmsk=0xff; oldvshift=vshift; vshift=0;
        pline(bynibbles);
        fntmsk=x; vshift=oldvshift;
        linemode=2;
      }
      b=0;
      linemode++;
    }
  }
  return(b);
}

// Act upon possible control codes.
// This is subject to replacement with switch().
// Reserved for future upgrades: Escape (27 ESC).
uint8_t docontrols(uint8_t b) {
  if(!b) return(0);
  if(b==1) fullreset();
  if(b==2) vshift++;
  if(b==3 && brightness<=BRIGHTNESSLEVELS) setbright(++brightness);
  if(b==5) pause(1);
  if(b==6) direction=0;
  if(b==8) backspace();
  if(b==10 || b==13) cursor=0;
  if(b==11 && brightness>0) setbright(--brightness);
  if(b==12) cls();
  if(b==14 && fnt<3) setfnt(++fnt);
  if(b==15) setfnt(0);
  if(b==16) { plotmode=1; linemode=0; }
  if(b==17) plotmode=linemode=0;
  if(b==18) { linemode=1; plotmode=0; }
  if(b==21) direction=1;
  if(b==22) reversevideo=0;
  if(b==23) reversevideo=0xff;
  if(b==24 && speed>0) speed--;
  if(b==25) speed++;
  if(b==26) resetdefaults();
  if(b==28) scrolldown();
  if(b==29) scrollup();
  if(b==30) scrollleft();
  if(b==31) scrollright();
  return(b);
}

void inithardware(void) {
  uint8_t i;
  CLKPR=0x80; CLKPR=0;                    // Full speed please.
  MCUSR&=~(1<<WDRF);                      // Clear WatchDog Reset Flag.
  WDTCSR|=(1<<WDCE);                      // WatchDog Change Enable.
  WDTCSR=0;                               // WatchDog Fully disabled.
  RDPORT|=(1<<RDBIT);                     // Pull-up on RD in case of open.
  for(i=0;i<254;i++) {
    __asm__ ("nop\nnop\nnop\nnop\n");     // Let RD settle.
  }
  if(RDPIN&(1<<RDBIT)) {
    rs232inverter=(1<<RDBIT);             // Do we need to invert?
  }
  SERDDR|=(1<<SERBIT);                    // First 595's serial input
  RCLKDDR|=(1<<RCLKBIT);                  // All 595s' Storage Register clocks.
  SRCLKDDR|=(1<<SRCLKBIT);                // All 595s' Shift register clocks.
  CTSDDR|=(1<<CTSBIT);                    // Clear to Send.
  //
  // These depend on the MCU used.  Basically, you want an interrupt that's
  // triggered every F_CPU/BAUDRATE/4 cycles.   You might have to change
  // the ISR vector as well.
  //
  TCCR1B|=(1<<WGM12)|(1<<CS10);           // CTC, CLKI/O.
  OCR1A=F_CPU/BAUDRATE/4-1;               // 4 X Baud rate.
  TIMSK1|=(1<<OCIE1A); sei();             // Start interrupts.
  //
  //
  wdt_enable(WDTO_1S);                    // Set watchdog to one second.
}

int main(void) {
  uint8_t showinit=1;
  uint8_t b;
  inithardware();
  resetdefaults(); setbright(brightness=DFLTBRIGHT);
  while(1) {                              // Main loop.
    wdt_reset();                          // We're alive.
    b=recbyte();                          // Get serial data.
    if(b!=0 && showinit!=0) {
      showinit=0;
      resetdefaults();
      cls();
    }
    if(showinit) b=getinitstring();       // Show initial string.
    b=dolineplot(b);                      // Line or plot mode.
    b=docontrols(b);                      // Interpret CTRL chars.
    marquee(b);                           // Show printable character.
  }
}

/*
 *
 * Notes:
 *
 * The frequency at which the interrupt routine runs determines the baud
 * rate and how fast the display is refreshed.  Obviously, targetting the
 * right value for the baud rate is preferable, as long as the display doesn't
 * flicker.
 *
 * The display is refreshed one column per 8x8 matrix per iteration at a time,
 * plus an extra iteration for the cathode selector shift register.
 * Furthermore, 16 (BRIGHTNESSLEVELS) of those full refresh cycles are
 * either lit or not depending on the brightness value.
 *
 * The serial receiver has to run every 1/4th bit so the desired
 * interrupt frequency has to be four times the baud rate.
 * For example, a baute rate of 19200 requires an interrupt that runs about
 * every 260 cycles at 20Mhz; that's 76800 times per second.
 * I havent't counted cycles but I've eye-balled it with an oscilloscope
 * and it looks like the micro spends about 40% of its time in interrupt at
 * full brightness (less when dimmer).
 *
 * 19200 baud is therefore the reasonable upper limit, but that's plenty.
 *
 * Still, all of that makes for a matrix controller that can receive data
 * at speeds and can do so while displaying a marquee perfectly and smoothly.
 *
 * If you want to use lower clock rates you may have to consider reducing
 * the BAUDRATE and BRIGHTNESSLEVELS to reduce how much time is spent in
 * interrupt.
 *
 * If you want to use a lot of 8x8 matrices, you'll have to buffer or
 * fan-out the RCLK and SRCLK signals.  Using a baud rate of 2400 and
 * only 4 brightness levels, up to 100 matrices can be driven.
 * That would make an 800x8 pixel display.  I'll be in my bunk.
 *
 * You also must consider how much current you're drawing.  In the worst
 * case scenario (full brightness and all pixels lit), 8 LEDs per module
 * are lit, each drawing about 14 milliamps when using 220 ohm resistors.
 * That's about one amp for 7 modules.  Each mosfet has to be able to
 * handle that much 1/8th of the time.  I'm using ZVNL110A mosfets in a
 * six-module display, they're rated at 320ma but 6A pulsed.  They're
 * running absolutely cool.
 *
 *
 * Line mode (CTRL-R) must be followed by pairs of hexadecimal characters
 * (0-9, a-f or A-F).  The first pair is used to set the cursor position, any
 * invalid values (like FF) don't set the cursor.
 * Following pairs are used to print a single line of pixels at the cursor
 * position.  When at the right end of the screen, the whole display scrolls
 * one line of pixels.
 *
 * Line mode is terminated by CTRL-Q.
 *
 * For example, CTRL-R, 0, 0, F, F, CTRL-Q prints a full line of 8 pixels
 * at the first column.  CTRL-R, FFAA55AA55AA5500, CTRL-Q prints a neat
 * grey block at the current cursor position.
 *
 * Line mode can also be used to only set the cursor position by doing
 * CTRL-R, X, X, CTRL-Q, where X and X form the hexadecimal value.
 *
 * Plot mode (CTRL-P) must be followed by triplets of hexadecimal characters
 * (0-9, a-f or A-F).  The first two define which column is selected, and the
 * last character defines which row pixel is plotted (0-7) or reset (8-F).
 *
 * Plot mode is also terminated by CTRL-Q.
 *
 *
 * On start-up, the controller will animate the contents of the eeprom
 * repeatedly as if the byte data were received from serial input.  The
 * first 0xFF byte ends the string.  This stops as soon as a real serial
 * printable byte is received.
 *
 *
 * Danny Chouinard, January 2013.
 *
 */

// Control codes:
//
//  DEC OCT HEX ASC   BIN    CTL Function
//   1    1   1 SOH 00000001  A  Full reset.
//   2    2   2 STX 00000010  B  Shift text vertically.
//   3    3   3 ETX 00000011  C  Brightness up.
//   5    5   5 ENQ 00000101  E  Pause for 0.1 sec.
//   6    6   6 ACK 00000110  F  Scroll forward (right to left).
//   8   10   8 BS  00001000  H  Backspace.
//  10   12   A LF  00001010  J  Cursor reset @ left.
//  11   13   B VT  00001011  K  Brightness down.
//  12   14   C FF  00001100  L  Clear screen, cursor @ left.
//  13   15   C CR  00001101  M  Cursor reset @ left.
//  14   16   E SO  00001110  N  Next font.
//  15   17   F SI  00001111  O  Standard font.
//  16   20  10 DLE 00010000  P  Enter Plot mode (hex triplets).
//  17   21  11 DC1 00010001  Q  Exit Plot or Line mode.
//  18   22  12 DC2 00010010  R  Enter line mode (hex pairs, first sets cursor).
//  21   25  15 NAK 00010101  U  Scroll backward (left to right).
//  22   26  16 SYN 00010110  V  Non-reverse video.
//  23   27  17 ETB 00010111  W  Reverse video.
//  24   30  18 CAN 00011000  X  Faster scroll speed.
//  25   31  19 EM  00011001  Y  Slower scroll speed.
//  26   32  1A SUB 00011010  Z  Default values for all parameters.
//  28   34  1C FS  00011100  4  Scroll down.
//  29   35  1D GS  00011101  5  Scroll up.
//  30   36  1E RS  00011110  6  Scroll left.
//  31   37  1F US  00011111  7  Scroll right.
//

ċ
ark.tgz
(105k)
Danny Chouinard,
Apr 14, 2014, 4:51 AM
ċ
ark.zip
(104k)
Danny Chouinard,
Apr 14, 2014, 4:51 AM
č
synched.mpg
(12366k)
Danny Chouinard,
Jan 21, 2013, 10:35 PM
Comments