How to build your very own keyboard firmware
Posted: 02 Jan 2014, 13:08
This is a tutorial about building your own custom firmware with Hasu's keyboard firmware.
It may seem daunting to work with Hasu's wall of code but the files you need to modify are not many and you don't actually need to fully understand the code. I myself don't understand 100% of it, so if I write some bullshits I hope Hasu will chime in.
Development environment
First of all you need to set up the develpment environment. That depends on your system.
Unfortunately Windows is not the best development environment for this kind of things. My suggestion would be to install a minimal linux distribution on a virtual machine (VirtualBox is free) and follow the linux instructions below. Most of issues people are encountering are due to Windows. I you feel lucky and insist on using windows I believe your best option is to install AtmelStudio. I can't help you with that because I haven't personally used it, but it should contain all you need to compile your firmware.
On Mac get CrossPack and from the App Store install XCode.
On Linux you need to install the development tools (eg: on Ubuntu the package should be called "build-essential") and the AVR dev tools. Again, naming varies based on the distro, search for AVR in your package manager and you should find all you need (eg: for Ubuntu should be "gcc-avr gdb-avr binutils-avr avr-libc avrdude").
To actually burn the firmware in the controller you need a programmer. You can use Atmel's FLIP or dfu-programmer, but I find the easiest to use is the Teensy loader.
If you can't actually build the firmware yourself, don't worry, you can still make the modifications needed to the code and I bet someone here on the forum can send you the HEX file you'll be uploading to the keyboard (you still need to install the programmer above).
Get the code
Download and unzip Hasu's code from github. https://github.com/tmk/tmk_keyboard/archive/master.zip
Fortunately you can ignore most of the code there. Go to the keyboard/gh60 directory. We won't never leave that directory, I've told you it was easy.
Remember to check Hasu's repository once in a while, it is very active and it often gets updated.
Main config
Open Makefile file and check the following globals:
if you are using the Teensy++ change the MCU to at90usb1287. Also comment out (place a # at the beginning of the line) the NKRO_ENABLE feature. You don't need it, and it is not supported by LUFA. More about this later.
Now open the config.h file.
You can give custom values to MANUFACTURER, PRODUCT and DESCRIPTION, but that's completely optional. What's important is:
Change the values to respectively the number of rows and columns of your keyboard.
Setting up rows and columns
Now back to the teensy. Write down how you connected the rows/cols (and eventually the capslock LED) to the teensy's pins. Remember to avoid pin D6 that is dedicated to the teensy's internal LED.
Just for the sake of this tutorial let's say you have 15 cols and 5 rows. In this example the columns are connected like this:
Rows look like this instead:
Remember, this is just an example, your pin configuration might be different.
You can check the name of the pins from PJRC pinout page or reading them directly from the Teensy.
Open the matrix.c file. Search for the init_cols function and change it according to your pinout
It's easier than it looks. The name of the pins are composed by one letter and one number from 0 to 7. If you look at the list of columns, I have one pin starting with F and the number for that pin is 7. So to activate that pin I have to add the following to the list:
DDRF and PORTF are the hardware registers that control the port F pins. 1<<7 is a bitwise operator that picks the 8th bit of that port (remember we start counting from 0, so the 8th pin is actually number 7).
Let's proceed with the second port.
In this case we are on port B (DDRB and PORTB) and if you look at our cols schema you'll see that we are using the pins number B7, B6, B5, B4, B3, B2 and B1 (even if they are not consecutive on the matrix, it doesn't matter). We use | (or) to smartly react to any of the selected pins. 1<<7 means B7, 1<<6 means B6, and so on.
I believe at this point it should be clear how it works. Proceed with all the other columns.
Cool. We just initiated the columns. Now we have to read them. Search for the read_cols function:
This function associates each pin to its col. So
Tells the controller that if pin F7 (PINF...7) is active then we are on the first column (again zero based, 1<<0 means first column).
If pin B6 (PINB...6) is high then we are on the second column (1<<1). And so on. A piece of cake.
Now to the rows. Search the unselect_rows function.
Hasu chose a different syntax here. We are talking binary this time, but it's still pretty easy. If you look at the rows schema you'll see that we are using pins number 0, 1, 4, 5, 6 of the F port.
So we write to the F port with DDRF and PORTF like we did for the columns but to choose the port number we set it to 1. (actually we are unselecting them, but this is meant for newbie and I'm not going deeper into this).
0b just tells the compiler that we are talking binary. Then you have 8 zeros and ones. If you look closely you'll see that the 1s are --reading from right to left-- in the 1st, 2nd, 5th, 6th, 7th positions, or pin number 0, 1, 4, 5, 6 that are the columns that we are using for our keyboard.
Start counting from the right. The first bit is PIN0, the last is PIN7. Just set the pins you use to 1. Peachy, isn't it?
Now to selecting the rows. Search for the select_row function.
At this point it should pretty clear what's going on here.
case 0: means: "if we are on the first row".
Means: "select port F, bit 0".
Now modify your ports/pins according to your schema.
Lastly open the led.c file and change the port to the one you connected the LED to.
In this case we have the LED on pin D4. Let's say it were on pin F0 it would have been:
Okay, a lot to digest. That's enough for the first lesson. In the next session we will be customizing the actual key matrix and the function layers. If you have questions or you spotted some errors, just let me know.
Part 2 is just around the bend.
It may seem daunting to work with Hasu's wall of code but the files you need to modify are not many and you don't actually need to fully understand the code. I myself don't understand 100% of it, so if I write some bullshits I hope Hasu will chime in.
Development environment
First of all you need to set up the develpment environment. That depends on your system.
Unfortunately Windows is not the best development environment for this kind of things. My suggestion would be to install a minimal linux distribution on a virtual machine (VirtualBox is free) and follow the linux instructions below. Most of issues people are encountering are due to Windows. I you feel lucky and insist on using windows I believe your best option is to install AtmelStudio. I can't help you with that because I haven't personally used it, but it should contain all you need to compile your firmware.
On Mac get CrossPack and from the App Store install XCode.
On Linux you need to install the development tools (eg: on Ubuntu the package should be called "build-essential") and the AVR dev tools. Again, naming varies based on the distro, search for AVR in your package manager and you should find all you need (eg: for Ubuntu should be "gcc-avr gdb-avr binutils-avr avr-libc avrdude").
To actually burn the firmware in the controller you need a programmer. You can use Atmel's FLIP or dfu-programmer, but I find the easiest to use is the Teensy loader.
If you can't actually build the firmware yourself, don't worry, you can still make the modifications needed to the code and I bet someone here on the forum can send you the HEX file you'll be uploading to the keyboard (you still need to install the programmer above).
Get the code
Download and unzip Hasu's code from github. https://github.com/tmk/tmk_keyboard/archive/master.zip
Fortunately you can ignore most of the code there. Go to the keyboard/gh60 directory. We won't never leave that directory, I've told you it was easy.
Remember to check Hasu's repository once in a while, it is very active and it often gets updated.
Main config
Open Makefile file and check the following globals:
Code: Select all
TARGET = gh60_lufa
...
...
MCU = atmega32u4
...
...
#NKRO_ENABLE = yes
Now open the config.h file.
You can give custom values to MANUFACTURER, PRODUCT and DESCRIPTION, but that's completely optional. What's important is:
Code: Select all
#define MATRIX_ROWS 5
#define MATRIX_COLS 14
Setting up rows and columns
Now back to the teensy. Write down how you connected the rows/cols (and eventually the capslock LED) to the teensy's pins. Remember to avoid pin D6 that is dedicated to the teensy's internal LED.
Just for the sake of this tutorial let's say you have 15 cols and 5 rows. In this example the columns are connected like this:
Code: Select all
col: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
pin: F7 B6 B5 B4 D7 C7 C6 D3 D2 D1 D0 B7 B3 B2 B1
Code: Select all
row: 0 1 2 3 4
pin: F0 F1 F4 F5 F6
You can check the name of the pins from PJRC pinout page or reading them directly from the Teensy.
Open the matrix.c file. Search for the init_cols function and change it according to your pinout
Code: Select all
static void init_cols(void)
{
// Input with pull-up(DDR:0, PORT:1)
DDRF &= ~(1<<7);
PORTF |= (1<<7);
DDRB &= ~(1<<7 | 1<<6 | 1<<5 | 1<<4 | 1<<3 | 1<<2 | 1<<1);
PORTB |= (1<<7 | 1<<6 | 1<<5 | 1<<4 | 1<<3 | 1<<2 | 1<<1);
DDRD &= ~(1<<7 | 1<<3 | 1<<2 | 1<<1 | 1<<0 );
PORTD |= (1<<7 | 1<<3 | 1<<2 | 1<<1 | 1<<0 );
DDRC &= ~(1<<7 | 1<<6);
PORTC |= (1<<7 | 1<<6);
}
Code: Select all
DDRF &= ~(1<<7);
PORTF |= (1<<7);
Let's proceed with the second port.
Code: Select all
DDRB &= ~(1<<7 | 1<<6 | 1<<5 | 1<<4 | 1<<3 | 1<<2 | 1<<1);
PORTB |= (1<<7 | 1<<6 | 1<<5 | 1<<4 | 1<<3 | 1<<2 | 1<<1);
I believe at this point it should be clear how it works. Proceed with all the other columns.
Cool. We just initiated the columns. Now we have to read them. Search for the read_cols function:
Code: Select all
static matrix_row_t read_cols(void)
{
return (PINF&(1<<7) ? 0 : (1<<0)) |
(PINB&(1<<6) ? 0 : (1<<1)) |
(PINB&(1<<5) ? 0 : (1<<2)) |
(PINB&(1<<4) ? 0 : (1<<3)) |
(PIND&(1<<7) ? 0 : (1<<4)) |
(PINC&(1<<7) ? 0 : (1<<5)) |
(PINC&(1<<6) ? 0 : (1<<6)) |
(PIND&(1<<3) ? 0 : (1<<7)) |
(PIND&(1<<2) ? 0 : (1<<8)) |
(PIND&(1<<1) ? 0 : (1<<9)) |
(PIND&(1<<0) ? 0 : (1<<10)) |
(PINB&(1<<7) ? 0 : (1<<11)) |
(PINB&(1<<3) ? 0 : (1<<12)) |
(PINB&(1<<2) ? 0 : (1<<13)) |
(PINB&(1<<1) ? 0 : (1<<14));
}
Code: Select all
(PINF&(1<<7) ? 0 : (1<<0))
Code: Select all
(PINB&(1<<6) ? 0 : (1<<1))
Now to the rows. Search the unselect_rows function.
Code: Select all
static void unselect_rows(void)
{
// Hi-Z(DDR:0, PORT:0) to unselect
DDRF &= ~0b01110011;
PORTF &= ~0b01110011;
}
So we write to the F port with DDRF and PORTF like we did for the columns but to choose the port number we set it to 1. (actually we are unselecting them, but this is meant for newbie and I'm not going deeper into this).
0b just tells the compiler that we are talking binary. Then you have 8 zeros and ones. If you look closely you'll see that the 1s are --reading from right to left-- in the 1st, 2nd, 5th, 6th, 7th positions, or pin number 0, 1, 4, 5, 6 that are the columns that we are using for our keyboard.
Start counting from the right. The first bit is PIN0, the last is PIN7. Just set the pins you use to 1. Peachy, isn't it?
Now to selecting the rows. Search for the select_row function.
Code: Select all
static void select_row(uint8_t row)
{
// Output low(DDR:1, PORT:0) to select
switch (row) {
case 0:
DDRF |= (1<<0);
PORTF &= ~(1<<0);
break;
case 1:
DDRF |= (1<<1);
PORTF &= ~(1<<1);
break;
case 2:
DDRF |= (1<<4);
PORTF &= ~(1<<4);
break;
case 3:
DDRF |= (1<<5);
PORTF &= ~(1<<5);
break;
case 4:
DDRF |= (1<<6);
PORTF &= ~(1<<6);
break;
}
}
case 0: means: "if we are on the first row".
Code: Select all
DDRF |= (1<<0);
PORTF &= ~(1<<0);
Now modify your ports/pins according to your schema.
Lastly open the led.c file and change the port to the one you connected the LED to.
Code: Select all
void led_set(uint8_t usb_led)
{
if (usb_led & (1<<USB_LED_CAPS_LOCK)) {
// output low
DDRD |= (1<<4);
PORTD &= ~(1<<4);
} else {
// Hi-Z
DDRD &= ~(1<<4);
PORTD &= ~(1<<4);
}
}
Code: Select all
DDRF |= (1<<0);
PORTF &= ~(1<<0);
Part 2 is just around the bend.