VUSB Morse Keyboard       

by Ralf Beesner, DK5BU                      
Elektronik-Labor   Projekte   AVR 




Summary

There are some folks, smarter than me, who developed two USB software libraries that empower a simple, cheap AtTiny or AtMega micro controller to work as a USB device.

Of course, Atmel sells some bigger AVR controllers with a hardware USB interface, but using the software  libraries, there is no need to fiddle around with 64pin-SMD devices. Many projects use a simple AtTiny 45 in an 8-pin DIP package, a 20-pin AtTiny 2313 or an AtMega 8 in a 28-pin DIL-package.

In recent years, I built some USB gadgets (developed by other people also smarter than me) and made only small  changes in the source code to make the devices better fit my preferences.

Some of the projects emulate USB keyboards, which I find interesting, because you need no driver and no application software on the host PC (the devices are just recognized automatically).

The emulated keyboard may have special keys (that are not available on a standard keyboard) or serve a limited function, e.g. a slide show presenter with only two keys for "forward" and "backward".

The latest projects I built are two morse telegraphy keyboards, and I had to add some bigger chunks of own code.

So, whats a morse keyboard? It is a virtual USB keyboard with only one key (or two keys), which translates an operator's morse telegraphy input into keyboard key presses. If you know morse code, you may use it to write into a PC application (e.g. mailer, word processor or spread sheet program) by using a morse keyer. There are two flavors: the first one serves a straight morse key (a straight key works more or less like a doorbell switch), the second emulates an electronic morse keyer (elbug), which is operated using a paddle switch or (two) squeeze paddle switches.




Software

