├── README.md ├── config.h ├── gpib.cpp ├── gpib.h └── plotadapter.ino /README.md: -------------------------------------------------------------------------------- 1 | # PlotAdapter: Serial-GPIB converter for HP Plotters 2 | 3 | ## Introduction 4 | 5 | Older Hewlett-Packard pen plotters usually had an RS232 interface, but they were 6 | also available with GPIB (a.k.a. HP-IB, a.k.a. IEEE-488), a popular interface 7 | for laboratory equipment, also seen on HP's own 9800 series of computers. 8 | 9 | While GPIB interfaces are still available, both as (expensive) commercial 10 | products and as open-source hardware, they are not well-suited to the task of 11 | controlling a plotter - most PC software expects to talk to a plotter over a 12 | serial port, not the VISA APIs typically used by GPIB adapters. Additonally, 13 | many of the serial-port-based GPIB adapters are more suited to 14 | instrument-control applications - sending and receiving short commands and 15 | responses - and as such lack the flow-control mechanisms needed for successful 16 | plotter operation. 17 | 18 | Furthermore, HP's RS232 plotters supported a wide range of serial handshaking 19 | options, and implemented an additional set of escape sequences to configure 20 | this. Many applications send these escape sequences before and during plotting, 21 | and expect the plotter to respond accordingly - for example, HPGL plots 22 | generated by Autocad contain escape sequences to put the plotter into 23 | `XON`/`XOFF` handshaking mode. 24 | 25 | PlotAdapter aims to emulate this extended command set, allowing a GPIB plotter 26 | to be 'converted' to an RS232 interface and used by software that expects to 27 | communicate with an RS232-connected plotter. 28 | 29 | ## Getting Started 30 | 31 | ### Hardware 32 | 33 | You will need: 34 | 35 | * An HPGL plotter with a GPIB interface (e.g. a 7470A with Option 002) 36 | 37 | * An AVR-based Arduino with 5 volt IO and 14 available pins (I am using a Mega 38 | 2560) 39 | 40 | * A GPIB cable with some way of connecting it to the Arduino 41 | 42 | * (optional) a USB-serial interface with support for serial handshaking (see 43 | "Flow Control Limitations" section below for details) 44 | 45 | ### Software 46 | 47 | It is crucial to use some form of flow control when sending data to the plotter. 48 | Many plotter operations are time-consuming, and without flow control, it is very 49 | likely that a buffer overrun will occur. This is complicated by the fact that 50 | the driver for the Arduino's USB-serial interface does not handle `XON`/`XOFF` 51 | flow-control correctly. 52 | 53 | The best way to send data to the plotter is to use a dedicated program that is 54 | aware of the plotter's escape sequences, and can set up a workable flow-control 55 | configuration itself. I wrote [hpplot](https://github.com/rhalkyard/hpplot) for 56 | this purpose. 57 | 58 | ### Building `plotadapter` 59 | 60 | 1. Open `plotadapter.ino` in any recent version of the Arduino IDE 61 | 62 | 2. Using the Library Manager (Tools ➡ Manage Libraries...), install the 63 | "FreeRTOS" library 64 | 65 | 3. Open `config.h` and edit the pin definitions, plotter address, and other 66 | parameters to suit your setup. 67 | 68 | ## Notes 69 | 70 | ### Plotter Models 71 | 72 | I wrote this project for my HP 7470A, but there is no reason this shouldn't work 73 | for other similar plotters in the series. 74 | 75 | ### Software Architecture 76 | 77 | `plotadapter` is implemented as a pair of tasks running on FreeRTOS - 78 | `serial_task` reads bytes from the serial port, handles escape sequences, and 79 | inserts received data into a ring buffer. `gpib_task` reads data from the ring 80 | buffer, and transmits it via a very crude bit-banged GPIB implementation. 81 | 82 | `gpib_task` also monitors the data stream for HPGL commands that are known to 83 | generate output (any command beginning with `O` - `OI`, `OE`, `OD`, etc.). When 84 | an output command has been sent, it sends a GPIB `TALK` command to the plotter 85 | and waits for a response, which it sends to the serial port. 86 | 87 | If a GPIB operation times out (the timeout is configurable as `GPIB_TIMEOUT` in 88 | `config.h.`, default is 30 seconds), the bus is reset by asserting `IFC`, the 89 | command buffer is purged, and `gpib_thread` is restarted. This also occurrs if 90 | the "Abort Output" escape sequence `ESC . K` is received. 91 | 92 | In hindsight, FreeRTOS is probably overkill for a simple project such as this, 93 | and it could be rewritten using [protothreads](http://dunkels.com/adam/pt/) or 94 | even just as a plain event loop. However, implementing GPIB and serial IO as two 95 | independent tasks made it easier to reason about their interactions, and allowed 96 | the GPIB implementation to be a simple piece of procedural code, rather than a 97 | convoluted state machine. I regret nothing. 98 | 99 | ### Features 100 | 101 | The following RS232 interface features are supported: 102 | 103 | * DTR flow control (when using an external serial adapter) 104 | 105 | * Echo mode setting with `ESC . @` (echo off, immediate echo, echo when character 106 | processed) 107 | 108 | * Reporting available buffer space with `ESC . B` 109 | 110 | * Reporting total buffer size with `ESC . L` 111 | 112 | * Software low control (either `ENQ`/`ACK` or `XON`/`XOFF`) using Handshake 113 | Modes 1 (`ESC . H`) or 2 (`ESC . I`) - see section below on limitations 114 | 115 | * Output-initiator and output-terminator characters 116 | 117 | The following features are NOT supported due to architectural limitations: 118 | 119 | * Reporting plotter status with `ESC . O` (querying plotter would interrupt HPGL 120 | command stream - returns status byte of `0` instead) 121 | 122 | * Plotter enable/disable with `ESC . (` and `ESC . )` 123 | 124 | The following features could be supported but I'm too lazy right now 125 | 126 | * Inter-character and turnaround delay 127 | 128 | * Echo-terminator and output-trigger characters 129 | 130 | * Reporting RS232 error status with `ESC . E` (currently always returns `0`) 131 | 132 | * Emulation of Circle and Arc HPGL commands that are present on RS232-equipped 133 | 7470As, but not on GPIB models. 134 | 135 | ### Flow-Control Limitations 136 | 137 | The driver for the onboard USB-Serial converter appears to ignore `XON`/`XOFF` 138 | flow-control. Since we cannot use DTR flow control either, this needs to be 139 | worked around by either: 140 | 141 | * Attaching an external USB-serial interface (with funcitoning flow control) to 142 | the Arduino's serial pins 143 | 144 | * Configuring/rewriting the application to use an alternate form of flow control 145 | (either `ENQ`/`ACK` or by checking available buffer space with `ESC . B` 146 | before sending) 147 | 148 | ### GPIB Bus limitations 149 | 150 | This is *not* a fully-compliant GPIB implementation by any means - it works OK 151 | if the plotter is the only device on the bus, but it will probably not be able 152 | to drive a longer bus, and may not coexist well with other devices anyway. 153 | -------------------------------------------------------------------------------- /config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Size of GPIB transmit buffer 4 | #define TXBUF_SZ 256 5 | 6 | // Timeout for GPIB operations. This has to be relatively long to account for 7 | // time-consuming operations such as writing labels or slow pen movements. If a 8 | // timeout is reached, the GPIB transmit buffer is flushed and the bus reset. 9 | #define GPIB_TIMEOUT 30000 10 | 11 | // Length of argument buffer for escape sequences. 80 bytes is probably overkill 12 | #define ARGBUF_LEN 80 13 | 14 | // GPIB address of this adapter 15 | #define MY_ADDR 0 16 | 17 | // GPIB address of plotter. Must match DIP switches on plotter 18 | #define PLOTTER_ADDR 5 19 | 20 | // Pin to use for RS232 DTR line. Only useful if using a separate serial adapter 21 | //#define DTR_PIN 4 22 | 23 | // If true, invert DTR to be active-low 24 | //#define DTR_INVERT false 25 | 26 | // Define HANDSHAKE_DEBUG to add an ESC . P escape sequence that prints out the 27 | // current handshake configuration 28 | //#define HANDSHAKE_DEBUG 29 | 30 | // GPIB pin definitions 31 | #define DIO1 50 // Pin 1 on GPIB connector 32 | #define DIO2 48 // Pin 2 on GPIB connector 33 | #define DIO3 46 // Pin 3 on GPIB connector 34 | #define DIO4 44 // Pin 4 on GPIB connector 35 | #define DIO5 42 // Pin 13 on GPIB connector 36 | #define DIO6 40 // Pin 14 on GPIB connector 37 | #define DIO7 38 // Pin 15 on GPIB connector 38 | #define DIO8 36 // Pin 16 on GPIB connector 39 | 40 | #define EOI 47 // Pin 5 on GPIB connector 41 | #define DAV 49 // Pin 6 on GPIB connector 42 | #define NRFD 51 // Pin 7 on GPIB connector 43 | #define NDAC 53 // Pin 8 on GPIB connector 44 | #define IFC 35 // Pin 9 on GPIB connector 45 | #define SRQ 41 // Pin 10 on GPIB connector - not used 46 | #define ATN 39 // Pin 11 on GPIB connector 47 | #define REN 37 // Pin 17 on GPIB connector - not used 48 | 49 | 50 | #if defined(DTR_PIN) 51 | #if DTR_INVERT 52 | #define DTR_TRUE LOW 53 | #define DTR_FALSE HIGH 54 | #else 55 | #define DTR_TRUE HIGH 56 | #define DTR_FALSE LOW 57 | #endif 58 | #endif -------------------------------------------------------------------------------- /gpib.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | /*** FreeRTOS headers - requires FreeRTOS library from Library Manager ***/ 4 | #include 5 | 6 | #include "config.h" 7 | #include "gpib.h" 8 | 9 | /* Wait for pin to be set (pulled low) */ 10 | int wait_set(int pin, int timeout) { 11 | unsigned long startTime = millis(); 12 | CLEAR(pin); 13 | while (IS_CLEAR(pin)) { 14 | if (timeout >= 0 && millis() > startTime + timeout) { 15 | return -1; 16 | } 17 | vTaskDelay(0); 18 | } 19 | return 0; 20 | } 21 | 22 | /* Wait for pin to be cleared (floating high) */ 23 | int wait_clear(int pin, int timeout) { 24 | unsigned long startTime = millis(); 25 | CLEAR(pin); 26 | while (IS_SET(pin)) { 27 | if (timeout >= 0 && millis() > startTime + timeout) { 28 | return -1; 29 | } 30 | vTaskDelay(0); 31 | } 32 | return 0; 33 | } 34 | 35 | /* Clear data pins */ 36 | void data_clear() { 37 | CLEAR(DIO1); 38 | CLEAR(DIO2); 39 | CLEAR(DIO3); 40 | CLEAR(DIO4); 41 | CLEAR(DIO5); 42 | CLEAR(DIO6); 43 | CLEAR(DIO7); 44 | CLEAR(DIO8); 45 | } 46 | 47 | /* Write byte to DIO1..8 */ 48 | void data_write(uint8_t b) { 49 | OUT(DIO1, bitRead(b, 0)); 50 | OUT(DIO2, bitRead(b, 1)); 51 | OUT(DIO3, bitRead(b, 2)); 52 | OUT(DIO4, bitRead(b, 3)); 53 | OUT(DIO5, bitRead(b, 4)); 54 | OUT(DIO6, bitRead(b, 5)); 55 | OUT(DIO7, bitRead(b, 6)); 56 | OUT(DIO8, bitRead(b, 7)); 57 | } 58 | 59 | /* Read byte form DIO1..8 */ 60 | uint8_t data_read() { 61 | uint8_t b = 0; 62 | data_clear(); 63 | bitWrite(b, 0, IS_SET(DIO1)); 64 | bitWrite(b, 1, IS_SET(DIO2)); 65 | bitWrite(b, 2, IS_SET(DIO3)); 66 | bitWrite(b, 3, IS_SET(DIO4)); 67 | bitWrite(b, 4, IS_SET(DIO5)); 68 | bitWrite(b, 5, IS_SET(DIO6)); 69 | bitWrite(b, 6, IS_SET(DIO7)); 70 | bitWrite(b, 7, IS_SET(DIO8)); 71 | return b; 72 | } 73 | 74 | /* Initialize bus */ 75 | void gpib_init() { 76 | /* Clear all data and signal lines */ 77 | data_clear(); 78 | CLEAR(EOI); 79 | CLEAR(DAV); 80 | CLEAR(IFC); 81 | CLEAR(ATN); 82 | CLEAR(NDAC); 83 | CLEAR(NRFD); 84 | #ifdef SRQ 85 | CLEAR(SRQ); 86 | #endif 87 | #ifdef REN 88 | CLEAR(REN); 89 | #endif 90 | 91 | /* Assert IFC to reset the bus */ 92 | SET(IFC); 93 | delay(100); 94 | CLEAR(IFC); 95 | } 96 | 97 | /* Write a byte */ 98 | int gpib_write(uint8_t b, bool command, bool eoi, int timeout) { 99 | int ret = 0; 100 | if (command) { 101 | SET(ATN); 102 | } else { 103 | CLEAR(ATN); 104 | } 105 | 106 | CLEAR(EOI); 107 | CLEAR(DAV); 108 | CLEAR(NRFD); 109 | CLEAR(NDAC); 110 | digitalWrite(LED_BUILTIN, HIGH); 111 | 112 | /* Wait for listeners to be ready */ 113 | if (wait_set(NDAC, timeout)) { 114 | ret = -1; 115 | goto cleanup; 116 | } 117 | if (wait_clear(NRFD, timeout)) { 118 | ret = -1; 119 | goto cleanup; 120 | } 121 | 122 | data_write(b); // Put data byte on the bus 123 | if (!command && eoi) { 124 | SET(EOI); 125 | } 126 | SET(DAV); // Signal data available 127 | /* Wait for listeners to accept data */ 128 | if (wait_clear(NDAC, timeout)) { 129 | ret = -1; 130 | goto cleanup; 131 | } 132 | 133 | cleanup: 134 | digitalWrite(LED_BUILTIN, LOW); 135 | CLEAR(DAV); // Clear data available, reset lines 136 | SET(NDAC); 137 | SET(NRFD); 138 | data_clear(); 139 | if (command) { 140 | CLEAR(ATN); 141 | } else if (eoi) { 142 | CLEAR(EOI); 143 | } 144 | 145 | return ret; 146 | } 147 | 148 | /* Read a byte from GPIB */ 149 | int gpib_read(uint8_t* b, int timeout) { 150 | int ret = 0; 151 | bool eoi; 152 | 153 | SET(NDAC); 154 | CLEAR(NRFD); // Signal ready for data 155 | digitalWrite(LED_BUILTIN, HIGH); 156 | /* Wait for data available */ 157 | if (wait_set(DAV, timeout)) { 158 | ret = -1; 159 | goto cleanup; 160 | } 161 | SET(NRFD); 162 | *b = data_read(); 163 | eoi = !digitalRead(EOI); 164 | if (eoi) { 165 | ret = 1; 166 | }; 167 | 168 | CLEAR(NDAC); // Accept data 169 | /* Wait for talker to finish */ 170 | if (wait_clear(DAV, -1)) { 171 | ret = -1; 172 | goto cleanup; 173 | } 174 | cleanup: 175 | digitalWrite(LED_BUILTIN, LOW); 176 | SET(NDAC); 177 | 178 | return ret; 179 | } 180 | 181 | /* Send a GPIB command byte */ 182 | int gpib_cmd(uint8_t cmd, int timeout) { 183 | return gpib_write(cmd, true, false, timeout); 184 | } 185 | 186 | /* Start a TALK/LISTEN transaction */ 187 | int gpib_start(uint8_t talk_addr, uint8_t listen_addr, int timeout) { 188 | if (gpib_cmd(G_TAD | talk_addr, timeout) == -1) { 189 | return -1; 190 | }; 191 | if (gpib_cmd(G_LAD | listen_addr, timeout) == -1) { 192 | return -1; 193 | } 194 | return 0; 195 | } 196 | 197 | /* End a transaction */ 198 | int gpib_stop(int timeout) { 199 | if (gpib_cmd(G_UNT, timeout) == -1) { 200 | return -1; 201 | } 202 | if (gpib_cmd(G_UNL, timeout) == -1) { 203 | return -1; 204 | } 205 | return 0; 206 | } 207 | -------------------------------------------------------------------------------- /gpib.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | /**** GPIB Commands ****/ 7 | /* Addressing */ 8 | #define G_TAD 0x40 // Talk address 9 | #define G_LAD 0x20 // Listen address 10 | /* Universal Commands */ 11 | #define G_UNL 0x3f // Unlisten 12 | #define G_UNT 0x5f // Untalk 13 | #define G_LLO 0x11 // Local Lockout 14 | #define G_DCL 0x14 // Device Clear 15 | #define G_PPU 0x15 // Parallel Poll Unconfigure 16 | #define G_SPE 0x18 // Serial Poll Enable 17 | #define G_SPD 0x19 // Serial Poll Disable 18 | /* Addressed Commands */ 19 | #define G_GTL 0x01 // Go To Local 20 | #define G_SDC 0x04 // Selected Device Clear 21 | #define G_PPC 0x05 // Parallel Poll Configure 22 | #define G_GET 0x08 // Group Execute Trigger 23 | #define G_TCT 0x09 // Take Control 24 | 25 | /* Utility macros for GPIB pin control 26 | * 27 | * GPIB is an open-collector bus - a line is asserted by pulling it low, and 28 | * de-asserted by leaving it floating 29 | */ 30 | #define SET(pin) \ 31 | pinMode(pin, OUTPUT); \ 32 | digitalWrite(pin, LOW); 33 | #define CLEAR(pin) pinMode(pin, INPUT_PULLUP); 34 | #define OUT(pin, level) \ 35 | if (level) { \ 36 | SET(pin); \ 37 | } else { \ 38 | CLEAR(pin); \ 39 | } 40 | #define IS_SET(pin) (digitalRead(pin) == LOW) 41 | #define IS_CLEAR(pin) (digitalRead(pin) == HIGH) 42 | 43 | /* Wait for a pin to be asserted, yielding task for 1 timer tick between checks 44 | * 45 | * pin: Arduino pin number 46 | * 47 | * timeout: timeout value in ms, -1 to block indefinitely 48 | * 49 | * returns: 0 if pin has been asserted, -1 if timeout has been reached 50 | */ 51 | int wait_set(int pin, int timeout); 52 | 53 | /* Wait for a pin to be cleared, yielding task for 1 timer tick between checks 54 | * 55 | * pin: Arduino pin number 56 | * 57 | * timeout: timeout value in ms, -1 to block indefinitely 58 | * 59 | * returns: 0 if pin has been cleared, -1 if timeout has been reached 60 | */ 61 | int wait_clear(int pin, int timeout); 62 | 63 | /* Initialize GPIB bus 64 | * 65 | * Clear all pins, and assert IFC for 100ms to reset the bus 66 | */ 67 | void gpib_init(); 68 | 69 | /* Output byte b on pins DIO1..DIO8 */ 70 | void data_write(uint8_t b); 71 | 72 | /* Read a byte from pins DOI1..DIO8 */ 73 | uint8_t data_read(); 74 | 75 | /* Write a byte on the GPIB bus 76 | * 77 | * b: byte to send 78 | * 79 | * command: if true, send as command (with ATN asserted) 80 | * 81 | * eoi: if true, send with EOI asserted 82 | * 83 | * timeout: timeout for GPIB handshake, -1 to block indefinitely 84 | * 85 | * returns: 0 if byte transmitted and accepted, -1 if timeout reached 86 | */ 87 | int gpib_write(uint8_t b, bool command, bool eoi, int timeout); 88 | 89 | /* Read a byte on the GPIB bus 90 | * 91 | * *b (out-parameter): pointer to location to output byte to 92 | * 93 | * timeout: timeout for GPIB handshake, -1 to block indefinitely 94 | * 95 | * returns: 0 if byte received successfully, 1 if byte received with EOI, -1 if 96 | * timeout reached 97 | */ 98 | int gpib_read(uint8_t* b, int timeout); 99 | 100 | /* Write a command byte on the GPIB bus 101 | * 102 | * Shorthand for gpib_write(b, true, false, timeout) 103 | */ 104 | int gpib_cmd(uint8_t cmd, int timeout); 105 | 106 | /* Start a GPIB TALK/LISTEN transaction 107 | * 108 | * talk_addr: address of talking device 109 | * 110 | * listen_addr: address of listening device 111 | * 112 | * timeout: timeout for GPIB handshake, -1 to block indefinitely 113 | * 114 | * returns: 0 if transaction initiated successfully, -1 if timeout reached 115 | */ 116 | int gpib_start(uint8_t talk_addr, uint8_t listen_addr, int timeout); 117 | 118 | /* End a GPIB TALK/LISTEN transaction (i.e. send UNTALK, UNLISTEN) 119 | * 120 | * timeout: timeout for GPIB handshake, -1 to block indefinitely 121 | * 122 | * returns: 0 if transaction ended successfully, -1 if timeout reached 123 | */ 124 | int gpib_stop(int timeout); 125 | -------------------------------------------------------------------------------- /plotadapter.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | /*** FreeRTOS headers - requires FreeRTOS library from Library Manager ***/ 4 | #include 5 | #include 6 | 7 | #include "config.h" 8 | #include "gpib.h" 9 | 10 | #define CH_ETX '\x03' 11 | #define CH_ESC '\x1b' 12 | 13 | enum esc_state_t { ESC_IDLE, ESC_EXPECT_DOT, ESC_EXPECT_CMD, ESC_EXPECT_ARGS }; 14 | 15 | enum echo_mode_t { ECHO_OFF, ECHO_IMMEDIATE, ECHO_SENT }; 16 | 17 | /*** Serial interface parameters, configured with escape sequences ***/ 18 | /* ESC . @ */ 19 | bool dtr_mode = false; 20 | enum echo_mode_t echo_mode = ECHO_OFF; 21 | /* ESC . H, ESC . I */ 22 | int handshake_mode = 1; 23 | int block_size = 80; 24 | char enq_char = '\0'; 25 | char ack_xon[11] = ""; 26 | /* ESC . M */ 27 | int turnaround_delay = 0; // Not used 28 | char output_trigger = '\0'; // Not used 29 | char echo_terminator = '\0'; // Not used 30 | char output_terminator[3] = "\r"; 31 | char output_initiator = '\0'; 32 | /* ESC . N */ 33 | int interchar_delay = 0; // Not used 34 | char imm_xoff[11] = ""; 35 | /* ESC . (, ESC . ) */ 36 | bool plotter_enable = true; 37 | 38 | /* Buffer for escape-sequence arguments */ 39 | char argbuf[ARGBUF_LEN]; 40 | 41 | /* Terminator character for LB command. Set with DT command. Default is ETX */ 42 | char label_terminator = CH_ETX; 43 | 44 | /* Handle for GPIB task */ 45 | TaskHandle_t gpib_taskhandle = NULL; 46 | 47 | /* GPIB transmit buffer */ 48 | StreamBufferHandle_t txBuf = NULL; 49 | 50 | /* GPIB interface task. 51 | 52 | Reads bytes from GPIB TX buffer and outputs them over GPIB. Does some 53 | rudimentary command-stream parsing to keep track of commands that might 54 | generate output. 55 | */ 56 | void gpibTask(void *pvParams) { 57 | char cmd[3] = {'\0', '\0', '\0'}; /* Command currently being processed */ 58 | bool in_label = false; /* Are we currently writing a label? */ 59 | bool talking = false; /* Are we currently talking? */ 60 | 61 | restart: 62 | /* We restart from here if a timeout is reached */ 63 | cmd[0] = '\0'; 64 | cmd[1] = '\0'; 65 | cmd[2] = '\0'; 66 | in_label = false; 67 | talking = false; 68 | xStreamBufferReset(txBuf); 69 | gpib_init(); 70 | 71 | while (1) { 72 | uint8_t ch = '\0'; 73 | while (xStreamBufferBytesAvailable(txBuf)) { 74 | /* Get a byte from the GPIB TX buffer */ 75 | xStreamBufferReceive(txBuf, &ch, 1, 0); 76 | 77 | /* Become talker if necessary */ 78 | if (!talking) { 79 | /* Stop any currently-active bus transaction */ 80 | if (gpib_stop(GPIB_TIMEOUT)) { 81 | goto restart; 82 | } 83 | 84 | /* Start new bus transaction with adapter as talker and plotter 85 | * as listener */ 86 | if (gpib_start(MY_ADDR, PLOTTER_ADDR, GPIB_TIMEOUT) == -1) { 87 | goto restart; 88 | } 89 | 90 | talking = true; 91 | } 92 | 93 | /* Handle argument to previously-received DT command (set new 94 | * label-terminator character). We have to do this now because we 95 | * need to get the argument, and DT does not obey normal 96 | * command-termination rules. */ 97 | if (!strcmp(cmd, "DT")) { 98 | label_terminator = ch; 99 | } 100 | 101 | /* Check if we are beginning a new command */ 102 | if (!strcmp(cmd, "LB")) { 103 | /* Inside a label, commands are only terminated with the 104 | * label_terminator character set by the DT command (ETX by 105 | * default) */ 106 | if (ch == label_terminator) { 107 | cmd[0] = '\0'; 108 | cmd[1] = '\0'; 109 | } 110 | } else { 111 | /* Outside of labels, many commands are terminated by anything 112 | * that isn't a comma, digit or whitespace */ 113 | if (cmd[0] && cmd[1]) { 114 | if (!(isdigit(ch) || ch == ',' || ch == ' ')) { 115 | cmd[0] = '\0'; 116 | cmd[1] = '\0'; 117 | } 118 | } 119 | } 120 | 121 | /* Get command characters */ 122 | if (cmd[0] == '\0') { 123 | if (isalpha(ch)) { 124 | cmd[0] = toupper(ch); 125 | } 126 | } else if (cmd[1] == '\0') { 127 | if (isalpha(ch)) { 128 | cmd[1] = toupper(ch); 129 | } 130 | } 131 | 132 | /* IN and DF commands reset label terminator to ETX */ 133 | if (!strcmp(cmd, "IN") || !strcmp(cmd, "DF")) { 134 | label_terminator = CH_ETX; 135 | } 136 | 137 | /* Write byte to GPIB. Don't bother using EOI, the 7470 doesn't seem 138 | * to care about it */ 139 | if (gpib_write(ch, false, false, GPIB_TIMEOUT) == -1) { 140 | goto restart; 141 | } 142 | 143 | /* If we are in "delay echo until character processed" echo mode, 144 | * echo the character back now that we have sent it */ 145 | if (echo_mode == ECHO_SENT) { 146 | Serial.write(ch); 147 | } 148 | 149 | /* If we just sent an output command, break out of the 150 | * transmit loop and wait for a response */ 151 | if (cmd[1] && toupper(cmd[0] == 'O')) { 152 | /* Ask plotter to talk */ 153 | talking = false; 154 | 155 | /* Stop any currently-active bus transaction */ 156 | if (gpib_stop(GPIB_TIMEOUT) == -1) { 157 | goto restart; 158 | } 159 | 160 | /* Lock control lines so that plotter doesn't start talking early */ 161 | SET(NRFD); 162 | CLEAR(NDAC); 163 | 164 | /* Start new bus transaction with adapter as talker and plotter 165 | * as listener */ 166 | if (gpib_start(PLOTTER_ADDR, MY_ADDR, GPIB_TIMEOUT) == -1) { 167 | goto restart; 168 | } 169 | 170 | /* Send optional output-initiator string */ 171 | if (output_initiator) { 172 | Serial.print(output_initiator); 173 | } 174 | 175 | /* Read response from plotter and send to serial port */ 176 | int ret; 177 | do { 178 | ret = gpib_read(&ch, 1000); 179 | if (ret == -1) { 180 | goto restart; 181 | } else { 182 | if (ch != '\r' && ch != '\n') { 183 | /* Suppress CRLF output terminator; we send our own */ 184 | Serial.write(ch); 185 | } 186 | } 187 | } while (ret == 0); 188 | 189 | /* Send output-terminator string (CR by default) */ 190 | Serial.print(output_terminator); 191 | Serial.flush(); 192 | } 193 | 194 | vTaskDelay(0); 195 | } 196 | vTaskDelay(0); 197 | } 198 | } 199 | 200 | /* Read a semicolon-delimited list of integers from argbuf, return them as an 201 | * array in outval. Blank/unparseable values are replaced with -1. */ 202 | int read_args(char *argbuf, int *outvals, int nvals) { 203 | int i, val; 204 | char *current = argbuf; 205 | char *tok_end; 206 | 207 | for (i = 0; i < nvals; i++) { 208 | outvals[i] = -1; 209 | } 210 | 211 | i = 0; 212 | while (current != NULL && i < nvals) { 213 | val = strtod(current, &tok_end); 214 | if (tok_end == current) { 215 | outvals[i] = -1; 216 | } else { 217 | outvals[i] = val; 218 | } 219 | i++; 220 | current = strchr(tok_end, ';'); 221 | if (*current != '\0') { 222 | current++; 223 | } else { 224 | current = NULL; 225 | } 226 | } 227 | 228 | return i; 229 | } 230 | 231 | /* ESC . @ [; ] : 232 | 233 | Set plotter configuration 234 | 235 | First argument is ignored. Second is interpreted as a bitfield as follows: 236 | 237 | 0: 0 = hold DTR high, 1= use DTR for flow control 238 | 239 | 1: Unused 240 | 241 | 2: 0 = immediate echo, 1 = delayed echo 242 | 243 | 3: 0 = disable echo, 1 = enable echo 244 | 245 | 4-7: Unused 246 | 247 | "Immediate echo" mode echoes characters as soon as they are received. "Delayed 248 | echo" mode echoes characters only after they have been accepted by the 249 | plotter. 250 | */ 251 | bool handle_config(char ch) { 252 | const int max_args = 2; 253 | static int i = 0; 254 | if (ch != ':' && i < ARGBUF_LEN) { 255 | argbuf[i++] = ch; 256 | return true; 257 | } else { 258 | int args[max_args]; 259 | int nargs; 260 | argbuf[i] = '\0'; 261 | nargs = read_args(argbuf, args, max_args); 262 | 263 | /* First argument is ignored */ 264 | 265 | /* Second argument controls DTR pin and echo mode */ 266 | if (args[1] == -1) { 267 | args[1] = 0; 268 | } 269 | 270 | dtr_mode = args[1] & 0b0001; 271 | #ifdef DTR_PIN 272 | /* If we disable DTR flow control, permanently assert DTR */ 273 | if (!dtr_mode) { 274 | digitalWrite(DTR_PIN, DTR_TRUE); 275 | } 276 | #endif 277 | 278 | if (args[1] & 0b1000) { 279 | if (args[1] & 0b0100) { 280 | echo_mode = ECHO_SENT; 281 | } else { 282 | echo_mode = ECHO_IMMEDIATE; 283 | } 284 | } else { 285 | echo_mode = ECHO_OFF; 286 | } 287 | 288 | i = 0; 289 | return false; 290 | } 291 | } 292 | 293 | /* ESC . H [; ; [; ...]] : 294 | 295 | Set Handshake Mode 1 296 | 297 | Argument 1: Block size 298 | 299 | Argument 2: Character to expect for enquiry 300 | 301 | Argument 3..12: Null-terminated string of up to 10 characters to send as 302 | acknowledgement 303 | */ 304 | bool handle_handshake_1(char ch) { 305 | const int max_args = 12; 306 | static int i = 0; 307 | if (ch != ':' && i < ARGBUF_LEN) { 308 | argbuf[i++] = ch; 309 | return true; 310 | } else { 311 | int args[max_args]; 312 | int nargs; 313 | argbuf[i] = '\0'; 314 | nargs = read_args(argbuf, args, max_args); 315 | 316 | handshake_mode = 1; 317 | 318 | if (args[0] == -1) { 319 | block_size = 80; 320 | } else { 321 | block_size = args[0]; 322 | } 323 | 324 | if (args[1] == -1) { 325 | enq_char = '\0'; 326 | } else { 327 | enq_char = args[1]; 328 | } 329 | 330 | for (int j = 2; j < max_args; j++) { 331 | if (args[j] == -1) { 332 | ack_xon[j - 2] = '\0'; 333 | } else { 334 | ack_xon[j - 2] = args[j]; 335 | } 336 | } 337 | i = 0; 338 | return false; 339 | } 340 | } 341 | 342 | /* ESC . I [; ; [; ...]] : 343 | 344 | Set Handshake Mode 2 345 | 346 | Arguments as per Handshake Mode 1 347 | */ 348 | bool handle_handshake_2(char ch) { 349 | const int max_args = 12; 350 | static int i = 0; 351 | if (ch != ':' && i < ARGBUF_LEN) { 352 | argbuf[i++] = ch; 353 | return true; 354 | } else { 355 | int args[max_args]; 356 | int nargs; 357 | argbuf[i] = '\0'; 358 | nargs = read_args(argbuf, args, max_args); 359 | 360 | handshake_mode = 2; 361 | 362 | if (args[0] == -1) { 363 | block_size = 80; 364 | } else { 365 | block_size = args[0]; 366 | } 367 | 368 | if (args[1] == -1) { 369 | enq_char = '\0'; 370 | } else { 371 | enq_char = args[1]; 372 | } 373 | 374 | for (int j = 2; j < max_args; j++) { 375 | if (args[j] == -1) { 376 | ack_xon[j - 2] = '\0'; 377 | } else { 378 | ack_xon[j - 2] = args[j]; 379 | } 380 | } 381 | 382 | i = 0; 383 | return false; 384 | } 385 | } 386 | 387 | /* ESC . M [; ; ; 388 | ; ; ] : 389 | 390 | Set Output Mode 391 | 392 | Argument 1: turnaround delay before response = ((param * 1.1875) % 65536) / 393 | 1.2 ms 394 | 395 | Argument 2: output trigger character 396 | 397 | Argument 3: Echo terminator character 398 | 399 | Argument 4,5: Output terminator characters 400 | 401 | Argument 6: Output initiator character 402 | */ 403 | bool handle_output_mode(char ch) { 404 | const int max_args = 6; 405 | static int i = 0; 406 | if (ch != ':' && i < ARGBUF_LEN) { 407 | argbuf[i++] = ch; 408 | return true; 409 | } else { 410 | int args[max_args]; 411 | int nargs; 412 | argbuf[i] = '\0'; 413 | nargs = read_args(argbuf, args, max_args); 414 | 415 | if (args[0] == -1) { 416 | turnaround_delay = 0; 417 | } else { 418 | turnaround_delay = args[0]; 419 | } 420 | 421 | if (args[1] == -1) { 422 | output_trigger = '\0'; 423 | } else { 424 | output_trigger = args[1]; 425 | } 426 | 427 | if (args[2] == -1) { 428 | echo_terminator = '\0'; 429 | } else { 430 | echo_terminator = args[2]; 431 | } 432 | 433 | for (int j = 3; j < 5; j++) { 434 | if (args[j] == -1) { 435 | output_terminator[j - 3] = '\0'; 436 | } else { 437 | output_terminator[j - 3] = args[j]; 438 | } 439 | } 440 | 441 | if (args[5] == -1) { 442 | output_initiator = '\0'; 443 | } else { 444 | output_initiator = args[5]; 445 | } 446 | 447 | i = 0; 448 | return false; 449 | } 450 | } 451 | 452 | /* ESC . N [; ... ] : 453 | 454 | Set extended handshake options 455 | 456 | Argument 1: Inter-character delay = ((param * 1.1875) % 65536) / 1.2 ms 457 | 458 | Argument 2-11: Immediate-response string to send when ENQ character is 459 | received 460 | */ 461 | bool handle_ext_output_mode(char ch) { 462 | const int max_args = 11; 463 | static int i = 0; 464 | if (ch != ':' && i < ARGBUF_LEN) { 465 | argbuf[i++] = ch; 466 | return true; 467 | } else { 468 | int args[max_args]; 469 | int nargs; 470 | argbuf[i] = '\0'; 471 | nargs = read_args(argbuf, args, max_args); 472 | 473 | if (args[0] == -1) { 474 | interchar_delay = 0; 475 | } else { 476 | interchar_delay = args[0]; 477 | } 478 | 479 | for (int j = 1; j < max_args; j++) { 480 | if (args[j] == -1) { 481 | imm_xoff[j - 1] = '\0'; 482 | } else { 483 | imm_xoff[j - 1] = args[j]; 484 | } 485 | } 486 | 487 | i = 0; 488 | return false; 489 | } 490 | } 491 | 492 | /* ESC . R 493 | 494 | Reset handshake parameters to default 495 | */ 496 | void reset_handshake(void) { 497 | #ifdef DTR_PIN 498 | pinMode(DTR_PIN, OUTPUT); 499 | digitalWrite(DTR_PIN, DTR_TRUE); 500 | #endif 501 | 502 | /* Reset configuration options to defaults */ 503 | dtr_mode = false; 504 | echo_mode = ECHO_OFF; 505 | 506 | handshake_mode = 0; 507 | block_size = 80; 508 | enq_char = '\0'; 509 | 510 | for (int i = 0; i < 11; i++) { 511 | ack_xon[i] = '\0'; 512 | } 513 | 514 | output_trigger = '\0'; 515 | echo_terminator = '\0'; 516 | output_terminator[0] = '\r'; 517 | output_terminator[1] = '\0'; 518 | output_terminator[2] = '\0'; 519 | output_initiator = '\0'; 520 | 521 | for (int i = 0; i < 11; i++) { 522 | imm_xoff[i] = '\0'; 523 | } 524 | } 525 | 526 | void restartGpibTask() { 527 | if (gpib_taskhandle != NULL) { 528 | vTaskDelete(gpib_taskhandle); 529 | } 530 | 531 | xTaskCreate(gpibTask, "gpib", 128, NULL, 1, &gpib_taskhandle); 532 | } 533 | 534 | #ifdef HANDSHAKE_DEBUG 535 | void dump_config() { 536 | Serial.println("Handshake Configuration:"); 537 | Serial.print("DTR mode: "); 538 | Serial.println(dtr_mode); 539 | Serial.print("Echo mode: "); 540 | Serial.println(echo_mode); 541 | Serial.print("Handshake mode: "); 542 | Serial.println(handshake_mode); 543 | Serial.print("Block size: "); 544 | Serial.println(block_size); 545 | Serial.print("ENQ character: "); 546 | Serial.println(enq_char, HEX); 547 | Serial.print("ACK/XON string: "); 548 | for (int i = 0; i < 11; i++) { 549 | Serial.print(ack_xon[i], HEX); 550 | Serial.print(' '); 551 | } 552 | Serial.println(); 553 | Serial.print("Turnaround delay: "); 554 | Serial.println(turnaround_delay); 555 | Serial.print("Output trigger: "); 556 | Serial.println(output_trigger, HEX); 557 | Serial.print("Echo terminator: "); 558 | Serial.println(echo_term, HEX); 559 | Serial.print("Output terminator: "); 560 | Serial.print(output_term[0], HEX); 561 | Serial.print(' '); 562 | Serial.println(output_term[1], HEX); 563 | Serial.print("Output initiator: "); 564 | Serial.println(output_initiator, HEX); 565 | Serial.print("Inter-character delay: "); 566 | Serial.println(interchar_delay); 567 | Serial.print("Immediate-response/XOFF string: "); 568 | for (int i = 0; i < 11; i++) { 569 | Serial.print(imm_xoff[i], HEX); 570 | Serial.print(' '); 571 | } 572 | } 573 | #endif 574 | 575 | /* Intercept characters and handle escape sequences 576 | * Returns true if character was 'consumed' by an escape sequence, false if 577 | * character should be processed further */ 578 | bool handle_esc(char ch) { 579 | static enum esc_state_t state = ESC_IDLE; 580 | 581 | /* Function-pointer to command argument handler. 582 | * Must return true while consuming characters, false once argument list is 583 | * complete. If NULL, command has no arguments */ 584 | static bool (*handler)(char) = NULL; 585 | 586 | if (ch == CH_ESC) { 587 | state = ESC_EXPECT_DOT; 588 | return true; 589 | } 590 | 591 | if (state == ESC_EXPECT_DOT && ch == '.') { 592 | state = ESC_EXPECT_CMD; 593 | return true; 594 | } 595 | 596 | if (state == ESC_EXPECT_CMD) { 597 | if (plotter_enable) { 598 | switch (toupper(ch)) { 599 | case '@': // Plotter Configuration 600 | handler = handle_config; 601 | break; 602 | case 'B': // Output available buffer space 603 | handler = NULL; 604 | if (output_initiator) { 605 | Serial.print(output_initiator); 606 | } 607 | Serial.print(xStreamBufferSpacesAvailable(txBuf)); 608 | Serial.print(output_terminator); 609 | Serial.flush(); 610 | break; 611 | case 'E': // Output extended error state 612 | handler = NULL; 613 | if (output_initiator) { 614 | Serial.print(output_initiator); 615 | } 616 | Serial.print(0); 617 | Serial.print(output_terminator); 618 | Serial.flush(); 619 | break; 620 | case 'H': // Set handshake mode 1 621 | handler = handle_handshake_1; 622 | break; 623 | case 'I': // Set handshake mode 2 624 | handler = handle_handshake_2; 625 | break; 626 | case 'J': // Abort device control string 627 | handler = NULL; 628 | break; 629 | case 'K': // Abort graphic 630 | handler = NULL; 631 | restartGpibTask(); 632 | break; 633 | case 'L': // Output buffer size 634 | handler = NULL; 635 | if (output_initiator) { 636 | Serial.print(output_initiator); 637 | } 638 | Serial.print(TXBUF_SZ); 639 | Serial.print(output_terminator); 640 | Serial.flush(); 641 | break; 642 | case 'M': // Set output mode 643 | handler = handle_output_mode; 644 | break; 645 | case 'N': // Set extended output and handshake mode 646 | handler = handle_ext_output_mode; 647 | break; 648 | case 'O': // Output extended status 649 | handler = NULL; 650 | if (output_initiator) { 651 | Serial.print(output_initiator); 652 | } 653 | Serial.print(0); 654 | Serial.print(output_terminator); 655 | Serial.flush(); 656 | break; 657 | #ifdef HANDSHAKE_DEBUG 658 | case 'P': 659 | handler = NULL; 660 | if (output_initiator) { 661 | Serial.print(output_initiator); 662 | } 663 | dump_config(); 664 | Serial.print(output_term); 665 | break; 666 | #endif 667 | case 'R': // Reset handshake parameters 668 | reset_handshake(); 669 | break; 670 | case '(': 671 | case 'Y': // Plotter on 672 | handler = NULL; 673 | plotter_enable = true; 674 | break; 675 | case ')': 676 | case 'Z': // Plotter off 677 | handler = NULL; // Ignore 678 | plotter_enable = false; 679 | break; 680 | default: 681 | handler = NULL; 682 | break; 683 | } 684 | } else { 685 | handler = NULL; 686 | if (ch == '(' || ch == 'Y') { 687 | plotter_enable = true; 688 | } 689 | } 690 | 691 | if (handler == NULL) { 692 | state = ESC_IDLE; 693 | return true; 694 | } else { 695 | state = ESC_EXPECT_ARGS; 696 | return true; 697 | } 698 | } 699 | 700 | if (state == ESC_EXPECT_ARGS) { 701 | if (handler(ch)) { 702 | return true; 703 | } else { 704 | state = ESC_IDLE; 705 | return true; 706 | } 707 | } 708 | return false; 709 | } 710 | 711 | void serialTask(void *pvParams) { 712 | bool in_xoff = false; 713 | char ch = '\0'; 714 | reset_handshake(); 715 | while (1) { 716 | /* Space has freed up for another block */ 717 | if (in_xoff && xStreamBufferSpacesAvailable(txBuf) > block_size) { 718 | if (in_xoff) { 719 | #ifdef DTR_PIN 720 | if (dtr_mode) { 721 | digitalWrite(DTR_PIN, DTR_TRUE); 722 | } 723 | #endif 724 | /* Send XON string if we're not in ENQ/ACK mode */ 725 | if (!enq_char) { 726 | Serial.print(ack_xon); 727 | if (handshake_mode == 1) { 728 | Serial.print(output_terminator); 729 | } 730 | Serial.flush(); 731 | } 732 | in_xoff = false; 733 | } 734 | } 735 | 736 | /* Read bytes from serial port and insert into GPIB TX buffer */ 737 | while (Serial.available()) { 738 | ch = Serial.read(); 739 | 740 | if (!handle_esc(ch)) { 741 | if (enq_char && ch == enq_char) { 742 | /* We're in ENQ/ACK mode and Incoming character is enquiry */ 743 | 744 | /* Send immediate response */ 745 | Serial.print(imm_xoff); 746 | 747 | /* In handshake mode 1, we send output terminators after 748 | * handshake strings */ 749 | if (handshake_mode == 1) { 750 | Serial.print(output_terminator); 751 | } 752 | Serial.flush(); 753 | 754 | /* Wait for buffer to have enough space for a block */ 755 | while (xStreamBufferSpacesAvailable(txBuf) < block_size) { 756 | vTaskDelay(1); 757 | } 758 | 759 | /* Send acknowledgement */ 760 | Serial.print(ack_xon); 761 | 762 | /* In handshake mode 1, we send output terminators after 763 | * handshake strings */ 764 | if (handshake_mode == 1) { 765 | Serial.print(output_terminator); 766 | } 767 | Serial.flush(); 768 | } else if (plotter_enable) { 769 | /* In immediate-echo mode, echo character back when we 770 | * receive it */ 771 | if (echo_mode == ECHO_IMMEDIATE) { 772 | Serial.write(ch); 773 | } 774 | xStreamBufferSend(txBuf, &ch, 1, 0); 775 | } 776 | } 777 | 778 | if (xStreamBufferSpacesAvailable(txBuf) < block_size) { 779 | /* No room for another block */ 780 | in_xoff = true; 781 | #ifdef DTR_PIN 782 | if (dtr_mode) { 783 | digitalWrite(DTR_PIN, DTR_FALSE); 784 | } 785 | #endif 786 | /* Send XOFF string if we're not in ENQ/ACK mode */ 787 | if (!enq_char) { 788 | Serial.print(imm_xoff); 789 | if (handshake_mode == 1) { 790 | Serial.print(output_terminator); 791 | } 792 | Serial.flush(); 793 | } 794 | } 795 | } 796 | vTaskDelay(1); 797 | } 798 | } 799 | 800 | void setup() { 801 | /* put your setup code here, to run once: */ 802 | Serial.begin(9600); 803 | 804 | txBuf = xStreamBufferCreate(TXBUF_SZ, 1); 805 | 806 | xTaskCreate(gpibTask, "gpib", 128, NULL, 1, &gpib_taskhandle); 807 | xTaskCreate(serialTask, "serial", 128, NULL, 2, NULL); 808 | } 809 | 810 | void loop() { /* Nothing to do here, it all happens in tasks */ 811 | } 812 | --------------------------------------------------------------------------------