Communicating with a CAN bus through an LPT printer port

… because, why not?

A few months back, I found myself in a position where I needed to develop a proof of concept for a CAN bus data logging system, but the hardware hadn’t been finalised. At the time, there were talks of using x86-based hardware, which would eventually communicate with the CAN bus via an SPI CAN controller. Unfortunately the dev board was on back-order and I wanted to keep the project moving.

So I went digging through the store-room and found a laptop. This was a good start, but how to interface with the CAN bus? There are lots of USB CAN bus dongles available, but this wouldn’t give me a true representation of the final hardware that was going to communicate directly with CAN controller ICs and I already had a nice MCP2515 breakout board that would serve this purpose. The ‘eureka’ moment was when I remembered something my tutor had said during my MSc. He was talking about x86 systems, and he made a throw-away comment about using the old parallel printer ports for GPIO. Not hardware SPI, but GPIO at least meant I could bit-bang the protocol. More digging in the store-room and I found a docking station for the laptop which had a LPT port.

History lesson

For those too young to remember, in the olden days, before the introduction of USB, printers used to be connected to computers using a parallel port called a Line Print Terminal (LPT) port. Some machines still have them – it’s that big pink Dsub you don’t use on the back of your machine

25 pin D-sub Female connector, Plug

This huge 25-pin cable was used to send characters to the printer 1 byte at a time by setting 8 data pins.  Various handshake pins were used to control the flow of data. This website explains the process in great detail, and helped me out a lot in understanding how to control the port in software. It turns out the printer port is directly accessible at the base address of 0x378 using the stdio outb() and inb() function calls. The individual pins can therefore be set or read by sending the appropriate binary value to the corresponding address. Some pins are read-only and some are write-only, but there were more than enough for an SPI implementation. The following tables, taken from the beyondlogic page show this in more detail:

Table 1. Pin Assignments of the D-Type 25 pin Parallel Port Connector.
Pin No (D-Type 25) Pin No (Centronics) SPP Signal Direction In/out Register Hardware Inverted
1 1 nStrobe In/Out Control Yes
2 2 Data 0 Out Data
3 3 Data 1 Out Data
4 4 Data 2 Out Data
5 5 Data 3 Out Data
6 6 Data 4 Out Data
7 7 Data 5 Out Data
8 8 Data 6 Out Data
9 9 Data 7 Out Data
10 10 nAck In Status
11 11 Busy In Status Yes
12 12 Paper-Out / Paper-End In Status
13 13 Select In Status
14 14 nAuto-Linefeed In/Out Control Yes
15 32 nError / nFault In Status
16 31 nInitialize In/Out Control
17 36 nSelect-Printer / nSelect-In In/Out Control Yes
18 – 25 19-30 Ground Gnd

“The above table uses “n” in front of the signal name to denote that the signal is active low. e.g. nError. If the printer has occurred an error then this line is low. This line normally is high, should the printer be functioning correctly. The “Hardware Inverted” means the signal is inverted by the Parallel card’s hardware. Such an example is the Busy line. If +5v (Logic 1) was applied to this pin and the status register read, it would return back a 0 in Bit 7 of the Status Register.The output of the Parallel Port is normally TTL logic levels. The voltage levels are the easy part. The current you can sink and source varies from port to port. Most Parallel Ports implemented in ASIC, can sink and source around 12mA. However these are just some of the figures taken from Data sheets, Sink/Source 6mA, Source 12mA/Sink 20mA, Sink 16mA/Source 4mA, Sink/Source 12mA. As you can see they vary quite a bit. The best bet is to use a buffer, so the least current is drawn from the Parallel Port.”

Table 4 Data Port
Offset Name Read/Write Bit No. Properties
Base + 0 Data Port Write (Note-1) Bit 7 Data 7
Bit 6 Data 6
Bit 5 Data 5
Bit 4 Data 4
Bit 3 Data 3
Bit 2 Data 2
Bit 1 Data 1
Bit 0 Data 0

“The base address, usually called the Data Port or Data Register is simply used for outputting data on the Parallel Port’s data lines (Pins 2-9). This register is normally a write only port. If you read from the port, you should get the last byte sent. However if your port is bi-directional, you can receive data on this address.”

Table 5 Status Port
Offset Name Read/Write Bit No. Properties
Base + 1 Status Port Read Only Bit 7 Busy
Bit 6 Ack
Bit 5 Paper Out
Bit 4 Select In
Bit 3 Error
Bit 2 IRQ (Not)
Bit 1 Reserved
Bit 0 Reserved

