Make a Digital Piano with a Matrix Keyboard and Buzzer Using MCC | Embedded C Programming - Part 17
Hi there! Let’s keep exploring the Timer1 and ECCP modules of the PIC18F14K50 MCU using the MCC plugin but this time we will use the compare mode. Also, we will learn how to work with a matrix keypad. This tutorial will have the same approach as the non-MCC one, but the method of reading the keypad will be a bit different, so even if you don’t use the MCC you can find something interesting for use.
The task today is to create something like a piano, with a 4x4 matrix keypad and a passive buzzer. When pressing any button, one of the notes should start sounding. When no button is pressed, it shouldn’t produce any sound.
Obviously, it would be better to have a line of buttons, not a matrix, if we’re emulating a keyboard. But if you are interested in this device you can design your own keypad. And for learning purposes the matrix will be sufficient. Let’s consider in detail what the matrix keypad is and how to work with it.
4x4 Matrix Keypad
Such keypads are very widespread and quite cheap. They are often included into an Arduino set. They can look different (Figure 1 - 3) but the principle of operation is the same.
All the keypads in Figure 1-3 have the same schematics diagram (Figure 4).
As you can see, the keypad consists of 16 pushbuttons S1 - S16. They are connected into a matrix 4x4, so each of four buttons in a row have one common pin (R1 - R4), and each of four buttons in a column also have one common pin (C1 - C4). Such a connection allows us to significantly reduce the number of the pins used for the keypad connection. Let’s compare the number of pins required for connecting the buttons separately and in matrix for different button numbers (Table 1).
Table 1 - Comparison of Pins Number for Separate Buttons and for Matrixes
|Number of Buttons||Matrix Size||Pin Numbers for Separate Connection||Pin Numbers for Matrix Connection|
As you can see, the matrix connection becomes much more beneficial with an increasing number of buttons. This is similar to the dynamic indication when you use the LED indicators.
But how do we operate with the matrix keypad? The algorithm is quite simple in fact:
- Configure all MCU pins to which columns are connected (C1 - C4) as outputs, and set them all high.
- Configure all MCU pins to which rows are connected (R1 - R4) as inputs with the pull-up resistors.
- Set one column pin low.
- Read all the row pins (R1 - R4). If no button in this row is pressed then all the readings will be “1” due to the pull-up resistors. If any button is pressed, then the corresponding reading will be “0” because initially we set the column pin low.
- Set the next column low and repeat step 4 for it.
- Repeat step 5 for all the columns.
In this way, you can distinguish what button was pressed. For example if we have pressed button S7 (Figure 4), we can find this out as we apply “0” to column C2 and read row R3. The same with all other buttons.
We will return to this algorithm when we consider the program code - for now let’s recall the math of music.
Notes Frequencies Calculation
I already described these calculations in the PIC10F200 series where I told how to play music using the PIC10F200 MCU. So I’ll just copy-paste this information here for your convenience.
As you most likely know, music is made of notes. There are seven basic notes - C, D, E, F, G, A, B (or H). Also, there are some “half-notes” which are located between some of the basic notes, they are called sharps or flats. So with the sharps (or flats) there are 12 “notes”: C, C#, D, D#, E, F, F#, G, G#, A, A#, B (or H).
Each note has its own frequency. As the foundation, we’ll use “A” of the 4’th octave and its frequency is 440 Hz. The frequencies of the neighboring notes can be calculated with the formula:
Yeah, music and mathematics are interconnected very tightly.
If you’ve ever seen a piano you might notice that there are more keys on it than 12. Yes, the notes are repeated. These 12 notes form an octave. And there can be several octaves on any musical instrument. The frequency of the same notes in the neighboring octaves differs by double. E.g. Note A at the fourth octave is 440 Hz, and note A one octave above is 880 Hz (Table 2).
Table 2 - Notes Frequencies
|Button||Note||F, Hz||Period, us||Half Period, 0.125us|
In column “Button” there is a button from Figure 4 which will play the corresponding note from column “Note”. In column “F, Hz” there is a frequency of each used note in Hz, and in column “Period, us” there is a period that corresponds to this frequency, in microseconds. I will explain the last three columns later, when we consider the program code. For now just believe me, we need these values in our program.
But first, before proceeding to the program code, we need to consider the operation of the ECCP module in Compare mode.
Enhanced Capture/Compare/PWM (ECCP) Module
In tutorial 15 we already familiarized ourselves with the ECCP module, but that time we considered the Capture mode in detail.
As I already mentioned, in compare mode you can write some 16-bit value into the ECCP module register (CCPR1H and CCPR1L), and when the timer register reaches this value some events may happen, like: timer reset, pin change, interrupt generation, A/D conversion start. The event that will happen is configured by the CCP1M0-CCP1M3 bits of the CCP1CON register. We already considered them in tutorial 15 but let’s now look at the modes table one more time from the Capture mode perspective (Table 3).
Table 3 - ECCP Module Operation Modes Configuration
|0||0||0||0||Capture/Compare/PWM off (module reset)|
|0||0||1||0||Compare mode, toggle output on match|
|0||1||0||0||Capture mode, every falling edge|
|0||1||0||1||Capture mode, every rising edge|
|0||1||1||0||Capture mode, every 4th rising edge|
|0||1||1||1||Capture mode, every 16th rising edge|
|1||0||0||0||Compare mode, initialize CCP1 pin low, set output high on compare match (set CC1IPF bit)|
|1||0||0||1||Compare mode, initialize CCP1 pin high, set output low on compare match (set CC1IPF bit)|
|1||0||1||0||Compare mode, generate software interrupt only, CCP1 pin reverts the state|
|1||0||1||1||Compare mode, trigger special event (ECCP resets Timer1 or Timer3, start A/D conversion, set CC1IPF bit)|
|1||1||0||0||PWM mode: P1A, P1C active high; P1B, P1D active high|
|1||1||0||1||PWM mode: P1A, P1C active high; P1B, P1D active low|
|1||1||1||0||PWM mode: P1A, P1C active low; P1B, P1D active high|
|1||1||1||1||PWM mode: P1A, P1C active low; P1B, P1D active high|
In Table 3 I highlighted the Compare modes in green. Let’s consider them in detail.
When CCP1Mx bits are 0b0010, the ECCP module automatically toggles the output pin CCP1 (RC5) when the Timer1 register matches the ECCP register. I was considering using this mode in the current program but then decided to use another one.
When CCP1Mx bits are 0b1000 the ECCP module sets the CCP1 pin high when the Timer1 register matches the ECCP register.
When CCP1Mx bits are 0b1001 the ECCP module sets the CCP1 pin low when the Timer1 register matches the ECCP register.
When CCP1Mx bits are 0b1010 the ECCP module doesn’t affect the CCP1 pin state, it only triggers the interrupt.
When CCP1Mx bits are 0b1011 the ECCP module triggers the special event, which means that when the timer register matches the ECCP register, the timer registers resets, also A/D conversion can start if the ADC module is configured and enabled.
In all modes the match event triggers the interrupt and sets the CC1PF interrupt flag. which, as you should remember, must be cleared manually by the firmware.
In our program we will use the special event to reset the timer register at the compare event and thus set the required timer period as precisely as possible. This mode doesn’t affect the CCP1 pin, so will need to toggle the output pin manually.
Let’s now consider the schematics diagram of the device (Figure 5).
This schematic diagram consists of the PIC18F14K50 MCU (DD1), PICKit debugger (X1), 4x4 keypad (S1-S16), passive buzzer (BZ1), and the simple amplifier circuit based on transistor T1. The columns of the keypad (C1-C4) are connected to the RC0-RC3, and the rows of the keypad (R1-R4) are connected to pins RB4-RB7. These pins weren’t selected randomly. It’s more convenient if the columns and rows are connected to the consecutive pins of the same port, we will see why, when we consider the program code. Pins RB4-RB7 were selected because they have the internal pull-up resistors, unlike the pins of port C.
We will play the sound using the passive buzzer BZ1. I already explained in the doorbell tutorial of the previous series that passive buzzers can be piezoelectric or electromagnetic. The electromagnetic buzzer has the same construction as a regular speaker and has a low resistance, so to prevent the MCU pin from burning out, you need to add the amplification circuit. In our case it’s built with transistor T1 of the NPN structure, and resistor R1 of 1 kOhm that limits the transistor’s base current. The T1 transistor can be almost any type: I used BC547C, for instance, and it works quite well. If you have a piezoelectric buzzer, which has high input resistance, you can get rid of the R1 and T1 parts and connect the buzzer directly to the RA5 pin of the MCU.
Speaking of, as we will use the special trigger event of the ECCP module, which doesn’t directly affect the CCP1 pin, we can use any MCU pin to connect the buzzer, as we will toggle it manually. Thus I decided to use the RA5 pin, but actually you can use any available pin you want.
And that’s actually all about the schematic diagram, so let’s now create a new project, and run the MCC plugin.
Configuration of the Project using MCC
First, open the System Module page and set the Internal Clock as 8MHz_HF, and enable the PLL (Software or not - doesn’t matter), thus you will have the CPU frequency as 32 MHz (the same as we did in tutorial 11. Also, don’t forget to remove the check from the “Low-voltage programming enable” field.
Then we need to go to the Device resources tab and add “TMR1” and “ECCP1” like we did in tutorial 15.
Now, let’s open the TMR1 window and configure the Timer1 module according to Figure 6.
We can actually leave all the fields unchanged. As we need the highest possible frequency to get the greatest notes accuracy, we leave the prescaler as 1:1. Also, this time we don’t need to enable the 16-bit reading/writing option, as we won’t operate with the timer registers directly: this will be done by the ECCP module.
Let’s now switch to the ECCP1 tab and configure the ECCP module (Figure 7).
There are fewer settings here, so let’s briefly consider them:
- This time we will configure the ECCP module in the Compare mode.
- As we will use the ECCP with Timer1, we need to select the last in the drop-down list.
- Here is a glitch in the ECCP module. For some reason the required point “Special event” is absent in the drop-down list for the current MCU (Figure 8)
- So we can select any option here, because we will fix this problem by editing the auto-generated MCC code.
- We need to enable the CCP interrupt because we will toggle the buzzer pin inside this interrupt callback.
Let’s now switch to the Pin Module and Pin Manager and configure all the required pins according to the schematics diagram (Figure 9).
As you may notice, the RC5 pin now has the CCP1 function because we enabled the ECCP module. Even though we’re not going to use it in our device, it’s still occupied by this module.
We configured pins RC1-RC4 as outputs and set them all high according to point 1 of the algorithm. Also, we configured pins RB4-RB7 as inputs and enabled the pull-up resistors for them. And finally, we configured the RA5 pin as an output and called it “BUZZER”.
Now, let’s switch to the Interrupt Module tab and make sure that the CCPI interrupt is enabled (Figure 10).
We still don’t need to enable the priorities of the interrupts because only one is enabled.
Now everything is configured, and we can click the “Generate” button and switch to the program code.
Program Code Description
First, let’s open the file “eccp1.c” generated by the MCC and make the required changes in it.
Section: Compare Module APIs:
// Set the ECCP1 to the options selected in the User Interface
// CCP1M Special event; DC1B 0; P1M single;
CCP1CON = 0x0B; // CCP1Mx bits are 0b1011
// CCPR1H 0;
CCPR1H = 0x00;
// CCPR1L 0;
CCPR1L = 0x00;
// Clear the ECCP1 interrupt flag
PIR1bits.CCP1IF = 0;
// Enable the ECCP1 interrupt
PIE1bits.CCP1IE = 1;
// Selecting Timer1
T3CONbits.T3CCP1 = 0x0;
void ECCP1_SetCompareCount(uint16_t compareCount)
// Write the 16-bit compare value
module.ccpr1_16Bit = compareCount;
CCPR1L = module.ccpr1l;
CCPR1H = module.ccpr1h;
// Clear the ECCP1 interrupt flag
PIR1bits.CCP1IF = 0;
In this file we need to change one line, and to add one line. I marked all the changes in green.
In line 14 we need to replace the value of the CCP1CON register from whatever value generated by the MCC to the 0x0B (which is 0b1101 in binary format). This value corresponds to the special event Compare mode.
In line 47 we add the BUZZER_Toggle function which will toggle the BUZZER pin every time the ECCP module interrupt occurs.
That’s all we need to change in the “eccp1.c” file, now we can move to the “main.c” file and add the following code there.
// Initialize the device
// Enable the Global Interrupts
// Disable the Global Interrupts
// Enable the Peripheral Interrupts
// Disable the Peripheral Interrupts
button = (!R1_GetValue()) + (!R2_GetValue() << 1) + (!R3_GetValue() << 2) + (!R4_GetValue() << 3);
button += (!R1_GetValue() << 4) + (!R2_GetValue() << 5) + (!R3_GetValue() << 6) + (!R4_GetValue() << 7);
button += (!R1_GetValue() << 8) + (!R2_GetValue() << 9) + (!R3_GetValue() << 10) + (!R4_GetValue() << 11);
button += (!R1_GetValue() << 12) + (!R2_GetValue() << 13) + (!R3_GetValue() << 14) + (!R4_GetValue() << 15);
if (button) //If button value is not 0
switch (button) //Check the button value
case 0x0001: ECCP1_SetCompareCount(15289); break; //C4
case 0x0002: ECCP1_SetCompareCount(14431); break; //C#4
case 0x0004: ECCP1_SetCompareCount(13621); break; //D4
case 0x0008: ECCP1_SetCompareCount(12856); break; //D#4
case 0x0010: ECCP1_SetCompareCount(12135); break; //E4
case 0x0020: ECCP1_SetCompareCount(11454); break; //F4
case 0x0040: ECCP1_SetCompareCount(10811); break; //F#4
case 0x0080: ECCP1_SetCompareCount(10204); break; //G4
case 0x0100: ECCP1_SetCompareCount(9632); break; //G#4
case 0x0200: ECCP1_SetCompareCount(9091); break; //A4
case 0x0400: ECCP1_SetCompareCount(8581); break; //A#4
case 0x0800: ECCP1_SetCompareCount(8099); break; //B4
case 0x1000: ECCP1_SetCompareCount(7645); break; //C5
case 0x2000: ECCP1_SetCompareCount(7215); break; //C#5
case 0x4000: ECCP1_SetCompareCount(6810); break; //D5
case 0x8000: ECCP1_SetCompareCount(6428); break; //D#5
else //If button value is 0
BUZZER_SetLow(); //Set BUZZER pin low
Here, in line 5 we declare the variable button of type uint16_t. This variable has 16 bits, each of which will represent the corresponding button state: “0” if the button is not pressed, and “1” if it is pressed.
In lines 5-73 there is the main function of the program.
In line 8 there is the auto-generated function SYSTEM_Initnalize() about which I talked several times in previous tutorials.
In line 11, we uncomment the function INTERRUPT_GlobalInterruptEnable() to enable global interrupts but don’t uncomment the INTERRUPT_PeripheralInterruptEnable() function in line 17. Without the last function, the ECCP module interrupt will not happen, so we will not get into the ECCP1_CompareISR() function, and the BUZZER pin will not be toggled, thus no sound will be produced. We won’t need to enable this interrupt until later, when the button is pressed.
Below, there is the main loop of the program (lines 22-72) in which we first implement points 3-5 of the keypad reading algorithm (lines 24-42).
In line 24, we set the C1 pin low, then wait for 1 ms to let the voltage stabilize (line 25).
In line 26, we form the lower 4 bits of the button variable. Let’s consider this in more detail. Functions R1_GetValue() - R4_GetValue() return the states of the MCU pins to which the rows of the keypad are connected. These functions return 1 if the button is not pressed, or 0 otherwise. Assuming we want the value “1” to correspond to a pressed button, if we want to save the state of the button in the aptly named button variable, we need to invert the Rx_GetValue() value, which we do with the “!” sign. When we apply 0 to the C1 pin, the R1 value corresponds to the S1 button, R2 corresponds to S2, R3 corresponds to S3, and R4 corresponds to S4 (see Figure 5). So we save the R1 value in bit #0, then we shift the R2 value one bit to the left to save it as bit #1, then we shift the R3 value at two bits to the left to save it as bit #2, and so on. Thus after implementation of line 26, the button variable will have the values of buttons S1-S4 in four lower bits.
In line 27, we set the C1 pin high again, thus we return it to the initial state.
Lines 29-32, 34-37, and 39-42 are very similar to lines 24-27, we first set one of the column pins (C2, C3, or C4) low (lines 29, 34, 39), then wait for 1 ms to stabilize the voltage (lines 30, 35, 40). Then we accumulate the buttons S5-S16 states into the button variable. Please note that in line 26 we used simple assignment (“=”) to the variable button, and in lines 31, 36, 41 we use the assignment with the summing (“+=”) to add the new value to the existing one. Also note that we shift the row pins values more and more to the left: for column C2, we fill bits 4-7 which correspond to buttons S5-S8, for column C3 we fill bits 8-11 (S9-S12), for column C4 we fill bits 12-15 (S13-S16).
In lines 32, 37, and 42 we return column pins to high.
And actually this is the entirety of the keypad scanning algorithm implementation. Now we can process the button variable. In line 44 we check if the button value is not 0. If so, we check what value has the button variable in the switch construction (lines 46 - 64). Let’s consider just one “case” line because the others are the same, for example, line 48:
case 0x0001: ECCP1_SetCompareCount(15289); break; //C4
The values at the “case” sentence (0x0001 and similar) correspond to the values from Table 4, just in the hexadecimal system instead of binary (you can check this by yourself if you want). Function ECCP1_SetCompareCount assigns the ECCP register with the transmitted parameter. Please note that we can set the 16-bit value here, and it will be split into two 8-bit values inside the ECCP1_SetCompareCount function. Let’s now consider why we send exactly this value - 15289 to the ECCP register to generate the C4 note.
So, button S1 corresponds to note C4 which has the frequency 261.63 Hz, or a period of 3822.19 us. The main frequency of the CPU is 32 MHz, so the Timer1 input frequency is 32 / 4 = 8 MHz (because we didn’t use the timer prescaler). Thus, each tick of the timer is 1 / 8 = 0.125us. To reach the period of 3822 us we need 3822.19 / 0.125 = 30578 ticks of the timer. But during this period we need to toggle the output pin twice - first time from low to high in the middle of the period, and the second time from high to low at the end of the period, so we need to toggle the pin every 30578 / 2 = 15289 ticks. This is the value we have in the column “Half Period, 0.125us” of Table 2. So in this column there is a number of timer ticks between the RA5 pin toggle.
In the same way the other ECCP register values for other notes are calculated (lines 49 - 63).
Now, when we set the proper values of the ECCP register, we enable the peripheral interrupts (line 65), to which the ECCP interrupt also belongs to let the program toggle the RA5 pin inside the interrupt subroutine. Actually this approach is not very good and is applicable only when we have a single interrupt source. If we have more, we should enable or disable the individual interrupts, not all of them at once.
If the button value is 0 (line 65) we disable the peripheral interrupts (line 70) to stop the BUZZER pin toggling, and set the BUZZER pin low (line 69) to remove power from the buzzer.
And that’s all about the program code. The device doesn’t need any adjustment and, being assembled correctly, starts working at once.
As for homework, try to make the same task but connect the buzzer to the CCP1 pin and toggle it by the ECCP module (set the ECCP mode as “Toggle output”), and set the timer period to the same as in tutorial 9 by assigning the initial value for the timer register inside the Timer1 overflow interrupt.
- 212 Tutorials
- 9 Textbooks
- 12 Study Guides
- 31 Tools
- 97 EE FAQs
- 295 Equations Library
- 200 Reference Materials
- 91 Glossary of Terms
Friends of CircuitBread
Search Hundreds of Component Distributors + 2 Perks
Check Out Our Free Ohm's Law Calculator
Free YouTube Product Training Guides
Get the latest tools and tutorials, fresh from the toaster.