There are two AVR software USB libraries available, one is the VUSB library (http://www.obdev.at/products/vusb/index.html), the other is the BASCOM- SWUSB-Library (http://www.sloservers.com/swusb).

As I am a lousy programmer, I preferred BASCOM (http://www.mcselec.com) The Basic dialect is much easier than the C language, but has it's limits. One of the limits being the SWUSB-library, which is closed source (its core is a binary named swusb.lbx) and doesn't seem to be maintained any more by its creator.

So my BASCOM morse keyboard (http://www.elektronik-labor.de/AVR/USBbascomCW.html) didn't make me happy any more.

VUSB, however, is licensed under the GPL and seems to be regularly maintained by its creator.

Imho C is a rather horrible language for micro controllers (for example, it has no commands for direct bit manipulations, so you have to use cryptic workarounds), but the VUSB library is so useful that I tried to write two C programs. I wrote only the morse routines, most of the keyboard code is "borrowed" from the 4-Key-Keyboard (Author: Flip van den Berg - http://www.flipwork.nl).

My code may not be elegant, as I am a bloody beginner in C, but both versions are working reliably and fit easily into an 8-pin AtTiny45.

A morse decoder may be quite easy, if you use blocking code, e.g. delays, to decide if a morse element was a short one (dit) or a long one (dah), or if a pause was a space between morse elements or between characters.

But as the USB keeps the micro controller very busy, the interrupts and routines of the USB library must have priority. So the morse part has to work without delays, and therefore it's a bit more complicated, it is a sort of state machine.

Timer0 generates the morse sidetone, Timer1 generates overflows every 4 ms, and the overflow flag is polled by the program. When Timer1 has overflown, the program counts (increases) the number of overflows, and steps through the state machine. When a timing event has occurred (e.g. the length of a "dit" or "dah" was exceeded), the state machine jumps into the next state, until a morse character is completed.

The bit pattern of the morse character is looked up in a table. The table is provided by the include file "morsetable.h". It returns the ASCII value of the morse character. However, the HID (Human Interface Device) keyboard table is different from the ASCII table, so the ASCII byte has to be remapped into a HID byte. The mapping table is provided by the include file "hidkbd-xx.h". The HID byte is then handed over to the VUSB routines.

Unfortunately, all HID keyboards behave like US keyboards. On a German keyboard, when you press the "Z" key, the keyboard hands a "Y" to the PC and it is the task of the PC keyboard driver to convert it into a "Z", which is then presented to the application and to the user.

So the morse keybord must send an "Y", too, when it wants to present a "Z" to the user. Therefore, the HID mapping table has to be modified, if you don't want to switch your PC's keyboard driver to "US English" temporarily.

There are two HID tables provided - the standard HID table hidkbd-us.h and a patched version named hidkbd-de.h. One of them is chosen by an include statement at the beginning of main.c .

If you want a different keyboard layout, you must try to patch it by yourself.

By the way, on a Linux machine you may switch the keyboard language temporarily by the command "setxkbmap us", "setxkbmap de" or for example "setxkbmap fr".




Hardware

The electronic circuit is quite simple. The straight key version differs a little from the elbug version. The straight key version has one key and two switches. Switch 1 is used to chose the speed range - either from about 50 characters per minute (cpm) to 80 or 90 cpm, or from about 80 cpm to 120 cpm. Switch 2 activates the autoSpace function. As there was no free digital input pin available, I misused the analog input shared with the reset pin - as long as the voltage keeps above 50%, the chip does not reset (this trick avoids fusing the reset pin away).







The elbug version uses the analog input shared with the reset pin, too. The potentiometer decides the elbug morse speed, and the voltage is kept above 50% by a series resistor, which limits the voltage swing of the potentiometer.






There are two choices for the paddle - you may use an external one plugged into the 3.5 mm stereo jack or the two vertical dip buttons, which are arranged as a tiny provisional squeeze keyer.

The morse keyer inputs are debounced by hardware to keep the software more simple. So. the capacitors C1 and C2 are important and may not be omitted!

The USB part follows the standard recommendation by obdev.at.

The photo of my prototype hardware is the elbug version, but it differs a little, because I added a dip switch to convert the elbug hardware into the straight key hardware.




Compiling and Fusing

I didn't use the AVR Studio under Windows, but AVR-GCC under Linux. Anyway, AVR Studio uses WinAVR, which is the Windows version of AVR-GCC. So compilation should be possible without the AVR Studio mumble by just opening a shell (called "DOS window" by some folks), changing to the folder with the source code and typing  "make" and "make flash". I use an USBASP programmer, if you use a different one, you have to edit the programmer section of the Makefile.

When the AtTiny45 is delivered from factory, it is clocked by its internal 8 MHz RC oscillator and its 1/8 prescaler. It must be be re-fused to use its PLL clock (which is aligned to 16.5 MHz at runtime of the VUSB software). You may re-fuse it by the command "make fuses".

If you prefer a different program for changing the fuses, this are the correct bytes: HFUSE: 0xDD LFUSE: 0xE1 .




Usage

Using the elbug version, dits and dahs are formed by pressing the paddles, the right one produces dahs and the left one produces dits. They are automatically timed to the proper length. Pressing both paddles, produces a dit-dah-dit-dah-dit sequence. The Squeeze Mode is Mode A, it may be changed to Mode B by deleting an if-condition in man.c and recompiling the code (hopefully, as I don't have much practice with squeeze keyers).

The morse speed is adjusted by the potentiometer.

As the morse code has no characters for <return> and <blank>, two morse traffic abbreviations
are used:

Return: <kn>
Blank:  <as>

I added a 3rd one which is handy for jumping from left to right in an Excel or OpenOffice/LibreOffice calculation sheet:

TAB: <ab>

The device has an autoSpace function (autoSpace inserts blanks automatically when the pause was long enough to indicate a space between words). As there was no free I/O-pin any more, autoSpace is activated by pressing the dit paddle when the device is powered up.

Using the straight key version, switch 1 is used for roughly choosing the morse speed, switch 2 is used to activate the autoSpace function.

Download: VUSB-MorseKB-soft.zip



/* Name: main.c
License: GPL V2 (see /doc/GPL.txt)

This is a special 1-key keyboard - it "understands" morse code

The average speed may be chosen by a switch (60 chars per minute or 90 chars per minute).
As the morse code has no characters for <return> and <blank>, three morse traffic abbreviations
are used:

Return: <kn>
Blank: <as>
TAB: <ab>

A second switch activates an autoSpace function. One Blank is inserted automatically after the
correct waiting time between two words has passed.
As all digital I/Os are in use already, we use the Analog Input at ADC0/Reset. Normaly the ADC
value is 255, but when the switch is closed, a 560 kOhm resistor pulls down the ADC0/Reset pin
a little, so that the ADC value will be below 250.


Attiny45 Pins
PB0 USB D -
PB1 (OC0B) Sidetone Buzzer
PB2 USB D +
PB3 Morse Key
PB4 Speed switch

The code emulates a German keyboard. To emulate an american keyboard, you have to change
#include "keyboard-de.h" to #include "keyboard-us.h"

* the code is based on Project: 4-Key-Keyboard (Author: Flip van den Berg - www.flipwork.nl)
* and using the V-USB drivers from Objective Developments - http://www.obdev.at/products/vusb/index.html
*/

/*

fuse settings for ATTiny45/85:

EXTENDED: 0xFF
HIGH: 0xDD
LOW: 0xE1

*/

#include <avr/io.h>
#include <avr/wdt.h>
#include <avr/eeprom.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <util/delay.h>
#include <stdlib.h>

#include "usbdrv.h"
#include "hidkbd-de.h"
// #include "hidkbd-us.h"
#include "morsetable.h"


/* ------------------------------------------------------------------------- */

static uchar reportBuffer[8] = {0,0,0,0,0,0,0,0}; /* buffer for HID reports */

/* Reportbuffer format:

0 Modifier byte
1 reserved
2 keycode array (0)
3 keycode array (1)
4 keycode array (2)
5 keycode array (3)
6 keycode array (4)
7 keycode array (5)

<< This is the standard usb-keyboard reportbuffer. It allows for 6 simultaneous keypresses to be detected (excl. modifier keys). In this application we only use 1, so the last 5 bytes in this buffer will always remain 0. >>
<< I decided not to optimize this in order to make it easy to add extra keys that can be pressed simultaneously>>

Modifier byte: 8 bits, each individual bit represents one of the modifier keys.

bit0 LEFT CTRL (1<<0)
bit1 LEFT SHIFT (1<<1)
bit2 LEFT ALT (1<<2)
bit3 LEFT GUI (1<<3)
bit4 RIGHT CTRL (1<<4)
bit5 RIGHT SHIFT (1<<5)
bit6 RIGHT ALT (1<<6)
bit7 RIGHT GUI (1<<7)

an example of a reportBuffer for a CTRL+ALT+Delete keypress:

{((1<<0)+(1<<2)),0,76,0,0,0,0,0}

the first byte holds both the LEFT CTRL and LEFT modifier keys the 3rd byte holds the delete key (== decimal 76)

*/

static uchar idleRate;
static uchar newReport = 1;
static uchar modifier;
static uchar key;


/* ---------------------- morsepart ---------------------------------------- */


static uchar morseByte = 1;
static uchar serByte;
static uchar ditlen;
static uchar dahlen;
static uchar timeCnt;
static uchar state;
static uchar autoSpace;
static uchar ADCval;

#define tonefreq 112



static void morsedec(void){


if (state == 0){ // idle
timeCnt = 0;
if ((PINB & (1 << PB3)) == 0) { // key pressed, new morsechar starts
state = 1;
}
}

if (state == 1){ // morsechar has started
DDRB |= (1 << PB1); // buzzer on
if ((PINB & (1 << PB3)) != 0){ // key open, time for a dit has ended
if (timeCnt > ditlen){
state = 2;
morseByte = morseByte << 1; // shift charCode
}
else{ // this is for debouncing
DDRB &= ~(1 << PB1); // buzzer off
state = 0; // all to start
}
}

if ((PINB & (1 << PB3)) == 0){ // key pressed
DDRB |= (1 << PB1); // buzzer on
if (timeCnt > dahlen){ // time for a dah has ended
morseByte = morseByte << 1; // shift charcode
morseByte++; // dah -> lowest bit = 1
state = 2;
}
}
}

if (state == 2){ // this state means: dit is finished or dah still active
if ((PINB & (1 << PB3)) != 0){ // key open, dit or dah finished
DDRB &= ~(1 << PB1); // buzzer off
timeCnt = 0;
state = 3;
}
}

if (state == 3){
if ((PINB & (1 << PB3)) == 0){ // new dit or dah has started
state = 0;
}
if ((PINB & (1 << PB3)) != 0){ // key still open
if (timeCnt > dahlen){ // waiting time is greater than space between two morsechars
state = 4;
}
}
}


if (state == 4){ // morsechar complete
serByte = pgm_read_byte (&morsetable[morseByte]);
morseByte = 1;
DDRB &= ~(1 << PB1); // buzzer = 0
timeCnt = 0;
key = pgm_read_byte (&keycode[serByte]); // ascii to usb keycode
if (key > 128){ // values with shift modifier
key = key - 128;
modifier = 2;
}
else{
modifier = 0;
}
newReport = 0;

if (autoSpace > 0){
state = 5;
}
else{
state = 0;
}
}


if (state == 5){ // morsechar complete, autoSpace active
if ((PINB & (1 << PINB3)) != 0){ // keys still open
if (timeCnt > (dahlen*3 )){ // waiting time is greater than space between two morsechars
serByte = 32; // space
key = pgm_read_byte (&keycode[serByte]); // ascii to usb keycode
newReport = 0;
timeCnt = 0;
morseByte = 1;
state = 0; // ready!
}
}
else{ // if key is pressed
timeCnt = 0;
morseByte = 1;
state = 0; // ready!
}
}

}


/* ------------------------------------------------------------------------- */


PROGMEM char 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) ** Modifier Byte **
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x08, // REPORT_SIZE (8)
0x81, 0x03, // INPUT (Cnst,Var,Abs) ** Reserved Byte **
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) ** LED Report **
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x03, // REPORT_SIZE (3)
0x91, 0x03, // OUTPUT (Cnst,Var,Abs) ** LED Report Padding **
0x95, 0x06, // REPORT_COUNT (6) ** here we define the maximum number of simultaneous keystrokes we can detect **
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) ** Key arrays (6 bytes) **
0xc0 // END_COLLECTION
};


static void buildReport(void){

if(newReport == 0){
reportBuffer[0] = modifier;
reportBuffer[2] = key;
}

if(newReport != 0){
reportBuffer[0] = 0;
reportBuffer[2] = 0;
}
}


/* -------------------------------------------------------------------------------- */
/* ------------------------ interface to USB driver ------------------------ */
/* -------------------------------------------------------------------------------- */

uchar usbFunctionSetup(uchar 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 */
buildReport();
return sizeof(reportBuffer);
}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{
/* no vendor specific requests implemented */
}
return 0;
}

