Relocatable Quick-Tape installer for the Sharp PC-1500

Saturday, 26th April 2025

After my previous journal post about faster saving and loading of Sharp PC-1500 programs to and from tape I discovered that I was using a somewhat out-of-date version of Sharp Pocket Tools. The later version I upgraded to adds support for the "SuperTape" and "Quick-Tape" formats, and also includes copies of the PC-1500 programs for those formats.

Both formats appear to support file names and verification of the recorded data (a minor gripe I had with the Fast Load was its lack of these features). SuperTape apparently manages even higher speeds, though at the cost of somewhat more RAM than Fast Load (711 bytes, or 761 for the full version with the ability to merge a program with the existing one in memory, instead of Fast Load's 540 bytes). Quick-Tape seems to be about the same speed as Fast Load, but actually consumes less memory overall (only 508 bytes). Quick-Tape also explicitly supports saving machine language programs by specifying a start and end address, something that I cannot see is supported with the SuperTape format, though it looks like it might support loading machine language programs if the correct load address is stored in the file.

Based on my experiences with Fast Load and the documentation that accompanies SuperTape and Quick-Tape I came up with a rough feature comparison table (I can't claim it's completely accurate, as it's based on what I've gleaned from the documentation):

Format Fast Load SuperTape Quick-Tape
Speed Fast Fastest Fast
Filenames
Verification
Machine language (Maybe?)
Variables
RAM usage (bytes) 540 711–761 508
Relocatable (Partial)

To get a rough idea of speed I tried converting the Globe application to a wave file. Both Fast Load and Quick-Tape produce a file that's 30 seconds long — a huge improvement over the seven minutes that the native cassette routines take. SuperTape still manages to blow both of these out of the water with a file that's only 13 seconds long! Only Fast Load seems to have the ability to save BASIC's variables to tape, though this is not a feature I personally have any use for.

The ability to relocate the machine language routines that make up the tape routines is a very important one, however! The available range of memory addresses in a PC-1500 computer will depend on which memory expansion module is installed and whether any other machine language routines are also loaded. Fast Load can be relocated to anywhere you want in memory via its BASIC installer program. SuperTape has a few different versions for different starting addresses based on certain memory modules as well as a BASIC installer, however as far as I can see it doesn't seem to allow being loaded at an arbitrary address which would make it trickier for it to coexist alongside other machine language programs on the computer. Quick-Tape doesn't have any relocation support at all, only working if loaded to address &00C5 and used with the CE-161 memory expansion module.

However, Quick-Tape seems like it would otherwise be ideal — it consumes the last RAM, it seems the most feature-complete and though it's not quite as fast as SuperTape it's still a massive improvement over the native cassette routines.

I therefore decided I'd try to make a relocatable version of Quick-Tape. To assist with this I first disassembled it using lhTools. It doesn't load any data nor jump to any absolute addresses within its own memory space, which is a good start — in fact, it only calls three subroutines inside itself: &01BE, &01BF and &01CF. You can spot these easily in the disassembled code as they are assigned named labels by lhTools instead of absolute addresses, for example in this snippet:

lbl_0_159:
	SJP	lbl_0_1be ; labelled address inside our memory
	ADR	Y
	LOP	lbl_0_159
	LDA	YH
	SJP	lbl_0_1bf ; labelled address inside our memory
	LDA	YL
	SJP	lbl_0_1bf ; labelled address inside our memory

...as opposed to:

lbl_0_117:
	SJP	BBD6 ; absolute address outside our memory
	VMJ	0A
	SJP	BBC0 ; another absolute address outside our memory

The version of lhTools I'm using is one I've modified to use the standard LH5801 instruction mnemonics instead of the Z80-inspired ones it normally uses; it also disassembles vectored subroutine calls into standard VMJ instructions instead of the lhTools pseudo-instructions, though as I haven't got it to re-assembled these instructions properly yet this is not something I'm able to share just yet. However, the important thing here is the subroutine called with the SJP instruction.

Now that we know where these subroutine calls are, a BASIC program can be written that patches the loaded machine language program with the correct target addresses for where it's been relocated to. Similarly, the reserve program (which contains BASIC CALL statements to call the entry points in the machine language program) needs to have its addresses patched to point at the relocated machine language program. The resulting installer looks like this:

    10 M=PEEK &7863*256+197:B=PEEK &7865*256+PEEK &7866
    20 IF B-M<508 PRINT "No room": END
    30 L=M: IF B-M>508 WAIT 0: PRINT "Load @";L;" ";: INPUT L
    40 CLS : WAIT : IF (L<M) OR (L+508>B) PRINT "Bad address": GOTO 10
    50 WAIT 0: PRINT "Play QUICK-TAPE...": CLOAD M"QUICK-TAPE";L
    60 FOR I=1 TO 3: READ A,C: FOR J=1 TO C
    70 READ O: POKE O+L,(A+L)/256,(A+L) AND 255
    80 NEXT J: NEXT I
    90 DATA 249,1,149,250,2,157,161,266,3,411,443,452
   100 M=M-197: PRINT "Play QUICK-RESERVE...": CLOAD M"QUICK-RESERVE";M
   110 FOR I=1 TO 6: READ A,O:A$=RIGHT$("00"+STR$ (A+L),5)
   120 FOR J=1 TO 5: POKE M+O+J,ASC MID$(A$,J,1): NEXT J: NEXT I
   130 DATA 4,89,6,101,5,113,2,130,1,153,0,165
   140 CLEAR :Z$="": WAIT : PRINT "QUICK-TAPE installed"

You can download the installer and reserve program as ready-to-go WAV files in Quick-Tape-Install.zip. You'll also need to create a WAV file for Quick-Tape itself, which can be downloaded as part of Pocket Tools. Instructions can be found inside the installer zip archive.

Faster tape loading on the Sharp PC-1500 with Fast Load

Sunday, 20th April 2025

After I've typed a program into my Sharp PC-1500, I tend to save it to my PC for long-term storage using the CE-150 cassette interface. I use the same cassette interface to download programs shared by others over the Internet, and have written a previous post about resampling those shared wave files if you're struggling to load them.

Loading a program this way is quicker than typing them in, but not by much! The Globe application is 5697 bytes in length but takes the best part of seven minutes to load from tape. I was therefore very intrigued by the Fast Load application, which "enables fast loading (4000 baud, 13 times the normal speed) of all Basic or machine language programs and variables".

I didn't have the easiest time using this program, so thought I'd put together this post in case it helps anyone else with similar struggles!

Fast Load's component parts

The included documentation is a little confusing. I assume it's been abbreviated from what would have been included with the original tape, but I'm really not sure. It mentions using CLOAD M to load FAST-LOAD, however if this is to be a relocatable module then how should it be relocated once loaded, and after that point how are you to invoke the saving and loading routines?

Included in the zip archive there are actually three different recordings for three different files, and after studying them a little they need to be loaded in the following order:

  • FAST-RELO: This is a BASIC program that loads the other two files and performs the relocation.
  • FAST-RESERVE: This is a reserve program that provides the key mappings to call the save/load routines.
  • FAST-LOAD: This is the machine language program that implements the save/load routines.

Understanding how all three parts operate will require a bit of knowledge of the PC-1500 and how memory is arranged.

PC-1500 memory layout for reserve program, machine language program and BASIC program

A reserve program occupies the first 197 bytes of RAM. This program can redefine the six keys positioned directly under the display to type in a sequence of characters when pressed. Three separate groups of key mappings are provided (the current group is indicated by the presence of an I, II or III icon on the display) which can be cycled through by pressing the select key. A string can also be stored for each group to act as a reminder of which key does what, and this can be displayed by pressing the RCL key. In the case of Fast Load, the reserve program sets up four keys (two in reserve group II, two in reserve group III) to execute the program/variable save/load routines which it does by typing in CALL <address> and pressing the Enter key. The appropriate routine address for each key is patched into the reserve program by the FAST-RELO BASIC program.

The routines themselves need to be loaded somewhere into memory. By default, the current BASIC program starts in memory immediately after the reserve program's fixed 197 bytes at the bottom of memory, however it is possible to move the start of the BASIC program by passing a numeric parameter to NEW. The numeric parameter defines the start of the BASIC program in memory, but it will not allow you to move it below the end of the reserve program and so a good way to reset the BASIC memory to its largest default is with NEW 0 – a command that you are prompted to enter whenever the device has been reset or a memory expansion module has been changed.

By moving the start of the BASIC program to a higher address in memory it leaves a gap between the end of the reserve program area and the BASIC program that is free for us to load persistent machine language programs.

Note that memory does not necessarily start at address 0, either! In its base configuration RAM starts at &4000, with a CE-155 (8KB) RAM expansion module RAM starts at &3800 and with a CE-161 (16KB) RAM expansion module RAM starts at &0000. Fortunately you can read the most significant byte of the RAM start address from address &7863, which makes it a bit easier to calculate sensible values to load to.

Loading Fast Load into memory using its loader

For simplicity, if we assume we're only going to load Fast Load (and don't need to worry about reserving any additional RAM) then the process should be:

  1. Move the start of the BASIC program up to start of RAM + 197 bytes for the reserve program + 540 bytes for Fast Load.
  2. Use CLOAD to load the FAST-RELO BASIC program.
  3. RUN the BASIC program, it should start trying to load the next part.
  4. Play FAST-RESERVE to load the reserve program into memory.
  5. Once this has loaded the computer will beep and prompt @: to ask where to load Fast Load to.
  6. Enter the start of RAM + 197 bytes for the reserve program, then when it starts loading again play FAST-LOAD.
  7. The reserve program and machine language program are then patched to their new addresses and you should be good to go – the save/load routines are now accessible using F1 (!) and F5 (%) when using reserve groups II or III.

As mentioned before, the most significant byte of the start of memory can be retrieved from &7863, and so the process would look like this on the computer's screen (using values from the readme included with Fast Load):

NEW PEEK &7863*256+736
CLOAD
RUN
@:PEEK &7863*256+197

Unfortunately, this didn't work; attempting to save seemed to do something sensible, but loads didn't work and after that everything seemed to completely haywire – garbled memory, weird crashes and lockups. What's wrong?

Off by one!

I earlier mentioned that the reserve program is 197 bytes and Fast Load is 540 bytes, so we need to set the start of the BASIC program to 197+540 bytes from the start of memory. 197+540 is 737, however the figure given in the readme is 736. This is one byte too short, and as a result the BASIC program ends up overwriting the last byte of the Fast Load routines. This is a RTN instruction and as a result is fairly important, as without it a subroutine will not return and instead end up executing whatever instructions the start of the BASIC program has put there instead. Not good! Fortunately, it's a very simple fix – just move the start of the BASIC program to the correct position (NEW PEEK &7863*256+737).

It's only a phase…

Once everything is loaded in again (and at the correct location) the routines should now work. I loaded the Globe application using a conventional CLOAD and then used Fast Load to save the program back to my PC. This only took around 30 seconds, roughly fourteen times faster than the native routines! Very impressive, but only useful if the programs could then be loaded back onto the computer. Unfortunately, attempting to load the program back didn't work – the recording would finish playing but the computer was still stuck in its "Busy" state. On a hunch, I tried inverting the wave in the recording software, as I know from previous experience that cassette recorders tend to invert the phase between recording and playback. Doing that did the trick, and I could load back a program in 30 seconds that previously took seven minutes. Brilliant!

Unfortunately, Fast Load does seem to be fairly bare-bones. I haven't been able to find a verification routine similar to the native CLOAD ?, which compares the data on tape to the data in memory to allow you to verify that it was saved correctly, for example. Similarly I've only been able to find routines that save/load from the BASIC program area or the variable area, there doesn't seem to be a user-accessible way to save or load an arbitrary block of data based on its start and end addresses which would be required to save or load machine language programs. It doesn't even support filenames or file types to differentiate between programs and variable data! I have dug through a dissassembly of the Fast Load routines but couldn't spot anything, and the routines that are there do seem to go one byte beyond where they should to include a terminator byte as well. It's entirely possible that I missed something, but I'm still very happy about the speed improvements so don't mind the other limitations too much.

Digging into the protocol

Of course, once I'd got this working I wanted to dig into the cassette protocol used by Fast Load. Fortunately, it's very straightforward! There are two bit states, with the phase being 0°:

  • A "0" (zero) bit is represented by one cycle of a 2500Hz tone.
  • A "1" (one) bit is represented by two cycles of a 5000Hz tone.