“The Status Port (base address + 1) is a read only port. Any data written to this port will be ignored. The Status Port is made up of 5 input lines (Pins 10,11,12,13 & 15), a IRQ status register and two reserved bits. Please note that Bit 7 (Busy) is a active low input. E.g. If bit 7 happens to show a logic 0, this means that there is +5v at pin 11. Likewise with Bit 2. (nIRQ) If this bit shows a ‘1’ then an interrupt has not occurred.”

Table 6 Control Port
Offset Name Read/Write Bit No. Properties
Base + 2 Control Port Read/Write Bit 7 Unused
Bit 6 Unused
Bit 5 Enable Bi-Directional Port
Bit 4 Enable IRQ Via Ack Line
Bit 3 Select Printer
Bit 2 Initialize Printer (Reset)
Bit 1 Auto Linefeed
Bit 0 Strobe

“The Control Port (base address + 2) was intended as a write only port. When a printer is attached to the Parallel Port, four “controls” are used. These are Strobe, Auto Linefeed, Initialize and Select Printer, all of which are inverted except Initialize.”

Hello World

To test this, I created a quick set-up with an LED connected between pin 9 of the connector (Data 7) and pin 18 (ground) so:

#include <stdio.h>

/* Flashes the LED - the 'Hello World' of embedded systems */
void testLed(void){
		/* Set data 7 high */
		/* Set all data pins low */

will toggle the LED every second.

I tested the ability to read the pins by setting pins and reading back their values:

#include <stdio.h>

#define LPT_PORT_BASE 0x378

/* Sets the data pins one by one, then clears them
 * Outputs the value read from the port */
void lptTestPort(void){
	uint8_t in = 0, i = 0, out = 0;

	printf("Testing Output on parallel port data pins\n");

	in = inb(LPT_PORT_DATA);
	printf("S = 0x%02X\n", in);

	for(i = 0; i < 8; i++){
		out |= (1<<i);
		outb(LPT_PORT_DATA, out);

		in = inb(LPT_PORT_DATA);
		printf("%u = 0x%02X\n", i, in);

	for(i = 0; i < 8; i++){
		out &= ~(1<<i);
		outb(LPT_PORT_DATA, out);

		in = inb(LPT_PORT_DATA);
		printf("%u = 0x%02X\n", i, in);

	in = inb(LPT_PORT_CTRL);
	printf("Ctrl = 0x%02X\n\n", in);

Then I wrote some simple functions to control and read the individual data pins:

/* Sets a specified pin in the data register */
void lptSetDataPin(uint8_t pin){
	dataPortShadow |= (1<<pin);
	outb(LPT_PORT_DATA, dataPortShadow);
/* Clears a specified pin in the data register */
void lptClearDataPin(uint8_t pin){
	dataPortShadow &= ~(1<<pin);
	outb(LPT_PORT_DATA, dataPortShadow);
/* Reads a specified pin in the status register */
pinstate_t lptReadStatusPin(uint8_t pin){
	uint8_t value = 0;

	value = inb(LPT_PORT_STAT);
	return (pinstate_t)(value>>pin)&1;

dataPortShadow is used so that we don’t have to read back the value of the port each time we change the value of an output to do so would double the time taken to change the state of a pin and drastically impact timing (see “Limitations” below). Since the data port is output-only by default, I used the status port for input pins. It’s meant to be possible, with some machines, to change the data port to bi-directional by setting bit 5 of the control port, but I didn’t have any luck with this (and it wasn’t a pressing issue since I had plenty of spare pins).

Bit-banging SPI

Once I had control over some IO, I needed to talk the SPI protocol. In modern microcontrollers, there is a hardware peripheral built-in that is accessed by writing to or reading from certain registers. The peripheral then controls and reads IO pins according to the SPI protocol. I don’t have the luxury of an SPI peripheral but, luckily, the the protocol is really straightforward.

There are 4 signals in SPI:

  • Slave select (SS) – This is used when there are multiple devices on the same SPI bus
  • Clock (SCLK)
  • Master In, Slave Out (MISO)
  • Master Out, Slave In (MOSI)

The master (our software) controls the CS, CLK and MOSI signals and the slave (the CAN controller in this case) controls the MISO signal. In order to send data to the peripheral, we first pull the CS pin low, and then ‘clock out’ the necessary data. Every clock pulse indicates a bit of data, and the state of the MOSI signal represents its value. The CS pin is set high when transmission has finished. For example, the following signals (assuming msb first) will send a 16 bit value of 52045 (0xCB4D):

Receiving data is performed in the same way, except we read the value of the MISO each time we cycle the clock signal.

The protocol can be controlled by manually setting / clearing the appropriate pins and reading the values of other pins that the CAN controller is setting. The process of doing this with GPIO is known as ‘bit-banging’:

/* Simultaneously sends a byte to the SPI slave
 * and reads one back (standard SPI implementation) */
uint8_t SPIExchange(uint8_t spiByte){
	int i;
	uint8_t byteIn = 0x00;

	for(i = 7; i >= 0; i--){
		byteIn >>= 1; /* I need this comment so WordPress doesn't break my line endings >>*/
		byteIn |= lptReadStatusPin(IO_SPI_MISO);

		if((spiByte>>i) & 0x01){


	return byteIn;
/* Sends a byte to the SPI slave */
void SPISendByte(uint8_t spiByte){
	int i;
	for(i = 7; i >= 0; i--){
		if((spiByte>>i) & 0x01){
/* Reads a byte from the SPI slave */
uint8_t SPIReadByte(void){
	int i;
	uint8_t byteIn = 0x00;

	for(i = 7; i >= 0; i--){
		byteIn <<= 1;
		byteIn |= lptReadStatusPin(IO_SPI_MISO);

	return byteIn;

Putting it all together

From here, it was a case of using code I already had for the MCP2515 on a PIC32-based system, with a couple of glue functions to complete the interface. I made the following connections:

SCK Pin 8 (Data 6)
MOSI Pin 9 (Data 7)
MISO Pin 10 (Status ACK)
SS Pin 7  (Data 5)

I powered the CAN breakout from a USB port and added a pull-up resistor to the reset pin (RST) to keep it out of reset mode. A 120 Ω termination resistor was also needed between the CAN H and CAN L terminals.




As a proof of concept, this worked great, however the main limitation is the speed of the parallel port. I measured the time taken to toggle access the port at around 1 μs. This gives a maximum SPI clock speed of 333 kHz for SPISendByte() or SPIReadByte() and 250 kHz using SPIExchange().

In my data-logging application, the most used SPI transactions with the MCP2515 are RX STATUS (to check for new messages) and READ RX BUFFER for each message received (the CAN controller can hold 2 messages at a time).


From Microchip’s MCP2515 datasheet

/* Reads the status of the two MCP2515 Rx buffers */
unsigned char CANReadStatus(unsigned char statReadChannel){
	unsigned char Status;
	SelectCANChip(statReadChannel); /* Pulls CS pin low */
	SPISendByte(CAN_RD_STATUS);     /* Command */
	Status = SPIReadByte();         /* Response */
	DeselectAllCANChips();          /* Sets CS pin high */
	return Status;


From Microchip’s MCP2515 datasheet

/* Reads a specified Rx buffer from the MCP2515
 * CS pin pulled low prior to entering this function */
static void RxCANbuffer(unsigned char bufferID, CANdataBuffer_t *buffer){
	int i;
	unsigned char TmpRxAddress[4];

	SPISendByte(bufferID);            /* Command */
	TmpRxAddress[0] = SPIReadByte();  /* The first 4 bytes are the frame ID */
	TmpRxAddress[1] = SPIReadByte();
	TmpRxAddress[2] = SPIReadByte();
	TmpRxAddress[3] = SPIReadByte();
	buffer->CANdlc = SPIReadByte();   /* Data Length Character */
	if(buffer->CANdlc > 8){
		buffer->CANdlc = 8;
	for(i = 0; i < buffer->CANdlc; i++){
                /* CAN data */
		buffer->CANdataRx[i] = SPIReadByte();

	buffer->CANid = RegsToCANID(TmpRxAddress);
/* CS pin set high after leaving this function */

I started out using SPIExchange() for all transactions (most standard microcontroller SPI implementations use this method), but I found there were massive overheads that could be saved by using the individual send and read functions. To read a CAN message from a buffer on the MCP2515 takes 16 SPI clocks for RX STATUS plus 112 SPI clocks per CAN message (1 byte for instruction and 13 bytes of received data for an 8 byte frame). Plus the time taken to toggle the CS pin.

Using SPIExchange (4 port accesses per clock), the worst case timing (two 8-byte CAN frames waiting) equates to (16 * 4)  + (112 * 4)  +  (112 * 4)  +  6 =  996 μs.

Using the individual send and receive functions (3 port accesses per clock) this is reduced to (16 * 3)  + (112 * 3)  +  (112 * 3)  +  6 =  726 μs.

Logging data at this rate wasn’t ideal, but it did allow me to integrate my proof of concept with low-level CAN handing code, and work with real data transmitted from a PCAN dongle on another machine without relying on a 3rd party library that wouldn’t make it into the production implementation.



Tell me what you think...

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s