/* ------------------------------------------------------------------------- */
/* ------------------------ Oscillator Calibration ------------------------- */
/* ------------------------------------------------------------------------- */

/* Calibrate the RC oscillator to 8.25 MHz. The core clock of 16.5 MHz is
* derived from the 66 MHz peripheral clock by dividing. Our timing reference
* is the Start Of Frame signal (a single SE0 bit) available immediately after
* a USB RESET. We first do a binary search for the OSCCAL value and then
* optimize this value with a neighboorhod search.
* This algorithm may also be used to calibrate the RC oscillator directly to
* 12 MHz (no PLL involved, can therefore be used on almost ALL AVRs), but this
* is wide outside the spec for the OSCCAL value and the required precision for
* the 12 MHz clock! Use the RC oscillator calibrated to 12 MHz for
* experimental purposes only!
*/
static void calibrateOscillator(void)
{
uchar step = 128;
uchar trialValue = 0, optimumValue;
int x, optimumDev, targetValue = (unsigned)(1499 * (double)F_CPU / 10.5e6 + 0.5);

/* do a binary search: */
do{
OSCCAL = trialValue + step;
x = usbMeasureFrameLength(); /* proportional to current real frequency */
if(x < targetValue) /* frequency still too low */
trialValue += step;
step >>= 1;
}while(step > 0);
/* We have a precision of +/- 1 for optimum OSCCAL here */
/* now do a neighborhood search for optimum value */
optimumValue = trialValue;
optimumDev = x; /* this is certainly far away from optimum */
for(OSCCAL = trialValue - 1; OSCCAL <= trialValue + 1; OSCCAL++){
x = usbMeasureFrameLength() - targetValue;
if(x < 0)
x = -x;
if(x < optimumDev){
optimumDev = x;
optimumValue = OSCCAL;
}
}
OSCCAL = optimumValue;
}
/*
Note: This calibration algorithm may try OSCCAL values of up to 192 even if
the optimum value is far below 192. It may therefore exceed the allowed clock
frequency of the CPU in low voltage designs!
You may replace this search algorithm with any other algorithm you like if
you have additional constraints such as a maximum CPU clock.
For version 5.x RC oscillators (those with a split range of 2x128 steps, e.g.
ATTiny25, ATTiny45, ATTiny85), it may be useful to search for the optimum in
both regions.
*/