Each byte is sent as follows:

  1. A "0" (zero) start bit.
  2. Eight data bits, least significant bit first.
  3. A "1" (one) stop bit.

There are two "1" bits between each byte, and the transfer starts with 3 seconds of 5000Hz pilot tone. The data format within the transfer is:

  1. Two byte data size (MSB first).
  2. Data bytes (including &FF program terminator).
  3. Three byte checksum (MSB first).

The checksum is calculated by adding up all of the bytes in the data section (including the &FF terminator).

All of this puts the "4000 baud" claim somewhat in question. Each bit takes the same amount of time – two 5000Hz cycles take as much time as one 2500Hz cycle, so the bit rate would appear to be 2500 baud, not 4000. In our "Globe" example, a 5697 byte program is saved in around 30 seconds. 5697 bytes becomes 5703 when we add the two bytes for the size prefix, the one byte terminator and three byte checksum. Each transmitted byte takes up eight bits for the data bits, two bits for the start and stop and two bits between each byte, so that comes to 5703×12=68436 bits in total. Divided by our 2500 baud rate gives us 27.37 seconds, plus the three seconds for the pilot tone comes to 30.37 seconds, matching our recording – so I think it's fair to say the baud rate is 2500.

Buiding some tools to work with recordings

There is a suite of tools called Pocket Tools that allows you to work with recordings of programs in the native PC-1500 format. You can extract binary data from a wave file recorded from the PC-1500, or detokenise a text BASIC program listing from one, or convert binary data or a text BASIC program listing into a wave file that can be played into the PC-1500. I've written a pair of tools, fwav2bin and fbin2wav, that perform a similar function for Fast Tools. These programs only implement saving and loading raw binary data – if you want to convert to or from BASIC program listings you'll need to first convert to or from a binary image (e.g. using Pocket Tools or lhTools).

Overall I've been very impressed with the speed boost you can achieve with Fast Load, once I managed to get it working.

Building your own analogue sensors for the SmartBox

Friday, 29th November 2024

This is an extremely belated followup to a post from November last year, though one of the reasons for the delay was repeatedly being sent the wrong electronic components which ultimately led to the post from December. I'll try to explain as best I can...

Towards the end of last year I bought myself a SmartBox, a computer control system that's based around a 65C02 CPU and that runs programs written in an interpreted BASIC-like programming language, with software uploaded via a computer's serial connection (and once the programs are loaded onto the box, the serial connection can be severed and the program will continue to run). In the previous journal entry I focussed more on the software side, though to get practical use out of the box I had experimented with some of the hardware too and had built some analogue sensors that I was hoping to write up for the benefit of anyone else who'd acquired a SmartBox but none of the sensors to go with it.

Photo of a SmartBox computer control system with two home-made User Adaptors and two home-made temperature sensors
Two home-made User Adaptors and two home-made temperature sensors.

The four analogue sensor ports on the side of the SmartBox use 5-pin DIN sockets (180° to differentiate them from the 240° 5-pin DIN socket used for the serial port). Each sensor port can measure an analogue value with an 8-bit resolution (between 0 and 255) with the maximum voltage refererence being set to 2.55V – that is, each unit in the 0–255 range reported by the sensor corresponds to 1/100V (10mV), so if you connected a 1.5V source to the sensor port you'd see a value of 150 returned.

Sensors were made that measured specific physical properties (such as light level, humidity, temperature, or sound level) as well as generic "User Adaptor" boxes that allowed users to connect their own sensors:

Photo and description of the User Adaptor from a brochure
The SmartBox "User Adaptor" pictured in a brochure.

One other nice trick that the analogue sensors had was that they could report which type of sensor they were directly to the SmartBox, so when you plugged in a temperature probe (for example) it would know that it was measuring temperature on that sensor port. The way this was implemented is via a "sense signal" output pin from the SmartBox to the sensor. In normal operation it was low, and the voltage read on the analogue sense input pin was used as the sensor's current value. Once a second or so the SmartBox would drive this pin high, and the voltage read on the analogue sense input pin was used to determine the type of sensor.

There are 32 possible sensor IDs. The ADC reading mentioned in the table below is the minimum value that will cause the Smart Move software to identify a particular sensor, for example a humidity sensor can be identified with an ADC reading between 157 and 164:

ADCSmart Move 1.16Smart Move 1.18
0No sensorNo sensor
5mT
13mmTemp
21No sensorNo sensor
29No sensorNo sensor
37TempVolts
45TempTemp
53mAVolts
61SoundTemp
69SoundSound
77SoundPH
85Pulse
93PositionPosition
101
109
117
125LightLight
133
141
149
157HumidityHumidity
165Sound
173Light
181PressureSound
189
197PHAtmos
205Light
213User
221AdaptorAdaptor
229LuxTemp
237VoltsLGate
245Wind
253TempTemp

Note that Smart Move 1.16 and Smart Move 1.18 have some different names for some sensors. If a space is left blank in the table then the sensor is treated as present but is labelled "SENSORA" to "SENSORD" instead of being renamed according to the sensor type.

An analogue multiplexer (such as a 4053 IC) can be used as the basis for an analogue sensor for the SmartBox, sending either the voltage to measure or a reference voltage set with a preset potentiometer to identify the sensor type. The "sense signal" output from the SmartBox chooses which of the two voltages to return:

Schematic for a DIY SmartBox 'User Adaptor'

When the adaptor is connected to the SmartBox the potentiometer RV1 can be adjusted to choose the sensor type. As this is only checked once per second it can be a little fiddly to dial in the specific value you want — temporarily bridging A0 and A1 on the 4053 (so that RV1 is used as both sensor type and input value) and watching the reported sensor value can make life easier (making sure any sensor is disconnected from the input socket, of course).

Photo of the internals of the home-made User Adaptor
The internal components of the home-made User Adaptor.

Whilst this is a useful circuit for building custom sensors, or making a replica of the generic User Adaptor (my DIY effort pictured above) there is a simpler option that can be used for temperature sensors. The original temperature sensor doesn't seem to have very much circuitry of its own, being just a simple probe with a DIN socket on the end of a wire:

Photo and description of the Temperature Sensor from a brochure
The SmartBox "Temperature Sensor" pictured in a brochure.

The temperature sensor provides a reading in Celsius directly in the software, e.g. a temperature of 25°C is reported with a value of 25. This corresponds to a 10mV/°C sensor, and an example of such a sensor is the LM35. One limitation of the LM35 is that without a negative supply voltage pulling down its output it reports a minimum temperature of 2°C, yet the brochure snippet shown above reports a range of 0°C to 100°C. However, reading the manual for the SmartBox it mentions that the Smart Sense temperature sensor measures temperatures between 2°C and 100°C which seems to further indicate that it is indeed something like the LM35.

We could therefore just connect an LM35 to the User Adaptor circuit shown above and that would work, however there is a simpler circuit. If you consult the table of sensor types at the start of this post you'll notice that the temperature sensor appears at the bottom of the table with the highest sensor value. This means that to be identified as a temperature sensor, the sensor must output at least 2.53V when the "sense" pin goes high. A crude way to implement this is to connect a diode from the sense output pin back to the analogue input pin:

Schematic for a DIY SmartBox 'Temperature Sensor'

When the sense pin is low the voltage at "input" will be higher than the voltage at "sense" so the diode will not conduct and not influence the output of the temperature sensor. When the sense pin is high, however, the voltage at "sense" will be higher than the voltage at "input" and so the diode will conduct and raise the voltage at "input" to above 2.53V, indicating that the connected sensor is a temperature sensor.

Photo of the internals of the home-made temperature sensor
The internal components of the home-made temperature sensor.

According to the documentation for the SmartBox the inputs are protected against overvoltage so I believe this is a safe circuit to use, however as with the User Adaptor all circuits here are based on some rough guesses as to how the devices would have originally worked as I don't have any real sensors to test with myself. In any case, these sensors work for me as I've not been able to source any of the original parts, so maybe if you like me have an old SmartBox knocking around these may help you get some more use out of it.

Resampling Sharp PC-1500 tape recordings

Wednesday, 16th October 2024

This is a quick post about problems I'd been having loading tape cassette recordings from my PC to a Sharp PC-1500 Pocket Computer along with a potential solution for anyone having similar issues.

Photo of Sharp PC-1500 Pocket Computer connected to the CE-150 Printer and Cassette Interface
Sharp PC-1500 Pocket Computer connected to the CE-150 Printer and Cassette Interface

The Sharp PC-1500 is a small computer from the early 1980s that can run programs primarily written in BASIC. Programs can be saved to and loaded back from cassette tape but to do this requires it to be connected to the CE-150, a cassette interface that also includes a four-colour plotter. It's the plotter that really attracted me to the computer in the first place, but being able to load programs from cassette rather than having to type them in by hand is definitely a very handy feature!

Unfortunately this CE-150 interface contains an internal battery pack of five Ni-Cd cells. A certain amount of battery leakage is a risk in any old piece of electronics hardware, but having said battery pack soldered directly in with no easy way for the user to remove it for long-term storage makes it more of a certainty than a risk. My CE-150 had not escaped, with heavy corrosion visible on the metal plate under the computer…

Photo of corroded metal plate on CE-150        Photo of corroded metal plate on CE-150

The inside of the PC-1500 was a bit better and not quite as musty-smelling, but circuit board traces had definitely been eaten away in places and some of the internal metal structure was looking a bit crusty.

Photo of corroded circuit traces inside the PC-1500        Photo of corroded metal bracket inside the PC-1500

I cleaned up the rusty metal using some appliance descaler (and recorded a video of the process, which can be found on YouTube) then got to work with a scalpel and some fine enamelled copper wire to repair the damaged traces.

Photo of repaired circuit traces inside the PC-1500        Photo of repaired traces inside the PC-1500

Thankfully when I put it all back together again I was greeted by the NEW0? :CHECK prompt, so I connected the computer to the CE-150 and confirmed I could save and load back programs using my PC in place of a cassette recorder. Hooray!

Photo of Sharp PC-1500 Pocket Computer showing the 'NEW0? :CHECK' prompt

When looking to see what other people had managed to achieve with the computer I found the Sharp PC-1500 (TRS-80 PC-2) resource page which hosts several games and applications which can be downloaded in WAV format. I downloaded a few, but when playing them back on my PC the PC-1500 would refuse to load them. It could load from my own recordings, why not these?

The odd thing is that my own recordings started with a few seconds of constant pilot tone followed by the varying pulses of the program data I was trying to load. When playing back the downloaded recordings they'd start with silence and only make sound when they got to the actual data – and that data sounded quite unlike what my own recordings sounded like. What was going on? I opened a recording in Audacity to see what it looked like, and zoomed in:

Screenshot of Audacity showing the pilot tone of the recorded file

That looks like the pilot tone, in that it alternates between high and low at a fixed frequency, though it's only one sample high then one sample low so is a square wave rather than the smooth sine wave I was expecting. The cassette format uses frequency-shift keying so I would expect to see data further on in the recording at half the frequency, or twice the period, i.e. two samples high and then two samples low. Scrolling along, I certainly find that:

Screenshot of Audacity showing the frequency-shift keyed data in the recorded file

Why does it look so spiky, though? It would only need to look like that if it had been sampled at an extremely low rate, and indeed that's what I found – these files are recorded at a 5kHz sample rate:

Screenshot of MediaInfo showing the 5kHz sample rate of the recording

5kHz is a pretty unusual (and very low!) sample rate that I suspected my sound card was not handling particularly gracefully. Typically audio would need to be resampled to 44.1kHz or 48kHz before playing back, so I thought I'd try resampling in Audacity. This produced this weirdness:

Screenshot of Audacity showing the signal being distorted when being resampled.

Instead of a sine wave that alternates between two frequencies, I ended up with something more akin to an amplitude-modulated signal! The high-frequency pilot also disappears entirely, apart from what looks to be some initial ringing:

Screenshot of Audacity showing the pilot signal disappearing when being resampled.

