It's no surprise: I like blinky lights. In fact, I can't see much need for an electronic project if it doesn't have LEDs in it!
That being said, there are times when I want / need to control more LEDs than I have available I/O lines for on my micro-controller. I've implemented my Binary Clock project using two I/O expander chips. These work great; you simply give them power, and I2C communications...and your two I2C lines can effectively become 16 I/O lines (per expander chip). However, I don't always have the real-estate on a PCB for a port expander, or, frankly, want to pay for the Port Expander if I can work around it. Fortunately, by combining the properties of LEDs (they only bias or "work" when powered in a certain direction) with the I/O functionality of modern M/Cs, I'm able to control individually many more LEDs than I have I/O lines for! This type of LED wiring is called, "Charlieplexing".
An LED only needs two wires...Power and Ground. When forward biased (turned on) the LED will light; when reverse-biased (turned off) the LED will not light. If we take two I/O lines from an M/C chip, and connect them to an LED, we can turn the LED "on" by setting the Anode-I/O "High" and the Cathode-I/O "Low". We could connect two LEDs in parallel to the same I/O lines...but turn the second LED around...so setting PIN 1 "High" and Pin 2 "Low" would turn on LED #1, but would reverse-bias LED #2 (not turn it on). We could then switch PIN 1 to be "Low" and PIN 2 to be "High" and LED #1 would go out (now being reverse-biased), and LED #2 would light.
We can take this further...since I/O pins on modern M/C chips have 3 states: Output High, Output Low, and High-Impedance "Input" mode. When an PIN is set to "input" it is neither high nor low...it acts like an open circuit.
So, using the three states of I/O pins (High, Low, Input) we can drive many more LEDs than PINS. The formula is: (n-squared) - n. So, with 3 I/O pins, we can drive (9) - 3 or 6 LEDs. With 20 I/O pins, we can drive ( 400 ) - 20 or 380 LEDs!
Attached is an example schematic of 4 I/O lines being used to drive ( 16 ) - 4 = 12 LEDs. Below the schematic is a "Truth Table" which shows all the pin settings necessary to drive the 12 LEDs individually.
Attached is a picture of an 8-Pin PIC Microcontroller (PIC 12F1840) driving 12 LEDs. The PIC is using one pin for the status LED (blinking at 1Hz) and 1 pin for the push-button input...leaving 4 pins to handle Charlieplexing. I have laid out the LEDs so they are numbered from the top, left to right...so upper-left is #1, upper-right is #3, lower-right is #12. This made the wiring a little easier.
This microcontroller is also fast enough (32Mhz) that it can turn on, and then off, an LED and then move onto another and turn it on-and-off...this happens fast enough that human eyes can't descern the power cycles, and to us, it appears more than 1 LED is on at a time. In fact, this circuit can light up all 12 LEDs at the "same time".
I'll bring this PCB in if you'd like to take a look. The Push-button will interrupt the various displays, and allow you to cycle through each LED individually...if you leave the circuit alone for 10 seconds, it'll go back to its random display.
If you have a project, and you're looking for a way to light up more LEDs (say for a status display) than you have I/O...or if you're just looking to save the I/O space, and you have room in your code...this might be something you'd like to play with.
Here is the C-Code for this project...I've tried to clean it up and make it readable...but let me know if you have any questions.
This was written in MPLAB X. A free IDE for working with Microchip microcontrollers.
Normally, I would have broken this up into multiple files...but for the ease of posting / sharing, I've included everything in one file.
Brendan
Code:
/*
* File: CharliePlexMain.c
* Author: brendan
*
* Created on September 15, 2015
*/
#define _XTAL_FREQ 32000000
#include <stdio.h>
#include <stdlib.h>
#include "serial.h"
#include <xc.h>
// #pragma config statements should precede project file includes.
// Since I'm using a bootloader; these config statemens don't matter...
// The config bytes are set in the bootloader...I have to really try
// to change the config settings...and it generally isn't necessary
// these are just FYI
// CONFIG1
#pragma config FOSC = INTOSC // Oscillator Selection (HS Oscillator, High-speed crystal/resonator connected between OSC1 and OSC2 pins)
#pragma config WDTE = OFF // Watchdog Timer Enable (WDT disabled)
#pragma config PWRTE = OFF // Power-up Timer Enable (PWRT disabled)
#pragma config MCLRE = OFF // MCLR Pin Function Select (MCLR/VPP pin function is digital input)
#pragma config CP = OFF // Flash Program Memory Code Protection (Program memory code protection is disabled)
#pragma config CPD = OFF // Data Memory Code Protection (Data memory code protection is disabled)
#pragma config BOREN = OFF // Brown-out Reset Enable (Brown-out Reset disabled)
#pragma config CLKOUTEN = OFF // Clock Out Enable (CLKOUT function is disabled. I/O or oscillator function on the CLKOUT pin)
#pragma config IESO = OFF // Internal/External Switchover (Internal/External Switchover mode is disabled)
#pragma config FCMEN = OFF // Fail-Safe Clock Monitor Enable (Fail-Safe Clock Monitor is disabled)
// CONFIG2
#pragma config WRT = OFF // Flash Memory Self-Write Protection (Write protection off)
#pragma config PLLEN = ON // PLL Enable (4x PLL enabled)
#pragma config STVREN = ON // Stack Overflow/Underflow Reset Enable (Stack Overflow or Underflow will cause a Reset)
#pragma config BORV = LO // Brown-out Reset Voltage Selection (Brown-out Reset Voltage (Vbor), low trip point selected.)
#pragma config LVP = ON // Low-Voltage Programming Enable (Low-voltage programming enabled)
#define LED LATAbits.LATA0 // P1B PWM output
#define PB PORTAbits.RA3 // Push Button
#define L1_set LATAbits.LATA1 // Line 1
#define L1_tris TRISAbits.TRISA1
#define L2_set LATAbits.LATA2 // Line 2
#define L2_tris TRISAbits.TRISA2
#define L3_set LATAbits.LATA4 // Line 3
#define L3_tris TRISAbits.TRISA4
#define L4_set LATAbits.LATA5 // Line 4
#define L4_tris TRISAbits.TRISA5
#define testbit(var, bit) ((var) & (1L <<(bit)))
#define setbit(var, bit) ((var) |= (1L << (bit)))
#define clrbit(var, bit) ((var) &= ~(1L << (bit)))
// Lay out where LEDs are in the array, so I can call them by color, row, column, etc.
#define red 0b001000000100
#define green 0b010000000010
#define blue 0b100000000001
#define yellow 0b000010101000
#define white 0b000101010000
#define row1 0b000000000111
#define row2 0b000000111000
#define row3 0b000111000000
#define row4 0b111000000000
#define column1 0b001001001001
#define column2 0b010010010010
#define column3 0b100100100100
// Declare my subroutines here
void setup();
void interrupt timeout();
void one();
void two();
void three();
void four();
void five();
void six();
void seven();
void eight();
void nine();
void ten();
void eleven();
void twelve();
void zero();
void manual();
void random();
void rows();
void columns();
void police();
void colors();
void cyclethru();
void allon();
// define my variables...these are global, can be used anywhere in the program
unsigned long millis; // keep track of elapsed milliseconds...50 days at 32-bit
unsigned int millis2, LEDs; // for LED blinking timeout
void main() {
// Set up the system
setup();
LEDs = 0; // All off...
// Do forever...
while (1){
cyclethru(); // One at a time, slow
allon(); // Light 'em all up
LEDs = 0; // All off
colors(); // Cycle through R G B Y W
LEDs = 0; // All off
police(); // Pull over!
rows(); // Rows 1 - 4
columns(); // Columns 1 - 3
random(); // Blinky!
} // End of While
} // End of Main
void setup(){
// Setup
// CPU Speed
OSCCON = 0b11110000; // Set up the oscillator for 32 MHZ 1, 1110, 0, 00
// Ports
TRISA=0b00111110; // set up 0 as output (This will change like crazy in the code...)
PORTA=0b00000000;
// Serial Disabled since using SEROUT line for LED
// SPBRG = 51; // 9600 baud @ 32MHz
// TXSTA = 0b10100010; // setup USART transmit
// RCSTA = 0b10010000; // Enable serial port, but no receive
// Interrupts
// INTCONbits.IOCIE = 1; // Enable interrupt on change
// IOCANbits.IOCAN3 = 1; // Enable interrupt on negative for RA3
// not using Interrupt on Change for the Push Button...because staying in that
// manual code prevents other interrupts from working.
// Timers...
// Add a 1 millisecond timer here to service stuff...
//Timer0 Registers Prescaler= 64 - TMR0 Preset = 131 - Freq = 1000.00 Hz - Period = 0.001000 seconds
OPTION_REGbits.T0CS = 0; // bit 5 TMR0 Clock Source Select bit...0 = Internal Clock (CLKO) 1 = Transition on T0CKI pin
OPTION_REGbits.T0SE = 0; // bit 4 TMR0 Source Edge Select bit 0 = low/high 1 = high/low
OPTION_REGbits.PSA = 0; // bit 3 Prescaler Assignment bit...0 = Prescaler is assigned to the Timer0
OPTION_REGbits.PS2 = 1; // bits 2-0 PS2:PS0: Prescaler Rate Select bits
OPTION_REGbits.PS1 = 0;
OPTION_REGbits.PS0 = 1;
TMR0 = 131; // preset for timer register
INTCONbits.TMR0IE = 1; // Enable timer 0 Interupt
INTCONbits.TMR0IF = 0; // clear Timer 0 flag...just in case
INTCONbits.GIE = 1; // Enable Global interrupts
// INTCONbits.PEIE = 1; // Enable periphial interrupts (is this needed for TMR 0? No, it's not)
// PWM Configuration
PR2 = 0xFF ; // Needs to be FF for 1.95kHz frequency at 32 MHz
T2CON = 0b00000110 ; // 7=0, 6-3 = Post scaler, 2=timer on, 1-0 = pre-scaler
CCP1CON = 0b00001100 ;
// PWM Enabled on P1B out but P1A is low...I don't want this...
// need to use PWM steering to steer over to P1B...that's the LED pin....
PSTR1CONbits.STR1SYNC = 0;
PSTR1CONbits.STR1B = 1; // Steer PWM to P1B
PSTR1CONbits.STR1A = 0; // Leave P1A as a port pin
} // end of setup
void interrupt timeout(){
if (INTCONbits.TMR0IF == 1) { // Tmr 0 has overflowed...millisecond
TMR0 = 131; // reset the preload
millis ++; // Increment milliseconds
millis2 ++; // Increment LED timer
// if (millis2 < 900) {
// LED = 0;
// }
// else if (millis2 < 1000){
// LED = 1;
// }
// else millis2 = 0;
// Heartbeat LED tasks
int dc = millis2 / 4; // fade up over a second
if (dc > 250){
dc = 500 - dc; // fade down over a second
}
CCPR1L = dc; // change PWM to dc value
if (millis2 == 2000){
millis2 = 0;
}
INTCONbits.TMR0IF = 0; // Clear the flag.
// check the LEDs bits for which lights to turn on...
if (LEDs == 0) {
zero();
return;
}
if testbit(LEDs,0) {
one();
__delay_us (70);
}
if testbit(LEDs,1) {
two();
__delay_us (70);
}
if testbit(LEDs,2) {
three();
__delay_us (70);
}
if testbit(LEDs,3) {
four();
__delay_us (70);
}
if testbit (LEDs,4) {
five();
__delay_us (70);
}
if testbit (LEDs,5) {
six();
__delay_us (70);
}
if testbit (LEDs,6) {
seven();
__delay_us (70);
}
if testbit (LEDs,7) {
eight();
__delay_us (70);
}
if testbit (LEDs,8) {
nine();
__delay_us (70);
}
if testbit (LEDs,9) {
ten();
__delay_us (70);
}
if testbit (LEDs,10) {
eleven();
__delay_us (70);
}
if testbit (LEDs,11) {
twelve();
__delay_us (70);
}
zero();
} // end of TMR0 tasks
// else if (IOCAFbits.IOCAF3 == 1){ // Button is pressed...
// __delay_ms(300);
// if (PB == 0){
// manual();
// }
// IOCANbits.IOCAN3 = 1; // reenable interrupts
// IOCAFbits.IOCAF3 = 0; // clear the flag
// } // End of Interrupt on change tasks...
} // End of interrupt
void one(){ // high 1 low 2
L3_tris = 1; // input
L4_tris = 1; // Input
L1_tris = 0; // output
L1_set = 1; // high
L2_tris = 0; // output
L2_set = 0; // Low
}
void two(){ // high 1 low 3
L3_tris = 0; // output
L4_tris = 1; // Input
L1_tris = 0; // output
L2_tris = 1; // input
L1_set = 1;
L3_set = 0;
}
void three(){ // high 2 low 4
L3_tris = 1; // input
L4_tris = 0; // out
L1_tris = 1; // input
L2_tris = 0; // output
L2_set = 1;
L4_set = 0;
}
void four(){ // low 1 high 2
L3_tris = 1; // input
L4_tris = 1; // Input
L1_tris = 0; // output
L2_tris = 0; // output
L1_set = 0;
L2_set = 1;
}
void five(){ // low 1 high 3
L3_tris = 0; // output
L4_tris = 1; // Input
L1_tris = 0; // output
L2_tris = 1; // input
L1_set = 0;
L3_set = 1;
}
void six(){ // low 2 high 4
L3_tris = 1; // input
L4_tris = 0; // output
L1_tris = 1; // input
L2_tris = 0; // output
L2_set = 0;
L4_set = 1;
}
void seven(){ // high 1 low 4
L3_tris = 1; // input
L4_tris = 0; // output
L1_tris = 0; // output
L2_tris = 1; // input
L1_set = 1;
L4_set = 0;
}
void eight(){ // high 2 low 3
L3_tris = 0; // output
L4_tris = 1; // Input
L1_tris = 1; // input
L2_tris = 0; // output
L2_set = 1;
L3_set = 0;
}
void nine(){ // high 3 low 4
L3_tris = 0; // output
L4_tris = 0; // output
L1_tris = 1; // input
L2_tris = 1; // input
L3_set = 1;
L4_set = 0;
}
void ten(){ // low 1 high 4
L3_tris = 1; // input
L4_tris = 0; // output
L1_tris = 0; // output
L2_tris = 1; // input
L1_set = 0;
L4_set = 1;
}
void eleven(){ // low 2 high 3
L3_tris = 0; // output
L4_tris = 1; // Input
L1_tris = 1; // input
L2_tris = 0; // output
L2_set = 0;
L3_set = 1;
}
void twelve(){ // low 3 high 4
L3_tris = 0; // output
L4_tris = 0; // output
L1_tris = 1; // input
L2_tris = 1; // input
L3_set = 0;
L4_set = 1;
}
void zero(){
L3_tris = 1; // input
L4_tris = 1; // Input
L1_tris = 1; // input
L2_tris = 1; // input
}
void manual (){
IOCANbits.IOCAN3 = 0; // disable interrupts
LEDs = 0;
unsigned long exitTime = millis + 10000;
while (millis < exitTime){
if (PB == 0){
__delay_ms(200);
if (PB == 0) {
if (LEDs == 0){
LEDs = 1;
}
else {
LEDs *= 2;
if (LEDs > 2048){
LEDs = 0;
}
}
exitTime = millis + 10000; // reset the countdown
}
}
}
}
void random(){
// Random
LEDs = 0;
for (int x = 0; x < 1000; x++){
unsigned int rnum = rand();
if (rnum % 12 == 0){
if testbit (LEDs, 11) {
clrbit (LEDs, 11);
}
else setbit (LEDs, 11);
}
else if (rnum % 11 == 0){
if testbit (LEDs, 10) {
clrbit (LEDs, 10);
}
else setbit (LEDs, 10);
}
else if (rnum % 10 == 0){
if testbit (LEDs, 9) {
clrbit (LEDs, 9);
}
else setbit (LEDs, 9);
}
else if (rnum % 9 == 0){
if testbit (LEDs, 8) {
clrbit (LEDs, 8);
}
else setbit (LEDs, 8);
}
else if (rnum % 8 == 0){
if testbit (LEDs, 7) {
clrbit (LEDs, 7);
}
else setbit (LEDs, 7);
}
else if (rnum % 7 == 0){
if testbit (LEDs, 6) {
clrbit (LEDs, 6);
}
else setbit (LEDs, 6);
}
else if (rnum % 6 == 0){
if testbit (LEDs, 5) {
clrbit (LEDs, 5);
}
else setbit (LEDs, 5);
}
else if (rnum % 5 == 0){
if testbit (LEDs, 4) {
clrbit (LEDs, 4);
}
else setbit (LEDs, 4);
}
else if (rnum % 4 == 0){
if testbit (LEDs, 3) {
clrbit (LEDs, 3);
}
else setbit (LEDs, 3);
}
else if (rnum % 3 == 0){
if testbit (LEDs, 2) {
clrbit (LEDs, 2);
}
else setbit (LEDs, 2);
}
else if (rnum % 2 == 0){
if testbit (LEDs, 1) {
clrbit (LEDs, 1);
}
else setbit (LEDs, 1);
}
else {
if testbit (LEDs, 0) {
clrbit (LEDs, 0);
}
else setbit (LEDs, 0);
}
__delay_ms(100);
if (PB == 0) manual();
}
}
void rows(){
// Rows
LEDs = 0;
LEDs = row1;
__delay_ms(500);
if (PB == 0) manual();
LEDs = 0;
__delay_ms(100);
LEDs = row2;
__delay_ms(500);
if (PB == 0) manual();
LEDs = 0;
__delay_ms(100);
LEDs = row3;
__delay_ms(500);
if (PB == 0) manual();
LEDs = 0;
__delay_ms(100);
LEDs = row4;
__delay_ms(500);
if (PB == 0) manual();
LEDs = 0;
}
void columns(){
// Columns
LEDs = column1;
__delay_ms(500);
if (PB == 0) manual();
LEDs = 0;
__delay_ms(100);
LEDs = column2;
__delay_ms(500);
if (PB == 0) manual();
LEDs = 0;
__delay_ms(100);
LEDs = column3;
__delay_ms(500);
if (PB == 0) manual();
}
void police(){
// Police
LEDs = 0;
for (int x = 0; x < 10; x++){
for (int y = 0; y < 6; y++){
LEDs = red | white;
__delay_ms(30);
LEDs = white;
__delay_ms(30);
if (PB == 0) manual();
}
for (int y = 0; y < 6; y++){
LEDs = yellow | blue;
__delay_ms(30);
LEDs &= yellow;
__delay_ms(30);
if (PB == 0) manual();
}
}
}
void colors(){
// Colors
LEDs = 0;
LEDs = red;
__delay_ms(500);
if (PB == 0) manual();
LEDs = blue;
__delay_ms(500);
if (PB == 0) manual();
LEDs = green;
__delay_ms(500);
if (PB == 0) manual();
LEDs = yellow;
__delay_ms(500);
if (PB == 0) manual();
LEDs = white;
__delay_ms(500);
if (PB == 0) manual();
}
void cyclethru () {
LEDs = 1;
while (LEDs <= 2048) {
__delay_ms(500);
LEDs *= 2;
if (PB == 0) manual();
}
}
void allon() {
unsigned long timeout = millis + 2000; // Another way to "delay"
while (millis < timeout){
LEDs = 0b111111111111; // All LEDs on...
}
}