Project Summary: The PianoBit project is an innovative and educational digital mini-piano that utilizes an Arduino Uno as the core control unit. Designed to mimic the functionality of a basic piano, the system includes a 4×4 button matrix, a set of 16 LEDs, an active buzzer, and an LCD screen. The aim of the project is to build a simple, interactive musical instrument while exploring various hardware techniques such as button matrix scanning, LED multiplexing, and sound generation.
The block diagram illustrates how the core modules of the PianoBit project are functionally interconnected to achieve real-time user interaction. At the center of the system lies the Arduino Uno, which acts as the main control unit. It manages data flow between input and output modules, orchestrating the behavior of the piano. The button matrix connects directly to the Arduino's digital pins, and the Arduino detects exactly which key has been pressed. This key is translated into a musical note. Once a key is identified, the Arduino triggers two parallel outputs: it sends a corresponding frequency signal to the buzzer, generating an audible tone and it lights up the matching LED by transmitting data serially to the shift registers. These convert the serial input into parallel output, illuminating the specific LED assigned to the pressed key.
Simultaneously, the Arduino communicates with the I2C LCD display to visually show the name of the note being played. This communication happens via the I2C protocol using only two data lines (SCL and SDA), allowing the LCD to operate without consuming multiple digital I/O pins.All modules draw power from a shared 5V supply. This architecture ensures that pressing a single key results in synchronized audio, visual, and textual feedback, making the PianoBit both interactive and educational.
Component List:
Component | Quantity | Description |
---|---|---|
Arduino Uno | 1 | Microcontroller for project control |
Buttons | 16 | 16 buttons arranged in a matrix configuration |
74HC595 Shift Register | 2 | Serial-to-parallel shift registers for controlling LEDs |
LEDs | 16 | LED indicators to correspond to each key |
220Ω Resistors | 16 | Current limiting resistors for LEDs |
Buzzer | 1 | Passive Buzzer for sound output |
Breadboard | 4 | For connecting components without soldering |
Jumper Wires | Multiple | For making connections between components |
I2C LCD | 1 | Displays message for user |
Overview on hardware:
The LCD uses the I2C protocol with two dedicated lines:
This communication interface allows sending commands and data over just two wires, greatly reducing wiring complexity. The LCD module typically includes a PCF8574 I/O expander chip, which converts the serial I2C data to parallel signals needed to drive the LCD. Pull-up resistors (usually onboard) maintain stable HIGH levels on SDA and SCL lines. The I2C address used for the LCD in this project is 0x27 (7-bit), shifted left by one bit in code to account for the read/write flag.
In this project, I’ve utilized multiplexing for the button matrix to reduce the number of pins required for detecting 16 buttons. By multiplexing, one row at a time is activated while scanning for button presses in the corresponding columns. This allows the usage of only 8 I/O pins to control 16 buttons instead of needing 16 individual pins.
For the LEDs, I used 74HC595 shift registers to control 16 LEDs with only 3 I/O pins (Data, Clock, and Latch). The shift registers are connected in daisy-chain mode, meaning the output of the first shift register is connected to the input of the second. This allows the Arduino to control a total of 16 outputs (8 from each shift register) while only using 3 pins, significantly reducing the number of I/O pins required for controlling the LEDs.
Below there are some pictures that showcase the whole circuit:
Button Matrix:
Shift Registers:
Pin 10: Buzzer – connected to the passive buzzer for generating sound.
LEDs: The shift registers control the LEDs through the Q0-Q7 pins on each shift register. The first shift register controls LEDs 0-7, and the second shift register controls LEDs 8-15.
I2C LCD Display Pins:
Development Environment:
Libraries Used:
No external Arduino libraries were used in this project. All communication protocols and peripheral controls were implemented manually by direct register manipulation and custom functions.
Explanation:
- The project uses low-level control of the I2C bus through direct manipulation of the AVR TWI (Two Wire Interface) registers (`TWBR`, `TWCR`, `TWDR`, `TWSR`), instead of relying on the Arduino Wire library. - LCD communication is implemented manually by sending commands and data over I2C using bit-banged sequences with precise timing. - Shift registers and keypad scanning are handled using direct port manipulation and interrupts without external libraries.
Global Constants and Defines for LCD:
- `define LCD_ADDR (0x27 « 1)`
Defines the 7-bit I2C address of the LCD module shifted left by one bit to form the 8-bit address used by the TWI hardware.
- `define LCD_BACKLIGHT 0x08`
Controls the LCD backlight bit on the PCF8574 I/O expander. Setting this bit turns the backlight on.
- `define ENABLE 0x04`
The Enable (EN) signal bit for the LCD controller, used to latch commands or data.
- `define READ_WRITE 0x02`
Read/Write bit; set low for write operations (not used for reading in this code).
- `define REGISTER_SELECT 0x01`
Selects whether data sent to the LCD is a command (`0`) or data (`1`).
Software Structure:
* Initialization Functions:
* Interrupt Service Routines (ISRs):
* Keypad Scanning:
* LCD Control:
* LED Control:
* Main Loop:
* Sound Generation:
Functions:
Function | Role / Description |
---|---|
`setup()` | Initializes pins, timers, interrupts, LCD, and hardware peripherals. Prepares the system for operation. |
`loop()` | Main loop that checks for keypad changes via interrupts or periodic scanning, then processes key states. |
`scanKeypad()` | Scans the 4×4 button matrix to detect which button is pressed, implements debounce logic, updates state. |
`updateLED(int)` | Controls two 74HC595 shift registers to turn on the LED corresponding to the pressed key, clears others. |
`clearAllLEDs()` | Turns off all LEDs by sending zeros to the shift registers. |
`lcd_init_custom()` | Sends initialization commands to the LCD over I2C to configure 4-bit mode, display on, cursor off, etc. |
`lcd_command(uint8_t)` | Sends a command byte to the LCD to control cursor, clear display, or other instructions. |
`lcd_data(uint8_t)` | Sends a data byte (character) to the LCD to be displayed at the current cursor position. |
`lcd_send(uint8_t, uint8_t)` | Sends one byte to the LCD split into two 4-bit nibbles with control bits (command or data). |
`lcd_write4bits(uint8_t)` | Sends 4 bits to the LCD via I2C, including backlight and enable signal sequencing for proper timing. |
`lcd_clear_custom()` | Clears the LCD display and delays for command execution. |
`lcd_setCursor_custom(int, int)` | Sets the cursor position on the LCD based on row and column arguments. |
`lcd_print_custom(const char*)` | Prints a null-terminated string character by character on the LCD. |
`i2c_init()` | Initializes I2C hardware registers for communication at approximately 100 kHz clock speed. |
`i2c_start(uint8_t)` | Generates an I2C start condition and sends the device address, waits for acknowledgment. |
`i2c_write(uint8_t)` | Writes one byte of data on the I2C bus and waits for acknowledgment from the slave device. |
`i2c_stop()` | Sends an I2C stop condition to end communication. |
`ISR(TIMER1_COMPA_vect)` | Timer1 Compare Interrupt Service Routine that multiplexes rows of the button matrix by cycling active row pins. |
`ISR(PCINT2_vect)` | Pin Change Interrupt Service Routine for Port D columns that signals when keypad state has changed. |
`ISR(PCINT0_vect)` | Pin Change Interrupt Service Routine for Port B columns that signals when keypad state has changed. |
Program Flow:
* Setup Phase:
* Interrupt Service Routines (ISRs):
* Main Loop (`loop`):
* Keypad Scanning (`scanKeypad`):
* LED Update (`updateLED`):
* Debounce Mechanism:
This project taught me a lot about working efficiently with limited Arduino pins using multiplexing and shift registers. Implementing I2C manually for the LCD really helped me understand how communication protocols work at a low level. I also learned why software debouncing is crucial for reliable button inputs. Overall, building this from scratch boosted my skills in embedded systems and gave me more confidence in handling hardware and software together without relying on external libraries.
Project Repo:
Websites used for buying the components:
Bibliography: