The ASCII keyboard for an ADDS Terminal from 1983.
The keyboard communicates using a special serial protocol. The keyboard is powered with 12VDC.
| RJ | Color | Signal | AVR |
|---|---|---|---|
| 1 | Black | +12V | |
| 2 | Red | Data | PD0 |
| 3 | Green | Ground | GND |
| 4 | Yellow | PE |
The ASCII keyboard for an ADDS Terminal from 1983.
The keyboard communicates using a special serial protocol. The keyboard is powered with 12VDC.
| RJ | Color | Signal | AVR |
|---|---|---|---|
| 1 | Black | +12V | |
| 2 | Red | Data | PD0 |
| 3 | Green | Ground | GND |
| 4 | Yellow | PE |
| import sigrokdecode as srd | |
| from collections import namedtuple | |
| class Ann: | |
| SHORT, LONG, MASKED, CODE, ASCII = range(5) | |
| Pulse = namedtuple('Pulse', 'ss es is_long') | |
| class Decoder(srd.Decoder): | |
| api_version = 3 | |
| id = 'adds_viewpoint' | |
| name = 'VPT' | |
| longname = 'ADDS Viewpoint' | |
| desc = 'ADDS Viewpoint keyboard interface.' | |
| license = 'mit' | |
| inputs = ['logic'] | |
| outputs = [] | |
| tags = ['PC'] | |
| channels = ( | |
| {'id': 'data', 'name': 'Data', 'desc': 'Data line'}, | |
| {'id': 'mask', 'name': 'Mask', 'desc': 'Mask line for own strobes'}, | |
| ) | |
| annotations = ( | |
| ('short', 'Short'), | |
| ('long', 'Long'), | |
| ('masked', 'Masked'), | |
| ('code', 'Code'), | |
| ('ascii', 'ASCII'), | |
| ) | |
| annotation_rows = ( | |
| ('bits', 'Bits', (0, 1, 2)), | |
| ('fields', 'Fields', (3,)), | |
| ('ascii', 'ASCII', (4,)), | |
| ) | |
| def __init__(self): | |
| self.reset() | |
| def metadata(self, key, value): | |
| if key == srd.SRD_CONF_SAMPLERATE: | |
| self.samplerate = value | |
| def reset(self): | |
| self.bits = [] | |
| self.bitcount = 0 | |
| def start(self): | |
| self.out_ann = self.register(srd.OUTPUT_ANN) | |
| def next_bit(self, ss, es, masked): | |
| if masked: | |
| self.put(ss, es, self.out_ann, [Ann.MASKED, ['Masked', 'M']]) | |
| self.reset() | |
| return | |
| length = (es - ss) / self.samplerate | |
| is_long = length > 100e-6 | |
| self.bits.append(Pulse(ss, es, is_long)) | |
| self.bitcount += 1 | |
| if self.bitcount < 8: | |
| return | |
| for i in range(self.bitcount): | |
| bit = self.bits[i] | |
| ann = [Ann.LONG, ['Long', 'L']] if bit.is_long else [Ann.SHORT, ['Short', 'S']] | |
| self.put(bit.ss, bit.es, self.out_ann, ann) | |
| word = 0 | |
| for i in range(8): | |
| if self.bits[i].is_long: | |
| word |= (1 << i) | |
| code = [Ann.CODE, ['Code: %02X' % word, 'C: %02X' % word, '%02X' % word]] | |
| self.put(self.bits[0].ss, self.bits[7].es, self.out_ann, code) | |
| if word >= 32 and word < 127: | |
| ascii = [Ann.ASCII, ['ASCII: %c' % word, 'A: %c' % word, '%c' % word]] | |
| self.put(self.bits[0].ss, self.bits[7].es, self.out_ann, ascii) | |
| self.reset() | |
| def decode(self): | |
| while True: | |
| masked = self.wait({0: 'f'})[1] | |
| ss = self.samplenum | |
| self.wait({0: 'r'}) | |
| es = self.samplenum | |
| self.next_bit(ss, es, masked) |
| #define wait_us delayMicroseconds | |
| #define timer_read32 millis | |
| // Bidirectional data line on PD0. | |
| #define DATA_DDR DDRD | |
| #define DATA_PIN PIND | |
| #define DATA_PORT PORTD | |
| #define DATA_MASK (1 << 0) | |
| #define LED_DDR DDRC | |
| #define LED_PORT PORTC | |
| #define LED_MASK (1 << 7) | |
| // Arduino's micros() requires interrupts. So Timer3 is set up to count once every 16us. | |
| #define SHORT_MIN_WIDTH 1 | |
| #define LONG_MIN_WIDTH 6 | |
| #define LONG_MAX_WIDTH 12 | |
| #define XMIT_WAIT_US 26 | |
| #define POLL_INTERVAL_MS 10 | |
| #define QUEUE_SIZE 16 | |
| static uint16_t char_queue[QUEUE_SIZE]; | |
| static uint8_t char_queue_in, char_queue_out; | |
| static inline void queue_clear(void) { | |
| char_queue_in = char_queue_out = 0; | |
| } | |
| static inline bool queue_is_empty(void) { | |
| return (char_queue_in == char_queue_out); | |
| } | |
| static inline bool queue_is_full(void) { | |
| // One entry wasted to be able to check this easily. | |
| return (((char_queue_in + 1) % QUEUE_SIZE) == char_queue_out); | |
| } | |
| static inline uint16_t queue_remove(void) { | |
| uint16_t frame = char_queue[char_queue_out]; | |
| char_queue_out = (char_queue_out + 1) % QUEUE_SIZE; | |
| return frame; | |
| } | |
| static inline void queue_add(uint16_t frame) { | |
| char_queue[char_queue_in] = frame; | |
| char_queue_in = (char_queue_in + 1) % QUEUE_SIZE; | |
| } | |
| ISR(INT0_vect) { | |
| uint16_t time = TCNT3; | |
| bool state = (DATA_PIN & DATA_MASK) != 0; | |
| static uint16_t pulse_start_time; | |
| if (state) { | |
| // Going high, end of a bit pulse. | |
| uint16_t delta = time - pulse_start_time; | |
| static uint8_t bit_count = 0; | |
| static uint8_t bits = 0; | |
| if (delta < SHORT_MIN_WIDTH || delta > LONG_MAX_WIDTH) { | |
| bit_count = 0; | |
| bits = 0; | |
| return; | |
| } | |
| if (delta >= LONG_MIN_WIDTH) { | |
| bits |= (1 << bit_count); | |
| } | |
| bit_count++; | |
| if (bit_count >= 8) { | |
| if (!queue_is_full()) { | |
| queue_add(bits); | |
| } | |
| bit_count = 0; | |
| bits = 0; | |
| LED_PORT &= ~LED_MASK; | |
| } | |
| } else { | |
| // Going low, start of bit pulse. | |
| pulse_start_time = time; | |
| LED_PORT |= LED_MASK; | |
| } | |
| } | |
| // I am not sure why only these 16 keys send what seem to be raw codes with shift bits. | |
| static const uint8_t PROGMEM shifted[3][16] = { | |
| { '2', '3', '4', '6', 'q', 'w', 'y', 'a', 'z', 'm', '`', '[', ']', '\\', ';', '\'' }, | |
| { '@', '#', '$', '^', 'Q', 'W', 'Y', 'A', 'Z', 'M', '~', '{', '}', '|', ':', '"' }, | |
| { 0x12, 0x13, 0x14, 0x16, 0x11, 0x17, 0x19, 0x01, 0x1A, 0x0D, 0x00, 0x1B, 0x1D, 0x1C, 0x1B, 0x07 } | |
| }; | |
| static void output_char(uint8_t ch) { | |
| static bool caps_lock = false; | |
| if (ch == 0xC4) { | |
| caps_lock = !caps_lock; | |
| return; | |
| } | |
| if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')) { | |
| if (!caps_lock) { | |
| ch ^= 0x20; | |
| } | |
| } else if (ch >= 0x80 && ch <= 0xAF) { | |
| uint8_t shift = (ch >> 4) - 0x08; | |
| if (caps_lock && (ch < 0xA0)) { | |
| shift ^= 1; | |
| } | |
| const uint8_t *pch = shifted[shift] + (ch & 0x0F); | |
| ch = pgm_read_byte(pch); | |
| } | |
| Serial.write(ch); | |
| } | |
| void setup() { | |
| Serial.begin(115200); | |
| while (!Serial) { | |
| } | |
| // Input pull-up to start. | |
| DATA_DDR &= ~DATA_MASK; | |
| DATA_PORT |= DATA_MASK; | |
| #if F_CPU != 16000000 | |
| #error Wrong prescalar for clock rate | |
| #endif | |
| // Timer3 count at 16us. | |
| TCCR3A = 0; // Normal | |
| TCCR3B = _BV(CS32); // Prescalar = 256 | |
| // Interrupt 0 on any edge. | |
| EIMSK |= (1 << INT0); | |
| EICRA |= (1 << ISC00); | |
| // Turn on LED while receiving. | |
| LED_DDR |= LED_MASK; | |
| LED_PORT &= ~LED_MASK; | |
| } | |
| void loop() { | |
| static uint8_t last_ch; | |
| static bool ch_received = false; | |
| static bool key_down = false; | |
| static uint32_t last_poll_time = 0; | |
| uint32_t now = timer_read32(); | |
| if (!queue_is_empty()) { | |
| uint8_t ch = queue_remove(); | |
| ch_received = true; | |
| if (!key_down || last_ch != ch) { | |
| output_char(ch); | |
| last_ch = ch; | |
| key_down = true; | |
| } | |
| } | |
| if (now - last_poll_time > POLL_INTERVAL_MS) { | |
| if (!ch_received) { | |
| key_down = false; | |
| } | |
| ch_received = false; | |
| last_poll_time = now; | |
| cli(); | |
| DATA_DDR |= DATA_MASK; | |
| DATA_PORT &= ~DATA_MASK; | |
| wait_us(XMIT_WAIT_US); | |
| DATA_DDR &= ~DATA_MASK; | |
| DATA_PORT |= DATA_MASK; | |
| sei(); | |
| } | |
| } |