Finally, we are at the end of this series, where I am going to give an overview of the software behind all the menu system and of course, all the handling of user inputs and sending of MIDI messages. We’ve spent so much time configuring the various peripherals and writing driver code to abstract away the nitty-gritty implementation stuff, this is where it all comes together!
For the project files, do check out the Github
repo.
To put it simply, there are 3 files which are relevant for this section, which are main.c
, User_Params.h
and Menu.h
.
User_Params.h has all of the parameters/options that can be configured by the user via the menu system. Menu.h contains all the menu text strings, and pointers to all the corresponding user parameter variables.
I will mainly be breaking down main.c here, since that’s where all the stuff happens.
Global Variables – just so that we’re all on the same page#
volatileuint8_tbrightness[4]={1,1,1,1};//initialize array holding brightness values for the 4 indicator LED's
volatileuint8_tBAMIndex=0;//which step are we at in the BAM driver
volatileuint8_tblocked=0;//time critical BAM code running, do not disable interrupts
volatileuint8_tcurrentEncoder=0;//which encoder are we polling right now?
volatileuint8_tlastEncoder[5]={0,0,0,0,0};//initialize array containing past encoder readoff
volatileintencoderValues[5]={0,0,0,0,0};//initialize array containing encoder values
volatileint8_tencoderLUT[16]={0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0};intlastEncoderValues[5]={0,0,0,0,0};uint8_tlastButtonState=0;//menu encoder's button
uint32_tlastKeyMatrix=0;//last state of the key matrix
uint32_tcurrentKeyMatrix=0;//current state of the key matrix
int8_tMidiCCValues[128];//values for all 128 MIDI CC channels
intadcSmooth[3];//adc data after its passed thru the IIR filter
intlastFaderValues[3]={0,0,0};charLCDQueueTop[17];charLCDQueueBottom[17];//unused
uint8_tLCDTopQueued;//does the LCD have to be updated this round?
uint8_tLCDBottomQueued;//menu button debounce.
#define MAXIMUM 15
int8_tdIntegrator=0;int8_tdOutput=0;
//read in all user parameters from the EEPROM
I2C2->CR1|=(1<<8);//send start condition
while((I2C2->SR1&1)==0);//clear SB
I2C2->DR=0xA0;//address the EEPROM in write mode
while((I2C2->SR1&(1<<1))==0);//wait for ADDR flag
while((I2C2->SR2&(1<<2))==0);//read I2C SR2
while((I2C2->SR1&(1<<7))==0);//make sure TxE is 1
I2C2->DR=0x0;//address
while((I2C2->SR1&(1<<7))==0);//make sure TxE is 1
I2C2->DR=0x0;//address
while((I2C2->SR1&(1<<7))==0);//make sure TxE is 1
while((I2C2->SR1&(1<<2))==0);//make sure BTF is 1
I2C2->CR1|=1<<10;I2C2->CR1|=(1<<8);//send start condition
while((I2C2->SR1&1)==0);//clear SB
I2C2->DR=0xA1;//address the EEPROM in read mode
while((I2C2->SR1&(1<<1))==0);//wait for ADDR flag
while((I2C2->SR2)&0);//read I2C SR2
while((I2C2->SR1&1<<6)==0);//wait for RXNE
MidiNoteOffset=(I2C2->DR)&0xff;while((I2C2->SR1&1<<6)==0);//wait for RXNE
MidiNoteVelo=(I2C2->DR)&0xff;while((I2C2->SR1&1<<6)==0);//wait for RXNE
MidiChannel=(I2C2->DR)&0xff;while((I2C2->SR1&1<<6)==0);//wait for RXNE
MidiCCFaderLUT[0]=(I2C2->DR)&0xff;while((I2C2->SR1&1<<6)==0);//wait for RXNE
MidiCCFaderLUT[1]=(I2C2->DR)&0xff;while((I2C2->SR1&1<<6)==0);//wait for RXNE
MidiCCFaderLUT[2]=(I2C2->DR)&0xff;while((I2C2->SR1&1<<6)==0);//wait for RXNE
MidiCCEncoderLUT[0]=(I2C2->DR)&0xff;while((I2C2->SR1&1<<6)==0);//wait for RXNE
MidiCCEncoderLUT[1]=(I2C2->DR)&0xff;while((I2C2->SR1&1<<6)==0);//wait for RXNE
MidiCCEncoderLUT[2]=(I2C2->DR)&0xff;while((I2C2->SR1&1<<6)==0);//wait for RXNE
MidiCCEncoderLUT[3]=(I2C2->DR)&0xff;while((I2C2->SR1&1<<6)==0);//wait for RXNE
EncoderSpeed[0]=(I2C2->DR)&0xff;while((I2C2->SR1&1<<6)==0);//wait for RXNE
EncoderSpeed[1]=(I2C2->DR)&0xff;while((I2C2->SR1&1<<6)==0);//wait for RXNE
EncoderSpeed[2]=(I2C2->DR)&0xff;while((I2C2->SR1&1<<6)==0);//wait for RXNE
EncoderSpeed[3]=(I2C2->DR)&0xff;while((I2C2->SR1&1<<6)==0);//wait for RXNE
EncoderNote[0]=(I2C2->DR)&0xff;while((I2C2->SR1&1<<6)==0);//wait for RXNE
EncoderNote[1]=(I2C2->DR)&0xff;while((I2C2->SR1&1<<6)==0);//wait for RXNE
EncoderNote[2]=(I2C2->DR)&0xff;while((I2C2->SR1&1<<6)==0);//wait for RXNE
EncoderNote[3]=(I2C2->DR)&0xff;while((I2C2->SR1&1<<6)==0);//wait for RXNE
I2C2->CR1&=~(1<<10);//NACK
I2C2->CR1|=1<<9;//STOP
filterBeta=I2C2->DR;
Customizability was one of the things that I really focused on in the development of this project, not so much because I anticipated that I would actually be changing loads of parameters when using this MIDI controller, but because products like the Ableton Pushes, Maschines, Komplete Kontrols, MIDI Fighters seemed so nerdy, so flexible, so beautiful thanks to their very well written and tightly integrated software that I get really excited every time I see someone use them on YouTube. When I was just starting to explore the world of orchestral programming, it seemed like there were just a million things you could change on these devices to tailor them to your specific workflow. At the very least, I wanted a rudimentary version of that.
Plus, I have never actually programmed a menu interface that allows a user to change settings before, and this seemed like the perfect chance to give it a go.
So before anything else, we’ve got to read back all the parameters, which are stored in EEPROM every time you change them. This retains changes you’ve made across power cycles, because it would just be crazy if you’d have to reprogram everything every single time you plug the controller in.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
IWDG->KR=0xAAAA;//reset the watchdog timer
blocked=0;I2C2->CR1|=1;//enable i2c 2 peripheral for LCD and EEPROM
I2C1->CR1|=1;//enable i2c 1 peripheral for LED Matrix
LCDInit(LCD_Address);LEDMatrixInit(LEDMatrix_Address);LCDClear(LCD_Address);LCDSetCursor(1,1,LCD_Address);LCDPrepareInt();TIM2->CR1|=1;//enable BAM Driver
TIM3->CR1|=1;//enable encoder scan driver
Then, we can go ahead and enable all the relevant hardware peripherals. (The configuring of these peripherals has been taken care of by the IDE’s code generation tool. I’ve omitted them here)
//indicate where are all the note C's
for(int8_ti=0;i<4;i++){LEDMatrix[i]&=0xf;//clear all the red channels
}for(int8_ti=0;i<16;i++){//we have 16 keys
int8_trow=3-(i>>2);int8_tcol=(i%4);if((MidiNoteOffset+i)%12==0){//this key is a C
LEDMatrix[row]|=(1<<(4+col));}}for(inti=0;i<4;i++){//function to drive the LED's
LEDMatrixBuffer[i*4]=0b1111;//clear all pins first to prevent ghosting
LEDMatrixBuffer[i*4+1]=0x00;LEDMatrixBuffer[i*4+2]=~(1<<i);LEDMatrixBuffer[i*4+3]=LEDMatrix[i];}LEDMatrixStart(LEDMatrix_Address);
The way I chose to utilize the LED Matrix is to use the red LEDs to indicate the start of each octave, indexed at the note C. That way, one can easily know the positions of the notes in the 4×4 grid. MidiNoteOffset is the MIDI note number of the bottom-leftmost key. In MIDI, notes are numbered sequentially up the scale, so 60 is C4 or middle C, 61 is C#4 and so on. The highlighted bit basically steps through every single switch, and sets the red LED at that position to be on if that particular note is a C.
The rest of the code is basically the straight from Part 3
on the LED Matrix. Then at the end, we call LEDMatrixStart to start the DMA and therefore enable the LED matrix.
1
2
3
4
5
6
7
8
9
10
for(inti=0;i<4;i++){//function to fill in the MidiNoteLut
for(intj=0;j<4;j++){MidiNoteLUT[5*(3-i)+j+1]=MidiNoteOffset+(4*i+j);//math...
}MidiNoteLUT[5*i]=EncoderNote[i];}
For the actual keys themselves, I decided to use the array MidiNoteLUT[]to store their MIDI note values so they can easily be recalled when it comes time to send over the MIDI messages. This bit basically fills in this LUT. For now, the encoder buttons also trigger notes, so they too share the LUT. The code is kinda messy since I didn’t really give much thought when I wired up the key matrix, but oh well, nothing a little math can’t fix…
Then, we gotta fill in lastFaderValues[] with, well, the last fader values. Notice that the for loop starts with i = 1, this is because I actually broke the first fader, and so only had 3 left. I also did some averaging since things did get quite noisy. In the end I had to implement a simple IIR filter to prevent the ADC values from fluctuating too much. More on that later.
/* USER CODE BEGIN WHILE */while(1){IWDG->KR=0xAAAA;//reset the watchdog timer
flushMidi();/* USER CODE END WHILE *//* USER CODE BEGIN 3 */brightness[0]=MidiCCValues[MidiCCEncoderLUT[3]]<<1;brightness[1]=MidiCCValues[MidiCCEncoderLUT[2]]<<1;brightness[2]=MidiCCValues[MidiCCEncoderLUT[1]]<<1;brightness[3]=MidiCCValues[MidiCCEncoderLUT[0]]<<1;
The way I chose to do the MIDI messages is basically to have the onboard USB act as a serial port, and use the ever popular Hairless MIDI to Serial Bridge
to send incoming serial messages to an internal MIDI loopback (such as loopMIDI
). I initially naively sent the MIDI messages as soon as they were received. However, that proved to be an unwise idea, since the USB CDC driver doesn’t really like being bombarded with calls to send messages over USB, and multiple keypresses/releases (chords) didn’t work well at all.
So I basically implemented a FIFO buffer again, and every time the loop executes, it would flush the buffer out. Check out Part 2
for a primer on FIFO buffers, and Midi.c
for the specific implementation.
Then, the code updates the four BAM
dimmed LEDs to represent the current values of the CC channels assigned to each encoder.
uint8_tcurrentButtonState=((GPIOB->IDR)&1);/*
*
* debounce.c
written by Kenneth A. Kuhn
version 1.00
*//* Step 1: Update the integrator based on the input signal. Note that the
integrator follows the input, decreasing or increasing towards the limits as
determined by the input state (0 or 1). */if(currentButtonState==0){//button is currently depressed
if(dIntegrator<MAXIMUM){dIntegrator++;}}elseif(dIntegrator>0){//button is not depressed
dIntegrator--;}/* Step 2: Update the output state based on the integrator. Note that the
output will only change states if the integrator has reached a limit, either
0 or MAXIMUM. */if(dIntegrator==0){dOutput=0;}elseif(dIntegrator>=MAXIMUM){dOutput=1;dIntegrator=MAXIMUM;/* defensive code if integrator got corrupted */}
Then, we read the current state of the menu navigation encoder’s button, and do some debouncing. I did not write this debounce bit, but instead got it from here
.
Initially, I wanted to use a tree based approach to implement the menu, where there will be nicely defined data structures for nodes which would represent each menu page. That would make adding new functionality to the menu system really elegant. But alas, I’m quite a noob when it comes to programming, and after trying really hard I just gave up and fell back to a pretty basic approach that ultimately got the job done.
Before moving on, have Menu.h
open, otherwise none of this is going to make sense.
I chose to use a state machine approach to implement the menu, where each level in the menu hierarchy is a state. Depending on the current state, the code then knows what to do when it receives user input.
/*begin button handler*/if(dOutput==1&&lastButtonState==0){//button has been pressed
switch(status){caseStatus://we are now entering menu mode
status=Menu;menuItemSelected=0;//start from the first item
snprintf(LCDQueueTop,17,"\x7E%s",menuItems[menuItemSelected]);snprintf(LCDQueueBottom,17," %s",menuItems[menuItemSelected+1]);//normally you'd have to check if there is a next item available, but since this is only the menu init, we don't have to. (We will have to in the encoder-rotated handler)
LCDTopQueued=1;//signal that we need to update the LCD
LCDBottomQueued=1;break;caseMenu://we are already in our menu, time to enter whatever submenu is selected (or exit)
if(menuItemSelected!=MENU_SIZE){//we have not selected "back", go into the selected SubMenu;
status=SubMenu;subMenuSelected=menuItemSelected;parameterSelected=0;//start afresh
snprintf(LCDQueueTop,17,"\x7E%s",(*(subMenus[subMenuSelected]+parameterSelected)));snprintf(LCDQueueBottom,17," %s",(*(subMenus[subMenuSelected]+parameterSelected+1)));LCDTopQueued=1;//signal that we need to update the LCD
LCDBottomQueued=1;}else{//exit menu back into status
status=Status;snprintf(LCDQueueTop,17," ");//clear LCD
snprintf(LCDQueueBottom,17," ");LCDTopQueued=1;//signal that we need to update the LCD
LCDBottomQueued=1;}break;caseSubMenu:if(parameterSelected!=subMenuSizes[subMenuSelected]){//we have not selected "back", go into selected parameter page
status=ParaSet;snprintf(LCDQueueTop,17,"%s",(*(subMenus[subMenuSelected]+parameterSelected)));//print the current parameter on the top line
if((subMenuSelected==2)|(subMenuSelected==3&¶meterSelected==0)){//if we are changing notenames
snprintf(LCDQueueBottom,17,"%s%d",noteNames[(*(*(parameters[subMenuSelected]+parameterSelected)))%12],(int8_t)(((*(*(parameters[subMenuSelected]+parameterSelected)))/12)-1));//print the NOTENAME of the parameter under question
}else{snprintf(LCDQueueBottom,17,"%d",(*(*(parameters[subMenuSelected]+parameterSelected))));//print the current value of the parameter under question
}LCDTopQueued=1;//signal that we need to update the LCD
LCDBottomQueued=1;}else{//exit sub menu back into main menu
status=Menu;snprintf(LCDQueueTop,17,"\x7E%s",menuItems[menuItemSelected]);snprintf(LCDQueueBottom,17," %s",menuItems[menuItemSelected+1]);//normally you'd have to check if there is a next item available, but since this is only the menu init, we don't have to. (We will have to in the encoder-rotated handler)
LCDTopQueued=1;//signal that we need to update the LCD
LCDBottomQueued=1;}break;caseParaSet://save parameter and return to the SubMenu
status=SubMenu;EEPROMWriteParameter(*(parameterEAddrs[subMenuSelected]+parameterSelected),*(*(parameters[subMenuSelected]+parameterSelected)));//if we were changing notenames
if((subMenuSelected==2)|(subMenuSelected==3&¶meterSelected==0)){//update the note definitions
for(inti=0;i<4;i++){//function to fill in the MidiNoteLut
for(intj=0;j<4;j++){MidiNoteLUT[5*(3-i)+j+1]=MidiNoteOffset+(4*i+j);//math...
}MidiNoteLUT[5*i]=EncoderNote[i];}}snprintf(LCDQueueTop,17,"\x7E%s",(*(subMenus[subMenuSelected]+parameterSelected)));snprintf(LCDQueueBottom,17," %s",(*(subMenus[subMenuSelected]+parameterSelected+1)));LCDTopQueued=1;//signal that we need to update the LCD
LCDBottomQueued=1;break;default:break;}}lastButtonState=dOutput;/* end button handler */
So, on detecting that the menu navigation encoder’s button has been pressed, the code enters the state machine, with the current state stored in status. Then, based on the current state, the code advances to the next state and updates the LCD accordingly to let the user know where they are in the menu system. On power up, status is set to Status, where the LCD displays the last user input (on the key matrix or one of the CC controllers).
If ParaSet is the current state, that means that a parameter is currently being configured. So, apart from updating the LCD, if the user presses the button i.e. they are happy with whatever change they’ve made, that change is also saved to the relevant location in the EEPROM. Then, if the user changed what notes the key matrix triggers, we need to go ahead and update MidiNoteLUT too.
Parameter pages have the parameter name displayed on the top row of the LCD, and the current value of that parameter on the bottom row. However, I wanted note names to be displayed as note names, not some lame, unintuitive MIDI note number. Hence the whole kaabodle in the highlighted part.
In each level of the menu (except parameter pages), the last item always is a “back” option. Therefore, if the currently selected item is the last item, we reverse one step in the state machine.
/* begin control encoder rotated handler */if(((encoderValues[4]-lastEncoderValues[4])>=2)|((lastEncoderValues[4]-encoderValues[4])>=2)){//control encoder has been rotated
int8_tincrement=encoderValues[4]>lastEncoderValues[4]?1:-1;//this control encoder is 2 counts per indent
switch(status){caseMenu:clampedIncrement(&menuItemSelected,increment,0,MENU_SIZE);if(increment>0&&menuItemSelected!=0){//we advance in the menu, pointer should be in second row
snprintf(LCDQueueTop,17," %s",menuItems[menuItemSelected-1]);snprintf(LCDQueueBottom,17,"\x7E%s",menuItems[menuItemSelected]);}elseif(menuItemSelected!=MENU_SIZE){snprintf(LCDQueueTop,17,"\x7E%s",menuItems[menuItemSelected]);snprintf(LCDQueueBottom,17," %s",menuItems[menuItemSelected+1]);}LCDTopQueued=1;//signal that we need to update the LCD
LCDBottomQueued=1;break;caseSubMenu:clampedIncrement(¶meterSelected,increment,0,subMenuSizes[subMenuSelected]);if(increment>0&¶meterSelected!=0){//we advance in the menu, pointer should be in second row
snprintf(LCDQueueTop,17," %s",(*(subMenus[subMenuSelected]+parameterSelected-1)));snprintf(LCDQueueBottom,17,"\x7E%s",(*(subMenus[subMenuSelected]+parameterSelected)));}elseif(parameterSelected!=subMenuSizes[subMenuSelected]){snprintf(LCDQueueTop,17,"\x7E%s",(*(subMenus[subMenuSelected]+parameterSelected)));snprintf(LCDQueueBottom,17," %s",(*(subMenus[subMenuSelected]+parameterSelected+1)));}LCDTopQueued=1;//signal that we need to update the LCD
LCDBottomQueued=1;break;caseParaSet:clampedIncrement((*(parameters[subMenuSelected]+parameterSelected)),increment,(*(parameterLBs[subMenuSelected]+parameterSelected)),(*(parameterUBs[subMenuSelected]+parameterSelected)));//if we are changing notenames
if((subMenuSelected==2)|(subMenuSelected==3&¶meterSelected==0)){snprintf(LCDQueueBottom,17,"%s%d",noteNames[(*(*(parameters[subMenuSelected]+parameterSelected)))%12],(int8_t)(((*(*(parameters[subMenuSelected]+parameterSelected)))/12)-1));//print the NOTENAME of the parameter under question
}else{//snprintf(LCDQueueBottom, 17, "%d", *(parameterLBs[subMenuSelected]+parameterSelected));
snprintf(LCDQueueBottom,17,"%d",(*(*(parameters[subMenuSelected]+parameterSelected))));//print the current value of the parameter under question
}//update red LED's to indicate positions of C
if(subMenuSelected==3&¶meterSelected==0){for(int8_ti=0;i<4;i++){LEDMatrix[i]&=0xf;//clear all the red channels
}for(int8_ti=0;i<16;i++){//we have 16 keys
int8_trow=3-(i>>2);int8_tcol=(i%4);if((MidiNoteOffset+i)%12==0){//this key is a C
LEDMatrix[row]|=(1<<(4+col));}}for(inti=0;i<4;i++){//function to drive the LED's
LEDMatrixBuffer[i*4]=0b1111;//clear all pins first to prevent ghosting
LEDMatrixBuffer[i*4+1]=0x00;LEDMatrixBuffer[i*4+2]=~(1<<i);LEDMatrixBuffer[i*4+3]=LEDMatrix[i];}}//nothing special
LCDBottomQueued=1;break;default:break;}lastEncoderValues[4]=encoderValues[4];}/* end control encoder rotated handler */
Now, lets code for the case where the user rotates the menu navigation encoder. Note that the encoder I used generates 2 state changes per indent, so we only react if the value of the encoder increments by 2 or more.
In this case, if we are currently adjusting the note assignments for the key matrix, the LEDs should react to every change we make so that we can know what we’re actually doing. So, if the current page is the MIDI note offset page, update the LED matrix every single time the encoder is rotated.
clampedIncrement() is a small helper function I wrote to ensure that parameters don’t get set out of bounds. It goes something like this:
Since MIDI values only go up to 127, we can rather safely use 8 bit fixed width integers (-127 to 127). However, if you’re not short of RAM, don’t, because all it takes is one overlooked value and there goes hours of your life chasing red herrings.
I initially wanted to use branchless programming for this bit, but after a bit of digging online, it seems like with modern compiler optimization, the above code is as efficient as it gets for such an implementation.
SubMenu defines a state where the menu shows a list of parameters that can be changed for the user to select. Therefore, some sort of indication regarding which parameter is currently selected is needed, i.e. a cursor of sorts. The highlighted bit above implements this functionality by inserting a small arrow at the beginning of the row.
I considered doing a custom character thing for the indicator arrow, but if we look at the datasheet of the HD44780, there is the character map below (for ROM Code A00, if you try this and the character comes out different, your HD44780 is ROM Code A02 and has a different character map)
Which, conveniently has nice little arrows with code 0b01111110. That is 0x7E, so all we need to do is to insert this byte at the start of the selected row’s string before printing. The way to do this in C is by using the backslash, i.e. \x7E.
/*begin CC encoder handler*/for(inti=0;i<4;i++){//send encoder CC Values
if(encoderValues[i]!=lastEncoderValues[i]){//encoder was rotated
int8_tincrement=encoderValues[i]>lastEncoderValues[i]?1:-1;clampedIncrement(&MidiCCValues[MidiCCEncoderLUT[i]],increment*EncoderSpeed[i],0,127);MidiCC(MidiChannel,MidiCCEncoderLUT[i],MidiCCValues[MidiCCEncoderLUT[i]]);if(status==Status){snprintf(LCDQueueTop,17,"CC %d",MidiCCEncoderLUT[i]);LCDTopQueued=1;snprintf(LCDQueueBottom,17,"%-16d",MidiCCValues[MidiCCEncoderLUT[i]]);LCDBottomQueued=1;}lastEncoderValues[i]=encoderValues[i];}}/*end CC encoder handler*//*begin ADC handler*/for(intj=1;j<4;j++){inti=j-1;intcurrentADC=ADC1ReadVal(j);//IIR filter https://kiritchatterjee.wordpress.com/2014/11/10/a-simple-digital-low-pass-filter-in-c/
//but we are working in 12 bit fixed point anyways
adcSmooth[i]=(adcSmooth[i]<<filterBeta)-adcSmooth[i];adcSmooth[i]+=currentADC;adcSmooth[i]=adcSmooth[i]>>filterBeta;currentADC=adcSmooth[i]>>5;//convert the filter output to 7 bit, and store it currentADC
//Note: lastFaderValues[i] is 7 bit
//Note: this is NOT division for signed ints, due to the sign bit in front
if(lastFaderValues[i]!=currentADC){// this particular ADC Channel has been updated
MidiCCValues[MidiCCFaderLUT[i]]=currentADC&0x7f;//mask off only last 7 bits
MidiCC(MidiChannel,MidiCCFaderLUT[i],MidiCCValues[MidiCCFaderLUT[i]]);if(status==Status){snprintf(LCDQueueTop,17,"CC %d",MidiCCFaderLUT[i]);LCDTopQueued=1;snprintf(LCDQueueBottom,17,"%-16d",MidiCCValues[MidiCCFaderLUT[i]]);LCDBottomQueued=1;}lastFaderValues[i]=currentADC;}}/*end ADC handler*/
This next bit handles all the inputs that send MIDI CC messages, which are the 4 encoders and 4 (3) faders.
Since we have two ways of altering the value of a given MIDI CC channel, there is the 128 element array MidiCCValues[] that keeps track of everything. This also allows the MIDI CC value of each channel to be retained if the user changes the CC channel assignment for any of the encoders, and thus ensures that the intensity of the LED accurately reflects the value of the currently assigned channel. MidiCC()adds the relevant MIDI CC message to the USB FIFO (see Midi.c
).
The ADC of the STM32F103 is 12 bits (check ADC.c
for a really basic way to read from the ADC), so we need to discard the final 5 bits.
Also, as I kinda expected, the subpar construction techniques I used made the ADC readings really noisy. I experimented with rolling average filters, simple averaging and IIR filters to try to stabilize the ADC readings, and in the end IIR gave a vastly superior result, especially with the filter beta cranked.
However, it can’t be set too high, since at high filter beta values the output of the filter takes a really long time to approach the set value, and for our case no amount of fader manipulation would allow it to reach the max MIDI CC value of 127 in the end, so you need to experiment with the beta value to find the perfect balance. As it stands the value is configurable via the menu tho, so finding the best value shouldn’t be too inconvenient.
In any case, if the user is not currently in the menu (status is Status), we display the most recently modified CC channel and its value on the LCD to give the user some feedback.
//scan key matrix
for(inti=0;i<4;i++){GPIOA->BRR=(0b1111<<4);//clear all of PA 4,5,6,7
GPIOA->BSRR=(1<<(4+i));//energize the ith row
currentKeyMatrix|=((((GPIOB->IDR)>>3)&0b11111)<<(5*i));//hmmmmmmmmm
}//a key was pressed
if(currentKeyMatrix!=lastKeyMatrix){//handle keys here
for(inti=0;i<4;i++){LEDMatrix[3-i]&=0xf0;//clear the greens
LEDMatrix[3-i]|=(currentKeyMatrix>>((5*i)+1))&0b1111;//LEDMatrix[3-i] = (1<<i); //FRAK ZERO INDEXING alkfjngkjkfla (originally the idiot me had 4-i)
//hmmm, but on a more serious note tho, why is this array out of bounds not detected... that's definitely something to keep in mind
}for(inti=0;i<4;i++){//function to drive the LED's
LEDMatrixBuffer[i*4]=0b1111;//clear all pins first to prevent ghosting
LEDMatrixBuffer[i*4+1]=0x00;LEDMatrixBuffer[i*4+2]=~(1<<i);LEDMatrixBuffer[i*4+3]=LEDMatrix[i];}for(inti=0;i<20;i++){//iterate through all 20 bits and send out Midi Note messages as necessary
if((currentKeyMatrix&(1<<i))&&((lastKeyMatrix&(1<<i))==0)){//this key was pressed
MidiNoteOn(MidiChannel,MidiNoteLUT[i],MidiNoteVelo);}elseif((lastKeyMatrix&(1<<i))&&((currentKeyMatrix&(1<<i))==0)){MidiNoteOff(MidiChannel,MidiNoteLUT[i],0);}}lastKeyMatrix=currentKeyMatrix;}currentKeyMatrix=0;//start afresh
Notes are triggered via the 4×4 key matrix, and also through the 4 CC encoder pushbuttons. The code to implement that functionality is quite simple and self explanatory.
Of course, in programming mistakes do happen, and you can see one such instance in the snippet above. Before this bit, I actually thought that compilers will give warnings for array out of bounds errors, but it turns out that if you don’t directly try to access elements of an array that don’t exist (as is the case above with the array element controlled by the for loop), then the compiler doesn’t notice. Maybe static code analysis would have alerted me? That’d be a topic for another day…
With the key matrix being scanned once every time the loop executes, I found that contact bounce isn’t an issue at all, hence the complete lack of debouncing. Even if it were, the capacitive behavior of the input pins
would probably help smooth it out anyways. I found that out of the box, the Gateron yellow switches and also the buttons on the Bourns encoders didn’t really have too much contact bounce, so everything was fine.
if(!isLCDPrinting){//update LCD here
//assumption: at most only one parameter will be changed with each loop, therefore, only allow for 1 queued write
if(LCDTopQueued){LCDPrintStringTop(LCDQueueTop);LCDTopQueued=0;}elseif(LCDBottomQueued){LCDPrintStringBottom(LCDQueueBottom);LCDBottomQueued=0;}}}
Finally, we are at the end of the loop. Since we’ve done all the MIDI-ing and LCD-print-queueing, we can now call the functions to print whatever is queued on the LCD itself if the I2C bus is currently not doing anything. Check out Part 1B
for more details on how this works under the hood.
Phew! There we go, that’s all the firmware I have written for this thing. It took literally months of coding on and off (thanks STEP II) to arrive at this point, but I must say, I have learned a ton pushing through all the bugs and challenges that presented themselves along the way, and I hope you have learned somthing too!
Before you go, there will be one more post in this series, because there is so much stuff that I want to add to this project, yet didn’t have the time for. Stick around for the brief discussion on that.
Thank you for coming along with me on this journey!