DottyMatrix: A simple acknowledgement

Although it was gratifying (and surprising) that my DottyMatrix serial-to-parallel interface worked as soon as I plugged it into an actual printer, there was one nagging flaw. And that was a lack of acknowledgement.

The /ACK signal in the Centronics interface was often sadly neglected, if not ignored completely. But in theory, the sequence for printing a character goes like this (assuming no errors):

  • We’ll assume the printer is ready, which means that it holds the BUSY line low and the /ACK line high.
  • The host machine puts a byte’s worth of data on the eight data lines.
  • The host pulls the /STROBE line low for a fixed pulse length, then takes it high again. (The printer is usually pulling this line high by default.)
  • In response to the /STROBE pulse, the printer takes the BUSY line high while it prints.
  • When it’s about ready to receive more data, the printer pulls the /ACK line low for a fixed pulse length, then lets it go high again.
  • The printer pulls the BUSY line low.

All about the timing

Now, as with all signals, it’s all about the timing. In my code, the function that prints a character follows the process above. But it has an option as to whether to wait for the /ACK signal. (This is set via a global variable, waitForAck.) If the interface is set to ignore /ACK, then just the BUSY signal is used for flow control.

Maybe some code would help.

printer_state printChar(uint8_t chardata, ack_state waitForACK)
{
    updatePrinterState();
    if( !(curr_state == ERROR || curr_state == OFFLINE || curr_state == PAPER_END || errorState == ERR) ) {
        bool done = false;
        uint8_t waitTimeoutCounter = 0;
        while(!done) {
            if(curr_state == READY) { // Print character
                resetAck(); // an excess of caution
                DataRegister.shiftOut(chardata, MSBFIRST); // put data on shift reg
                _delay_us(PRE_STROBE_DELAY); // min 0.5us
                setPin(&OUTPUT_PORT, STROBE_PIN, LOW); // start strobe pulse
                _delay_us(STROBE_PULSE_LENGTH); // pulse time
                setPin(&OUTPUT_PORT, STROBE_PIN, HIGH); // end pulse
                if(waitForACK == ACK) {
                    uint8_t timeout_count = 0;
                    while(timeout_count <= ACK_TIMEOUT_LOOP_COUNTER && ackstate == NO_ACK) {
                        // the ACK line is set up on interrupt INT0 so we need to wait
                        // for the interrupt to trigger
                        _delay_us(ACK_TIMEOUT_LOOP_DELAY);
                        timeout_count++;
                    }
                    if(ackstate == NO_ACK) {
                        curr_state = ACK_TIMEOUT;
                    } else {
                        curr_state = DONE;
                        ackstate = NO_ACK; // reset
                    }
                } else {
                    curr_state = DONE; // not using ACK
                }
                done = true;
            } else {
                _delay_ms(BUSY_TIMEOUT_LOOP_DELAY);
                updatePrinterState();
                waitTimeoutCounter++;
                if(waitTimeoutCounter == BUSY_TIMEOUT_LOOP_COUNTER) {
                    curr_state = BUSY_TIMEOUT;
                    done = true;
                }
            }
        }
    }
    return curr_state;
}

I’m using a STROBE_PULSE_LENGTH of 1µs. The Epson manual stipulates a minimum of 0.5µs.

You can see that the loops involving the /ACK and BUSY signals have timeouts. In the case of /ACK, we go into a loop that pauses each time for a length of time set with ACK_TIMEOUT_LOOP_DELAY, which has been defined earlier to be 1µs. It will go through this loop a maximum of ACK_TIMEOUT_LOOP_COUNTER times, which I’d originally set to 50. However, it will drop out of this loop as soon as ackstate gets set to ACK (that and NO_ACK have been established earlier as an enum). This means the delay imposed by waiting for an /ACK signal could be as little as once through the loop – ie, 1µs.

So how does ackstate get set? That’s via an interrupt.

And I confirmed all this works with my virtual printer. But here’s where a problem crept in. You see, with the virtual printer I had to make some arbitrary decisions about when stuff happens and for how long. And what I’d done wrong was to have it send an /ACK pulse as soon as the /STROBE pulse had been received (more or less).

