“Charlieplexing” is a way to drive more than one LED per output pin on ports/processors that can set their pins to input in addition to 0 and 1. It’s done by connecting two LEDs in opposite directions between any two pairs of pins from the ones used. Here, four are, so 1<->2, 1<->3, 1<->4, 2<->3, 2<->4, 3<->4 (six pairs). When using them, all pins are set to INPUT (they will not provide nor sink voltage or current) except two. Those final two are 1,0 or 0,1 – i.e. +5v and ground, lighting one of the two LEDs connected to it depending on direction.
In order to appear to have more than one lit, cycle through and quickly light each that should be lit in turn and do it at a fast enough rate to not be seen (50 Hz at least). Here’s brief example code, should work on most Atmega based Arduinos, perhaps others too (dunno about interrupts – it’s usually similar but it’s not standardized).
/* * Charlieplexer example * * Controls 12 LEDs using 4 output pins. This is done using combinations of 0/1/INPUT and the * diode behavior of LEDs to selectively power arbitrary single ones, then using a timer * interrupt to rapidly cycle through those that should appear lit. The array "bri" determines which should be lit. * * Circuit: * The four pins used are connected to D12-D8 (the leftmost four on an Arduino Nano). * Each is connected to a resistor. Two LEDs, in opposite directions, are then connected * between each possible two-pin selection, i.e. 1-2 1-3 1-4 2-3 2-4 3-4 * The current will pass through two resistors and one LED when on. If they're evenly lit * ("extra" time from off-LEDs isn't given to on-LEDs) the duty cycle will be ~8% (8% lit, 92% unlit). * The resistor should be chosen accordingly. 2.4kOhmn works, but is pretty dim. 240 Ohm would still be * well within current limits. * * Patrik Lundin, 2016 * * This example code is public domain */ // Number of LEDs #define PL 12 // "Frequency", but really delay per pre-divisor on the timer interrupt #define fq 100 // Display state, i.e. which LED should be lit (or not) this cycle int ds = 0; // Array holding on/ff values for LEDs. byte because I might implement partially lit sometime. //byte bri[] = { 0,0,0,0,0,0,0,0,0,0,0,0 }; byte bri[] = { 255,0,255,0,255,0,255,0,255,0,255,0 }; //byte bri[] = { 255,0,0,0,0,0,0,0,0,0,0,0 }; // Values to write to PORTB. Determines which pin is powered byte pv[] = { 0b00001000, 0b00000100, 0b00010000, 0b00001000, 0b00010000, 0b00000100, 0b00000010, 0b00000100, 0b00000010, 0b00001000, 0b00000010, 0b00010000, 0 }; // Values to write to DDRB for each LED. Determines High impedence (disconnect) pins. // The two selected will be 5v and GND as determined by pv byte ps[] = { 0b00001100, 0b00001100, 0b00011000, 0b00011000, 0b00010100, 0b00010100, 0b00000110, 0b00000110, 0b00001010, 0b00001010, 0b00010010, 0b00010010, 0 }; ISR(TIMER1_COMPA_vect){ byte ods; // check if current LED is lit, if so light it. if(bri[ds]) { PORTB = pv[ds]; DDRB = ps[ds]; } else { DDRB = 0; PORTB = 0; } // next LED (for next cycle). Back to 0 at PL if(ds==PL) ds = 0; else ds++; /* // This snipplet can replace the above to skip over any unlit LEDs. // This gives lit LEDs more time (brightness), but unevenly. ods = ds; do { if(ds==PL) ds = 0; else ds++; } while(!bri[ds] && ods!=ds); */ } void setup_int() { // Start timer interrupt cli(); TCCR1A = 0; TCCR1B = 0; OCR1A = fq; // Some approximate refresh speeds at an unknown prescaler setting: //156 400 hz 125 500 hz 100 625 hz //78 800 hz, 69 905 hz, 50 1250 Hz TCCR1B |= (1 << WGM12); // CTC mode |8 // Don't have my notes on the prescaler - uncommenting the bottom |= puts it at "very slow". TCCR1B |= (1 << CS12); // |4 //TCCR1B |= (1 << CS10); // Prescaler |1 TIMSK1 |= (1 << OCIE1A); // Enable timer compare sei(); } long sttime; // time storage for the main loop void setup() { DDRB = 0; PORTB = 0; Serial.begin(9600); Serial.println("Go"); setup_int(); sttime = millis(); } void loop() { int inc; byte ch; long now; // This isn't part of the charlieplexing - it's just a loop that blinks (inverts) all LEDs once per second // so it does at least something even by itself. now = millis(); if(now-sttime>1000) { // if it's been 1000 milliseconds blink sttime+=1000; for(inc=0;inc<=PL;inc++) { // Invert on/off (or opposite brightness if that was implemented) ch = bri[inc]; bri[inc] = 255 - ch; } } if(Serial.available()>0) { // Does nothing, but here you/I could add actions for chars gotten over serial (debug) inc = Serial.read(); switch(inc) { case 'l': break; default: break; } } }