Typical transputer reference designs use IMS C011 (or IMS C012) link adapter chips (from INMOS) to connect the transputer to a HOST (PC) where the iserver application downloads the boot code and afterwards provides storage/console services over the link.
Links use two types of messages: [11-bit message for 8-bit DATA] and [2-bit message for ACK]. The difference is the bit following the start bit (1=DATA, 0=ACK).

Links use 5V signals, idling at 0V. Links can operate at 5, 10, or 20 Mbps.
For my transputer retroshield I was going to do the same, but when I squinted my eyes and looked at the link protocol, I realized 9-bit inverted UART would do the trick. Teensy supports this mode. Within minutes, I was able to peek and poke the transputer memory using 9bit uart at 5MBps, using Serial5.write9bit() and Serial5.read().
Wait, it gets better - Teensy can run the UART at 20Mbps. With careful signal conditioning on the board, I can now reliably download BTL files (transputer bootable files) onto my transputer, and run the occam/C++ compilers, the famous F editor and the transputer minix, on the transputer itself. I am going to explain a special scenario one needs to be careful and how to work-around it. Otherwise, 9bit serial trick has been rock solid.
Famous F editor running on transputer shield:
hello.c compiled by transputer and executed:
Careful readers must have noticed the UART based ACK message is actually longer than what it is supposed to be. Will this cause a problem?
Is there a scenario where transputer sends 2-bit ACK and then immediately sends a DATA packet, before the 9-bit uart ACK reception is complete?
Actually, it turns out this happens in a very specific scenario: peeking INTERNAL memory using 5Mbps link and relatively fast CPU clock.
// PEEK($80000070) link0_send_data_noack(0x01); delayNanoseconds(500); link0_send_data_noack(0x70); delayNanoseconds(500); link0_send_data_noack(0x00); delayNanoseconds(500); link0_send_data_noack(0x00); delayNanoseconds(500); link0_send_data_noack(0x80); delayNanoseconds(500);Why?
For peek, the host PC sends PEEK Command ($01) and then 4 bytes of 32-bit address. As soon as address is received, the transputer will go read the memory and respond with the lowest significant byte. If you peek internal memory, which takes one processor cycle, and the processor clock is relatively fast, then transputer will transmit the lowest significant byte BEFORE 9-bit ACK uart reception is complete. Teensy will miss the start bit of the data, and things will get out of sync. Best scenario is data has a 1 which will cause an incorrect value read and ACK’ed, or worse scenario is data is $00, Teensy will never see a start-bit and won’t send ACK which means transputer and teensy will get stuck in a deadlock.
We can see this in the logic analyzer capture. At 5Mbps, the gap is 10 bits, which is less than the minimum 11-bits required for ACK reception. At 20Mbps, the gap increases to 19 bits, which is enough time.
Work-around?
Use 20Mbps baud rate. Link runs at 20Mbps and processor runs at 25MHz. Both are fast.
This reminds a me joke - man goes to the doctor and says, “doctor, if I bend my arm behind my head and try to touch my back, my shoulder hurts.” Doctor looks at the man for a moment and says, “don’t touch your back then.” :) So don’t use 5Mbps :)
If one really needs 5Mbps, increasing the UART baud rate to slightly above 5Mbps (eating into tolerances) might work. Running the processor slower might also help. 20Mbps works in retroshield, so I will leave 5Mbps solution to the hackers who really need it :)
Teensy libraries support 9bit and the INVERTED mode. This must be enabled in HardwareSerial.h in core packages. 9bit is disabled by default because receive buffers uses more RAM (byte vs word).
// Uncomment to enable 9 bit formats. These are default disabled to save memory. #define SERIAL_9BIT_SUPPORT // #ifdef SERIAL_9BIT_SUPPORT #define SERIAL_9N1 0x84 #define SERIAL_9E1 0x8E #define SERIAL_9O1 0x8F #define SERIAL_9N1_RXINV 0x94 #define SERIAL_9E1_RXINV 0x9E #define SERIAL_9O1_RXINV 0x9F #define SERIAL_9N1_TXINV 0xA4 #define SERIAL_9E1_TXINV 0xAE #define SERIAL_9O1_TXINV 0xAF #define SERIAL_9N1_RXINV_TXINV 0xB4 <<<=== TRANSPUTER LINK PROTOCOL #define SERIAL_9E1_RXINV_TXINV 0xBE #define SERIAL_9O1_RXINV_TXINV 0xBFSerial5 Initialization
Then it’s a matter of initializing the Serial port. UART supports 5Mbps with default clock settings. For 20 Mbps, we need to bump up the UART clock source to a faster one. 20Mbps solution is taken from Teensy4 Max Baud Rate discussion.
// ################################################## // Link0 initialization // ################################################## void link0_init(int speed) { switch(speed) { case LINK0_SPEED_5MBPS: Serial5.begin(5000000, SERIAL_9N1_RXINV_TXINV ); Serial5.flush(); Serial5.clear(); Serial.println("Link Emulation = 5.0 Mb/s"); break; case LINK0_SPEED_20MBPS: Serial5.begin(20000000, SERIAL_9N1_RXINV_TXINV ); CCM_CSCDR1=105450240; // Feed high clk to UART block. Serial5.flush(); Serial5.clear(); Serial.println("Link Emulation = 20.0 Mb/s"); break; default: Serial.println("ERR: unknown transputer link speed."); Serial.println(" halting..."); while(1); } }When transmitting packets to transputer, we need to take the inversion into account.
Transmit ACK
// ################################################## // Link0 Send ACK // ################################################## void link0_send_ack() { word c1 = 0x1FF; // invert 8bit data, shift <<1, or 1 (data indicator bit) Serial5.write9bit(c1); }Transmit data
// ################################################## // Link0 Send DATA // ################################################## void link0_send_data_noack(byte b) { word c1 = ((byte) (~b)) << 1; Serial5.write9bit(c1); delayNanoseconds(200*15); // Wait for transmission complete. // Remember to check for ACK before sending the next data byte. link0_tx_buffer_waiting_for_ack = true; }Link Receive
////////////////////////////////////////////////// // Process incoming ACK/DATA ////////////////////////////////////////////////// void link0_process_incoming() { if (!Serial5.available()) // Return if nothing is received. return; word msg = ~(Serial5.read()); // invert received data. byte msg_type = msg & 0x01; if (msg_type == 0) { // ACK received received_ack_count++; Serial.println("HW ACK received and put into queue."); } else { // DATA received byte data = (msg >> 1) & 0xFF; link0_add_to_rxbuffer(data); Serial.printf("SERIAL5: Received %02x\n", data); // We must send an ACK back to transputer link0_send_ack(); } return; }Above functions must be called periodically in the main loop. I am skipping the tx/rx buffer details. If you want to see detailed code, you can look at the retroshield code when released.
// ################################################## // Link0 RX/TX Handler // ################################################## void link0_rxtx() { link0_process_incoming(); if (link0_tx_buffer_waiting_for_ack) { link0_check_for_ack() } else if ( !link0_tx_buffer_empty() ) { link0_send_data_noack(link0_tx_buffer_get_front()); } }Finally, main Arduino loop():
// ################################################## // # Run transputer // ################################################## void loop() { while(1) { cpu_tick(); link0_rxtx(); // check link0 messages and put into rx/tx buffers. iserver_loop(); // iserver handler for boot/file/console support. } }Closing
20Mbps serial hack was a big relief. I don’t have to worry about one more obsolete chip. Next step is to connect multiple transputer retroshields together and run some benchmarks.