Experiments led me to conclude that an ACK_TIMEOUT_LOOP_COUNTER value of 50 was good (40 didn’t work) with my virtual printer. And so the maximum wait before the interface decided it should time out was 50 times through the loop or 50µs. The length of the /ACK pulse itself is around 5µs, so I thought a maximum wait of 10 times this value was being generous.

It turns out that real life isn’t like that. And how do I know? RTFM.

The Epson MX-80 F/T III is from a time when manuals actually told you stuff you could use. And so, in addition to things like ASCII character tables, code samples and the pinout of the parallel interface, the Epson’s manual has a timing chart that I’d somehow previously overlooked.

And from this chart I can see that the length of time between the end of the /STROBE pulse and the beginning of the /ACK pulse can be as much as 1ms. That’s 20 times what I’d allowed for. Changing ACK_TIMEOUT_LOOP_COUNTER to 1000 solved the problem.

On the scope

But I still wasn’t satisfied. I wanted to see all this going on with my own eyes. And that meant it was time to warm up the oscilloscope. (And yes, a logic analyser would have been better in many ways, but I still haven’t got mine going yet.) So buckle up for some screengrabs.

I decided to probe the /STROBE, /ACK and BUSY signals, with triggering on the /STROBE signal (falling edge).

In the image above (click for a bigger version) you can see the traces representing slightly more than one character being sent to the printer. The start of each character is the dip on the /STROBE (yellow) line. You can see how the BUSY (blue) line goes high, and it’s not until just before that line goes low again that we see the /ACK pulse (magenta). So let’s measure some stuff.

The time between one /STROBE pulse and the next – ie, the time for each character – is 1.024ms. In theory, that means characters are being sent at a rate of 977 characters per second – somewhat in excess of the printer’s stated print speed of 80 characters per second. What do we deduce from this? That the printer has a buffer. And that’s good news, because it means my interface isn’t a bottleneck.

The time between the /STROBE pulse and the corresponding /ACK pulse is 866µs – within the 1ms mentioned by the manual. But you can see why my original settings were causing problems.

The /STROBE pulse itself lasts for 1.24µs. In my code, I hold the pin low for 1µs so I guess the rest is accounted for by the time it takes processor operations to execute and propagation delays. You can see how the BUSY line ramps up relatively slowly. However, by the time the /STROBE pulse has completed, BUSY is at 3V, so can be regarded as high.

Finally, and just to be completist, the time between the /ACK pulse starting and the BUSY line dropping (thereby making the printer ready to receive another character) is 10µs. Is that significant? Who knows, but there it is.

[UPDATE 26/05/2018] I forgot to measure how long the BUSY line is low before the next /STROBE pulse is sent. This effectively represents the time taken by the interface code to go around its loop and pump eight more bits into the shift register. But we can work it out very roughly from the above screengrabs.

The time between /STROBE pulses is 1.024ms. The time between /STROBE and /ACK is 866µs and the BUSY line drops very soon after the /ACK is done. Let’s round that up to 900µs. That means the BUSY line is low for (give or take) 100µs. All the other timings are pretty much determined by the printer, so if there’s any optimisation to be done it’s within that 100µs. But, to be honest, the interface is working so much faster than the printer that I don’t think I’m going to worry about that.

4 thoughts on “DottyMatrix: A simple acknowledgement

    1. Machina Post author

      Yeah, I’ve taken a look at that. I’ve already got a cheapo analyser – one of those Saleae clones. I just haven’t had time to set it up and mess with the software.

      Reply
      1. Richard P

        Agreed.. the software on the OLS is a pain too.. JAVA is not the most endearing runtime out there 😀

        Reply
        1. Machina Post author

          This conversation prompted me to dig out my logic analyser. And now I remember why I haven’t been using it. The Sigrok PulseView software I’ve installed on my Mac simply won’t recognise it. Nor will a couple of other packages I’ve tried. From comments and posts I’ve seen on the web, there is a way of flashing new firmware to the analyser. Alas, macOS is one of the least-supported platforms with Sigrok, which has a definite bias towards Linux. I could try this on my tatty old Win7 laptop, or even a Linux VM. But when you start spending hours trying to get a *tool* to work, you know you’re on a losing proposition. Tools are meant to save work, not make it. I’ll stick with the scope for now.

          Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.