Now, this is a complete guess, but I suspect that what is happening is an attempt to reduce unpleasant harmonics in the resampled audio. A square wave (such as the one we're resampling) produces a lot of harmonics at higher frequencies. As the original audio was sampled at 5kHz, the highest frequency that this could represent was 2.5kHz (alternating high and low each sample, as per the pilot tone). A low-pass filter at 2.5kHz could therefore reduce the harmonics, though I'd still expect some of it to get through (rather than it being apparently being attenuated to zero in this case!) In any case, the signal is being destroyed by the resampling process, so it's no wonder the computer can't load the data.

So, what's the solution? A simpler resampling algorithm that just duplicates samples (a sort of "nearest neighbour") should give us an acceptable square wave when processing, however I could not find any way to achieve this natively in Audacity. Fortunately, though, there's a Nyquist Plugin script that can be used to repeat all samples (install via Tools→Nyquish Plugin Installer, then check it's listed in Tools→Plugin Manager – it will appear as Effect→Repeat Samples). As we want to end up around 44.1kHz the plugin can be used to repeat each sample eight times, which would take our 5kHz sample rate to 40kHz:

Screenshot of Repeat Samples plugin.

Screenshot of Audacity showing the pilot signal having each sample repeated eight times.

When played back the pilot tone can finally be heard, but it's at much too low a pitch! This is because the samples have been repeated eight times, but the track's sample rate hasn't been increased to match. This can be done by right-clicking the track and selecting Rate→Other and entering 40kHz:

Screenshot of Set Rate dialog showing new 40kHz value.

Now when the clip is played back it sounds as it should. As a further experiment, the sample rate could be changed from its somewhat odd 40kHz to a more conventional 44.1kHz via Tracks→Resample. When this is done, the nice clean square wave loses definition again:

Screenshot of Audacity showing distortion on the square wave after resampling to 44.1kHz.

As I mentioned earlier, square waves have a lot of harmonics, and the resampling process makes these harmonics extremely visible riding on top and bottom of each cycle of the square wave. There is one way to clean this up quite nicely, however – as we want to cull high-frequency harmonics, and as the original recording could only represent frequencies up to 2.5kHz we can apply a low-pass filter at 2.5kHz from the Effects menu:

Screenshot of Low-Pass Filter plugin.

Screenshot of Audacity showing the low-pass filtered signal.

This resulting file sounds much better and loads into the computer as it should. It's a bit of a convoluted process, but it does at least bypass the resampling weirdness that distorts the original files in such a way that they can't be loaded. The only additional step I might recommend is to more neatly balance the signal around zero by normalising it as the first step:

Screenshot of Normalize plugin.

In summary, the steps are as follows:

  1. Normalise the file (Effect→Normalize)
  2. Repeat all samples by 8× (Effect→Repeat Samples)
  3. Set track sample rate to 8× what it was (right-click, Rate→Other, multiply the value by 8)
  4. Resample to 44.1kHz (Tracks→Resample)
  5. Low-pass filter at 2.5kHz (Effect→Low-Pass Filter)

Happy tape loading!

1-Wire interfacing with the Cambridge Z88

Sunday, 17th December 2023

I've been having a tricky time buying LM35DZ analogue temperature sensors for a project recently. One pair of probes and a bag of loose components labelled LM35DZ turned out to be regular NPN transistors with a fake label on them, and another pair of probes ended up being DS18B20 digital temperature sensors.

Whilst the DS18B20 temperature sensors were useless for the project I had in mind they were still functioning components. These use the 1-Wire serial bus, a bus named for the way that its single data line can also be used to parasitically power the devices on the bus. Electrically the bus is open drain with a pull-up resistor that idles in the high state which any device can drive low. The master initiates all communication and you can have multiple peripheral devices connected to the bus in an arrangement called a MicroLAN.

Photo of various 1-Wire devices around a Z88 computer which has a 1-Wire interface adaptor plugged into its serial port
A selection of 1-Wire devices and a 1-Wire interface adaptor plugged into a Z88 computer

I'd had some limited experience working with the 1-Wire bus as part of my version of the Superprobe but now that I had a collection of temperature sensors I thought it might be worth revisiting, this time on the Cambridge Z88.

1-Wire adaptor for the Z88 serial port

To connect 1-Wire devices to the Z88 some sort of adaptor is required and one that plugged into the computer's serial port seemed like a sensible enough option. The Z88's serial port hardware normally handles all the communications for you however it is possible to directly control the logic levels of the serial port's output pins and read back the status of the input pins via some hardware registers.

The TXD line can be +5V for a logic 0 and -6V for a logic 1, adhering to the RS-232 standard. When idle TXD is in its logic 1 state, outputting -6V. Bit ITX (3) in the TXC (&E4) register can be used to invert the behaviour of the TXD pin, so by setting this bit we can change the state of the pin from -6V to +5V.

As we need to have an open-drain bus we can use an NPN transistor with the base connected to the TXD line via a current-limiting resistor, the emitter connected to ground and the collector driving the 1-Wire bus. By default the TXD pin will output -6V, the transistor will be switched off and the bus will be pulled high. When the TXD pin state is inverted it will output +5V, the transistor will switch on and drive the line low.

The state of the RXD line can be read directly via bit RXD (4) in the RXE (&E1) register. The lines appear to be weakly held to 0V and read back a 0 bit in this state, flipping to a 1 bit when the voltage rises above around 2V. In this case we can connect the 1-Wire bus directly to the RXD input and be able to read back the current state.

The circuit for the adaptor, including the 4.7K pull-up resistor, appears as follows:

Circuit diagram for the Z88 to 1-Wire interface circuit

This can be tested in a BASIC program. To determine the input state we can read from the RXE port register (&E1) and check the state of the RXD bit (4):

10 RXE=&E1:M_RXERXD=&10
20 REPEAT
30   PRINT ~(GET(RXE) AND M_RXERXD)
40 UNTIL FALSE

The mask value M_RXERXD is specified as 24=&10 to correspond to the bit four. When run this program displays &10 in hex (showing bit 4 is set and the bus level is therefore high) until the 1-Wire bus line is connected to ground, when the value changes to 0 (showing bit 4 is reset and the bus level is therefore low).

To change the output state we need to write to bit ITX (3) of the TXC register (&E4). However, when writing to the hardware port we only want to change that bit and leave the others alone. The TXC register is a write-only port, so we can't retrieve its previous state by reading from the port. Fortunately the OS maintains a copy of the last value written as a "soft copy" in RAM at address &04E4 and this can be read with the ? indirection operator:

10 TXC=&E4:M_TXCITX=&08
20 SC=&400
30 TXC_OLD=SC?TXC
40 PUT TXC,TXC_OLD OR M_TXCITX
50 IF INKEY(100)
60 PUT TXC,TXC_OLD

The above program reads the old state of the TXC port from the soft copy, ORs it with the mask of the ITX bit (23=&08) and then outputs that to the TXC port. This has the effect of inverting the TXD line, driving the 1-Wire bus low. The program then waits one second with a dummy keyboard read before restoring the old value of the TXC port to release the 1-Wire bus.

Normally if changing the state of the serial port it would be good manners to update the soft copy of the serial port state however as the program is just going to be sending short low pulses before returning the port to its previous state this step is omitted.

Photo of the assembled 1-Wire interface for the Z88

After testing that the circuit worked on a breadboard a more permanent version was assembled in a DE-9 shell as above. As the clips that hold in the DE-9 connector had to cut off to allow it to fit in the Z88's recessed port the circuit ended up being secured with copious amounts of hot glue, which is far from ideal, but nobody will see when it's all screwed back together.

Bit-level protocol

Now that we can electrically control the bus we need to know how to transfer data on it. This is done by timed pulses, where the bus master will hold the bus line low for a certain amount of time, release it, then check to see if any devices on the bus are holding it low in return. This is summarised in the following timing diagram from Microchip's AN1199, 1-Wire Communication with PIC Microcontroller:

1-Wire protocol timing information diagram from Microchip AN1199

The first thing that needs to be done is to reset all devices on the bus. This is done by holding the bus low for 480μs then releasing it for at least 480μs. If any peripheral devices are present on the bus they will drive the line low after the low pulse from the master, so the full reset procedure is as follows:

  • Master drives bus low
  • Delay 480μs
  • Master releases bus high
  • Delay 70μs
  • Sample bus state: if high, no peripheral devices present, if low at least one device present.
  • Delay 410μs

Once reset, data can be transmitted from the master to peripheral devices bit-by-bit in a similar fashion to the reset pulse, albeit with different timing.

To send a 0 bit:

  • Master drives bus low
  • Delay 60μs
  • Master releases bus high
  • Delay 10μs

To send a 1 bit:

  • Master drives bus low
  • Delay 6μs
  • Master releases bus high
  • Delay 64μs

Bytes are transferred as eight individual bits, least-significant bit first. The protocol is also tolerant of large delays between individual bits.

Once data has been sent to a peripheral, it may respond with data of its own. The master is still in control of clocking the data out of the peripheral, and the process is as follows:

  • Master drives bus low
  • Delay 6μs
  • Master releases bus high
  • Delay 9μs
  • Sample bus state to read data bit from peripheral
  • Delay 55μs

The overall timing for reading a bit is the same as the timing for sending a 1 bit (an initial 6μs low pulse from the master and a total bit time of 70μs) so in practice only one routine needs to be implemented and the value returned from the bus during read operations can be ignored during write operations.

Software choice for the Z88

I thought it would be nice to be able to interact with 1-Wire devices from a BASIC program. BBC BASIC on the Z88 does provide direct access to the hardware and would make controlling the 1-Wire bus line possible, as demonstrated earlier, however I don't think it would provide the timing accuracy required to produce the appropriate pulses from the master. Fortunately it does include a Z80 assembler and so a mixture of a BASIC program that provides the high-level routines and assembly snippets for the low-level 1-Wire protocol implementation seemed like an appropriate mix of languages.

When you CALL an assembly routine from BASIC the Z80's registers are initialised to the values of the corresponding static variables, for example A is set to A%, H to H%, L to L% etc. You can't return a value directly – for that you'd need USR – however it's a bit easier to just store the return value in memory and retrieve that from BASIC after the CALL returns.

A rough starting point for the 1-Wire program is as follows:

 10 REM 1-WIRE DEMO
 20 PROC_1W_INIT
 30 PRINT FN_1W_RESET
 40 END
 50 :
 60 REM 1-WIRE ROUTINES
 70 END
 80 DEFPROC_1W_INIT
 90 ow_code_size=256:DIM ow_code ow_code_size-1
100 RXE=&E1:M_RXERXD=&10
110 TXC=&E4:M_TXCITX=&08
120 SC=&400
130 FOR opt=0 TO 2 STEP 2
140   P%=ow_code
150   [OPT opt
160   .ow_buf DEFB 0 \ temporary transfer buffer
170   :
180   .ow_reset
190   IN A,(RXE):AND M_RXERXD:CP M_RXERXD:SBC A,A:LD (ow_buf),A:RET NZ \ check bus is idle
200   DI:LD A,(SC+TXC):OR M_TXCITX:OUT (TXC),A \ hold bus low
210   LD B,120:DJNZ P% \ delay
220   AND NOT M_TXCITX:OUT (TXC),A \ release bus
230   LD B,18:DJNZ P% \ delay
240   IN A,(RXE):AND M_RXERXD:CP M_RXERXD:CCF:SBC A,A:LD (ow_buf),A \ sample presence
250   LD B,100:DJNZ P% \ delay
260   EI:RET
270   :
280   ]
290 NEXT
300 ENDPROC
310 :
320 REM Resets bus, retuns TRUE if any devices are present
330 DEFFN_1W_RESET:CALL ow_reset:=?ow_buf=0

The first few lines are going to be where our BASIC program is. This calls the procedure PROC_1W_INIT which will set things up by assembling any required Z80 code. It then calls FN_1W_RESET which is a function that resets the 1-Wire bus and checks to see if any devices assert their presence.

PROC_1W_INIT starts by allocating some memory for the assembled code to live, defines some constants for the IO ports and then runs through the two passes of the assembly process in a loop. Within the assembly block is a variable (ow_buf) which will be used to store data due to be returned by the assembly routines. The ow_reset assembly routine then follows – this first checks to see if the bus is idle (floating high) and if so it disables interrupts, holds the bus low for 480μs, releases the bus and waits 70μs, samples the state of the bus to check for device presence (storing the result in ow_buf), then delays another 410μs.

The delay loops are simple DJNZ loops with B corresponding to the length of the delay and the timings were roughly calculated first based on the number of cycles each loop would take and the Z88's 3.2768MHz CPU clock speed. They were then adjusted slightly using a logic analyser to ensure the timing was as close as could be managed to the 1-Wire protocol's specifications.

The ow_reset routine has been written so that following a successful presence check ow_buf should contain 0, and if there is a problem it will contain a non-zero value. This is used by the FN_1W_RESET wrapper function which just calls ow_reset and returns TRUE if ow_buf is zero afterwards.

If you run the program you should see that the program will display 0 (FALSE) on the screen until a 1-Wire device is connected to the adaptor, at which point it will display -1 (TRUE) instead to indicate the device's presence. This isn't a very useful program, but shows how BASIC and assembly will be mixed to build the rest of the 1-Wire routines.

Sending and receiving bits and bytes

Now that we know a device is present on the bus after a reset we need to be able to send and receive bits and bytes. Sending a 0 bit is a bit simpler than resetting, as we don't need to check for any response – just hold the line low for 60μs then release it back high for 10μs. This can be implemented as follows:

.ow_put_0
DI
LD A,(SC+TXC):OR M_TXCITX:OUT (TXC),A \ hold bus low
LD B,15:DJNZ P% \ delay
AND NOT M_TXCITX:OUT (TXC),A \ release bus
NOP \ delay
EI:RET

Sending a 1 bit has the same overall timing as reading a bit, so instead of writing separate routines to send a 1 bit and read a bit just one routine is required that handles both situations:

.ow_put_1
DI
LD A,(SC+TXC):OR M_TXCITX:OUT (TXC),A \ hold bus low
NOP \ delay
AND NOT M_TXCITX:OUT (TXC),A \ release bus
PUSH HL:POP HL \ delay
IN A,(RXE):AND M_RXERXD:SUB M_RXERXD:CCF \ sample bit
LD A,(ow_buf):RRA:LD (ow_buf),A \ store bit
LD B,7:DJNZ P% \ delay
EI:RET

This holds the bus low for 6μs, releases it and waits 9μs, samples a bit from the bus and rotates it into the ow_buf transfer buffer, then waits 55μs.

These routines could be wrapped up for use in BASIC but it's not too useful to be able to send or receive single bits, normally we'd need to transfer whole 8-bit bytes. The ow_put_1 routine already handles updating the ow_buf with each received bit, so a byte receiving routine can be put together by just calling ow_put_1 eight times in a loop:

.ow_get_byte
LD B,8 \ 8 bits to receive
.ow_get_loop
PUSH BC:CALL ow_put_1:POP BC \ receive single bit
DJNZ ow_get_loop \ loop
LD A,(ow_buf):RET \ store

A send routine can be put together with a similar loop that shifts out the bit to send and then calls either the ow_put_0 or ow_put_1 routine depending on whether it's a 0 or 1 bit that's required. Bits will usually be shifted out into the carry register, so a new ow_put_carry routine that sends the bit stored in the carry flag makes this a bit easier, e.g.

.ow_put_carry
JR C,ow_put_1
JR ow_put_0

...which will be called by the ow_put_byte routine, as follows:

.ow_put_byte
LD C,A:LD B,8 \ value to send in C, send 8 bits
.ow_put_loop
SRL C:PUSH BC:CALL ow_put_carry:POP BC \ shift and send single bit
DJNZ ow_put_loop \ loop
RET

It is also quite useful to be able to send or receive blocks of data at once – for example, sending or receiving the 64-bit device IDs requires sending or receiving 8 bytes of data at a time. To complement ow_get_byte and ow_put_byte we can write ow_get_bytes and ow_put_bytes routines to send or receive the block of data addressed by HL, length BC:

.ow_get_bytes
LD A,B:OR C:RET Z:DEC BC \ have we finished?
PUSH BC:CALL ow_get_byte:POP BC \ get a byte
LD (HL),A:INC HL:JR ow_get_bytes \ store and loop
:
.ow_put_bytes
LD A,B:OR C:RET Z:DEC BC \ have we finished?
LD A,(HL):INC HL \ fetch
PUSH BC:CALL ow_put_byte:POP BC:JR ow_put_bytes \ send and loop

All of these can now be wrapped up as procedures or functions so they can be more easily used from a BASIC program:

REM Transmits a single byte
DEFPROC_1W_PUT(A%)CALL ow_put_byte:ENDPROC
REM Transmits a block of bytes
DEFPROC_1W_PUTS(L%,C%)LOCAL H%,B%:H%=L%DIV256:B%=C%DIV256:CALL ow_put_bytes:ENDPROC
REM Receives a single byte
DEFFN_1W_GET:CALL ow_get_byte:=?ow_buf
REM Receives a block of bytes
DEFPROC_1W_GETS(L%,C%)LOCAL H%,B%:H%=L%DIV256:B%=C%DIV256:CALL ow_get_bytes:ENDPROC

BASIC's integer variables are 32-bit integers so when passing the 16-bit address or length parameters the target register is the least-significant one (L for HL, C for BC) and the most-significant register (H or B) is populated by dividing the value by 256.

This can all be put together in the following demonstration program. It initialises the routines, resets the bus and checks for presence, then sends the "read ROM" command &33 which will make any connected devices respond with their ROM ID. It then reads back the eight bytes corresponding to the device ID then prints them back in hexadecimal.

 10 DIM ID 7:REM Storage for device ID
 20 PROC_1W_INIT
 30 IF FN_1W_RESET=FALSE PRINT "No devices found.":END
 40 PROC_1W_PUT(&33):REM "Read ROM" command
 50 PROC_1W_GETS(ID,8):REM Read eight bytes of device ID
 60 FOR I=7 TO 0 STEP -1:PRINT ~ID?I;:NEXT:PRINT:REM Print device ID bytes
 70 END
 80 :
 90 REM 1-WIRE ROUTINES
100 END
110 DEFPROC_1W_INIT
120 ow_code_size=256:DIM ow_code ow_code_size-1
130 RXE=&E1:M_RXERXD=&10
140 TXC=&E4:M_TXCITX=&08
150 SC=&400
160 FOR opt=0 TO 2 STEP 2
170   P%=ow_code
180   [OPT opt
190   .ow_buf DEFB 0 \ temporary transfer buffer
200   :
210   .ow_reset
220   IN A,(RXE):AND M_RXERXD:CP M_RXERXD:SBC A,A:LD (ow_buf),A:RET NZ \ check bus is idle
230   DI:LD A,(SC+TXC):OR M_TXCITX:OUT (TXC),A \ hold bus low
240   LD B,120:DJNZ P% \ delay
250   AND NOT M_TXCITX:OUT (TXC),A \ release bus
260   LD B,18:DJNZ P% \ delay
270   IN A,(RXE):AND M_RXERXD:CP M_RXERXD:CCF:SBC A,A:LD (ow_buf),A \ sample presence
280   LD B,100:DJNZ P% \ delay
290   EI:RET
300   :
310   .ow_put_carry
320   JR C,ow_put_1 \ fall-through
330   :
340   .ow_put_0
350   DI
360   LD A,(SC+TXC):OR M_TXCITX:OUT (TXC),A \ hold bus low
370   LD B,15:DJNZ P% \ delay
380   AND NOT M_TXCITX:OUT (TXC),A \ release bus
390   NOP \ delay
400   EI:RET
410   :
420   .ow_put_1
430   DI
440   LD A,(SC+TXC):OR M_TXCITX:OUT (TXC),A \ hold bus low
450   NOP \ delay
460   AND NOT M_TXCITX:OUT (TXC),A \ release bus
470   PUSH HL:POP HL \ delay
480   IN A,(RXE):AND M_RXERXD:SUB M_RXERXD:CCF \ sample bit
490   LD A,(ow_buf):RRA:LD (ow_buf),A \ store bit
500   LD B,7:DJNZ P% \ delay
510   EI:RET
520   :
530   .ow_put_byte
540   LD C,A:LD B,8 \ value to send in C, send 8 bits
550   .ow_put_loop
560   SRL C:PUSH BC:CALL ow_put_carry:POP BC \ shift and send single bit
570   DJNZ ow_put_loop \ loop
580   RET
590   :
600   .ow_put_bytes
610   LD A,B:OR C:RET Z:DEC BC \ have we finished?
620   LD A,(HL):INC HL \ fetch
630   PUSH BC:CALL ow_put_byte:POP BC:JR ow_put_bytes \ send and loop
640   :
650   .ow_get_byte
660   LD B,8 \ 8 bits to receive
670   .ow_get_loop
680   PUSH BC:CALL ow_put_1:POP BC \ receive single bit
690   DJNZ ow_get_loop \ loop
700   LD A,(ow_buf):RET \ store
710   :
720   .ow_get_bytes
730   LD A,B:OR C:RET Z:DEC BC \ have we finished?
740   PUSH BC:CALL ow_get_byte:POP BC \ get a byte
750   LD (HL),A:INC HL:JR ow_get_bytes \ store and loop
760   :
770   ]
780 NEXT
790 IF P%-ow_code>ow_code_size PRINT"Code size: "P%-ow_code:END
800 ENDPROC
810 :
820 REM Resets bus, retuns TRUE if any devices are present
830 DEFFN_1W_RESET:CALL ow_reset:=?ow_buf=0
840 REM Transmits a single byte
850 DEFPROC_1W_PUT(A%)CALL ow_put_byte:ENDPROC
860 REM Transmits a block of bytes
870 DEFPROC_1W_PUTS(L%,C%)LOCAL H%,B%:H%=L%DIV256:B%=C%DIV256:CALL ow_put_bytes:ENDPROC
880 REM Receives a single byte
890 DEFFN_1W_GET:CALL ow_get_byte:=?ow_buf
900 REM Receives a block of bytes
910 DEFPROC_1W_GETS(L%,C%)LOCAL H%,B%:H%=L%DIV256:B%=C%DIV256:CALL ow_get_bytes:ENDPROC

When connected to an iButton fob the program prints

  55   0   0   1  A0  1A  57   1

...which matches the ID printed on it.

Photo of a TM1990A iButton on a fob with a probe to read it with
An example of a TM1990A iButton fob (middle) along with the probe used to read it (top)

When connected to a DS18B20 temperature sensor the program prints

  B9   0   0   1  D1  97  5D  28

The least significant byte of the 64-bit ID is the family code – &01 for the iButton fob indicates it's a "silicon serial number" type device and &28 for the DS18B20 indicates it's a "programmable resolution digital thermometer".

The 1-Wire bus supports multiple peripheral devices connected to a single master. If we try that we still get something that looks like an ID back:

  11   0   0   1  80  12  55   0

This happens because it's an open-drain bus and any device holding the line low will take priority over any device releasing the line high. In effect the data read back is ANDed together, so the most-significant byte received is &55 AND &B9 which gives us the &11 we see. Fortunately that most-significant byte does give us a good opportunity to detect such invalid data!

Error detection with a CRC

Some data payloads include a CRC value. The most-significant byte of a 64-bit device ID is such a CRC, with the least-significant byte being the family code. The exact details for the CRC calculation can be found in the article Understanding and Using Cyclic Redundancy Checks with Maxim 1-Wire and iButton Products however for our purposes a Z80 implementation can be written as follows:

.ow_crc
LD B,8:LD DE,(ow_buf):LD D,A \ E = accumulated CRC, D = value to add
.ow_crc_loop
LD A,E:XOR D:SRL D:SRL A:JR C,ow_crc_odd \ XOR and shift bits
SRL E:DJNZ ow_crc_loop:LD A,E:LD (ow_buf),A:RET \ even CRC value
.ow_crc_odd:SRL E:LD A,&8C:XOR E:LD E,A:DJNZ ow_crc_loop:LD (ow_buf),A:RET \ odd CRC value
:
.ow_crc_block
XOR A:LD (ow_buf),A \ reset CRC
.ow_crc_block_loop
LD A,B:OR C:LD A,(ow_buf):RET Z:DEC BC \ have we finished?
LD A,(HL):INC HL:PUSH BC:CALL ow_crc:POP BC:JR ow_crc_block_loop \ update CRC

ow_crc updates the current calculated CRC value (stored in ow_buf) with the next data byte from the accumulator. ow_crc_block calculates the CRC for a block of data pointed to by HL, length BC, using the ow_crc routine. A couple of BASIC functions can then be written, one to calculate the CRC of a block of data and another to check that the last byte of the block corresponds to the CRC of the preceding data:

REM Calculates the CRC of a block of data
DEFFN_1W_CRC(L%,C%)LOCAL H%,B%:H%=L% DIV256:B%=C%DIV256:CALL ow_crc_block:=?ow_buf
REM Checks if a CRC at the end of a block of data matches
DEFFN_1W_CRC_CHECK(L%,C%)=FN_1W_CRC(L%,C%)=(L%?C%)

These two can now be used to check that a device ID is valid. The CRC is also appended to other data reports, such as reading the scratchpad memory of a temperature sensor, so it's a useful routine to have. A new program which checks the CRC is as follows:

 10 DIM ID 7
 20 PROC_1W_INIT
 30 REPEAT
 40   REPEAT UNTIL FN_1W_RESET:REM Wait for device to be present
 50   PROC_1W_PUT(&33):REM Read ROM
 60   PROC_1W_GETS(ID,8):REM Fetch ID
 70   IF FN_1W_CRC_CHECK(ID,7) VDU 7:PRINT "Detected ";FN_1W_ID$(ID);" at ";TIME$:REM Print if valid
 80   REPEAT UNTIL FN_1W_RESET=FALSE:REM Wait for device to be disconnected
 90 UNTIL FALSE
100 END
110 :
120 REM 1-WIRE ROUTINES
130 END
140 DEFPROC_1W_INIT
150 ow_code_size=256:DIM ow_code ow_code_size-1
160 RXE=&E1:M_RXERXD=&10
170 TXC=&E4:M_TXCITX=&08
180 SC=&400
190 FOR opt=0 TO 2 STEP 2
200   P%=ow_code
210   [OPT opt
220   .ow_buf DEFB 0 \ temporary transfer buffer
230   .ow_conf DEFB 0 \ stores last bit conflict index
240   :
250   .ow_reset
260   IN A,(RXE):AND M_RXERXD:CP M_RXERXD:SBC A,A:LD (ow_buf),A:RET NZ \ check bus is idle
270   DI:LD A,(SC+TXC):OR M_TXCITX:OUT (TXC),A \ hold bus low
280   LD B,120:DJNZ P% \ delay
290   AND NOT M_TXCITX:OUT (TXC),A \ release bus
300   LD B,18:DJNZ P% \ delay
310   IN A,(RXE):AND M_RXERXD:CP M_RXERXD:CCF:SBC A,A:LD (ow_buf),A \ sample presence
320   LD B,100:DJNZ P% \ delay
330   EI:RET
340   :
350   .ow_put_carry
360   JR C,ow_put_1 \ fall-through
370   :
380   .ow_put_0
390   DI
400   LD A,(SC+TXC):OR M_TXCITX:OUT (TXC),A \ hold bus low
410   LD B,15:DJNZ P% \ delay
420   AND NOT M_TXCITX:OUT (TXC),A \ release bus
430   NOP \ delay
440   EI:RET
450   :
460   .ow_put_1
470   DI
480   LD A,(SC+TXC):OR M_TXCITX:OUT (TXC),A \ hold bus low
490   NOP \ delay
500   AND NOT M_TXCITX:OUT (TXC),A \ release bus
510   PUSH HL:POP HL \ delay
520   IN A,(RXE):AND M_RXERXD:SUB M_RXERXD:CCF \ sample bit
530   LD A,(ow_buf):RRA:LD (ow_buf),A \ store bit
540   LD B,7:DJNZ P% \ delay
550   EI:RET
560   :
570   .ow_put_byte
580   LD C,A:LD B,8 \ value to send in C, send 8 bits
590   .ow_put_loop
600   SRL C:PUSH BC:CALL ow_put_carry:POP BC \ shift and send single bit
610   DJNZ ow_put_loop \ loop
620   RET
630   :
640   .ow_put_bytes
650   LD A,B:OR C:RET Z:DEC BC \ have we finished?
660   LD A,(HL):INC HL \ fetch
670   PUSH BC:CALL ow_put_byte:POP BC:JR ow_put_bytes \ send and loop
680   :
690   .ow_get_byte
700   LD B,8 \ 8 bits to receive
710   .ow_get_loop
720   PUSH BC:CALL ow_put_1:POP BC \ receive single bit
730   DJNZ ow_get_loop \ loop
740   LD A,(ow_buf):RET \ store
750   :
760   .ow_get_bytes
770   LD A,B:OR C:RET Z:DEC BC \ have we finished?
780   PUSH BC:CALL ow_get_byte:POP BC \ get a byte
790   LD (HL),A:INC HL:JR ow_get_bytes \ store and loop
800   :
810   .ow_crc
820   LD B,8:LD DE,(ow_buf):LD D,A \ E = accumulated CRC, D = value to add
830   .ow_crc_loop
840   LD A,E:XOR D:SRL D:SRL A:JR C,ow_crc_odd \ XOR and shift bits
850   SRL E:DJNZ ow_crc_loop:LD A,E:LD (ow_buf),A:RET \ even CRC value
860   .ow_crc_odd:SRL E:LD A,&8C:XOR E:LD E,A:DJNZ ow_crc_loop:LD (ow_buf),A:RET \ odd CRC value
870   :
880   .ow_crc_block
890   XOR A:LD (ow_buf),A \ reset CRC
900   .ow_crc_block_loop
910   LD A,B:OR C:LD A,(ow_buf):RET Z:DEC BC \ have we finished?
920   LD A,(HL):INC HL:PUSH BC:CALL ow_crc:POP BC:JR ow_crc_block_loop \ update CRC
930   ]
940 NEXT
950 IF P%-ow_code>ow_code_size PRINT"Code size: "P%-ow_code:END
960 ENDPROC
970 :
980 REM Resets bus, retuns TRUE if any devices are present
990 DEFFN_1W_RESET:CALL ow_reset:=?ow_buf=0
1000 REM Transmits a single byte
1010 DEFPROC_1W_PUT(A%)CALL ow_put_byte:ENDPROC
1020 REM Transmits a block of bytes
1030 DEFPROC_1W_PUTS(L%,C%)LOCAL H%,B%:H%=L%DIV256:B%=C%DIV256:CALL ow_put_bytes:ENDPROC
1040 REM Receives a single byte
1050 DEFFN_1W_GET:CALL ow_get_byte:=?ow_buf
1060 REM Receives a block of bytes
1070 DEFPROC_1W_GETS(L%,C%)LOCAL H%,B%:H%=L%DIV256:B%=C%DIV256:CALL ow_get_bytes:ENDPROC
1080 :
1090 REM Converts ID bytes into string
1100 DEFFN_1W_ID$(ID)LOCAL I%:S$="":FOR I%=7 TO 0 STEP -1:IF ID?I%>15:S$=S$+STR$~(ID?I%):NEXT:=S$:ELSE:S$=S$+"0"+STR$~(ID?I%):NEXT:=S$
1110 REM Converts string into ID bytes
1120 DEFPROC_1W_ID$(ID,ID$)LOCAL I%:FOR I%=0 TO 7:ID?I%=EVAL("&"+MID$(ID$,15-I%*2,2)):NEXT:ENDPROC
1130 :
1140 REM Calculates the CRC of a block of data
1150 DEFFN_1W_CRC(L%,C%)LOCAL H%,B%:H%=L% DIV256:B%=C%DIV256:CALL ow_crc_block:=?ow_buf
1160 REM Checks if a CRC at the end of a block of data matches
1170 DEFFN_1W_CRC_CHECK(L%,C%)=FN_1W_CRC(L%,C%)=(L%?C%)

The program waits for a device to be present, reads its ID, then prints it to the screen along with the date and time if its CRC is valid. It then waits for the device to be removed before looping around to check again. This allows you to tap iButtons to a reader and it will display the relevant ID, for example. It also adds a couple of utility routines – a function, FN_1W_ID$(ID), which turns a block of ID data bytes into a string and a procedure, PROC_1W_ID$(ID,ID$), which does the opposite.

Enumerating the 1-Wire bus

It's certainly useful to be able to detect a single device on the 1-Wire bus however it would be more useful to detect multiple devices and be able to address them individually. Checking every single possible 64-bit address for a response would take far too long, but fortunately there is a way to very quickly enumerate every peripheral device on the bus by means of a binary search.

Photo of a circuit board used to provide multiple sockets to connect more than one 1-Wire device at a time
Each socket is wired in parallel to allow multiple 1-Wire devices to be connected to the Z88

To start the search, the master sends either the normal search command &F0 or the alarm/conditional search command &EC. When using the conditional search only devices that are in some sort of alarm state will respond, allowing the master to more quickly identify the devices that need attention. As we're interested in all devices we'll use the normal search command &F0.

After issuing the search command all active devices on the bus will start to report their ID, bit by bit. Each device will send each bit twice, firstly in its normal state and then again in an inverted state. Due to the open-drain nature of the bus, this allows the master to detect conflicting bit values – if all active devices have a 0 in the current bit position then the bus will read 0 then 1, if all active devices have a 1 in the current bit position then the bus will read 1 then 0 but if there is a mixture of zeroes and ones then the bus will read 0 then 0.

After this the master sends a single bit that tells the active peripheral devices which bit it has identified. If this does not match the peripheral's current bit value then the peripheral will go into an idle state and stop responding until the bus is reset again, but if it does match then the device will continue to send bits of its ID. This allows the master to walk down both branches of the binary tree when searching for device IDs when it detects a conflict, by first selecting one bit value in one iteration of the search and then the other bit value in another iteration of the search.

The full procedure for enumerating the bus is more explicitly described in the app note 1-Wire Search Algorithm, and can be implemented with the following Z80 assembly code:

.ow_conf DEFB 0 \ stores last bit conflict index
:
.ow_search
LD DE,(ow_conf):LD D,0:LD C,1:LD B,64
.ow_search_loop
PUSH BC:CALL ow_put_1:CALL ow_put_1:POP BC:RLCA:RLCA \ get bit, !bit
AND 3:JR Z,ow_search_conf \ 00 = conflict
DEC A:JR Z,ow_search_1 \ 01 = 0 bit
DEC A:JR Z,ow_search_0 \ 10 =  1 bit
SCF:RET \ report failure
.ow_search_conf
LD A,B:CP E \ how does bit index compare to last conflict
JR C,ow_search_0_conf \ 0, update current discrepancy
JR Z,ow_search_1 \ 1, no update
LD A,(HL):AND C:JR NZ,ow_search_advance \ old bit = 1, just advance
LD D,B:JR ow_search_advance \ old bit = 0, update current discrepancy
.ow_search_1:LD A,C:OR (HL):LD (HL),A:JR ow_search_advance
.ow_search_0_conf:LD D,B \ fall-through
.ow_search_0:LD A,C:CPL:AND (HL):LD (HL),A \ fall-through
.ow_search_advance
LD A,(HL):AND C:SUB C:CCF:PUSH BC:CALL ow_put_carry:POP BC \ return the ID bit
RLC C:JR NC,P%+3:INC HL \ advance mask
DJNZ ow_search_loop
LD A,D:LD (ow_conf),A
XOR A:LD (ow_buf),A:RET \ report success

A pair of BASIC wrappers can make using this search routine a bit easier:

REM Starts enumerating devices on the bus
DEFPROC_1W_SEARCH_RESET:?ow_conf=TRUE:ENDPROC
REM Searches for next device on bus. Pass search type &F0 for all devices, &EC for alarming devices. Returns TRUE if next device found
DEFFN_1W_SEARCH(A%,ID)IF ?ow_conf=0:=FALSE ELSE IF FN_1W_RESET=0:=FALSE ELSE PROC_1W_PUT(A%):H%=ID DIV256:L%=ID:CALL ow_search:=?ow_buf=0

The "reset" routine just sets the last bit conflict index to -1 (TRUE=-1) and FN_1W_SEARCH will search based on the search type (&F0 for all devices, &EC for alarming devices only), the current ID and last conflict index and will return TRUE if an ID was found or FALSE if no more IDs were found.

A snippet of code that enumerates all devices on the bus and displays their IDs is as follows:

PROC_1W_SEARCH_RESET
REPEAT F%=FN_1W_SEARCH(&F0,ID):IF F% PRINT FN_1W_ID$(ID)
UNTIL F%=FALSE

Reading temperature sensors

So far the examples have been fairly uninteresting, but we now have enough support code to do something useful with devices on a 1-Wire network. The DS18B20 temperature sensors that inspired this whole project are probably the easiest way to show how useful the 1-Wire bus can be.

Photo of two DS18B20 sensors, one in a TO-92 package and the other in a cabled probe
A DS18B20 temperature sensor in a TO-92 package and another in a cabled probe

The idea here will be to search for all temperature sensors on the network and to display their current temperature reading alongside their ID on the screen. The temperature conversion is initiated by sending the "Convert T" command (&44) to the desired 1-Wire devices and then waiting for at least 750ms with the bus inactive, allowing the parasitically-powered devices enough power to complete the temperature conversion, after which the temperature can be read back from the sensor's scratchpad memory.

Due to the large delay when waiting for the sensors to handle the "Convert T" command it is easiest to send the command to all devices on the network rather than to each one individually. This can be done by first sending the "Skip ROM" command (&CC) which allows the master to skip sending a 64-bit ID to the specific device it's addressing before sending the "Convert T" command (&44). The process to tell all devices to perform a temperature conversion is as follows:

IF FN_1W_RESET=FALSE PRINT "No devices found":END
REM Start temperature conversion
PROC_1W_PUT(&CC):REM Skip ROM
PROC_1W_PUT(&44):REM Convert T
T=TIME:IF INKEY(75)>TRUE REPEAT:UNTIL TIME>T+75:REM Delay 750ms

INKEY(75) is used to delay for 750ms however as this can be skipped by pressing a key a delay loop is provided as a safety measure.

After this, all of the devices on the network are enumerated as before:

REM Search for all temperature sensors on the bus and display their readings
PROC_1W_SEARCH_RESET
REPEAT F%=FN_1W_SEARCH(&F0,ID)
  IF F% PROC_1W_PRINT_TEMP(ID)
UNTIL F%=FALSE

PROC_1W_PRINT_TEMP should check to see whether the device ID corresponds to a temperature sensor (its family code, the least-significant byte, should be &28) and if so it should retrieve the temperature value and print it:

REM Print a single sensor's reading
DEFPROC_1W_PRINT_TEMP(ID)
LOCAL T
IF ID?0<>&28 ENDPROC:REM Must be a temperature sensor
T=FN_1W_READ_TEMP(ID):IF T=-999 ENDPROC:REM Read sensor and check for error
@%=&20409:PRINT MID$(FN_1W_ID$(ID),3,12);":",T;" deg C":@%=&90A
ENDPROC

@% controls the way numbers are printed – in this case it is changed to show four decimal places in a field width of 9 characters. When printing the device ID the first two characters and last two characters are stripped off as these correspond to the CRC and family code which are not particularly useful in this case.

FN_1W_READ_TEMP(ID) needs to fetch the temperature from the sensor with the specified ID or return -999 on error. A specific sensor can be addressed by first sending the match ROM command (&55) followed by the 64-bit device ID. After this the scratchpad RAM can be read by sending the "read scratchpad" command (&BE) then reading as many bytes as are required. We only need the first two, but will read nine as this includes all eight bytes of scratchpad RAM plus a CRC so we can verify the data is valid:

REM Retrieve a single sensor's reading
DEFFN_1W_READ_TEMP(ID)
LOCAL T
IF FN_1W_RESET=FALSE =-999
PROC_1W_PUT(&55):PROC_1W_PUTS(ID,8):REM Match ROM
PROC_1W_PUT(&BE):PROC_1W_GETS(SCRATCH,9):REM Read scratchpad
IF FN_1W_CRC_CHECK(SCRATCH,8)=FALSE =-999:REM Check CRC
=SCRATCH!-2DIV65536/16:REM Convert to degrees C

The final line converts the reading to °C. This is a signed 16-bit value stored in the first two bytes of the scratchpad memory. BBC BASIC's ! indirection operator reads a 32-bit value, so by reading from two bytes earlier (-2) the 16-bit temperature value is loaded into the most significant word of a 32-bit integer, and an integer divide of this by 65536 shifts this back down into the least significant word (where it should be) with the sign properly extended (so if it was a negative value before it will still be negative after the division). The value is then divided by 16 using a regular floating-point division as each unit of the temperature sensor's reported value corresponds to 1/16°C.

A complete demo program listing is shown below. Choosing option "3) Show DS18B20 temperatures" will show the temperatures of any connected DS18B20 temperature sensors.

  10 REM 1-WIRE DEMONSTRATION FOR Z88 : BEN RYVES 2023
  20 *NAME 1-Wire Demo
  30 DIM ID 7,SCRATCH 8
  40 PROC_1W_INIT
  50 :
  60 REM Main demo loop
  70 REPEAT PROC_1W_DEMO_MENU
  80   ON ERROR PRINT:END
  90   PRINT "<Press any key>";
 100   REPEAT UNTIL INKEY(0)=TRUE:IF GET
 110 UNTIL FALSE
 120 END
 130 :
 140 REM Main menu
 150 DEFPROC_1W_DEMO_MENU
 160 CLS:PRINT CHR$1;"1B";"1-Wire Demonstration for Cambridge Z88";CHR$1;"1B"
 170 ON ERROR END
 180 REPEAT
 190   PRINT '"Please choose a demo: (press ESC to exit)"
 200   PRINT "1) Enumerate devices"
 210   PRINT "2) Scan iButton tags"
 220   PRINT "3) Show DS18B20 temperatures"
 230   M%=GET-ASC"0"
 240 UNTIL M%>0 AND M%<4
 250 ON ERROR OFF
 260 PRINT
 270 ON M% PROC_1W_DEMO_LIST_DEVICES, PROC_1W_DEMO_TAG, PROC_1W_DEMO_SHOW_TEMPERATURES
 280 ENDPROC
 290 END
 300 :
 310 REM Device search demo
 320 DEFPROC_1W_DEMO_LIST_DEVICES
 330 PROC_1W_SEARCH_RESET
 340 REPEAT F%=FN_1W_SEARCH(&F0,ID):IF F% PRINT FN_1W_ID$(ID)
 350 UNTIL F%=FALSE:ENDPROC
 360 :
 370 REM ID tag scanning demo
 380 DEFPROC_1W_DEMO_TAG
 390 ON ERROR GOTO 50
 400 PRINT "Tap a tag on the reader (press ESC to exit)"
 410 REPEAT
 420   REPEAT UNTIL FN_1W_RESET:REM Wait for device to be present
 430   PROC_1W_PUT(&33):REM Read ROM
 440   PROC_1W_GETS(ID,8):REM Fetch ID
 450   IF FN_1W_CRC_CHECK(ID,7) AND ID?0=1 VDU 7:PRINT "Detected ";FN_1W_ID$(ID);" at ";TIME$:REM Print if valid
 460   REPEAT UNTIL FN_1W_RESET=FALSE:REM Wait for device to be disconnected
 470 UNTIL FALSE
 480 ENDPROC
 490 :
 500 REM Temperature demo
 510 DEFPROC_1W_DEMO_SHOW_TEMPERATURES
 520 IF FN_1W_RESET=FALSE PRINT "No devices found":ENDPROC
 530 REM Start temperature conversion
 540 PROC_1W_PUT(&CC):REM Skip ROM
 550 PROC_1W_PUT(&44):REM Convert T
 560 T=TIME:IF INKEY(75)>TRUE REPEAT:UNTIL TIME>T+75:REM Delay 750ms
 570 REM Search for all temperature sensors on the bus and display their readings
 580 PROC_1W_SEARCH_RESET
 590 REPEAT F%=FN_1W_SEARCH(&F0,ID)
 600   IF F% PROC_1W_PRINT_TEMP(ID)
 610 UNTIL F%=FALSE
 620 ENDPROC
 630 REM Print a single sensor's reading
 640 DEFPROC_1W_PRINT_TEMP(ID)
 650 LOCAL T
 660 IF ID?0<>&28 ENDPROC:REM Must be a temperature sensor
 670 T=FN_1W_READ_TEMP(ID):IF T=-999 ENDPROC:REM Read sensor and check for error
 680 @%=&20409:PRINT MID$(FN_1W_ID$(ID),3,12);":",T;" deg C":@%=&90A
 690 ENDPROC
 700 REM Retrieve a single sensor's reading
 710 DEFFN_1W_READ_TEMP(ID)
 720 LOCAL T
 730 IF FN_1W_RESET=FALSE =-999
 740 PROC_1W_PUT(&55):PROC_1W_PUTS(ID,8):REM Match ROM
 750 PROC_1W_PUT(&BE):PROC_1W_GETS(SCRATCH,9):REM Read scratchpad
 760 IF FN_1W_CRC_CHECK(SCRATCH,8)=FALSE =-999:REM Check CRC
 770 =SCRATCH!-2DIV65536/16:REM Convert to degrees C
 780 :
 790 REM 1-WIRE ROUTINES
 800 END
 810 DEFPROC_1W_INIT
 820 ow_code_size=294:DIM ow_code ow_code_size-1
 830 RXE=&E1:M_RXERXD=&10
 840 TXC=&E4:M_TXCITX=&08
 850 SC=&400
 860 FOR opt=0 TO 2 STEP 2
 870   P%=ow_code
 880   [OPT opt
 890   .ow_buf DEFB 0 \ temporary transfer buffer
 900   .ow_conf DEFB 0 \ stores last bit conflict index
 910   :
 920   .ow_reset
 930   IN A,(RXE):AND M_RXERXD:CP M_RXERXD:SBC A,A:LD (ow_buf),A:RET NZ \ check bus is idle
 940   DI:LD A,(SC+TXC):OR M_TXCITX:OUT (TXC),A \ hold bus low
 950   LD B,120:DJNZ P% \ delay
 960   AND NOT M_TXCITX:OUT (TXC),A \ release bus
 970   LD B,18:DJNZ P% \ delay
 980   IN A,(RXE):AND M_RXERXD:CP M_RXERXD:CCF:SBC A,A:LD (ow_buf),A \ sample presence
 990   LD B,100:DJNZ P% \ delay
1000   EI:RET
1010   :
1020   .ow_put_carry
1030   JR C,ow_put_1 \ fall-through
1040   :
1050   .ow_put_0
1060   DI
1070   LD A,(SC+TXC):OR M_TXCITX:OUT (TXC),A \ hold bus low
1080   LD B,15:DJNZ P% \ delay
1090   AND NOT M_TXCITX:OUT (TXC),A \ release bus
1100   NOP \ delay
1110   EI:RET
1120   :
1130   .ow_put_1
1140   DI
1150   LD A,(SC+TXC):OR M_TXCITX:OUT (TXC),A \ hold bus low
1160   NOP \ delay
1170   AND NOT M_TXCITX:OUT (TXC),A \ release bus
1180   PUSH HL:POP HL \ delay
1190   IN A,(RXE):AND M_RXERXD:SUB M_RXERXD:CCF \ sample bit
1200   LD A,(ow_buf):RRA:LD (ow_buf),A \ store bit
1210   LD B,7:DJNZ P% \ delay
1220   EI:RET
1230   :
1240   .ow_put_byte
1250   LD C,A:LD B,8 \ value to send in C, send 8 bits
1260   .ow_put_loop
1270   SRL C:PUSH BC:CALL ow_put_carry:POP BC \ shift and send single bit
1280   DJNZ ow_put_loop \ loop
1290   RET
1300   :
1310   .ow_put_bytes
1320   LD A,B:OR C:RET Z:DEC BC \ have we finished?
1330   LD A,(HL):INC HL \ fetch
1340   PUSH BC:CALL ow_put_byte:POP BC:JR ow_put_bytes \ send and loop
1350   :
1360   .ow_get_byte
1370   LD B,8 \ 8 bits to receive
1380   .ow_get_loop
1390   PUSH BC:CALL ow_put_1:POP BC \ receive single bit
1400   DJNZ ow_get_loop \ loop
1410   LD A,(ow_buf):RET \ store
1420   :
1430   .ow_get_bytes
1440   LD A,B:OR C:RET Z:DEC BC \ have we finished?
1450   PUSH BC:CALL ow_get_byte:POP BC \ get a byte
1460   LD (HL),A:INC HL:JR ow_get_bytes \ store and loop
1470   :
1480   .ow_search
1490   LD DE,(ow_conf):LD D,0:LD C,1:LD B,64
1500   .ow_search_loop
1510   PUSH BC:CALL ow_put_1:CALL ow_put_1:POP BC:RLCA:RLCA \ get bit, !bit
1520   AND 3:JR Z,ow_search_conf \ 00 = conflict
1530   DEC A:JR Z,ow_search_1 \ 01 = 0 bit
1540   DEC A:JR Z,ow_search_0 \ 10 =  1 bit
1550   SCF:RET \ report failure
1560   .ow_search_conf
1570   LD A,B:CP E \ how does bit index compare to last conflict
1580   JR C,ow_search_0_conf \ 0, update current discrepancy
1590   JR Z,ow_search_1 \ 1, no update
1600   LD A,(HL):AND C:JR NZ,ow_search_advance \ old bit = 1, just advance
1610   LD D,B:JR ow_search_advance \ old bit = 0, update current discrepancy
1620   .ow_search_1:LD A,C:OR (HL):LD (HL),A:JR ow_search_advance
1630   .ow_search_0_conf:LD D,B \ fall-through
1640   .ow_search_0:LD A,C:CPL:AND (HL):LD (HL),A \ fall-through
1650   .ow_search_advance
1660   LD A,(HL):AND C:SUB C:CCF:PUSH BC:CALL ow_put_carry:POP BC \ return the ID bit
1670   RLC C:JR NC,P%+3:INC HL \ advance mask
1680   DJNZ ow_search_loop
1690   LD A,D:LD (ow_conf),A
1700   XOR A:LD (ow_buf),A:RET \ report success
1710   :
1720   .ow_crc
1730   LD B,8:LD DE,(ow_buf):LD D,A \ E = accumulated CRC, D = value to add
1740   .ow_crc_loop
1750   LD A,E:XOR D:SRL D:SRL A:JR C,ow_crc_odd \ XOR and shift bits
1760   SRL E:DJNZ ow_crc_loop:LD A,E:LD (ow_buf),A:RET \ even CRC value
1770   .ow_crc_odd:SRL E:LD A,&8C:XOR E:LD E,A:DJNZ ow_crc_loop:LD (ow_buf),A:RET \ odd CRC value
1780   :
1790   .ow_crc_block
1800   XOR A:LD (ow_buf),A \ reset CRC
1810   .ow_crc_block_loop
1820   LD A,B:OR C:LD A,(ow_buf):RET Z:DEC BC \ have we finished?
1830   LD A,(HL):INC HL:PUSH BC:CALL ow_crc:POP BC:JR ow_crc_block_loop \ update CRC
1840   ]
1850 NEXT
1860 IF P%-ow_code<>ow_code_size PRINT"Code size: "P%-ow_code:END
1870 ENDPROC
1880 :
1890 REM Resets bus, retuns TRUE if any devices are present
1900 DEFFN_1W_RESET:CALL ow_reset:=?ow_buf=0
1910 REM Transmits a single byte
1920 DEFPROC_1W_PUT(A%)CALL ow_put_byte:ENDPROC
1930 REM Transmits a block of bytes
1940 DEFPROC_1W_PUTS(L%,C%)LOCAL H%,B%:H%=L%DIV256:B%=C%DIV256:CALL ow_put_bytes:ENDPROC
1950 REM Receives a single byte
1960 DEFFN_1W_GET:CALL ow_get_byte:=?ow_buf
1970 REM Receives a block of bytes
1980 DEFPROC_1W_GETS(L%,C%)LOCAL H%,B%:H%=L%DIV256:B%=C%DIV256:CALL ow_get_bytes:ENDPROC
1990 :
2000 REM Starts enumerating devices on the bus
2010 DEFPROC_1W_SEARCH_RESET:?ow_conf=TRUE:ENDPROC
2020 REM Searches for next device on bus. Pass search type &F0 for all devices, &EC for alarming devices. Returns TRUE if next device found
2030 DEFFN_1W_SEARCH(A%,ID)IF ?ow_conf=0:=FALSE ELSE IF FN_1W_RESET=0:=FALSE ELSE PROC_1W_PUT(A%):H%=ID DIV256:L%=ID:CALL ow_search:=?ow_buf=0
2040 :
2050 REM Converts ID bytes into string
2060 DEFFN_1W_ID$(ID)LOCAL I%:S$="":FOR I%=7 TO 0 STEP -1:IF ID?I%>15:S$=S$+STR$~(ID?I%):NEXT:=S$:ELSE:S$=S$+"0"+STR$~(ID?I%):NEXT:=S$
2070 REM Converts string into ID bytes
2080 DEFPROC_1W_ID$(ID,ID$)LOCAL I%:FOR I%=0 TO 7:ID?I%=EVAL("&"+MID$(ID$,15-I%*2,2)):NEXT:ENDPROC
2090 :
2100 REM Calculates the CRC of a block of data
2110 DEFFN_1W_CRC(L%,C%)LOCAL H%,B%:H%=L% DIV256:B%=C%DIV256:CALL ow_crc_block:=?ow_buf
2120 REM Checks if a CRC at the end of a block of data matches
2130 DEFFN_1W_CRC_CHECK(L%,C%)=FN_1W_CRC(L%,C%)=(L%?C%)

Temperature logger

All of this can be put together into a program that logs the temperature from any connected sensors to a CSV file on the Z88. The main loop can look similar to the one above that searches for and displays the temperature readings for any connected DS18B20 sensors, however it will instead call a PROC_1W_LOG_TEMP procedure that handles logging the data to a file instead of printing it on the display:

REM Log a single sensor's reading
DEFPROC_1W_LOG_TEMP(ID)
LOCAL T
IF ID?0<>&28 ENDPROC:REM Must be a temperature sensor
T=FN_1W_READ_TEMP(ID):IF T=-999 ENDPROC:REM Read sensor and check for error
ENTRY$=FN_DATETIME$(TIME$)+","+STR$T:REM Timestamp and temperature reading
ID$=MID$(FN_1W_ID$(ID),3,12):REM ID without CRC and family code
CSV$=ID$+".CSV":REM Name of CSV file
C=OPENUP CSV$:REM Open the CSV for update
IF C=FALSE C=OPENOUT CSV$:PRINT#C,"Time,"+ID$:REM Create new CSV if required
PTR#C=EXT#C:PRINT#C,ENTRY$:REM Write entry to end of CSV
CLOSE#C:REM Close the CSV
PRINT CSV$,ENTRY$:REM Display on screen
ENDPROC

The procedure will fetch the value from the sensor and then turn the ID into a CSV filename by stripping off the CRC and family code and appending ".CSV". It will then try to open the existing file, and if one doesn't exist it will create a new one and write the column headers to it. It will then seek to the end of the file and append the timestamp and the temperature reading.

One further complication is that to make handling the CSV a bit easier, the timestamp is converted from the format returned by BBC BASIC's TIME$ function into "YYYY-MM-DD hh:mm:ss" format. This is handled by the following three functions, FN_DATE (which extracts and reformats the date component into YYYY-MM-DD format), FN_TIME (which extracts the time component into hh:mm:ss format) and FN_DATETIME which glues the date and time back together with a space in the middle:

REM Date formatting routines
DEF FN_DATE$(T$)
LOCAL C%,I%,J%,V%,R$
R$="":I%=1
FOR C%=0 TO 3 J%=INSTR(MID$(T$,I%)," ")
  IF C%=2 V%=1+INSTR("JanFebMarAprMayJunJulAugSepOctNovDec",MID$(T$,I%,3))DIV3 ELSE V%=VAL(MID$(T$,I%,J%-1))
  IF C%>1 R$="-"+R$
  IF C% R$=STR$(V%)+R$ IF V%<10 R$="0"+R$
  I%=I%+J%
NEXT
=R$
DEF FN_TIME$(T$) =MID$(T$,LEN(T$)-7)
DEF FN_DATETIME$(T$) =FN_DATE$(T$)+" "+FN_TIME$(T$)

One way to make this logging program more useful would be to get the computer to run it periodically (e.g. once per minute). The Z88's "Alarm" feature can execute a command whenever the alarm goes off and you can schedule recurring alarms so this sounds like an ideal starting point! When the program has run it would also be handy for the computer to switch itself off again. There is an OS call for this, OS_Off, which can be invoked from BASIC as follows:

REM SWITCH OFF ROUTINES
DEFPROC_SWITCH_OFF_INIT
switch_off_size=15:DIM switch_off switch_off_size-1
P%=switch_off
[OPT 2
LD HL,0:ADD HL,SP:LD SP,(&1FFE):PUSH HL
RST &20:DEFW &EC06:REM OS_Off
POP HL:LD SP,HL:RET:]
ENDPROC
DEFPROC_SWITCH_OFF:CALL switch_off:ENDPROC

As with the 1-Wire assembly routines you must first call an initialisation procedure (PROC_SWITCH_OFF_INIT) to assemble the routine before calling it with PROC_SWITCH_OFF. The actual OS_Off call is the RST &20H:DEFW &EC06 in the middle of all that. Unfortunately, OS calls tend to involve some memory paging and in the process BBC BASIC's RAM gets swapped out and when the OS routine tries to return it jumps back into some different memory – the computer certainly switches off, but then it soft resets instead of coming back on properly. This is why there's some additional boilerplate code around the OS call to move the stack pointer into a safe region of memory so the routine can return properly.

The complete temperature-logging program is now shown below:

  10 REM 1-WIRE TEMPERATURE LOGGER : BEN RYVES 2023
  20 *NAME 1-Wire Temperature Logger
  30 REPEAT UNTIL INKEY(0)=TRUE:REM Flush keyboard
  40 DIM ID 7,SCRATCH 8
  50 PROC_1W_INIT
  60 PROC_SWITCH_OFF_INIT
  70 REM Reset 1-Wire bus and check that at least one device is present
  80 IF FN_1W_RESET=FALSE PROC_SWITCH_OFF:END
  90 REM Start temperature conversion
 100 PROC_1W_PUT(&CC):REM Skip ROM
 110 PROC_1W_PUT(&44):REM Convert T
 120 T=TIME:IF INKEY(75)>TRUE REPEAT:UNTIL TIME>T+75:REM Delay 750ms
 130 REM Search for all temperature sensors on the bus and log their readings
 140 PROC_1W_SEARCH_RESET
 150 REPEAT F%=FN_1W_SEARCH(&F0,ID):IF F% PROC_1W_LOG_TEMP(ID)
 160 UNTIL F%=FALSE
 170 REM Switch the computer off
 180 PROC_SWITCH_OFF
 190 END
 200 :
 210 REM Log a single sensor's reading
 220 DEFPROC_1W_LOG_TEMP(ID)
 230 LOCAL T
 240 IF ID?0<>&28 ENDPROC:REM Must be a temperature sensor
 250 T=FN_1W_READ_TEMP(ID):IF T=-999 ENDPROC:REM Read sensor and check for error
 260 ENTRY$=FN_DATETIME$(TIME$)+","+STR$T:REM Timestamp and temperature reading
 270 ID$=MID$(FN_1W_ID$(ID),3,12):REM ID without CRC and family code
 280 CSV$=ID$+".CSV":REM Name of CSV file
 290 C=OPENUP CSV$:REM Open the CSV for update
 300 IF C=FALSE C=OPENOUT CSV$:PRINT#C,"Time,"+ID$:REM Create new CSV if required
 310 PTR#C=EXT#C:PRINT#C,ENTRY$:REM Write entry to end of CSV
 320 CLOSE#C:REM Close the CSV
 330 PRINT CSV$,ENTRY$:REM Display on screen
 340 ENDPROC
 350 REM Retrieve a single sensor's reading
 360 DEFFN_1W_READ_TEMP(ID)
 370 LOCAL T
 380 IF FN_1W_RESET=FALSE =-999
 390 PROC_1W_PUT(&55):PROC_1W_PUTS(ID,8):REM Match ROM
 400 PROC_1W_PUT(&BE):PROC_1W_GETS(SCRATCH,9):REM Read scratchpad
 410 IF FN_1W_CRC_CHECK(SCRATCH,8)=FALSE =-999:REM Check CRC
 420 =SCRATCH!-2DIV65536/16:REM Convert to degrees C
 430 :
 440 REM Date formatting routines
 450 DEF FN_DATE$(T$)
 460 LOCAL C%,I%,J%,V%,R$
 470 R$="":I%=1
 480 FOR C%=0 TO 3 J%=INSTR(MID$(T$,I%)," ")
 490   IF C%=2 V%=1+INSTR("JanFebMarAprMayJunJulAugSepOctNovDec",MID$(T$,I%,3))DIV3 ELSE V%=VAL(MID$(T$,I%,J%-1))
 500   IF C%>1 R$="-"+R$
 510   IF C% R$=STR$(V%)+R$ IF V%<10 R$="0"+R$
 520   I%=I%+J%
 530 NEXT
 540 =R$
 550 DEF FN_TIME$(T$) =MID$(T$,LEN(T$)-7)
 560 DEF FN_DATETIME$(T$) =FN_DATE$(T$)+" "+FN_TIME$(T$)
 570 :
 580 REM SWITCH OFF ROUTINES
 590 DEFPROC_SWITCH_OFF_INIT
 600 switch_off_size=15:DIM switch_off switch_off_size-1
 610 P%=switch_off
 620 [OPT 2
 630 LD HL,0:ADD HL,SP:LD SP,(&1FFE):PUSH HL
 640 RST &20:DEFW &EC06
 650 POP HL:LD SP,HL:RET:]
 660 ENDPROC
 670 DEFPROC_SWITCH_OFF:CALL switch_off:ENDPROC
 680 :
 690 REM 1-WIRE ROUTINES
 700 END
 710 DEFPROC_1W_INIT
 720 ow_code_size=294:DIM ow_code ow_code_size-1
 730 RXE=&E1:M_RXERXD=&10
 740 TXC=&E4:M_TXCITX=&08
 750 SC=&400
 760 FOR opt=0 TO 2 STEP 2
 770   P%=ow_code
 780   [OPT opt
 790   .ow_buf DEFB 0 \ temporary transfer buffer
 800   .ow_conf DEFB 0 \ stores last bit conflict index
 810   :
 820   .ow_reset
 830   IN A,(RXE):AND M_RXERXD:CP M_RXERXD:SBC A,A:LD (ow_buf),A:RET NZ \ check bus is idle
 840   DI:LD A,(SC+TXC):OR M_TXCITX:OUT (TXC),A \ hold bus low
 850   LD B,120:DJNZ P% \ delay
 860   AND NOT M_TXCITX:OUT (TXC),A \ release bus
 870   LD B,18:DJNZ P% \ delay
 880   IN A,(RXE):AND M_RXERXD:CP M_RXERXD:CCF:SBC A,A:LD (ow_buf),A \ sample presence
 890   LD B,100:DJNZ P% \ delay
 900   EI:RET
 910   :
 920   .ow_put_carry
 930   JR C,ow_put_1 \ fall-through
 940   :
 950   .ow_put_0
 960   DI
 970   LD A,(SC+TXC):OR M_TXCITX:OUT (TXC),A \ hold bus low
 980   LD B,15:DJNZ P% \ delay
 990   AND NOT M_TXCITX:OUT (TXC),A \ release bus
1000   NOP \ delay
1010   EI:RET
1020   :
1030   .ow_put_1
1040   DI
1050   LD A,(SC+TXC):OR M_TXCITX:OUT (TXC),A \ hold bus low
1060   NOP \ delay
1070   AND NOT M_TXCITX:OUT (TXC),A \ release bus
1080   PUSH HL:POP HL \ delay
1090   IN A,(RXE):AND M_RXERXD:SUB M_RXERXD:CCF \ sample bit
1100   LD A,(ow_buf):RRA:LD (ow_buf),A \ store bit
1110   LD B,7:DJNZ P% \ delay
1120   EI:RET
1130   :
1140   .ow_put_byte
1150   LD C,A:LD B,8 \ value to send in C, send 8 bits
1160   .ow_put_loop
1170   SRL C:PUSH BC:CALL ow_put_carry:POP BC \ shift and send single bit
1180   DJNZ ow_put_loop \ loop
1190   RET
1200   :
1210   .ow_put_bytes
1220   LD A,B:OR C:RET Z:DEC BC \ have we finished?
1230   LD A,(HL):INC HL \ fetch
1240   PUSH BC:CALL ow_put_byte:POP BC:JR ow_put_bytes \ send and loop
1250   :
1260   .ow_get_byte
1270   LD B,8 \ 8 bits to receive
1280   .ow_get_loop
1290   PUSH BC:CALL ow_put_1:POP BC \ receive single bit
1300   DJNZ ow_get_loop \ loop
1310   LD A,(ow_buf):RET \ store
1320   :
1330   .ow_get_bytes
1340   LD A,B:OR C:RET Z:DEC BC \ have we finished?
1350   PUSH BC:CALL ow_get_byte:POP BC \ get a byte
1360   LD (HL),A:INC HL:JR ow_get_bytes \ store and loop
1370   :
1380   .ow_search
1390   LD DE,(ow_conf):LD D,0:LD C,1:LD B,64
1400   .ow_search_loop
1410   PUSH BC:CALL ow_put_1:CALL ow_put_1:POP BC:RLCA:RLCA \ get bit, !bit
1420   AND 3:JR Z,ow_search_conf \ 00 = conflict
1430   DEC A:JR Z,ow_search_1 \ 01 = 0 bit
1440   DEC A:JR Z,ow_search_0 \ 10 =  1 bit
1450   SCF:RET \ report failure
1460   .ow_search_conf
1470   LD A,B:CP E \ how does bit index compare to last conflict
1480   JR C,ow_search_0_conf \ 0, update current discrepancy
1490   JR Z,ow_search_1 \ 1, no update
1500   LD A,(HL):AND C:JR NZ,ow_search_advance \ old bit = 1, just advance
1510   LD D,B:JR ow_search_advance \ old bit = 0, update current discrepancy
1520   .ow_search_1:LD A,C:OR (HL):LD (HL),A:JR ow_search_advance
1530   .ow_search_0_conf:LD D,B \ fall-through
1540   .ow_search_0:LD A,C:CPL:AND (HL):LD (HL),A \ fall-through
1550   .ow_search_advance
1560   LD A,(HL):AND C:SUB C:CCF:PUSH BC:CALL ow_put_carry:POP BC \ return the ID bit
1570   RLC C:JR NC,P%+3:INC HL \ advance mask
1580   DJNZ ow_search_loop
1590   LD A,D:LD (ow_conf),A
1600   XOR A:LD (ow_buf),A:RET \ report success
1610   :
1620   .ow_crc
1630   LD B,8:LD DE,(ow_buf):LD D,A \ E = accumulated CRC, D = value to add
1640   .ow_crc_loop
1650   LD A,E:XOR D:SRL D:SRL A:JR C,ow_crc_odd \ XOR and shift bits
1660   SRL E:DJNZ ow_crc_loop:LD A,E:LD (ow_buf),A:RET \ even CRC value
1670   .ow_crc_odd:SRL E:LD A,&8C:XOR E:LD E,A:DJNZ ow_crc_loop:LD (ow_buf),A:RET \ odd CRC value
1680   :
1690   .ow_crc_block
1700   XOR A:LD (ow_buf),A \ reset CRC
1710   .ow_crc_block_loop
1720   LD A,B:OR C:LD A,(ow_buf):RET Z:DEC BC \ have we finished?
1730   LD A,(HL):INC HL:PUSH BC:CALL ow_crc:POP BC:JR ow_crc_block_loop \ update CRC
1740   ]
1750 NEXT
1760 IF P%-ow_code<>ow_code_size PRINT"Code size: "P%-ow_code:END
1770 ENDPROC
1780 :
1790 REM Resets bus, retuns TRUE if any devices are present
1800 DEFFN_1W_RESET:CALL ow_reset:=?ow_buf=0
1810 REM Transmits a single byte
1820 DEFPROC_1W_PUT(A%)CALL ow_put_byte:ENDPROC
1830 REM Transmits a block of bytes
1840 DEFPROC_1W_PUTS(L%,C%)LOCAL H%,B%:H%=L%DIV256:B%=C%DIV256:CALL ow_put_bytes:ENDPROC
1850 REM Receives a single byte
1860 DEFFN_1W_GET:CALL ow_get_byte:=?ow_buf
1870 REM Receives a block of bytes
1880 DEFPROC_1W_GETS(L%,C%)LOCAL H%,B%:H%=L%DIV256:B%=C%DIV256:CALL ow_get_bytes:ENDPROC
1890 :
1900 REM Starts enumerating devices on the bus
1910 DEFPROC_1W_SEARCH_RESET:?ow_conf=TRUE:ENDPROC
1920 REM Searches for next device on bus. Pass search type &F0 for all devices, &EC for alarming devices. Returns TRUE if next device found
1930 DEFFN_1W_SEARCH(A%,ID)IF ?ow_conf=0:=FALSE ELSE IF FN_1W_RESET=0:=FALSE ELSE PROC_1W_PUT(A%):H%=ID DIV256:L%=ID:CALL ow_search:=?ow_buf=0
1940 :
1950 REM Converts ID bytes into string
1960 DEFFN_1W_ID$(ID)LOCAL I%:S$="":FOR I%=7 TO 0 STEP -1:IF ID?I%>15:S$=S$+STR$~(ID?I%):NEXT:=S$:ELSE:S$=S$+"0"+STR$~(ID?I%):NEXT:=S$
1970 REM Converts string into ID bytes
1980 DEFPROC_1W_ID$(ID,ID$)LOCAL I%:FOR I%=0 TO 7:ID?I%=EVAL("&"+MID$(ID$,15-I%*2,2)):NEXT:ENDPROC
1990 :
2000 REM Calculates the CRC of a block of data
2010 DEFFN_1W_CRC(L%,C%)LOCAL H%,B%:H%=L% DIV256:B%=C%DIV256:CALL ow_crc_block:=?ow_buf
2020 REM Checks if a CRC at the end of a block of data matches
2030 DEFFN_1W_CRC_CHECK(L%,C%)=FN_1W_CRC(L%,C%)=(L%?C%)

When run this will log the temperatures of all connected sensors to CSV files as described above then switch the Z88 off. The "Alarm" popdown can be used to set up an alarm that runs the program once per minute (or at any other desired interval) by choosing an alarm type of "execute". This will effectively type in the supplied command, and so by setting it to #BRUN"TEMPLOG.BBC"~E it will press □+B to switch to BASIC (#B), type in RUN"TEMPLOG" and then press Enter (~E).

Screenshot of the Z88 Alarm popdown being configured to run the task

Setting up the alarm this way each time can be a bit tedious, so to make things easier here's a CLI file that can be used to set up the alarm:

.;Set up temperature logging alarm
#A
~R~E
~D~D
##BRUN"TEMPLOG.BBC"~~E
~D
~D~R~D~R~S~U~S~U~R~D

This contains keystrokes in a similar fashion to the "command" field in the alarm settings and can be "executed" from the Z88's Filer; here #A presses □+A to enter the Alarm pop-down, ~R, ~D or ~U move the cursor right, down or up and where we need to type literal # or ~ signs they are doubled up (## or ~~). This will enter all of the required details to set up an alarm that will run the task once per minute forever, at which point they can be adjusted if required (e.g. to change the interval). Pressing Enter will create the alarm, and leaving the Alarm popdown will set it in motion. To finish data collection the Z88 can be switched back on as normal for the alarm to be cleared.

The only other point of note is that I found that the computer seemed to get a bit "gummed up" with queued keypresses. This could be because it never sits idle after handling the alarm; it runs the BASIC program then switches the computer off, waiting for the next alarm to be run. This is why a simple loop to flush the keyboard buffer occurs at the start of the program, and the computer seems much happier for it.

The temperature logs in the CSV files can be used to generate a chart like the following:

Chart of the three temperature sensors logged over a 24-hour period

I captured data from three sensors over a 24 hour period; one outside (green line), one in my bedroom (red line) and one in my office (blue line). You can see how the central heating kicks in at 07:30, and I turned it up a little after 12:00. During the day the temperature in the bedroom moves up and down as the heating switches on and off, but the temperature in the office appears to be more consistent and a bit higher – the sensor is near where I am sitting and my desktop computer, which is likely contributing some heat.

Conclusion

What was originally intended to be a quick project to make use a couple of electronic components I had been sent in error soon turned into what I thought was an interesting demonstration of what can be done with the Cambridge Z88 using its stock software and some very basic additional hardware, further cementing my appreciation for the well-designed device.

Photo of a Z88 running connected to many 1-wire devices and running the demo program

The files accompanying this post can be downloaded below:

Page 1 of 53 1 2 3 4 553

Older posts RSSSearchBrowse by dateIndexTags