Dulcimer/firmware/main.c

554 lines
21 KiB
C

/**
* \file firmware/main.c
* \brief Main functions for USB-keyboard
* \author Ronald Schaten <ronald@schatenseite.de>
* \version $Id: main.c,v 1.6 2008/07/15 05:16:41 rschaten Exp $
*
* License: GNU GPL v2 (see License.txt)
*/
/**
* \mainpage Dulcimer
*
* \section sec_intro Introduction
*
* A computer keyboard can be a very personal utensil. Especially if it is an
* extraordinary well built one, like for example the IBM Model M. The Model M
* design dates back to 1984, but it still has many fans even nowadays. It came
* with the usual keyboard connectors. First the old 5-pin one, later a PS/2
* plug. Unfortunately is that, at least to my knowledge, they never released a
* version with USB.
*
* A friend of mine knew that I already had built other USB-devices, and one of
* them even acted as a keyboard (it isn't really a keyboard, but that's a
* different story... ;-) ). He is a big fan of the Model M, so he asked if I
* could put new life in one of his old keyboards, which had a broken circuit
* inside. And this is the result...
*
* \subsection sec_technique Hard- and Software
*
* The main part of a computer keyboard circuit is the key matrix. You can
* imagine it as a number of keys, placed on a raster of horizontal (rows) and
* vertical (columns) wires. In the case of a Model M keyboard, we have a
* matrix of 8x16 lines. Eight columns in 16 rows, or the other way around,
* depending on how you look at it. Each key is connected to one column and one
* row. If you press the key, it will connect the column and the row on it's
* crossing of the lines.
*
* Connected to this matrix is a keyboard controller. That's a chip with a
* number of I/O-lines to detect the state of the matrix, and on the other side
* an interface that enables it to talk to the computer. Oh, and not to forget:
* it also has three output lines to drive the LEDs for Num-, Caps- and
* Scroll-lock.
*
* What I did in this project is, that I dumped the keyboard controller chip
* and its circuit, and replaced it by an ATmega32 and my own circuit. The
* ATmega scans the matrix for keyboard activity, controls the LEDs and talks
* to the computer.
*
* For further convenience, I added a boot-loader. With that, it is possible to
* update the keyboard's firmware without disassembling it, and without the
* need for a dedicated programmer.
*
* \subsection sec_hardware Other hardware?
*
* As mentioned, the controller in this project is just connected to an
* ordinary keyboard matrix. You can find this kind of matrix in all kinds of
* keyboards, from key-telephones over good old hardware like the Commodore
* C=64 or the Schneider CPC, keyboards with non-PC-connectors like those made
* by Sun, to modern hardware that could need a few more features.
*
* Till now, I just made a PCB layout for the IBM Model M, but I intend to
* modify at least a Sun keyboard. In order to do that, I expect having to
* refactor the key-scanning, since the key-matrix is not 16x8. The positions
* of the keys on the matrix will be different, I'll have to re-engineer that.
* And of course, I'll have to make another PCB.
*
* \subsection sec_features Features
*
* At the moment, the keyboard should be able to do everything that the average
* off-the-shelf-keyboard can do. But there are many features that are
* possible, regarding the fact that the ATmega32 is absolutely bored till now.
* You can think of 'magic keystrokes' that turn some hidden features on or
* off, like for example:
* - send complete phrases on one keystroke
* - 'autofire' feature on keys that don't repeat normally, for example Alt+F4
* - change keyboard layout without reconfiguring the computer
* - turn bouncing keys on or off, to annoy other people using your computer
* - random caps lock function
* - use arrow keys as mouse, without having to include a special driver in
* the OS.
*
* With a little tweaking on the hardware side, there should be even more
* possibilities:
* - turn the oldtimer-keyboard into a supermodern wireless bluetooth one
* - implement keylogger-funktionality, using for example an SD-card
* - include an USB-hub into the keyboard
*
* If you are just a little like me, it won't take you much brainstorming to
* come up with own useful -- or even better: useless -- ideas. ;-)
*
* \section sec_install Building and installing
*
* Both, the bootloader and firmware are simply built with "make". You may need
* to customize both makefiles to fit to your system. If you don't want to add
* new features, you don't need to build the software yourself. You can use the
* hex-files included in this package.
*
* \subsection sec_boot Bootloader
*
* I used the USBaspLoader from Objective Development, the same guys that wrote
* the AVR-USB-driver: http://www.obdev.at/products/avrusb/usbasploader.html
*
* The reason why I chose this over some other available USB-bootloaders is,
* that this one emulates a common ISP-programmer that is supported by avrdude.
* In this way, the same program can be used to program the chip that is used
* without a bootloader.
*
* To prepare the ATmega32, you have to connect it to your computer with the
* ISP-programmer of your choice and modify the makefile according to that.
* Then you enter the bootloader-directory and enter the following line:
*
* \code
* make fuse && make flash && make lock
* \endcode
*
* With 'fuse' you prepare the fuse-bits of your AVR, 'flash' transfers the
* bootloader to the device and 'lock' prevents you from overwriting the
* bootloader. Don't fear the locking: you can always reset it with your
* ordinary programmer. In fact, it is disabled in the moment you use your
* ordinary programmer to reflash the device, even without any special
* parameters. The locking only affects the bootloader behavior.
*
* Afterwards you can put the programmer back into the toolbox, you won't need
* it from here on.
*
* When you plug in the device while holding the minus-key on the number-keypad
* pressed, the keyboard indicates that it would like to get a new firmware by
* showing a running light on the LEDs. That firmware will be flashed over the
* normal USB-cable that the keyboard is connected with.
*
* \subsection sec_fw Firmware
*
* If you intend to recompile the firmware yourself, you will need avr-gcc and
* avr-libc (a C-library for the AVR controller). Please read the instructions
* at http://www.nongnu.org/avr-libc/user-manual/install_tools.html for how to
* install the GNU toolchain (avr-gcc, assembler, linker etc.) and avr-libc.
*
* Once you have the GNU toolchain for AVR microcontrollers installed, you can
* run "make" in the subdirectory "firmware".
*
* Afterwards -- or if you decided not to compile the firmware yourself -- you
* can flash it to the device:
*
* \code
* make program
* \endcode
*
* Remember that you have to start the bootloader at first: unplug the
* keyboard, hold the minus-key on the number-keypad pressed and replug it. If
* the modified keyboard is the only one within reach: good luck! ;-)
*
* \section sec_usage Usage
*
* Connect the keyboard to the USB-port. All LED should flash up to indicate
* that the device is initialized.
*
* Then you can use the keyboard as always. If additional features get
* implemented, you will be able to use them in their respective ways.
*
* \section sec_drawbacks Drawbacks
*
* I don't know if and how keyboard manufacturers face the problem of ghost
* keys, I didn't take special measurements for this. I hope that the engineers
* at IBM distributed the keys on the matrix in a way that minimizes this
* problem. Don't misunderstand: I haven't experienced that on this keyboard,
* but I know that it's a common problem on key-matrixes.
*
* \section sec_files Files in the distribution
*
* - \e Readme.txt: Documentation, created from the htmldoc-directory.
* - \e firmware: Source code of the controller firmware.
* - \e firmware/usbdrv: USB driver -- See Readme.txt in this directory for
* info.
* - \e bootloader: The USBaspLoader, properly configured for this project. I
* only modified the bootloaderconfig.h and the Makefile.
* - \e USBaspLoader.2008-02-05.tar.gz: The unmodified bootloader sources, for
* reference.
* - \e circuit: Circuit diagrams in PDF and KiCAD format. KiCAD is a free
* schematic- and layout-tool, you can learn more about it at its homepage:
* http://www.lis.inpg.fr/realise_au_lis/kicad/
* - \e License.txt: Public license for all contents of this project, except
* for the USB driver. Look in firmware/usbdrv/License.txt for further info.
* - \e Changelog.txt: Logfile documenting changes in soft-, firm- and
* hardware.
* - \e refman.pdf: Full documentation of the software.
*
* \section sec_thanks Thanks!
*
* I'd like to thank <b>Objective Development</b> for the possibility to use
* their driver for my project. In fact, this project wouldn't exist without
* the driver.
*
* I took great inspiration from <b>Spaceman Spiff</b>'s c64key, this software
* is based on his ideas.
*
* Further credits go to <b>xleave</b>, who etched the PCB for me, and also
* answered many stupid questions about electronics I had during the last few
* years.
*
* And of course I'd like to thank <b>FaUl</b> of the Chaostreff Dortmund who
* gave me the idea for this project.
*
* \section sec_license About the license
*
* My work - all contents except for the USB driver - is licensed under the GNU
* General Public License (GPL). A copy of the GPL is included in License.txt.
* The driver itself is licensed under a special license by Objective
* Development. See firmware/usbdrv/License.txt for further info.
*
* <b>(c) 2008 by Ronald Schaten - http://www.schatenseite.de</b>
*/
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <avr/wdt.h>
#include <util/delay.h>
#include <string.h>
#include <stdio.h>
#include "usbdrv.h"
#include "keycodes.h"
#include "tools.h"
#include "modelinterface.h"
/* ------------------------------------------------------------------------- */
/* ----------------------------- USB interface ----------------------------- */
/* ------------------------------------------------------------------------- */
static uint8_t reportBuffer[8]; ///< buffer for HID reports
static uint8_t oldReportBuffer[8]; ///< buffer for the last sent HID report
static uint8_t idleRate; ///< in 4ms units
static uint8_t protocolVer = 1; ///< 0 = boot protocol, 1 = report protocol
uint8_t expectReport = 0; ///< flag to indicate if we expect an USB-report
/** USB report descriptor (length is defined in usbconfig.h). The report
* descriptor has been created with usb.org's "HID Descriptor Tool" which can
* be downloaded from http://www.usb.org/developers/hidpage/ (it's an .exe, but
* it even runs under Wine).
*/
char PROGMEM usbHidReportDescriptor[USB_CFG_HID_REPORT_DESCRIPTOR_LENGTH] = {
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x06, // USAGE (Keyboard)
0xa1, 0x01, // COLLECTION (Application)
0x05, 0x07, // USAGE_PAGE (Keyboard)
0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl)
0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x75, 0x01, // REPORT_SIZE (1)
0x95, 0x08, // REPORT_COUNT (8)
0x81, 0x02, // INPUT (Data,Var,Abs)
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x08, // REPORT_SIZE (8)
0x81, 0x03, // INPUT (Cnst,Var,Abs)
0x95, 0x05, // REPORT_COUNT (5)
0x75, 0x01, // REPORT_SIZE (1)
0x05, 0x08, // USAGE_PAGE (LEDs)
0x19, 0x01, // USAGE_MINIMUM (Num Lock)
0x29, 0x05, // USAGE_MAXIMUM (Kana)
0x91, 0x02, // OUTPUT (Data,Var,Abs)
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x03, // REPORT_SIZE (3)
0x91, 0x03, // OUTPUT (Cnst,Var,Abs)
0x95, 0x06, // REPORT_COUNT (6)
0x75, 0x08, // REPORT_SIZE (8)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x65, // LOGICAL_MAXIMUM (101)
0x05, 0x07, // USAGE_PAGE (Keyboard)
0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated))
0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application)
0x81, 0x00, // INPUT (Data,Ary,Abs)
0xc0 // END_COLLECTION
};
/**
* This function is called whenever we receive a setup request via USB.
* \param data[8] eight bytes of data we received
* \return number of bytes to use, or 0xff if usbFunctionWrite() should be
* called
*/
uint8_t usbFunctionSetup(uint8_t data[8]) {
usbRequest_t *rq = (void *)data;
usbMsgPtr = reportBuffer;
if ((rq->bmRequestType & USBRQ_TYPE_MASK) == USBRQ_TYPE_CLASS) {
// class request type
if (rq->bRequest == USBRQ_HID_GET_REPORT) {
// wValue: ReportType (highbyte), ReportID (lowbyte)
// we only have one report type, so don't look at wValue
return sizeof(reportBuffer);
} else if (rq->bRequest == USBRQ_HID_SET_REPORT) {
if (rq->wLength.word == 1) {
// We expect one byte reports
expectReport = 1;
return 0xff; // Call usbFunctionWrite with data
}
} else if (rq->bRequest == USBRQ_HID_GET_IDLE) {
usbMsgPtr = &idleRate;
return 1;
} else if (rq->bRequest == USBRQ_HID_SET_IDLE) {
idleRate = rq->wValue.bytes[1];
} else if (rq->bRequest == USBRQ_HID_GET_PROTOCOL) {
if (rq->wValue.bytes[1] < 1) {
protocolVer = rq->wValue.bytes[1];
}
} else if(rq->bRequest == USBRQ_HID_SET_PROTOCOL) {
usbMsgPtr = &protocolVer;
return 1;
}
} else {
// no vendor specific requests implemented
}
return 0;
}
/**
* The write function is called when LEDs should be set. Normally, we get only
* one byte that contains info about the LED states.
* \param data pointer to received data
* \param len number ob bytes received
* \return 0x01
*/
uint8_t usbFunctionWrite(uchar *data, uchar len) {
if (expectReport && (len == 1)) {
setLeds(data[0]);
}
expectReport = 0;
return 0x01;
}
/**
* Send a single report to the computer. This function is not used during
* normal typing, it is only used to send non-pressed keys to simulate input.
* \param mode modifier-byte
* \param key key-code
*/
void usbSendReport(uint8_t mode, uint8_t key) {
// buffer for HID reports. we use a private one, so nobody gets disturbed
uint8_t repBuffer[8] = { 0, 0, 0, 0, 0, 0, 0, 0 };
repBuffer[0] = mode;
repBuffer[2] = key;
while (!usbInterruptIsReady()); // wait
usbSetInterrupt(repBuffer, sizeof(repBuffer)); // send
}
/* ------------------------------------------------------------------------- */
/**
* This structure can be used as a container for a single 'key'. It consists of
* the key-code and the modifier-code.
*/
typedef struct {
uint8_t mode;
uint8_t key;
} Key;
/**
* Convert an ASCII-character to the corresponding key-code and modifier-code
* combination.
* \param character ASCII-character to convert
* \return structure containing the combination
*/
Key charToKey(char character) {
Key key;
// initialize with reserved values
key.mode = MOD_NONE;
key.key = KEY_Reserved;
if ((character >= 'a') && (character <= 'z')) {
// a..z
key.key = (character - 'a') + 0x04;
} else if ((character >= 'A') && (character <= 'Z')) {
// A..Z
key.mode = MOD_SHIFT_LEFT;
key.key = (character - 'A') + 0x04;
} else if ((character >= '1') && (character <= '9')) {
// 1..9
key.key = (character - '1') + 0x1E;
}
// we can't map the other characters directly, so we switch...
switch (character) {
case '0':
key.key = KEY_0; break;
case '!':
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_1; break;
/*
case '@':
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_2; break;
case '#':
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_3; break;
*/
case '$':
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_4; break;
case '%':
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_5; break;
case '^':
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_6; break;
case '&':
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_7; break;
case '*':
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_8; break;
case '(':
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_9; break;
case ')':
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_0; break;
case ' ':
key.key = KEY_Spacebar; break;
case '-':
key.key = KEY_minus; break;
case '_':
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_minus; break;
case '=':
key.key = KEY_equals; break;
case '+':
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_equals; break;
case '[':
key.key = KEY_lbracket; break;
case '{':
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_lbracket; break;
case ']':
key.key = KEY_rbracket; break;
case '}':
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_rbracket; break;
case '\\':
key.key = KEY_backslash; break;
case '|':
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_backslash; break;
case '#':
key.key = KEY_hash; break;
case '@':
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_hash; break;
case ';':
key.key = KEY_semicolon; break;
case ':':
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_semicolon; break;
case '\'':
key.key = KEY_apostroph; break;
case '"':
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_apostroph; break;
case '`':
key.key = KEY_grave; break;
case '~':
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_grave; break;
case ',':
key.key = KEY_comma; break;
case '<':
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_comma; break;
case '.':
key.key = KEY_dot; break;
case '>':
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_dot; break;
case '/':
key.key = KEY_slash; break;
case '?':
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_slash; break;
}
if (key.key == KEY_Reserved) {
// still reserved? WTF? return question mark...
key.mode = MOD_SHIFT_LEFT;
key.key = KEY_slash;
}
return key;
}
/**
* Send a key to the computer, followed by the release of all keys. This can be
* used repetitively to send a string.
* \param keytosend key structure to send
*/
void sendKey(Key keytosend) {
usbSendReport(keytosend.mode, keytosend.key);
usbSendReport(0, 0);
}
/**
* Send a string to the computer. This function converts each character of an
* ASCII-string to a key-structure and uses sendKey() to send it.
* \param string string to send
*/
void sendString(char* string) {
for (uint8_t i = 0; i < strlen(string); i++) {
Key key = charToKey(string[i]);
sendKey(key);
}
}
/**
* Main function, containing the main loop that manages timer- and
* USB-functionality.
* /return the obligatory integer that nobody cares about...
*/
int main(void) {
uint8_t updateNeeded = 0;
uint8_t idleCounter = 0;
wdt_enable(WDTO_2S);
hardwareInit();
usbInit();
sei();
memset(oldReportBuffer, 0, sizeof(oldReportBuffer)); // clear old report buffer
scankeys(reportBuffer, oldReportBuffer, sizeof(reportBuffer));
while (1) {
// main event loop
wdt_reset();
usbPoll();
updateNeeded = scankeys(reportBuffer, oldReportBuffer, sizeof(reportBuffer)); // changes?
// check timer if we need periodic reports
if (TIFR & (1 << TOV0)) {
TIFR = (1 << TOV0); // reset flag
if (idleRate != 0) { // do we need periodic reports?
if(idleCounter > 4){ // yes, but not yet
idleCounter -= 5; // 22ms in units of 4ms
} else { // yes, it is time now
updateNeeded = 1;
idleCounter = idleRate;
}
}
}
// if an update is needed, send the report
if (updateNeeded && usbInterruptIsReady()) {
updateNeeded = 0;
usbSetInterrupt(reportBuffer, sizeof(reportBuffer));
memcpy(oldReportBuffer, reportBuffer, sizeof(oldReportBuffer));
}
}
return 0;
}
/* ------------------------------------------------------------------------- */