void hadUsbReset(void)
{
calibrateOscillator();
eeprom_write_byte(0, OSCCAL); /* store the calibrated value in EEPROM byte 0*/
}


/* ------------------------------------------------------------------------- */
/* --------------------------------- main ---------------------------------- */
/* ------------------------------------------------------------------------- */

int main(void)
{
uchar i;
uchar calibrationValue;

do {} while (!eeprom_is_ready());
calibrationValue = eeprom_read_byte(0); /* calibration value from last time */
if(calibrationValue != 0xff){
OSCCAL = calibrationValue;
}

usbInit();
usbDeviceDisconnect(); /* enforce re-enumeration, do this while interrupts are disabled! */
i = 0;
while(--i){ /* fake USB disconnect for > 250 ms */
wdt_reset();
_delay_ms(1);
}
usbDeviceConnect();

wdt_enable(WDTO_2S);

/* turn on internal pull-up resistors */
PORTB |= (1 << PB3) | (1 << PB4); // activates pullups PB3 and PB4
ACSR |= (1 << ACD); // analog comparator off
DIDR0 |= (1 << AIN1D); // deactivates digital input PB1

// Timer0 generates the morse tone at PB1 (OC0B)

TCCR0A |= (1 << COM0B1) | (1 << WGM01) | (1 << WGM00) ; // fast PWM at OC0B Pin, maximum value = OCR0A
TCCR0B |= (1 << WGM02) | (1 << CS02) ; // Prescaler / 256
OCR0A = tonefreq;

// Timer1 init
TCCR1 = 0x09; // select clock: 16.5M/256 -> overflow rate = 16.5M/(256*256) = 252 Hz -> 4 ms

ADMUX = 0x20; // select ADC channel (ADC0) / PB5 (Reset), 8 bit resolution
ADCSRA |= (1 << ADEN) | (1 << ADSC) | (1 << ADATE) | (1 << ADPS2); // ADC enabled, free running, clock prescaler / 16


_delay_ms(100); // waiting for high level to arrive at PB3


sei();

for(;;){ // main event loop
wdt_reset();
usbPoll();

if ((PINB & (1<<PB4)) == 0){ // lowest possible speed 60 chars / minute , 50 ms ditlen
ditlen = 12; // 12*4 ms
dahlen = 36;
}
else{ // lowest possilbe speed 40 chars / minute -> 75 ms ditlen
ditlen = 18; // 18*4 ms
dahlen = 54;
}

ADCval = ADCH; // 166 ... 255
if (ADCval < 250){
autoSpace = 1; // ditLen 90 ... 0
}
else{
autoSpace = 0;
}

if ((TIFR & (1 << TOV1)) != 0){ // Timer1 Overflow
TIFR |= (1 << TOV1); // Resets Timer1
timeCnt++;
morsedec();

}
if(usbInterruptIsReady() && newReport == 0){ // we may send another report
buildReport();
usbSetInterrupt(reportBuffer, sizeof(reportBuffer));
newReport = 1;
}
if(usbInterruptIsReady() && newReport == 1){ // we send an empty report to stop repetitions
buildReport();
usbSetInterrupt(reportBuffer, sizeof(reportBuffer));
newReport = 2; // we silence USB reports
}
}
return 0;
}





Elektronik-Labor   Projekte   AVR