Arduino IEEE488 Device
Introduction
This project came about because in 2021 I had bought an Acorn IEEE488 Interface for the BBC micro but at the time I didn't have any equipment with an IEE488 interface so I looked to using an Arduino to function as a simple voltmeter while providing an IEEE488 interface for testing purposes. While this page should be useful for anyone looking to use an Arduino with IEE488 compatible equipment it is geared toward using the Arduino with a BBC micro with the Acorn IEEE488 Interface.
Please note that IEEE488 is also known as HP-IB (Hewlett-Packard Interface Bus) and GPIB (General Purpose Interface Bus).
The images on this page are available for download from the bottom of the page for better viewing.
Getting to grips with IEE488
I looked online to see if anyone had already used an Arduino to emulate the IEE488 interface and found several examples. I started first with:
https://github.com/Twilight-Logic/AR488
Make sure to read the manual:
https://github.com/Twilight-Logic/AR488/blob/master/AR488-manual.pdf
The wiring between an Arduino Uno/Nano and the IEEE488 interface is given on page 40 and for Mega, see page 43.
I wrote a program on the Acorn side in BASIC to send a string and I put the Arduino into device mode and set it to listen (lon) but when I ran the BASIC program only a '?' appeared in the Arduino Serial Monitor. For some reason, when the Arduino is put into listen mode and it's connected to the Acorn IEEE488 the Arduino TX LED will flash and there is a constant pulsing on the DAV line. I did also try with buffer chips as mentioned as a recommended addition but I got the same results.
So, next I tried another Arduino IEEE488 implementation:
https://github.com/mathiashelsen/agipibi
It's designed for the Arduino Mega 1280.
You need to download the zip file, extract, and open arduino_mega.ino in agipibi-master\agipibi-master\arduino_mega. The documentation has the pin mapping in gpib_arduino_mapping.txt. No actual documentation for using the code, however.
There is example code which interacts with the Arduino via its serial port.
agipibi-master\agipibi-master\C use a HP 3478A multimeter to read out DC current
agipibi-master\agipibi-master\python Tektronix 2432 oscilloscope
In arduino_mega.ino it lists the commands under serial commands.
I went with the Python example but it requires the serial module which isn’t installed by default but can be downloaded from https://pypi.org/project/pyserial/. On Windows it’s not that easy to install as you need to install from the command line but there is a helpful guide to get serial installed:
Don’t forget when adding the variable value to use your Python version. Even after installing serial I had problems with importlib so I used Python 2.7; the serial module still had to be installed.
The Python example was geared toward Linux for opening the serial port as you’ll see that the serial port is referenced as /dev/ttyUSB0 but Windows uses a different format, such as COM3. So if you are using Windows, when the serial port is opened in the Python example code, line 39, replace /dev/ttyUSB0 with the COM port that the Arduino is connected to, such as COM3. Note that the baudrate is set in arduino.py, line 32.
I started a new Python sketch (agipibi_test.py) basing it on example_tektronix_2432.py, so I could make changes while still having the original test code.
Arduino.py also opens the serial port on line 31, so the correct serial port will also have to be put in.
Unfortunately, the Python script reported that there was no response from the Arduino; the ping test had failed. The test works by sending PING (which is essentially zero) from the Python script to the Arduino through the serial port and the Arduino responds with PONG (which also has the value of zero). To narrow down the fault I used RealTerm to send binary data directly to the Arduino via its serial port (PuTTY, for example, can only send ASCII characters). If you click in the black area and type ASCII characters they will be sent; the Display As options only determine how the data is displayed in the window not how it’s sent. To send binary data, select the Send tab, type a number in the drop-down box and click Send Numbers (for hex, use 0x).
Using Realterm I was able to send various values to the Arduino and it responded appropriately; for example, sending ‘0’, which is the PING command, which responds with ‘0’, the PONG response. Sending ‘1’ to the Arduino causes it to respond with the GPIB status, so it was clear the Arduino sketch was indeed working. This was only possible, however, by reducing the baud rate from 115200 to 57600 in the Arduino sketch.
Returning to the Python script I found that agipibi.py causes an error in interface_ping and that’s why the test script says there is no response. By adding various print statements I found that in arduino.py in the _read function it doesn’t find any serial data, that is, read() doesn’t return anything. By looking online it seemed that perhaps the Arduino wasn’t responding quick enough but even increasing the timeout generously didn’t help. The solution I found was in the test script to use time.sleep(1) after this line:
ctl = Agipibi(device='COM4', debug=False)
Need to also add import time before:
from agipibi import Agipibi, AgipibiError
I found that any lower value for time.sleep() caused the ping to fail.
The reasoning behind adding the delay is that after opening a connection to the Arduino serial port the Arduino resets and thus doesn’t respond quick enough to the value sent to it by the Python script. Using Realterm was also a problem if using 115200 baud rate (I had previously used 57600) but what I found helped was to click the Open button on the Port tab twice (first time closes the port and second time opens it).
There is still the question of why a delay was needed in the Python script as the original developer of the code had got it working on their Arduino but I was using a clone Arduino (MEGA 2560) so that may have had something to do with it.
Now I was getting a response to PING I was able to put the baud rate back to 115200 in the Arduino sketch and arduino.py, and PING still worked. If I use a genuine Arduino Uno, however, as I only have clone Megas, the PING fails at any baud rate unless I increase the delay I added to the Python script to 1.1 instead of 1.
The problem with the software and the previous one I tried, however, was that they were geared toward being a controller and responding to commands from the user, which made interacting with the Acorn IEEE488 interface difficult, which I wanted to use as a controller. So I started a new sketch based on arduino_mega.ino (IEEE488_IO_Device) and rewrote the code to function as a device; there was no need for the external program (Python/C).
Using some of the existing code and online information to understand how the IEEE488 bus works I came across a problem; I found that when the DAV line is high it’s at only 1.5V (instead of about 3V) which is low enough for an Arduino to think the DAV line has been pulled low. The DAV line is handled by the 75162 in the Acorn interface and the minimum high-level output voltage should be 2.7V, so I thought the chip was faulty.
It turns out that the low voltage I was seeing is correct and happens when a buffer in the 75162 is disabled but the input to the other buffer is pulled up. It still seemed strange that DAV should go from 1.5V to 0V as if the Acorn interface was pulling it low to signal that data was ready. I did buy some 75162 ICs in case the original one was faulty but unfortunately the chips in DIP form are expensive so I bought SMD versions and adapter boards, which could be useful for other projects too. Testing one of the 75162 ICs I had purchased it behaved the same in terms of putting the DAV pin at 1.5V when disabled but as it would be tricky to adapt the adapter board to fit in the chip holder in the Acorn interface I left that as a possible later option.
I noticed using my logic analyser that DAV was low even when driven high by the Arduino and I eventually found there was a low resistive short between DAV and NRFD on the GPIB connector I had soldered up. Although I couldn’t see a short, perhaps a strand of wire had got caught inbetween the connector legs as I was able to remove the short after some poking around.
While removing the short had allowed DAV to change correctly I still had the issue of INPUT$ working but BGET# resulting in an incomplete source handshake error even though bytes were successfully received. I realised the issue was that while INPUT$ will keep reading bytes until it receives the LF character, you must send the correct number of bytes specified when using the READ BINARY command otherwise you will get the incomplete handshake error. This was happening with me when I was sending more bytes than READ BINARY was expecting.
At this point I looked for better information about IEEE488 and came across a publication ‘PET and the IEEE 488 Bus’ which you can find below:
http://www.primrosebank.net/computers/pet/documents/PET_and_the_IEEE488_Bus_text.pdf
Although it’s an old book and geared towards the Commodore PET (which has a GPIB bus but uses a non-standard connector) it goes into detail about how IEEE488 works and how devices and controllers should handle the various signals. This gave me a better understanding of how to respond to the Acorn interface and I was able to get the DAV line to respond correctly and not show any errors. I still had to look at the various signals with my logic analyzer but it helped me, along with the PET book and the manual for the Acorn interface, to get the Arduino to respond to the various IEEE488 commands.
As well as learning what the various signals were used for and how to respond correctly to the Acorn interface I now knew that command values can either imply a command or an address (device number) for a particular mode. When sending commands ATN is low, when sending other data (ASCII, etc) ATN is high. This means we can use ATN going low as a form of trigger to get ready to receive a command but we must also be ready for ATN going high to possibly accept other types of data.
Note that all the IEEE488 signals use negative logic - true is low - and that includes the data bus so if you read in the state of the data lines you must invert them to get the correct value.
Let’s look at an example of sending an ASCII string to a device:
*IEEE
cmd%=OPENIN(“COMMAND”)
data%=OPENIN(“DATA”)
dvc%=OPENIN(“1”)
PRINT#cmd%,”LISTEN”,dvc%,”EXECUTE”
PRINT#data%,”HELLO WORLD”
There is no activity on the IEEE488 bus until the third line in which the LISTEN command is sent, which will result in 0x21 appearing on the data bus. Values 0x20 to 0x3E represent listen addresses 0x00 to 0x1E so the value specifies what device is to listen; just subtract 0x20 to get the actual listen address. If multiple devices are told to listen, using:
dvc%=OPENIN(“1”)
dvc2%=OPENIN(“2”)
PRINT#cmd%,”LISTEN”,dvc%,dvc2%,”EXECUTE”
Then 0x21 will appear on the data bus followed by 0x22. This isn’t something we have to worry about with the TALK command as there can only be one TALKER at once.
The UNLISTEN command behaves in a similar way but has the value of 0x3F, which unaddresses all active listeners.
The Write binary command can be used to send individual bytes but it is not a command to a device but to the Acorn interface so that it can signal that the last byte has been sent, which it does so by taking EOI low when the final byte is sent. Some devices, however, may allow you to signal to them that you wish to send them a number of bytes using a device message.
Arduino IEEE488 voltmeter
As my intention was to make a simple IEEE488 device using an Arduino as a voltmeter, I tested with an Arduino Mega, as it easily has enough I/O for the project. The Arduino sketch is called IEEE488_IO_Device.ino, available for download from the bottom of the page, and is built on top of Agipibi (see the 'Getting to grips with IEE488' section), note that there is some debug code and commented out code I used for testing.
The sketch enables the Arduino to emulate an IEEE488 device, designed to work with the Acorn IEEE488 Interface, and to keep things simple, acts as a simple DC voltmeter which can be controlled either with its own controls (buttons) or remotely via an IEEE488 controller, such as the Acorn IEEE488 Interface. It reads a single voltage (0 to 5V) and displays the voltage reading in either volts (V) or millivolts (mV) depending on the user setting. A trigger can be set up to detect when the input voltage reaches a certain voltage, which causes the trigger output to go low.
I had written the Arduino sketch to send data as soon as it was set to be a talker but I then changed the code so that a flag would be set instead and then that flag would be checked later and if set then data would be sent. This then caused the incomplete source handshake error to reappear but I found the solution was to add a 2ms delay after the Arduino sent the last byte.
A features summary of the voltmeter:
Inputs
Power on/off switch.
Voltage in (voltage to be read).
Rotary encoder with switch for selecting options and changing values.
Outputs
HD44780 LCD with backlight for outputting voltage reading and status. Backlight is on when in local mode and off when in remote mode.
Trigger out which goes low when target voltage is reached (when in trigger mode).
GPIB interface for connecting to GPIB controller.
To read the input voltage we use an analogue input (A0) with analogRead() which will return a value between 0 and 1023 representing 0 to 5V. A value of 1 from A0 represents 0.0049V (4.89mV) derived from 5V / 1024. Thus the voltage for the analogue value read can be calculated by multiplying it by 0.0049V, example reading of 72 = 72 * 0.0049 = 0.35V.
As a simple test I used a potentiometer with the wiper connected to the Arduino’s A0 and the other two contacts connected to GND and 5V respectively. With my multimeter on volts range and connected between GND and A0 I adjusted the potentiometer while checking the reading on my multimeter against the values reported by the Arduino. I found that with most values there was a difference of 0.01V or less, which is close enough for my needs.
In the main Arduino loop we get the current analog value and convert it into the voltage value and then for the rest of the loop we handle the GPIB bus. I checked the timing of how quickly the analogue value can be read by toggling a GPIO pin directly and found that it takes 1.62ms for the loop to execute if there is no activity on the GPIB bus which is a decent update speed.
The Arduino can respond to different GPIB device commands that allow for remote operation of the voltmeter. To send a device command it must be sent as ASCII uppercase characters only along with numeric values as required, and terminated either with the line feed character (10) or by taking EOI low (true) when the last character is sent.
I came up with short commands which may not be so easy to remember but helps keep processing as short as possible on the Arduino side. The commands are stored in an array in the Arduino sketch in alphabetical order and this is important as we check the received device command against each array entry in turn for a match. If you take ‘VOLT’ for example, it could be matched with both ‘VOLT’ and ‘MVOLT’ but ‘MVOLT’ could only be matched with itself.
The device commands are as follows:
Set device talk address
TADDRX
Where X is between 0 and 30 inclusive; if no value specified then 0 is assumed. Shouldn’t conflict with the GPIB controller's address.
E.g. TADDR5 sets device talk address to 5.
Set device listen address
LADDRX
Where X is between 0 and 30 inclusive; if no value specified then 0 is assumed. Shouldn’t conflict with the GPIB controller's address.
E.g. LADDR7 sets device listen address to 7.
Set to volts range
VOLT
Voltage reading will be in volts, e.g. 2.49V.
Set to millivolts range
MVOLT
Voltage reading will be in millivolts, e.g. 690mV.
Turn trigger on
TRON
When the input voltage matches the trigger voltage a pulse is sent on the trigger output.
Turn trigger off
TROFF
Do not check the input voltage against the trigger voltage.
Set trigger voltage
TRVOLTX
Where X is the trigger voltage (0 to 5V); if no value specified then 0 is assumed. Value will be treated as mV/V depending on the current V/mV setting but there is no need to specify ‘V’ or ‘mV’ with the value.
E.g. TRVOLT3.36 will trigger when the input voltage becomes 3.36V +/- 4.89mV.
OUTVER
Get version string. After sending this command, when the device is then made to talk it will send a series of ASCII characters detailing the version number, e.g., Version 1.0.
OUTHITTRIG
Find out whether the trigger value matches the input voltage. After the command is sent, set the device to talk and it will return either ASCII character Y if the trigger voltage has been reached or N if it hasn’t. If trigger mode is off then an N will always be sent.
OUTVOLTREAD
After the command is sent, set the device to talk and it will return the current voltage reading in mV/V depending on the current V/mV setting. Returns the value only with no ‘V’ or ‘mV’, e.g. 5.26.
Some of the options available from use of the device commands are also available using the local controls.
Note that if the device is made to talk without first receiving an OUT type device command, the error ‘ERROR: data type not set!’ string will be sent. Also, the device will only send the data requested once, use a different OUT command to get different data..
When the Arduino receives a series of bytes (sent by the controller) and then either detects the line feed character or EOI goes low then the complete message is checked against a list of known device commands so that some action can be performed. For device commands that are then to be followed up with the device sending data, a value is set so the sketch knows what data to send.
The current voltage reading, status and menu are displayed on a 16x2 HD44780 LCD module. To determine when to update the LCD the Arduino sketch waits for a minimum amount of time to pass by making use of the millis() function. This way the Arduino is free to respond to the GPIB bus and once it is finished processing a message, the check can be made to see if enough time has passed to update the LCD. As long as the GPIB processing doesn’t take too long the LCD will get updated frequently.
Getting this to work, however, meant further changes so that the Arduino isn’t waiting unnecessarily for data to be sent. The solution was to attach an interrupt to DAV (high to low) so that another device can indicate it’s sending data. The Arduino signals that it’s ready to receive data regardless of DAV’s state but once the interrupt is triggered a flag is set so that the Arduino can read the byte in the main loop. In this way an interrupt is generated for each byte sent from the other device, which is read in by the Arduino and then it signals again when ready that it can take another byte (even if there isn’t).
The LCD displays the voltage reading on the top row as either millivolt or volt depending on the current setting and at the end of the top row an ‘L’ is displayed if operating in local mode or an ‘R’ if in remote control mode. If trigger mode is on then on the bottom row a ‘T’ will be displayed as well as the target trigger value (millivolt or volt based on current setting). If the trigger voltage is reached then an ‘!’ will appear after the trigger value.
To enter the settings menu press the rotary encoder button once which will cause the first menu option to show and turn the encoder to select a different menu option. Press the button to select the menu option for editing, which will cause an ‘*’ to appear at the end of the row. Rotate the encoder to change the option value and then press the button to stop editing the option. If you press the button while the Exit option is selected the menu will turn off.
The options are as follows:
Talk addr X
Where X is the current talk address. Turn the encoder to select a different address from 0 to 30.
Listen addr X
Where X is the current listen address. Turn the encoder to select a different address from 0 to 30.
Volt (or Millivolt if that is the current setting).
Turn the encoder to select millivolt/volt.
Trig mode on (or Trig mode off if turned off)
Turn the encoder to turn on/off.
Trig@X
Where X is the current trigger value (volt/millivolt depending on the setting). Turn the encoder to select a different value. An offset -/+ of 4.88mV (the minimum volt value the ADC can represent) is used with the target trigger voltage when checking against the input voltage. For e.g., if the trigger voltage is set to 320mV then the trigger will be satisfied for any reading of 315.12mV to 324.88mV.
Exit
Press the encoder button to exit the options menu.
I had never used a rotary encoder before but thought it would be quite straightforward to use especially as I had found a suitable library called Encoder. I actually had a lot of trouble and part of the problem was the encoder I was using. The thing to realise is that ‘cheap’ encoders are noisy and need to be debounced like a normal switch. With the encoder I was using if I turned it too quickly it would act as if being turned in the opposite direction. I confirmed this by outputting the encoder position value to the serial port.
When I switched to a better encoder, which couldn’t be turned as quickly as the other one, I got consistent results with one rotation direction giving an increase in position value and the other decrease in value. However, the menu options were still changing too quickly so I needed to slow down the response. The solution I found was to check that the encoder position value had changed by a minimum amount since last time and if it had then a response was made (move to the next menu option, for e.g.) and then new position value was then stored as a reference for next time.
Note that I didn't use interrupts for the encoder. To disable interrupts use the following line before including the encoder library:
#define ENCODER_DO_NOT_USE_INTERRUPTS
Not only did I find the encoder worked well without interrupts (once I had fixed the problem with responding too fast to the encoder being turned), I was concerned that if the encoder was rotated while a GPIB message was being received it could cause issues as we require an interrupt for knowing when data is being sent.
Displaying the voltage reading proved a bit of a problem as sometimes the value would fluctuate at high speed and this turned out to be the result of a number of problems. Firstly, it’s quite normal for the ADC value to change by a small amount frequently and if we want better stability we can use an external ADC. But keeping with the one built in the Arduino’s microcontroller it is recommended to add a 0.1uF capacitor between AREF and GND, and another between the analog pin and GND. Another recommendation is to ensure the power supply to the Arduino is stable which may not be the case when powering from USB but even when the Arduino was running off my bench power supply there were still fluctuations.
Adding capacitors didn’t seem to help but they were a good idea to have anyway and it seems part of the problem with the fluctuations was reading the voltage too frequently so I added a delay by making use of millis(). I then noticed the voltage reading was changing between three or four values fairly slowly so I took five readings over an amount of time and stored them into an array but then settled on 10 readings. After the readings have been taken the average is calculated (added them up and divided by five) and used that as the voltage reading. This worked well and although the reading was still changing somewhat frequently so was the value on my multimeter, although less frequently, so it seemed a good trade-off. Note that you don’t want to spend too much time taking the readings otherwise the display may seem slow to react to changes in the voltage being measured. To add also, the average voltage reading is used for determining if the trigger voltage has been reached as to help ensure the trigger output is stable.
When I first started the project I only did a hand drawn circuit diagram but now I've put together a digital version using KiCad, which follows but is also available for download from the bottom of the page.
I've tried my best to ensure there are no errors but please use the diagram as a guide and double check all your wiring before applying power.
There was no Arduino Mega symbol built in to KiCad (V7.0.5) but I found online a suitable symbol at:
http://smisioto.no-ip.org/elettronica/kicad/kicad-en.htm
The exact one I used was Arduino_Mega_Header from lib_w_connectors.
The Arduino Mega signal names (inside the symbol) aren't quite conventional, ‘PIN’ is used fo non-PWM digital pins, for examples. The actual pin numbers are outside the symbol, with pin 1 near the power barrel jack connector, going anti-clockwise around the board.
The Arduino Mega has a lot of connections and the pin names are small, without adjusting the symbol I matched the label text size and grouped related connections together.
On the schematic Arduino Mega pin 2 is labelled as IOREF but on my Mega clone it is marked as 5V, since IOREF is normally connected to 5V on a Mega it's correct but I thought I should mention in case you are using a clone board to avoid confusion even though IOREF isn't used in the project anyway.
Surprisingly, KiCad had no generic HD44780 LCD symbol, but I found a few suitable symbols, such as WC1602A, which was the one I used.
A few other things to mention:
LCD DS1 R/W (pin 5) is connected to GND so the LCD is always in write mode as we have no need to read data from it.
Use an appropriate value for R1 depending on the requirements of your specific LCD, I chose a 'safe' value to keep the LED current low.
As well as helping you see the text on the LCD, the backlight is also an indicator of whether the circuit is receiving power. Of course you could add a discrete LED power indicator with limiting resistor if you desire.
RV1 (LCD contrast) needs to be adjusted while power is applied until you see text on the LCD.
It's a good idea to test the circuit first without connecting it up to the Acorn IEEE488 Interface, after applying power to the Arduino you should see the voltage reading on the LCD and 'L' displayed to the right. Check that operating the rotary encoding allows you to access the menus and change values. If this is working then you can move on to the next section.
BBC micro IEE488 programs
I wrote the following BBC BASIC programs to test the basics of the Acorn IEEE488 in reading data from an IEEE488 device (Arduino) and sending data to an IEEE488 device (Arduino). Each program opens the necessary channels, sets up the Acorn interface including setting the timeout on, performs operations and then outputs the Acorn interface status. I wrote and tested the programs on a BBC Master but they should also work on the model B. These are rough programs and can be improved as is also the Arduino program which, for example, needs to have a timeout facility for when waiting for a response from the Acorn interface in case it doesn’t respond for some reason.
GPIBTST Output string using PRINT# to device
GPIBTSB Output series of numbers using WRITE BINARY to device
GPIBINP Input a string from GPIB device using INPUT# and display the string read in.
GPIBRDB Read a series of numbers from GPIB device using READ BINARY and display the values read in.
GPIBTSF Tests the Arduino voltmeter.
Note that there is an apparent bug in the IEEE488 filing system if you send an empty string example:
PRINT#DAT%,””
The system will send x243 zeroes followed by string REMOTEXECUTE and end with Line Feed (10), totalling 255 characters not including the LF character.
While I was able to get both GPIBTST and GPIBTSB working, I had a lot of difficulty with GPIBINP and GPIBRDB with the difficulty being that there was a timeout issue. One thing that helped was to add a 1ms delay before outputting the byte to be sent and a 1ms delay after outputting the byte so at least the correct data values were received.
***More to be added soon***
Testing
With everything connected up, there is a certain order you should power up the items on the IEEE488 bus, with our setup it should be; power on Arduino, then the Acorn interface, lastly the BBC Micro. When powering off, switch the devices off in the opposite order; BBC Micro, Acorn interface, Arduino. This helps to ensure that everything connected to the IEEE488 bus starts up correctly, which is especially important for an Arduino, which can be powered just by the signals from the Acorn interface, which can cause glitches or in the worse case can have its flash memory corrupted.
***More to be added soon***
All content of this and related pages is copyright (c) James S. 2025