/** * \file firmware/main.c * \brief Main functions for USB-keyboard * \author Ronald Schaten * \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 Objective Development 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 Spaceman Spiff's c64key, this software * is based on his ideas. * * Further credits go to xleave, 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 FaUl 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. * * (c) 2008 by Ronald Schaten - http://www.schatenseite.de */ #include #include #include #include #include #include #include #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; wdt_reset(); while (!usbInterruptIsReady()) { usbPoll(); } 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; case '\n': key.key = KEY_Return; 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; } /* ------------------------------------------------------------------------- */