Continuing with ATtiny85, today I’d like to share how to build a music spectrum analyzer on 16×20 RGB led matrix. The music signal FFT transformation and LED Bit Angle Modulation are all carried out by one DigiSpark ATtiny85.
Please watch my video below:
Step 1: Things We Need
Main components are as follows:
- 1 x DigiSpark ATtiny85.
- 320 x Common Anode RGB LEDs.
- 16 x A1013 Transistors.
- 2 x Shift Register 74HC595N.
- 9 x Power Logic 8-Bit Shift Register TPIC6B595N.
- 12 x 0.1uF Decoupling Capacitors.
- 100 x 100Ω Resistors.
- 16 x 1kΩ Resistors.
- 4 x 10kΩ Resistors.
- 1 x Single-Side Copper Prototype PCB Size A4.
- 2 x Clear Acrylic Plate Size A4.
- 2 x Male & Female 40pin 2.54mm Header.
- 1 x Power Supply Adapter 5V/2A.
- 1 x DC Power Supply Female Socket.
- 1 x DC Power Supply Screw Type.
- 8 x Copper Standoff Spacers 20mm.
- 1 x 3.5mm Audio Jack.
- 2 meter x Rainbow Color Flat Ribbon Cable.
Step 2: Schematic
You can download the project schematic in PDF format HERE.
The components for controlling a RGB led matrix 16×20:
- Columns (cathodes) scanning: including 3 groups of TPIC6B595N to control 20 columns, each group includes 3 x TPIC6B595N for 3 colors (Red, Green & Blue), for example with blue color:
- Rows (anodes) scanning: including 2 x 74HC595N and 16 x A1013 transistor to control 16 rows.
Step 3: Soldering and Arrangement
Firstly, I soldered 320 RGB leds on the PCB, from outer edge of the PCB I left about 15~16 holes to reserve space for control components.
As picture above, I soldered all cathode pins in same column (total 20 columns – cathode with R, G, B pin) together after aligning leds on the top side.
After soldering 20 led columns, I soldered anode led pins in same row ( total 16 rows – anode) together by bending anode led pins so that there is a gap between the rows and columns, avoiding them touching each other.
The 16×20 RGB led matrix has been done!
Then around this led matrix, I soldered all row (2 x 74HC595N + 16 x A1013) and column (9 x TPIC6B595N) scanning components, female header for ATtiny85, 3.5mm audio jack and power supply socket.
I didn’t use wires in this project but instead I used the led pins which were left from many led related projects.
I want all components and connections to be seen, so I covered the top and botom of PCB by clear arcylic plates.
Done!!! And I can put it on my tea table for decoration.
If you have a PCB project, please visit the NEXTPCB website to get exciting discounts and coupons.
- Only $0 for 1-4 layer PCB Prototype: https://www.nextpcb.com/pcb-quote?act=2&code=tunen…
- New customer get $100 coupons, register at: https://www.nextpcb.com/register?code=tunendd
Step 4: Programing and How It Works
The project code is as below:
/* Tested with Arduino 1.8.13, the ATTinyCore and libraries: https://github.com/SpenceKonde/ATTinyCore https://github.com/JChristensen/tinySPI https://github.com/kosme/fix_fft Connections: - DigiSpark ATtiny85 P0 (PB0) - BLANK PIN OF 74HC595 & TPIC6B595. - DigiSpark ATtiny85 P1 (PB1) - DATA PIN OF TPIC6B595 - DigiSpark ATtiny85 P2 (PB2) - CLOCK PIN OF 74HC595 & TPIC6B595. - DigiSpark ATtiny85 P3 (PB3) - LATCH PIN OF TPIC6B595 & 74HC595 - DigiSpark ATtiny85 P4 (PB4) - AUDIO PIN. */ #include <tinySPI.h> // https://github.com/JChristensen/tinySPI #include "fix_fft.h" // https://github.com/kosme/fix_fft #define HARDWARE_SPI 1 // Set to 1 to use hardware SPI, set to 0 to use software SPI #define BAM_RESOLUTION 2 // Define Bit Angle Modulation BAM resolution, // Digispark Attiny85 pin definitions const int DATA_PIN(1), // Serial data in (Data pin) CLOCK_PIN(2), // Shift register clock (Clock pin) LATCH_PIN(3), // Storage register clock (Latch pin) BLANK_PIN(0); // Output enable pin (Blank pin) //AUDIO_PIN(4); // Input audio pin (Audio pin) //**************************************************BAM Variables**********************************************************// byte red[BAM_RESOLUTION][48]; byte green[BAM_RESOLUTION][48]; byte blue[BAM_RESOLUTION][48]; // Anode low and high byte for shifting out. byte anode[16][2]= {{B11111110, B11111111}, {B11111101, B11111111}, {B11111011, B11111111}, {B11110111, B11111111}, {B11101111, B11111111}, {B11011111, B11111111}, {B10111111, B11111111}, {B01111111, B11111111}, {B11111111, B11111110}, {B11111111, B11111101}, {B11111111, B11111011}, {B11111111, B11110111}, {B11111111, B11101111}, {B11111111, B11011111}, {B11111111, B10111111}, {B11111111, B01111111}}; int row; int level; int BAM_Bit, BAM_Counter=0; // Colorwheel array for spectrum analyzer byte colorwheels[20][3]={{0, 0, 3}, {0, 0, 3}, {1, 0, 3}, {2, 0, 3}, {3, 0, 3}, {3, 0, 2}, {3, 0, 1}, {3, 0, 0}, {3, 1, 0}, {3, 2, 0}, {3, 3, 0}, {2, 3, 0}, {1, 3, 0}, {0, 3, 0}, {0, 3, 1}, {0, 3, 2}, {0, 3, 3}, {0, 2, 3}, {0, 1, 3}, {0, 0, 3}}; //****************************************************Fix_FFT Variables********************************************************// int8_t data[32]; unsigned long useconds; int sum_data; //************************************************************************************************************// void setup() { row = 0; level = 0; #if HARDWARE_SPI == 1 SPI.begin(); // Start hardware SPI. #else pinMode(CLOCK_PIN, OUTPUT); // Set up the pins for software SPI pinMode(DATA_PIN, OUTPUT); #endif // Set up the pins for software SPI pinMode(LATCH_PIN, OUTPUT); digitalWrite(LATCH_PIN, HIGH); noInterrupts(); // Clear registers TCNT1 = 0; TCCR1 = 0; // Reset to $00 in the CPU clock cycle after a compare match with OCR1C register value // 50 x 3.636 = 181.8us // If software SPI is used, this value should be increased. OCR1C = 50; // A compare match does only occur if Timer/Counter1 counts to the OCR1A value OCR1A = OCR1C; // Clear Timer/Counter on Compare Match A TCCR1 |= (1 << CTC1); // Prescaler 64 - 16.5MHz/64 = 275Kz or 3,636us TCCR1 |= (1 << CS12) | (1 << CS11) | (1 << CS10); // Output Compare Match A Interrupt Enable TIMSK |= (1 << OCIE1A); interrupts(); clearfast(); // ADC Control and Status Register ADCSRA &= ~((1 << ADPS0) | (1 << ADPS1) | (1 << ADPS2)); ADCSRA |= ((1 << ADPS2) |(1 << ADPS0)); } void loop() { sum_data = 0; for (int i = 0; i < 32; i++) { useconds = micros(); data[i] = ((analogRead(A2)) >> 2) - 128; // DigiSpark ATtiny85 analog pin A2 at PB4 sum_data += data[i]; while (micros() < (useconds + 100)) { } } for (int i = 0; i < 32; i++) { data[i] -= sum_data/32; } fix_fftr(data, 5, 0); for(int j = 0; j < 16; j++) { data[j] = 4*((float)data[2*j]+ (float)data[2*j+1]); // Everage & scale 2 bars together to get 16 bars } for (byte xx=0; xx<20; xx++) { for (byte yy=0; yy < 16; yy++) { if (xx > data[yy]) { LED(19-xx, yy, 0, 0, 0); } else { LED(19-xx, yy, colorwheels[xx][0], colorwheels[xx][1], colorwheels[xx][2]); // Colorwheel spectrum bars } } } } void LED(int X, int Y, int R, int G, int B) { X = constrain(X, 0, 19); Y = constrain(Y, 0, 15); R = constrain(R, 0, 3); G = constrain(G, 0, 3); B = constrain(B, 0, 3); int WhichByte = int(Y*3+ X/8); int WhichBit = (X%8); for (byte BAM = 0; BAM < BAM_RESOLUTION; BAM++) { bitWrite(green[BAM][WhichByte], WhichBit, bitRead(G, BAM)); bitWrite(red[BAM][WhichByte], WhichBit, bitRead(R, BAM)); bitWrite(blue[BAM][WhichByte], WhichBit, bitRead(B, BAM)); } } void clearfast () { memset(green, 0, sizeof(green[0][0]) * BAM_RESOLUTION * 48); memset(red, 0, sizeof(red[0][0]) * BAM_RESOLUTION * 48); memset(blue, 0, sizeof(blue[0][0]) * BAM_RESOLUTION * 48); } ISR(TIMER1_COMPA_vect){ PORTB |= ((1<<BLANK_PIN)); // Set BLANK PIN low - 74HC595 & TPIC6B595 if(BAM_Counter==8) BAM_Bit++; BAM_Counter++; // Anode scanning DIY_SPI(anode[row][1]); // Send out the anode level high byte DIY_SPI(anode[row][0]); // Send out the anode level low byte switch (BAM_Bit) { case 0: DIY_SPI(green [0][level + 2]); DIY_SPI(green [0][level + 1]); DIY_SPI(green [0][level + 0]); DIY_SPI(red [0][level + 2]); DIY_SPI(red [0][level + 1]); DIY_SPI(red [0][level + 0]); DIY_SPI(blue [0][level + 2]); DIY_SPI(blue [0][level + 1]); DIY_SPI(blue [0][level + 0]); break; case 1: DIY_SPI(green [1][level + 2]); DIY_SPI(green [1][level + 1]); DIY_SPI(green [1][level + 0]); DIY_SPI(red [1][level + 2]); DIY_SPI(red [1][level + 1]); DIY_SPI(red [1][level + 0]); DIY_SPI(blue [1][level + 2]); DIY_SPI(blue [1][level + 1]); DIY_SPI(blue [1][level + 0]); if(BAM_Counter==24) { BAM_Counter=0; BAM_Bit=0; } break; } PORTB &= ~(1<<LATCH_PIN); // Set LATCH PIN low - 74HC595 & TPIC6B595 PORTB |= 1<<LATCH_PIN; // Set LATCH PIN high - 74HC595 & TPIC6B595 PORTB &= ~(1<<BLANK_PIN); // Set BLANK PIN low - 74HC595 & TPIC6B595 row++; level = row * 3; if (row == 16) row = 0; if (level == 48) level = 0; pinMode(BLANK_PIN, OUTPUT); } void DIY_SPI(uint8_t DATA) { uint8_t i; #if HARDWARE_SPI == 1 SPI.transfer(DATA); #else for (i = 0; i<8; i++) { digitalWrite(DATA_PIN, !!(DATA & (1 << i))); PORTB |= 1<<CLOCK_PIN; PORTB &= ~(1<<CLOCK_PIN); } #endif }<br>
The following core and libraries need to be installed in this project:
- ATTinyCore at: https://github.com/SpenceKonde/ATTinyCore
- TinySPI library at: https://github.com/JChristensen/tinySPI
- Fix_FFT library at: https://github.com/kosme/fix_fft
DigiSpark ATtiny85 pin usage:
- DigiSpark ATtiny85 P0 (PB0) – BLANK PIN OF 74HC595 & TPIC6B595.
- DigiSpark ATtiny85 P1 (PB1) – DATA PIN OF TPIC6B595
- DigiSpark ATtiny85 P2 (PB2) – CLOCK PIN OF 74HC595 & TPIC6B595.
- DigiSpark ATtiny85 P3 (PB3) – LATCH PIN OF TPIC6B595 & 74HC595
- DigiSpark ATtiny85 P4 (PB4) – AUDIO PIN.
Due to ATtiny85’s memory, to perform both B.A.M and FFT processes, I had to reduce B.A.M resolution to 2. That’s enough for me to customize 16 x spectrum bar colors.
Base on the hardware connection, the “shiftout” function is carried out in following order:
DIY_SPI(anode[row][1]); // Send out the anode level high byte DIY_SPI(anode[row][0]); // Send out the anode level low byte DIY_SPI(green[BAM_Bit][row * 3 + 2]); // Send the green third byte
DIY_SPI(green[BAM_Bit][row * 3 + 1]); // Send the green second byte
DIY_SPI(green[BAM_Bit][row * 3 + 0]); // Send the green first byte
DIY_SPI(red[BAM_Bit][row * 3 + 2]); // Send the red third byte
DIY_SPI(red[BAM_Bit][row * 3 + 1]); // Send the red second byte
DIY_SPI(red[BAM_Bit][row * 3 + 0]); // Send the red first byte
DIY_SPI(blue[BAM_Bit][row * 3 + 2]); // Send the blue third byte
DIY_SPI(blue[BAM_Bit][row * 3 + 1]); // Send the blue second byte
DIY_SPI(blue[BAM_Bit][row * 3 + 0]); // Send the blue first byte<br>
Each spectrum bar amplitude was shown following an 2-dimensions color-wheel array. It looks quite pretty in this way.
for (byte xx=0; xx<20; xx++) { for (byte yy=0; yy < 16; yy++) { if (xx > data[yy]) { LED(19-xx, yy, 0, 0, 0); } else { LED(19-xx, yy, colorwheels[xx][0], colorwheels[xx][1], colorwheels[xx][2]); } } }<br>
And colorwheel array was declared as follow:
byte colorwheels[20][3] = {{0, 0, 3}, {0, 0, 3}, {1, 0, 3}, {2, 0, 3}, {3, 0, 3}, {3, 0, 2}, {3, 0, 1}, {3, 0, 0}, {3, 1, 0}, {3, 2, 0}, {3, 3, 0}, {2, 3, 0}, {1, 3, 0}, {0, 3, 0}, {0, 3, 1}, {0, 3, 2}, {0, 3, 3}, {0, 2, 3}, {0, 1, 3}, {0, 0, 3}};
Source: ATtiny85 – Spectrum Analyzer on RGB Led Matrix 16×20