Advertise with Googlier.com Any Colour You Like &bull; Z80 http://benryves.com/ Personal development journal of Ben Ryves. benryves@benryves.com (Ben Ryves) benryves@benryves.com (Ben Ryves) Various calculator-related updates: BBC BASIC, Vinegar, Telnet 83, Brass and Latenite http://benryves.com/journal/3763204 http://benryves.com/journal/3763204 <p>My original TI-83 Plus feels like it's on its last legs. I suspect there's some issue with the flash memory, and I've already had to wipe its certificate page using <a href="http://benryves.com/journal/3763188">the BootExec utility that exploits a buffer overflow in the link routines</a> to get an operating system back on it. Even though it's working for now, I'm somewhat wary of installing flash applications on it.</p> <p>Recently, however, someone contacted me to let me know that there was a bug in the <a href="http://benryves.com/products/bbcbasic">TI-83 Plus version of Richard Russell's BBC BASIC</a> that I'd put together a few years ago. It turns out that on more recently-manufactured TI-84 Plus calculators uses a new LCD driver which modifies the data pointer when reading back the status register. The BBC BASIC host interface I'd put together polls the busy flag in this status register, and so the display was corrupting on these new calculators as writes to the LCD memory were not going to the correct address.</p> <p>It was a reasonably easy fix (replacing my own LCD busy test with <a href="https://github.com/benryves/BBCBasicTI83Plus/commit/ef70af2278f8b62729d5523ebe25c568a78d5b4b#diff-5f99ece5739b3ef7fc134ca1639f3cab211bcc1f8410cc5892b63434feb37bb3" rel="external">a call to the one provided by the calculator's operating system</a>), but not one I could test myself as I don't own a TI-84 Plus. I also wanted to check that there weren't any regressions with the fix, but I didn't want to risk damaging my TI-83 Plus further by reinstalling the flash application on it.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/ti83p-se-viewscreen.jpg" class="lightbox"><img src="http://benryves.com/images/ti83p-se-viewscreen.thumb.jpg" alt="Photo of a TI-83 Plus Silver Edition ViewScreen calculator with two ViewScreens" width="768" height="432" /></a></div> <p>I did find the above TI-83 Plus Silver Edition calculator on eBay, though, for a good price &ndash; and it's the ViewScreen model! This has a special socket on the back of the calculator that lets you plug in a large external LCD (it came with two of them) which is designed to be used on an overhead projector so a teacher can demonstrate using the calculator to their students. I'd previously experimented with <a href="http://benryves.com/products/tvdemonstrator" rel="external">a project that displays the calculator's screen on a TV</a>, but that captures a screenshot over the calculator's link port so is slow, requires a button to be pressed to update the image and only works in situations where the OS is idly waiting for a keypress. The ViewScreen taps into a buffered copy of the signals sent to the calculator's LCD driver, so will automatically and immediately show a copy of what's on the calculator's own screen.</p> <p>Now that I had a TI-83 Plus Silver Edition I installed BBC BASIC on it, saw that the LCD fix had worked and not broken anything else, but also encountered a few other bugs that I'd not noticed before which I've also fixed:</p> <ul><li>Pressing non-printable keys (e.g. cursor keys) in INKEY no longer slows CPU down to 6MHz until the app is restarted.</li> <li>Pressing Clear in INPUT statements now clears the whole input line instead of inserting a CHR$27 into the line.</li> <li>The On key only triggers escape when pressed. On the Silver Edition it was triggering when released as well.</li></ul> <p>These updates have been uploaded to <a href="http://benryves.com/products/bbcbasic">the project page on this site</a>, the <a href="https://github.com/benryves/BBCBasicTI83Plus" rel="external">GitHub page</a> and <a href="https://www.ticalc.org/archives/files/fileinfo/416/41608.html" rel="external">ticalc.org</a>.</p> <p><hr /></p> <p>Now that I had a shiny new calculator, I thought I'd try installing some other old programs on it to see how well they worked. One of these was <a href="http://benryves.com/products/vinegar">Vinegar</a>, a CHIP-8 and SCHIP 'emulator'/interpreter. I soon found that this also had an LCD bug on the TI-83 Plus Silver Edition, as enabling the "96x64 SCHIP mode" (which scales the 128&times;64 native resolution of SCHIP games down to the calculator's 96&times;64 display instead of cropping it) would erroneously switch the LCD driver into "6 bits per column" mode after a few seconds of play instead of the intended "8 bits per column" mode, corrupting the display.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/bin/vinegar/lcd-bug.jpg" class="lightbox"><img src="http://benryves.com/bin/vinegar/lcd-bug.thumb.jpg" alt="Photo of a TI-83 Plus next to a TI-83 Plus Silver Edition, with the display on the latter calculator showing a distorted and corrupt version of the intended image shown on the former." width="600" height="400" /></a><br /> <small>How the options menu should appear (left) versus how it appears on the Silver Edition (right) due to a bug.</small></div> <p>This project had some very old code and had never been checked into source control so I <a href="https://github.com/benryves/Vinegar" rel="external">set it up on GitHub</a>, combined the copy of the source code I had on my local machine with the source code that was publicly available for download and fixed the LCD bug. I also found and fixed a memory leak bug that occurred if no CHIP-8/SCHIP programs ("ROMs") were installed on the calculator and added a couple of keyboard shortcuts to control the display. These changes are available in release 1.2 on the <a href="http://benryves.com/products/vinegar">main project page</a>, the <a href="https://github.com/benryves/Vinegar" rel="external">GitHub page</a> and <a href="https://www.ticalc.org/archives/files/fileinfo/385/38530.html" rel="external">ticalc.org</a>.</p> <p><hr /></p> <p>The next bug I spotted was in Telnet&nbsp;83. I didn't originally write this particular program, but it is included as part of the <a href="https://github.com/benryves/TIWiFiModem" rel="external">TIWiFiModem</a> project where I've already made some bug fixes and improvements. When trying to use it to set up my TIWiFiModem with my new wireless access point I discovered that I couldn't type in the password as some of the keys were not being mapped correctly. I'd previously corrected other key mapping bugs in this program but must have missed a couple, so these are now fixed. There isn't an official release for this project but the compiled .8xp is checked directly into the repository so <a href="https://github.com/benryves/TIWiFiModem/tree/main/Telnet83Plus" rel="external">can be downloaded from there</a>.</p> <p><hr /></p> <p>I use my old <a href="http://benryves.com/products/brass">Brass assembler</a> to build the Telnet&nbsp;83 project. Some other people do still use this from time to time and so I decided it would be best for the community if the source code was available, no matter how embarassingly poorly-written it was. To this end I set up <a href="https://github.com/benryves/Brass" rel="external">a GitHub repository</a> a few years ago, but as the code had never been checked into source control before then there was no accompanying history. I did have a few old backups and so imported them into the repository in date order, but as these backups were taken around 20 years ago with no notes as to what I was thinking at the time it's all a bit of a mess!</p> <p>As this is not intended to be an actively-developed project I don't plan to make too many changes to it, but I have fixed a couple of bugs along the way. Notably, operator precedence is handled more correctly now (operators with the same precedence, such as <tt>*</tt> and <tt>/</tt>, are now evaluated from left to right whereas previously <tt>*</tt> had higher precedence than <tt>/</tt>). When working on Telnet&nbsp;83 I also found that if data overlaps in the output binary the reported address range was incorrect and also poorly-formatted, and this has now been fixed too:</p> <div class="source"><pre>Brass Z80 Assembler 1.0.5.4 - Ben Ryves 2005-2023 ------------------------------------------------- Assembling... Pass 1 complete. (310ms). Pass 2 complete. (84ms). Writing output file... Warning: Data overlap between <del>$14930-$14930</del>. <ins>&larr; Now shows the correct value '$AB9D'</ins> Errors: 0, Warnings: 1. Done!</pre></div> <p>String literals were also handled very oddly, and I've slightly improved this and added extremely rudimentary expression parsing on string, so <tt>#include&nbsp;"page"&nbsp;+&nbsp;pagenum&nbsp;+&nbsp;".asm"</tt> now works as you might expect. I've filled in some of the missing gaps in the documentation, too; updates can be downloaded from the <a href="http://benryves.com/products/brass">Brass page on this site</a> or <a href="https://github.com/benryves/Brass" rel="external">the GitHub repository</a>.</p> <p><hr /></p> <p>An even more ancient development tool that I put together was <a href="http://benryves.com/products/latenite">Latenite</a>, an IDE designed to be used for Z80 development. I hadn't used this myself in years (preferring to use Visual&nbsp;Studio&nbsp;Code as an editor and the <a href="http://benryves.com/products/brass3">Brass&nbsp;3 Project&nbsp;Builder</a> to build the code and launch a debugger) but Vinegar was set up to use this (including a project file, build scripts and debug scripts) so I thought I'd give it a go. Unfortunately, it really didn't work &ndash; the files I had on my local development copy didn't match what was in the publicly released zip files, and neither worked with what Vinegar was expecting.</p> <p>Again, I set up <a href="https://github.com/benryves/Latenite" rel="external">a GitHub repository</a> for the project and tried to piece together a working code base using the files I had locally, the files that had been publicly released and what I could remember of what had gone on twenty years before.</p> <p>The solution contained a series of projects for tools (an 8xp "linker", a TASM error processor and a WLA-DX error processor) that aren't included in later releases of the software. I suspect these are remnants of the project from before Brass became the bundled assembler. I have added the Brass repository as a submodule to the project so that it can be built alongside Latenite, and that will hopefully help keep things in sync.</p> <p>A notable sticking point was a conflict in the PindurTI-based debugging system. There appear to be two different versions, with different debug scripts and executable names. The older "PTIDebugger" was a project in the Latenite solution I had locally, and is what the Vinegar project was looking for, however the bundled project template for TI-83 (Plus) development uses a new executable called "PindurTI Debugger" and set of debug scripts with incompatible file names and I had no record of its source code anywhere.</p> <p>Fortunately, the <a href="https://github.com/benryves/Brass3/blob/master/PindurTI/LateniteInteraction.cs" rel="external">old Latenite interface for this debugger</a> is still present in the <a href="https://github.com/benryves/Brass3/tree/master/PindurTI">PindurTI debugger that's part of the Brass 3 repository</a>, so I suspect this was originally developed for Latenite before being incorporated in the Brass 3 project. I can't currently use this as a submodule of the Latenite project as it has a dependency on Brass 3 (and I don't want to have to bundle the whole of Brass 3 in the same project, as it's not relevant) so I just copied the code over and removed the Brass 3 bits.</p> <p><a href="https://github.com/cobbpg/pindurti" rel="external">PindurTI</a> is an emulator developed by the legendary Patai "cobb" Gergely, not that you'd know it from the lack of attribution in Latenite or my other development tools. Similarly, calculator shells were bundled directly for debugging purposes without mentioning their authors or including their documentation, just a raw binary. For the most part image resources used in the IDE were nicked directly from Windows XP or Visual Studio, too. This is all very naughty! Whilst I don't really intend to pick up development on Latenite again, beyond fixing some of the more obvious bugs, I would at least like to try to remedy this. I've already replaced the image resources with new icons and now provide links to the calculator shell documentation from within the IDE. There's still a way to go before I'm happy with this, though.</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/bin/latenite/images/icons-old-v-new.png" alt="Comparison of old (top) and new (bottom) icons in Latenite" width="676" height="500" /><br /> <small>A comparison of the old icons (top) compared to the new icons (bottom).</small></div> <p>I don't expect anyone to be still using this development tool, but in case there are old projects out there that were assembled using it then in the interest of making sure they can still be built in the future I think it's worth making sure the source code is available.</p> <p><hr /></p> <p>All of this relates to calculator programming and the place where I used to share all of this was <a href="https://maxcoderz.org/forum/" rel="external">the MaxCoderz forum</a>. This has had very little traffic for the past few years, but if you were one of the few people looking for it you might have noticed that until very recently it was unavailable. This was due to bots very aggressively scraping the content, causing phpBB to insert millions of rows onto the session table on the database &ndash; this one site that had virtually zero legitimate traffic ended up slowing down the entire server to a crawl a few times per day, and though I did at first selectively block the bots it ended up being a very tedious game of Wack-a-Mole and so I ended up taking down the whole forum.</p> <p>Nobody had complained about the site being down, but in the interest of keeping sites online for historical interest I have restored the site and it's now protected with CloudFlare. This is not something I'm too happy about doing, but I'd rather have a site accessible via CloudFlare than not accessible at all.</p> <p>One thing I have noticed is that this site is very poorly-equipped for distributing information about updates, with so many projects being scattered across different (and poorly-maintained) sections of the site. I am currently working on overhauling this site and shifting it onto a new platform, but one of the key requirements is to not break any of the existing links or content so it'll take a bit more work to get it ready.</p> Sun, 14 Jun 2026 18:51:39 +0100 Repairing and using a Sharp ZQ-700 organiser as a pocket computer http://benryves.com/journal/3763202 http://benryves.com/journal/3763202 <p>The Sharp ZQ-700 Electronic Organizer, also sold as the Sharp OZ-700 Wizard, was a pretty interesting device. Its large 239&times;80 pixel resolution back-lit LCD and QWERTY keyboard made it a comfortable device to use, though the built-in programs are somewhat simplified from Sharp's more sophisticated earlier offerings; there's also no card slot for software expansion and the connectivity is much more limited. Gone are the options to connect a serial modem, send a fax, print to a thermal printer or back up data to cassette tape, but perhaps this was all a sign of the times. The US version of the organiser proudly sports the <em>mywizard.com</em> domain name, and the features I mentioned were all pretty old hat in an era when the Internet was being rapidly embraced. Whilst the organiser could not directly connect to the Internet, the accompanying website allowed users to share and download data files for the organiser and synchronise them with a PC using the supplied data cable.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/sharp-organisers/zq-770-sierpinski-memo.jpg" class="lightbox" rel="external"><img src="http://benryves.com/images/sharp-organisers/zq-770-sierpinski-memo.thumb.jpg" alt="Photo of a ZQ-770 with a BASIC program for generating a Sierpinski triangle typed into a memo" width="600" height="450" /></a><small><br /> A programmable organiser: Sierpinski triangle code typed into a memo</small></div> <p>Where this becomes particularly interesting the organiser's <em>My&nbsp;Programs</em> button. Previous Sharp organisers could be turned into pocket computers via the addition of a Scientific Computer card which included a powerful BASIC interpreter. The ZQ-700 has a BASIC interpreter built-in, and up to ten BASIC programs can be stored on the organiser and accessed via the <em>My&nbsp;Programs</em> button. Unfortunately, these BASIC programs cannot be edited directly on the organiser itself and there is no interactive BASIC prompt, but Sharp supplied a free SDK which let you edit BASIC programs and convert them into the tokenised form that could be transferred to the organiser. Being able to write your own programs to run on your organiser is an extremely powerful feature.</p> <p>Even better, the BASIC interpreter does provide <tt>PEEK</tt>, <tt>POKE</tt> and <tt>CALL</tt> keywords even though these are not directly accessible when using Sharp's official SDK. By creating a BASIC program with a stub <tt>CALL</tt> at the start and appending machine code to the end of it it's possible to run native code on the organiser. The organiser is powered by a Z80 CPU, and so a <a href="https://github.com/arpruss/ozdev" rel="external">user-developed alternative SDK</a> (including a C compiler) was released, allowing people to write their own native code for the organiser.</p> <p>Unfortunately, most of the sites relating to the ZQ-700 and its community are long-gone. The official <em>mywizard.com</em> has been offline since at least 2009, though interestingly Sharp do still host <a href="https://global.sharp/contents/organizer/software/zq-700/index.html" rel="external">some downloads relating to the organiser on their global website</a>. The <em>mywizard.com</em> site eventually required user registration to download files, so very little of the user-generated content has been preserved by the Internet Archive. However, <a href="#oz-770-links">some of the hobbyist sites about the organiser</a> have been preserved there, so it is possible to scrape together a bit of a software collection that way.<br /> <h3 id="oz-770-lcd-repair">LCD repair</h3></p> <p>Aside from the link rot there are some more pressing age-related issues with the ZQ-700 relating to its LCD. Or, as the case may be, <em>non-</em>pressing issues as the flat flex cable that provides an electrical connection between the main PCB and the LCD's rows and columns gradually comes unstuck.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/sharp-organisers/oz-770-faulty-lcd.jpg" class="lightbox" rel="external"><img src="http://benryves.com/images/sharp-organisers/oz-770-faulty-lcd.thumb.jpg" alt="Photo of an OZ-770 with a faulty LCD" width="600" height="450" /></a></div> <p>The photo above shows the typical state of the organiser's LCD after all these years. The discoloured paintwork around the hinges is somewhat less typical; I bought two organisers recently for cheap due to their non-working condition, and both showed signs of severe alkaline battery leakage. The two organisers were sold as a pair, and both were showed the same owner's name and address when powered on. I always find it interesting if an electronic organiser has any old user data on it, as it gives you an impression of how much the owner appreciated the device; in this particular case these organisers were very heavily used, with around 5,000 records stored on each. This gave me all the more inclination to want to repair them. As well as thousands of contact details and diary entries there were also numerous BASIC programs in the <em>My&nbsp;Programs</em> section, all related to cars and financing, so I thought it would be worth trying to find a way of backing up said programs before erasing all of the personal data from memory.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/sharp-organisers/oz-770-lcd-pcb.jpg" class="lightbox" rel="external"><img src="http://benryves.com/images/sharp-organisers/oz-770-lcd-pcb.thumb.jpg" alt="Photo of the back of the OZ-770 screen with the back cover removed" width="360" height="270" /></a> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <a href="http://benryves.com/images/sharp-organisers/oz-770-lcd-glass.jpg" class="lightbox" rel="external"><img src="http://benryves.com/images/sharp-organisers/oz-770-lcd-glass.thumb.jpg" alt="Photo of the LCD glass mounted to the PCB inside the OZ-770 screen" width="360" height="270" /></a></div> <p>Getting access to the LCD is reasonably easy; the rear cover simply clips on. I find it easies to start popping it off near the hinge side, unclipping both sides and working up towards the top edge furthest away from the hinge. Some screws hold the PCB in, with each screw hole marked with a white triangle. In my case one screw was missing from the factory! The LCD glass itself is secured to the front of the screen housing with double-sided tape; some gentle pressure on the screen from the inside will unstick it.</p> <p>The flat flex cables that are stuck to the LCD glass use a heat-activated adhesive. One potential fix for the cables coming unstuck is to heat them with a soldering iron to reactivate the adhesive, though this is a somewhat risky procedure. In this case, however, that is not an option due to the use of two cables at right angles to each other, with the problematic column-driving cable being folded between the LCD and PCB with no easy way to access it with a soldering iron.</p> <p>A mechanical fix can be an option, however. This involves finding some way to put pressure between the flex cable and the PCB and/or LCD to physically hold the cable against the contacts. Thin rubber sheeting can work well for this, and for the ZQ-700 series I've found cutting a piece that's 3mm wide and about 105mm long from a 2mm thick sheet does a good job.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/sharp-organisers/oz-770-repair-rubber-strip-1.jpg" class="lightbox" rel="external"><img src="http://benryves.com/images/sharp-organisers/oz-770-repair-rubber-strip-1.thumb.jpg" alt="Photo of a rubber sheet with Kapton tape on it on a cutting mat" width="360" height="270" /></a> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <a href="http://benryves.com/images/sharp-organisers/oz-770-repair-rubber-strip-2.jpg" class="lightbox" rel="external"><img src="http://benryves.com/images/sharp-organisers/oz-770-repair-rubber-strip-2.thumb.jpg" alt="Photo of the cut rubber strip" width="360" height="270" /></a></div> <p>I also put two layers of Kapton tape on each side of the rubber strip before cutting it out. Aside from a little extra thickness, this gives the otherwise grippy rubber strip a smooth surface that will make it easier to slide into the fold of the flat flex cable between the LCD and its PCB.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/sharp-organisers/oz-770-repair-strip-installed-1.jpg" class="lightbox" rel="external"><img src="http://benryves.com/images/sharp-organisers/oz-770-repair-strip-installed-1.thumb.jpg" alt="Photo of a the rubber strip installed inside the fold of the flat flex cable" width="360" height="270" /></a> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <a href="http://benryves.com/images/sharp-organisers/oz-770-repair-strip-installed-2.jpg" class="lightbox" rel="external"><img src="http://benryves.com/images/sharp-organisers/oz-770-repair-strip-installed-2.thumb.jpg" alt="Photo of a the rubber strip installed inside the fold of the flat flex cable" width="360" height="270" /></a></div> <p>There are two pieces of white tape stuck to the back of the PCB and the flat flex cable which pull on the cable slightly and can make it harder to install the rubber strip. Rather than remove these entirely I very carefully peeled them off the PCB and then cut them rather than try to peel them off the fragile flat flex cable and cause further damage.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/sharp-organisers/oz-770-repair-improvement-partial.jpg" class="lightbox" rel="external"><img src="http://benryves.com/images/sharp-organisers/oz-770-repair-improvement-partial.thumb.jpg" alt="Photo the screen with most of the columns now visible, though a single region in the centre of the LCD is still missing" width="360" height="270" /></a> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <a href="http://benryves.com/images/sharp-organisers/oz-770-repair-improvement-squeezed.jpg" class="lightbox" rel="external"><img src="http://benryves.com/images/sharp-organisers/oz-770-repair-improvement-squeezed.thumb.jpg" alt="Photo of the screen being squeezed to bring back all of the columns" width="360" height="270" /></a></div> <p>After installing the rubber strip and screwing the PCB back down, there was a notable improvement but not all of the columns came back. Putting some additional pressure on the back of the board in just the right place resulted in a complete picture. One possible way to apply this pressure is to put something inside the back cover so that when it's clipped back on it simulates what my thumb was doing in the previous set of photos:</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/sharp-organisers/oz-770-repair-rubber-inside-back.jpg" class="lightbox" rel="external"><img src="http://benryves.com/images/sharp-organisers/oz-770-repair-rubber-inside-back.thumb.jpg" alt="Photo of rubber strips inside the back cover of the organiser's screen" width="500" height="375" /></a></div> <p>Unfortunately in this organiser's case there was still one missing column, and this was with so much rubber stuck inside the back cover that the whole screen was bowing outwards and could no longer be clipped shut. This clearly wasn't the answer, so the rubber pieces inside the back cover were peeled off. Some targeted application of the heat treatment seemed like the next best option.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/sharp-organisers/oz-770-repair-soldering-iron.jpg" class="lightbox" rel="external"><img src="http://benryves.com/images/sharp-organisers/oz-770-repair-soldering-iron.thumb.jpg" alt="Photo of a soldering iron being used to heat up the back of the organiser's PCB. Two spring clamps are clipped to either side of where it is being applied" width="500" height="375" /></a></div> <p>There is no direct access to the flex cable, however there is nothing too delicate on the opposite side of the PCB which we <em>do</em> have access to &ndash; mostly just a thick copper track. A pair of spring clamps were placed on either side of the missing columns, applying firm pressure to the cable courtesy of the rubber strip inside its fold. The soldering iron was set to 350&deg;C and held against the copper track for a few seconds. Everything was left to cool, then the results were checked &ndash; all columns were back!</p> <p>I must stress this is a risky operation, as the flex cable is very delicate and heating it can ruin it. 350&deg;C is far too hot for directly heating the cable and if the soldering iron slips and makes contact with the cable you'll probably melt a hole in it. When directly heating the cable I use an iron at around 240&deg;C, but even then I only lightly swipe it across the cable in the direction of the contacts &ndash; no prolonged contact and no firm pressure.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/sharp-organisers/oz-770-zq-770.jpg" class="lightbox" rel="external"><img src="http://benryves.com/images/sharp-organisers/oz-770-zq-770.thumb.jpg" alt="Photo of an OZ-770 and a ZQ-700 organiser, both with fully-working screens" width="720" height="405" /></a></div> <p>Once I had the OZ-770 working I turned my attention to the ZQ-770, the other organiser from the pair. This one also has faulty columns on its display, however the fault is rather more intermittent &ndash; gently flexing the screen brings the missing columns back, and once the organiser has been on for a short while they generally remain visible until the organiser is switched off for a while. It'll probably need repairing in the future, but for now it's working well enough that I don't want to risk accidentally making it worse.<br /> <h3 id="zq-700-backing-up-my-programs">Backing up <em>My&nbsp;Programs</em> from ZQ-700 series organisers</h3></p> <p>One of the two organisers I'd bought had a number of BASIC programs loaded into the <em>My&nbsp;Programs</em> area. Unlike the personal data, which I had wiped, I thought it would be interesting to preserve these BASIC programs. Connecting the organiser to a PC is easy enough via an RS-232 serial cable; though I don't have an original one, I was able to make my own from a 3.5mm TRRS connector and a DE-9 plug following the wiring diagram on <a href="https://imslsoft.com/ozdev.htm" rel="external">IMSL Software's copy of a page from the OZdev website</a>. IMSL Software also develop the XLink/Win software which can be used to synchronise data between a Windows PC and an organiser, though this won't let you back up the data from <em>My&nbsp;Programs</em>.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/sharp-organisers/zq-770-serial-cable.jpg" class="lightbox" rel="external"><img src="http://benryves.com/images/sharp-organisers/zq-770-serial-cable.thumb.jpg" alt="Photo of a home-made serial cable for the ZQ-770" width="360" height="270" /></a><small><br /> A home-made serial cable for the ZQ-770</small></div> <p>Sharp supplied a copy of <em>Day-Timer&nbsp;Organizer</em> for similar purposes and though this won't help back up <em>My&nbsp;Programs</em> either there is a handy backup utility on the CD that can dump the entire contents of the organiser to a file. Someone has uploaded a copy of this <a href="https://archive.org/details/oz-700_series" rel="external"><em>OZ-700&nbsp;Software&nbsp;CD-ROM</em> to the Internet Archive</a>, and though it can't seem to restore backups on modern versions of Windows it happily created backup images from my organisers.</p> <p>I did take a look at the created backup files and though they looked like nonsense at first I think this is because all of the bit values are inverted. After flipping the bits back I could find various program fragments, though they not contiguous so I suspect there's a file system or similar data structure embedded in the backups that make pulling out the data a little more difficult.</p> <p>Fortunately this is a solved problem: <a href="https://web.archive.org/web/20041029104016/http://wizardparadise.250free.com/win32software/SbkExplorer.zip" rel="external">SbkExplorer</a> can open the backup .sbk file and export the programs as individual .wzd files, ready for reinstallation on other organisers.</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/sharp-organisers/sbk-explorer-my-programs.png" alt="Screenshot of SbkExplorer" width="526" height="353" /><br /> <small>Extracting files from the <em>My Programs</em> portion of a backup using SbkExplorer.</small></div> <p>When these BASIC programs were originally developed using Sharp's SDK they were stored as a .bas containing the source code as a plain text file and a .prj project file that contained some information about the program such as its full name and a text description. The program would be "compiled" into a .obj file (effectively a tokenised BASIC program with the comments stripped out) and then published as a .wzd file which is an XML-like file containing the description from the project file and the compiled object file as raw binary data. Sharp's <a href="https://global.sharp/contents/organizer/software/zq-700/programs/downloader.exe">downloader</a> tool could then open the .wzd file, show the project description on the screen, and allow the user to transfer the compiled BASIC program to their organiser's <em>My&nbsp;Programs</em> menu.</p> <p>The .wzd file recovered by SbkExplorer won't be a perfect match for the one used to install the program originally, as it will be missing any descriptive text. However, it's good enough to back up an installable version of the original program file. If you wanted to edit the source code for the program, then the <a href="https://web.archive.org/web/20050829025802/http://home.iprimus.com.au/amurgshere/" rel="external">Sharp&nbsp;Wizard&nbsp;Decompiler</a> can be used to extract a .bas file from the .wzd. Again, this won't be a perfect match for the source material as any comments would have been stripped out by the Sharp SDK, but it's definitely a good starting point to recovering old programs.</p> <p><h3 id="oz-770-links">Links to OZ-770 resources</h3><br /> Unfortunately, a lot of the old sites that used to host information about this series of Sharp Organisers are long gone. Fortunately, the Internet Archive's Wayback Machine has copies of a lot of them, and I've linked to those archived copies where the original sites are no longer online.</p> <h4>General tools</h4> <ul> <li><a href="https://global.sharp/contents/organizer/software/zq-700/index.html" rel="external">Sharp's <em>Software Downloads ZQ-700 series</em> page</a> still hosts the <em>Downloader</em>, <em>Data Creation</em> and <em>SDK</em> tools for the organiser as well as some sample installable .wzd files.</li> <li>IMSL Software show <a href="https://imslsoft.com/ozdev.htm" rel="external">how to make a serial cable for the organiser</a> and sell <a href="https://imslsoft.com/">XLink/Win</a> linking software that is compatible with the organiser.</li> <li><a href="https://archive.org/details/oz-700_series" rel="external">Day-Time Organizer: Sharp Edition</a> was originally bundled with the organiser on CD-ROM and can be used to back up and restore data from the organiser.</li> <li><a href="https://web.archive.org/web/20041029104016/http://wizardparadise.250free.com/win32software/SbkExplorer.zip" rel="external">SbkExplorer</a> can be used to extract data from a .sbk backup (created using the Backup Utility on the previous <a href="https://archive.org/details/oz-700_series" rel="external">CD-ROM</a>).</li> </ul> <p> <h4>File archives and information about the organiser</h4></p> <ul> <li><a href="https://web.archive.org/web/20031124160457/http://www.wizworld.vze.com/" rel="external">Wizworld</a>.</li> <li><a href="https://web.archive.org/web/20040804062322/http://www.angelfire.com/wizard/oz-750/index.htm" rel="external">The OZ-750 Paradise</a>.</li> <li><a href="https://web.archive.org/web/20040925033502/http://www.geocities.com/pcawizman/index.html" rel="external">Marshall's Amazing Wizard Organizer</a>.</li> <li><a href="https://web.archive.org/web/20080101055543/http://www.sumuthu.lk/mywizard/" rel="external">Nadisha Ranmuthu's Wizard</a>.</li> <li><a href="https://web.archive.org/web/20050406001736/http://homepages.inf.ed.ac.uk/gfursin/research_sharp.html" rel="external">Grigori Fursin's Homepage (FSFM)</a>.</li> <li><a href="https://web.archive.org/web/20041015074422/http://www.prussfamily.us/softwareOZPC.html" rel="external">Software for the PC and for Sharp Wizard OZ/ZQ 7xx Organizers mainly by Alex Pruss</a>.</li> </ul> <p> <h4>Software development</h4></p> <ul> <li>OZdev Wizard Development: <a href="https://web.archive.org/web/20010301203957/http://www.ozdev.com/" rel="external">2001</a>, <a href="https://web.archive.org/web/20100526212145/http://lh.co.nz/ozdev/" rel="external">2002</a>. Lots of good information but the archives have some broken links, hence two captures from the two different domains are provided.</li> <li><a href="https://global.sharp/contents/organizer/software/sdk/SDK0903.exe" rel="external">Official Sharp SDK</a> to develop programs using BASIC.</li> <li><a href="https://web.archive.org/web/20040804214501/http://www.angelfire.com/wizard/oz-750/win32software/zifnab.zip" rel="external">Zifnab</a>, an alternative SDK for developing BASIC programs (includes additional keywords like <tt>PEEK</tt>, <tt>POKE</tt> and <tt>CALL</tt>).</li> <li><a href="https://web.archive.org/web/20160516094335/http://www.angelfire.com/wizard/oz-750/wizard/bacon.zip" rel="external">Bacon</a>, an organiser add-on that lets you execute BASIC programs created directly on the organiser as memos.</li> <li><a href="https://web.archive.org/web/20050829025802/http://home.iprimus.com.au/amurgshere/" rel="external">Sharp Wizard Decompiler</a> to decompile .wzd files into BASIC source files.</li> <li><a href="https://github.com/arpruss/ozdev" rel="external">C SDK</a> with a lot of sample code.</li> </ul> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/sharp-organisers/zq-770-sierpinski-finished.jpg" class="lightbox" rel="external"><img src="http://benryves.com/images/sharp-organisers/zq-770-sierpinski-finished.thumb.jpg" alt="Photo of a ZQ-770 showing a Sierpinski triangle on the screen" width="600" height="450" /></a><small><br /> The result of running the Sierpinski triangle code from the earlier memo using Bacon</small></div> <p>If you pick up one of these old organisers you can probably have quite a lot of fun with it as a pocket computer containing a Z80 CPU, a large LCD and QWERTY keyboard. It's just a shame about the poor durability of the screen.</p> Sat, 21 Mar 2026 14:07:37 +0000 Printing graphics from a Cambridge Z88 on a Serial 8056 via the BASIC patch http://benryves.com/journal/3763201 http://benryves.com/journal/3763201 <div class="html_center" style="text-align: center;"><a href="http://benryves.com/bin/z88/8056Z88.jpg" class="lightbox" rel="external"><img src="http://benryves.com/bin/z88/8056Z88.thumb.jpg" alt="Photo of a Serial&nbsp;8056 printer next to a Cambridge&nbsp;Z88 computer" width="768" height="432" /></a></div> <p>I've got a number of older computers that can print, but no printer for them. Quite often these computers require a serial printer, and so when a Serial&nbsp;8056 printer popped up on eBay for around a tenner I picked it up. This is a thermal printer that takes fax paper rolls, so it seemed like a safe bet as far as consumables go (no need to source awkward cartridges, ink ribbons or spark paper) and the listing claimed it was intended for the Sinclair QL.</p> <p>When it turned up I was a bit surprised by the plug on the end of the cable &ndash; two rows of eight pins, similar to a 16-way IDC connector, and not the phone jack style connector the QL needed. Fortunately the data (RD) and CTS pins were marked on the circuit board inside the printer and I was able to trace them out to the plug and bodge together a cable to plug it into my PC. Between articles from <a href="https://archive.org/details/format_mag/FormatIssue0510/page/n15/mode/1up?q=%22serial+8056%22" rel="external">Format magazine</a>, <a href="https://archive.org/details/qlworld-1986/QLWorld_1986-10/page/9/mode/1up?q=8056+printer">QL World</a> and <a href="https://archive.org/details/popular-computing-weekly-1986-10-09/page/n28/mode/1up?q=8056">Popular Computing Weekly</a> about the printer I was able to find the baud rate (1200), a few control codes for formatting and how to output graphics. Still puzzled by the non-QL plug I asked Reddit and that's when it was pointed out that the Serial&nbsp;8056 is really a rebadged IBM PC Compact Printer originally sold for use with the PCjr. If my particular printer had been intended for use with the Sinclair QL then it would have included the appropriate adaptor in the box.</p> <p>Knowing this, however, it made it easier to find information about the printer, including a <a href="https://www.minuszerodegrees.net/oa/OA%20-%20IBM%20PC%20Compact%20Printer.pdf" rel="external">reference manual</a>, confirming the information I'd gleaned from the magazine articles about the Serial&nbsp;8056.</p> <p>One of the computers I had planned to use the printer with was my Cambridge&nbsp;Z88. Setting this up as a text printer was easy enough, but I'd been intrigued by a feature of the <a href="https://www.bbcbasic.co.uk/bbcbasic/z88patch.html" rel="external">Z88 BASIC Patch</a>, as described by the notes:</p> <div class="quote">STOP PRESS Version 2.1 includes a graphics dump for Epson-compatible printers. The statement CALL 11011 will dump the graphics window.</div> The Serial&nbsp;8056 is not Epson-compatible, and so the printer just outputs nonsense instead of the promised graphics. However, the Z88 Patch has source code available, so I thought it might be possible to modify this to replace the Epson control sequences with Serial&nbsp;8056 ones.</p> <p>Unfortunately, the Z88 BASIC Patch source code release appears to be missing the printer code. I turned to <a href="https://github.com/NationalSecurityAgency/ghidra" rel="external">Ghidra</a> to disassemble the patch, and found the pertinent routines.</p> <p>As the routines send a dump of the graphics window (the "map" in Z88 parlance) to the printer, I named the main routine <tt>DUMPMAP</tt>. One of the first things it does is to reset the printer via a routine I named <tt>DUMPRESET</tt>. This sends <tt>ESC&nbsp;@</tt> to reset the printer (the <a href="https://files.support.epson.com/pdf/general/escp2ref.pdf" rel="external">Epson ESC/P reference</a> may be useful here), then sends two line feeds. It falls through to the routine that is used to send bytes to the printer, which I've named <tt>DUMPWRCH</tt>:</p> <div class="source"><pre> ************************************************************************* * Resets the printer to its initial settings and outputs two line feeds * ************************************************************************* DUMPRESET ram:2bb5 3e 1b LD A,0x1b ; ESC ram:2bb7 cd c6 2b CALL DUMPWRCH ram:2bba 3e 40 LD A,'@' ; ESC @ = Initialize printer ram:2bbc cd c6 2b CALL DUMPWRCH ram:2bbf 3e 0a LD A,'\n' ; Line feed ram:2bc1 cd c6 2b CALL DUMPWRCH ram:2bc4 3e 0a LD A,'\n' ; Line feed ************************************************************************* * Write a byte to the serial port with a 1 second timeout * ************************************************************************* DUMPWRCH ram:2bc6 f5 PUSH AF ram:2bc7 01 64 00 LD BC,100 ; 100cs timeout ram:2bca e7 RST SYS ram:2bcb 42 db OS_Pbt ; Write the byte to the serial port ram:2bcc f1 POP AF ram:2bcd c9 RET</pre></div> <p>The <tt>DUMPRESET</tt> routine is also used at the very end of printing to reset the printer and ensure two line feeds appear after the graphics dump. Graphics data are sent as 8 pixel high rows with condensed line spacing, one byte per column. The relevant code that starts this process of each row is as follows: first the line spacing is set to 1/9-inch using <tt>ESC&nbsp;3</tt>, a line feed is sent, there's a one second delay to give the mechanism time to advance and then graphics mode is entered with <tt>ESC&nbsp;L</tt> and a request to send 768 bytes:</p> <div class="source"><pre>ram:2b33 3e 1b LD A,0x1b ; ESC ram:2b35 cd c6 2b CALL DUMPWRCH ram:2b38 3e 33 LD A,'3' ; ESC 3 = Set n/216-inch line spacing ram:2b3a cd c6 2b CALL DUMPWRCH ram:2b3d 3e 18 LD A,24 ; 24/216 = 1/9-inch line spacing ram:2b3f cd c6 2b CALL DUMPWRCH ram:2b42 3e 0a LD A,'\n' ; Line feed ram:2b44 cd c6 2b CALL DUMPWRCH ram:2b47 01 64 00 LD BC,100 ; 100cs ram:2b4a e7 RST SYS ram:2b4b 2d db OS_Tin ; Wait for a key for 100cs ram:2b4c 3e 1b LD A,0x1b ; ESC ram:2b4e cd c6 2b CALL DUMPWRCH ram:2b51 3e 4c LD A,'L' ; ESC L = Select 120-dpi graphics ram:2b53 cd c6 2b CALL DUMPWRCH ram:2b56 3e 00 LD A,0 ; nL = 0 ram:2b58 cd c6 2b CALL DUMPWRCH ram:2b5b 3e 03 LD A,3 ; nH = 3: 768 bytes ram:2b5d cd c6 2b CALL DUMPWRCH</pre></div> <p>The graphics window (map) is only 256 pixels wide, though, so why 768 bytes? Well, the printing code actually scales the image up before printing: it doubles the height and triples the width of each pixel. When outputting a row of graphics data, each column byte is sent three times:</p> <div class="source"><pre>ram:2b7c cd c6 2b CALL DUMPWRCH ram:2b7f cd c6 2b CALL DUMPWRCH ram:2b82 cd c6 2b CALL DUMPWRCH</pre></div> <p>This is all of the Epson-specific printer code, and fortunately it maps pretty well to the Serial&nbsp;8056:</p> <p><table class="basic centred"> <thead><tr><th>Action</th><th>Epson ESC/P</th><th>Serial&nbsp;8056</th></tr></thead> <tbody> <tr><th>Initialise printer</th><td><tt>ESC&nbsp;@</tt></td><td><tt>CAN</tt></td></tr> <tr><th>Set 1/9-inch line spacing</th><td><tt>ESC&nbsp;3 n=24</tt></td><td><tt>ESC&nbsp;1</tt></td></tr> <tr><th>Output bitmapped graphics</th><td><tt>ESC&nbsp;L n=768 [768 bytes]</tt></td><td><tt>ESC&nbsp;K n=512 [512 bytes]</tt></td></tr> </tbody> </table><br /> Ideally, the Epson codes could simply be patched with the equivalent Serial&nbsp;8056 codes but there is one slight spanner in the works: the Serial&nbsp;8056 needs a carriage return to be sent after each line and the code doesn't do that and there's no easy way to insert it at the end of the relevant printing routines.</p> <p>However, it is possible to insert a carriage return at the <em>start</em> of each line, which means that each line will start by ending the preceding one. This does still leave the final line, but fortunately the code calls <tt>DUMPRESET</tt> after printing the last line and so an additional carriage return can be inserted at the start of that routine to terminate that line.</p> <p>It's not quite as elegant a patch, as the order of some code needs to be adjusted rather than just patching the Epson codes with the equivalent Serial&nbsp;8056 codes, but it's not too bad overall. The full list of code changes are as follows:</p> <div class="source"><pre> DUMPRESET ram:2bb5 3e 1b LD A,0x1b ; Change to CR: ?&amp;2BB6=13 ram:2bb7 cd c6 2b CALL DUMPWRCH ram:2bba 3e 40 LD A,'@' ; Change to ESC: ?&amp;2BBB=27 ram:2bbc cd c6 2b CALL DUMPWRCH ram:2bbf 3e 0a LD A,'\n' ; Change to '2': ?&amp;2BC0=50 ram:2bc1 cd c6 2b CALL DUMPWRCH ram:2bc4 3e 0a LD A,'\n'</pre></div> <p>The need to insert an extra carriage return at the start of the reset routine means we can only output a single line feed after resetting the printer instead of the original two. You may also be wondering why the printer is "reset" with <tt>ESC&nbsp;2</tt> instead of <tt>CAN</tt>, as that would save a byte &ndash; in my case it doesn't appear that resetting the printer that way resets the line spacing, which means that the printer gets left in the 1/9-inch line spacing mode. <tt>ESC&nbsp;2</tt> explicitly restores the 1/6-inch (default) line spacing mode.</p> <p>The code that runs at the start of each line of output is a bit more awkward to change, unfortunately. The original code currently works like this:</p> <ul><li>Send ESC</li> <li>Send '3'</li> <li>Send 24</li> <li>Send LF</li> <li>Wait 100cs</li></ul> <p>However, our new code needs to do this instead: </p> <ul><li>Send CR</li> <li>Send LF</li> <li>Wait 200cs*</li> <li>Send ESC</li> <li>Send '1'*</li></ul> <p>Three of the five operations line up, however two of them (sending a byte of data and introducing a delay, marked with an asterisk) are swapped, which means that two code blocks in the code need to be swapped. Very fortunately, the code for each operation is the same size (five bytes) which at least means that the code between them can be left in the same place.</p> <div class="source"><pre>ram:2b33 3e 1b LD A,0x1b ; Change to CR: ?&amp;2B34=13 ram:2b35 cd c6 2b CALL DUMPWRCH ram:2b38 3e 33 LD A,'3' ; Change to LF: ?&amp;2B39=10 ram:2b3a cd c6 2b CALL DUMPWRCH ram:2b3d 3e 18 LD A,24 ; Change to 200cs delay: ?&amp;2B3D=1 ?&amp;2B3E=200 ram:2b3f cd c6 2b CALL DUMPWRCH ; ?&amp;2B3F=0 ?&amp;2B40=231 ?&amp;2B41=45 ram:2b42 3e 0a LD A,'\n' ; Change to ESC: ?&amp;2B43=27 ram:2b44 cd c6 2b CALL DUMPWRCH ram:2b47 01 64 00 LD BC,100 ; Change to DUMPWRCH '1': ram:2b4a e7 RST SYS ; ?&amp;2B47=62 ?&amp;2B48=49 ram:2b4b 2d db OS_Tin ; ?&amp;2B49=205 ?&amp;2B4A=198 ?&amp;2B4B=43</pre></div> <p>The time delay is handled by calling the OS input routine with the timeout delay specified in register BC. The original code used 100cs, i.e. 1 second. When I was testing the code I ran into some issues: the first few lines printed fine, but the last couple of lines ended up failing to print, with the preceding lines showing some junk characters at the end of each line. Extending the delay to 200cs fixed the issue, but I was not sure why the first few lines printed fine and the problem only manifested itself at the end of the print until I looked at the movement of the print head more carefully.<br /> <br />The test image I was using was a row of Sierpinski triangles, and so the rightmost pixels were mostly white in the early rows but increasingly black as the triangles widened towards the bottom of the image. It turns out that if the end of the line is white the print head returns back home early, and so the one second delay was enough when the print head was skipping the end of the line but not quite enough when it had to travel the full distance back to the left edge. Extending the delay to two seconds provides more than enough time for the carriage to return.</p> <p>When it comes to sending the actual bitmap data to the printer only a simple modification is required:</p> <div class="source"><pre>ram:2b4c 3e 1b LD A,0x1b ram:2b4e cd c6 2b CALL DUMPWRCH ram:2b51 3e 4c LD A,'L' ; Change to 'K': ?&amp;2B52=75 ram:2b53 cd c6 2b CALL DUMPWRCH ram:2b56 3e 00 LD A,0 ram:2b58 cd c6 2b CALL DUMPWRCH ram:2b5b 3e 03 LD A,3 ; Change to 2: ?&amp;2B5C=2 ram:2b5d cd c6 2b CALL DUMPWRCH</pre></div> <p>Instead of <tt>ESC&nbsp;L</tt> with an argument of 768 bytes (&amp;0300) we need to send <tt>ESC&nbsp;K</tt> with an argument of 512 bytes (&amp;0200). The code will still try to send 768 bytes by repeating each column of the 256-pixel wide image three times, so instead we need to only send each column twice:</p> <div class="source"><pre>ram:2b7c cd c6 2b CALL DUMPWRCH ram:2b7f cd c6 2b CALL DUMPWRCH ram:2b82 cd c6 2b CALL DUMPWRCH ; Change to CALL &lt;dummy&gt;: ?&amp;2B83=&amp;B4 </pre></div> <p>The final <tt>CALL</tt> could be replaced by three <tt>NOP</tt> bytes but rather than do that the address of the target is patched to &amp;2BB4. This address contains a <tt>RET</tt> instruction as it's the final instruction of a nearby routine so effectively turns the <tt>CALL</tt> into a <tt>NOP</tt>.</p> <p>This completes the patch itself; the only thing needed to do is to wrap it up into a neat installer. Here is the result of that, in BBC BASIC:</p> <div class="source"><pre> 10 REM Serial&nbsp;8056 Patch for Z88 BASIC 20 C%=0:FORA%=&amp;2B03TO&amp;2BF6:C%=C%+?A%:NEXT 30 IFC%=&amp;5BF1PRINT"Patch already applied.":END 40 IFC%&lt;&gt;&amp;5BB9PRINT"Please load Z88PATCH.BBC first.":END 50 READA%,V%:REPEATA%?&amp;2B00=V%:READA%,V%:UNTILA%&lt;0 60 PRINT"Patch applied: use CALL 11011 to print.":END 70 DATA&amp;B6,13,&amp;BB,27,&amp;C0,50 80 DATA&amp;34,13,&amp;39,10,&amp;3D,1,&amp;3E,200,&amp;3F,0,&amp;40,231,&amp;41,45 90 DATA&amp;43,27,&amp;47,62,&amp;48,49,&amp;49,205,&amp;4A,198,&amp;4B,43 100 DATA&amp;52,75,&amp;5C,2,&amp;83,180,-1,0</pre></div> <p>Line 20 first calculates a checksum of the area targeted by the patch, which is then checked in lines 30 and 40 for two known states: Serial&nbsp;8056 patch already applied and Z88PATCH loaded but Serial&nbsp;8056 patch not applied. Line 50 reads the patch data itself (stored in lines 70 to 100) which is made up of addresses and patch value pairs; as all bytes to patch appear in the &amp;2Bxx address range only the least significant byte of the address is stored.</p> <p>In summary, if you have a Serial&nbsp;8056 and a Cambridge&nbsp;Z88 and wish to print graphics from BBC BASIC you may find <a href="http://benryves.com/bin/z88/8056Z88.zip">the Serial&nbsp;8056 for Z88 patch</a> useful. You will also need the <a href="https://www.bbcbasic.co.uk/bbcbasic/z88patch.html" rel="external">Z88 BASIC Patch</a> as a starting point.</p> Sat, 29 Nov 2025 14:27:04 +0000 1-Wire interfacing with the Cambridge Z88 http://benryves.com/journal/3763194 http://benryves.com/journal/3763194 <p>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.</p> <p>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.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/bin/z88_1wire/assorted-parts.jpg" class="lightbox" rel="external"><img src="http://benryves.com/bin/z88_1wire/assorted-parts.thumb.jpg" alt="Photo of various 1-Wire devices around a Z88 computer which has a 1-Wire interface adaptor plugged into its serial port" width="500" height="375" /></a><br /><small>A selection of 1-Wire devices and a 1-Wire interface adaptor plugged into a Z88 computer</small></div> <p>I'd had some limited experience working with the 1-Wire bus as part of my version of the <a href="http://benryves.com/journal/3632205">Superprobe</a> but now that I had a collection of temperature sensors I thought it might be worth revisiting, this time on the Cambridge Z88.</p> <h3>1-Wire adaptor for the Z88 serial port</h3> <p>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.</p> <p>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 <tt>ITX</tt> (3) in the <tt>TXC</tt> (&amp;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.</p> <p>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.</p> <p>The state of the RXD line can be read directly via bit <tt>RXD</tt> (4) in the <tt>RXE</tt> (&amp;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.</p> <p>The circuit for the adaptor, including the 4.7K pull-up resistor, appears as follows:</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/bin/z88_1wire/interface-circuit.png" alt="Circuit diagram for the Z88 to 1-Wire interface circuit" width="256" height="152" /></div> <p>This can be tested in a BASIC program. To determine the input state we can read from the <tt>RXE</tt> port register (&amp;E1) and check the state of the <tt>RXD</tt> bit (4):</p> <div class="source"><pre>10 RXE=&amp;E1:M_RXERXD=&amp;10 20 REPEAT 30 PRINT ~(GET(RXE) AND M_RXERXD) 40 UNTIL FALSE</pre></div> <p>The mask value <tt>M_RXERXD</tt> is specified as 2<sup>4</sup>=&amp;10 to correspond to the bit four. When run this program displays &amp;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).</p> <p>To change the output state we need to write to bit <tt>ITX</tt> (3) of the <tt>TXC</tt> register (&amp;E4). However, when writing to the hardware port we only want to change that bit and leave the others alone. The <tt>TXC</tt> 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 &amp;04E4 and this can be read with the <tt>?</tt> indirection operator:</p> <div class="source"><pre>10 TXC=&amp;E4:M_TXCITX=&amp;08 20 SC=&amp;400 30 TXC_OLD=SC?TXC 40 PUT TXC,TXC_OLD OR M_TXCITX 50 IF INKEY(100) 60 PUT TXC,TXC_OLD</pre></div> <p>The above program reads the old state of the <tt>TXC</tt> port from the soft copy, ORs it with the mask of the <tt>ITX</tt> bit (2<sup>3</sup>=&amp;08) and then outputs that to the <tt>TXC</tt> 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 <tt>TXC</tt> port to release the 1-Wire bus.</p> <p>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.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/bin/z88_1wire/z88-adaptor-inside.jpg" class="lightbox" rel="external"><img src="http://benryves.com/bin/z88_1wire/z88-adaptor-inside.thumb.jpg" alt="Photo of the assembled 1-Wire interface for the Z88" width="500" height="375" /></a></div> <p>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.</p> <h3>Bit-level protocol</h3> <p>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 <a href="https://ww1.microchip.com/downloads/en/appnotes/01199a.pdf" rel="external" rel="external">Microchip's AN1199, <em>1-Wire Communication with PIC Microcontroller</em></a>:</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/bin/z88_1wire/protocol-timing.png" alt="1-Wire protocol timing information diagram from Microchip AN1199" width="700" height="690" /></div> <p>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:</p> <ul> <li>Master drives bus low</li> <li>Delay 480μs</li> <li>Master releases bus high</li> <li>Delay 70μs</li> <li>Sample bus state: if high, no peripheral devices present, if low at least one device present.</li> <li>Delay 410μs</li> </ul> <p>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.</p> <p>To send a 0 bit:</p> <ul> <li>Master drives bus low</li> <li>Delay 60μs</li> <li>Master releases bus high</li> <li>Delay 10μs</li> </ul> <p>To send a 1 bit:</p> <ul> <li>Master drives bus low</li> <li>Delay 6μs</li> <li>Master releases bus high</li> <li>Delay 64μs</li> </ul> <p>Bytes are transferred as eight individual bits, least-significant bit first. The protocol is also tolerant of large delays between individual bits.</p> <p>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:</p> <ul> <li>Master drives bus low</li> <li>Delay 6μs</li> <li>Master releases bus high</li> <li>Delay 9μs</li> <li>Sample bus state to read data bit from peripheral</li> <li>Delay 55μs</li> </ul> <p>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.</p> <h3>Software choice for the Z88</h3> <p>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.</p> <p>When you <tt>CALL</tt> an assembly routine from BASIC the Z80's registers are initialised to the values of the corresponding static variables, for example <tt>A</tt> is set to <tt>A%</tt>, <tt>H</tt> to <tt>H%</tt>, <tt>L</tt> to <tt>L%</tt> etc. You can't return a value directly &ndash; for that you'd need <tt>USR</tt> &ndash; however it's a bit easier to just store the return value in memory and retrieve that from BASIC after the <tt>CALL</tt> returns.</p> <p>A rough starting point for the 1-Wire program is as follows:</p> <div class="source"><pre> 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=&amp;E1:M_RXERXD=&amp;10 110 TXC=&amp;E4:M_TXCITX=&amp;08 120 SC=&amp;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</pre></div> <p>The first few lines are going to be where our BASIC program is. This calls the procedure <tt>PROC_1W_INIT</tt> which will set things up by assembling any required Z80 code. It then calls <tt>FN_1W_RESET</tt> which is a function that resets the 1-Wire bus and checks to see if any devices assert their presence.</p> <p><tt>PROC_1W_INIT</tt> 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 (<tt>ow_buf</tt>) which will be used to store data due to be returned by the assembly routines. The <tt>ow_reset</tt> assembly routine then follows &ndash; 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 <tt>ow_buf</tt>), then delays another 410μs.</p> <p>The delay loops are simple <tt>DJNZ</tt> loops with <tt>B</tt> 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.</p> <p>The <tt>ow_reset</tt> routine has been written so that following a successful presence check <tt>ow_buf</tt> should contain 0, and if there is a problem it will contain a non-zero value. This is used by the <tt>FN_1W_RESET</tt> wrapper function which just calls <tt>ow_reset</tt> and returns <tt>TRUE</tt> if <tt>ow_buf</tt> is zero afterwards.</p> <p>If you run the program you should see that the program will display 0 (<tt>FALSE</tt>) on the screen until a 1-Wire device is connected to the adaptor, at which point it will display -1 (<tt>TRUE</tt>) 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.</p> <h3>Sending and receiving bits and bytes</h3> <p>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 &ndash; just hold the line low for 60μs then release it back high for 10μs. This can be implemented as follows:</p> <div class="source"><pre>.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</pre></div> <p>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:</p> <div class="source"><pre>.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</pre></div> <p>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 <tt>ow_buf</tt> transfer buffer, then waits 55μs.</p> <p>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 <tt>ow_put_1</tt> routine already handles updating the <tt>ow_buf</tt> with each received bit, so a byte receiving routine can be put together by just calling <tt>ow_put_1</tt> eight times in a loop:</p> <div class="source"><pre>.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</pre></div> <p>A send routine can be put together with a similar loop that shifts out the bit to send and then calls either the <tt>ow_put_0</tt> or <tt>ow_put_1</tt> 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 <tt>ow_put_carry</tt> routine that sends the bit stored in the carry flag makes this a bit easier, e.g.</p> <div class="source"><pre>.ow_put_carry JR C,ow_put_1 JR ow_put_0</pre></div> <p>...which will be called by the <tt>ow_put_byte routine</tt>, as follows:</p> <div class="source"><pre>.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</pre></div> <p>It is also quite useful to be able to send or receive blocks of data at once &ndash; for example, sending or receiving the 64-bit device IDs requires sending or receiving 8 bytes of data at a time. To complement <tt>ow_get_byte</tt> and <tt>ow_put_byte</tt> we can write <tt>ow_get_bytes</tt> and <tt>ow_put_bytes</tt> routines to send or receive the block of data addressed by <tt>HL</tt>, length <tt>BC</tt>:</p> <div class="source"><pre>.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</pre></div> <p>All of these can now be wrapped up as procedures or functions so they can be more easily used from a BASIC program:</p> <div class="source"><pre>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</pre></div> <p>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 (<tt>L</tt> for <tt>HL</tt>, <tt>C</tt> for <tt>BC</tt>) and the most-significant register (<tt>H</tt> or <tt>B</tt>) is populated by dividing the value by 256.</p> <p>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 &amp;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.</p> <div class="source"><pre> 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(&amp;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=&amp;E1:M_RXERXD=&amp;10 140 TXC=&amp;E4:M_TXCITX=&amp;08 150 SC=&amp;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&gt;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</pre></div> <p>When connected to an iButton fob the program prints</p> <p><pre> 55 0 0 1 A0 1A 57 1</pre></p> <p>...which matches the ID printed on it.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/bin/z88_1wire/tm1990a.jpg" class="lightbox" rel="external"><img src="http://benryves.com/bin/z88_1wire/tm1990a.thumb.jpg" alt="Photo of a TM1990A iButton on a fob with a probe to read it with" width="400" height="400" /></a><br /><small>An example of a TM1990A iButton fob (middle) along with the probe used to read it (top)</small></div> <p>When connected to a DS18B20 temperature sensor the program prints <br /> <pre> B9 0 0 1 D1 97 5D 28</pre></p> <p>The least significant byte of the 64-bit ID is the family code &ndash; &amp;01 for the iButton fob indicates it's a "silicon serial number" type device and &amp;28 for the DS18B20 indicates it's a "programmable resolution digital thermometer".</p> <p>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:</p> <p><pre> 11 0 0 1 80 12 55 0</pre></p> <p>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 &amp;55 AND &amp;B9 which gives us the &amp;11 we see. Fortunately that most-significant byte does give us a good opportunity to detect such invalid data!</p> <h3>Error detection with a CRC</h3> <p>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 <a href="https://www.analog.com/en/technical-articles/understanding-and-using-cyclic-redundancy-checks-with-maxim-1wire-and-ibutton-products.html" rel="external" rel="external">Understanding and Using Cyclic Redundancy Checks with Maxim 1-Wire and iButton Products</a> however for our purposes a Z80 implementation can be written as follows:</p> <div class="source"><pre>.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,&amp;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</pre></div> <p><tt>ow_crc</tt> updates the current calculated CRC value (stored in <tt>ow_buf</tt>) with the next data byte from the accumulator. <tt>ow_crc_block</tt> calculates the CRC for a block of data pointed to by <tt>HL</tt>, length <tt>BC</tt>, using the <tt>ow_crc</tt> 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:</p> <div class="source"><pre>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%)</pre></div> <p>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:</p> <div class="source"><pre> 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(&amp;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=&amp;E1:M_RXERXD=&amp;10 170 TXC=&amp;E4:M_TXCITX=&amp;08 180 SC=&amp;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,&amp;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&gt;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%&gt;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("&amp;"+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%)</pre></div> <p>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 &ndash; a function, <tt>FN_1W_ID$(ID)</tt>, which turns a block of ID data bytes into a string and a procedure, <tt>PROC_1W_ID$(ID,ID$)</tt>, which does the opposite.</p> <h3>Enumerating the 1-Wire bus</h3> <p>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.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/bin/z88_1wire/multi-device-adaptor.jpg" class="lightbox" rel="external"><img src="http://benryves.com/bin/z88_1wire/multi-device-adaptor.thumb.jpg" alt="Photo of a circuit board used to provide multiple sockets to connect more than one 1-Wire device at a time" width="500" height="375" /></a><br /><small>Each socket is wired in parallel to allow multiple 1-Wire devices to be connected to the Z88</small></div> <p>To start the search, the master sends either the normal search command &amp;F0 or the alarm/conditional search command &amp;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 &amp;F0.</p> <p>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 &ndash; 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.</p> <p>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.</p> <p>The full procedure for enumerating the bus is more explicitly described in the app note <a href="https://www.analog.com/en/app-notes/1wire-search-algorithm.html" rel="external" rel="external">1-Wire Search Algorithm</a>, and can be implemented with the following Z80 assembly code:</p> <div class="source"><pre>.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</pre></div> <p>A pair of BASIC wrappers can make using this search routine a bit easier:</p> <div class="source"><pre>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 &amp;F0 for all devices, &amp;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</pre></div> <p>The "reset" routine just sets the last bit conflict index to -1 (<tt>TRUE</tt>=-1) and <tt>FN_1W_SEARCH</tt> will search based on the search type (&amp;F0 for all devices, &amp;EC for alarming devices only), the current ID and last conflict index and will return <tt>TRUE</tt> if an ID was found or <tt>FALSE</tt> if no more IDs were found.</p> <p>A snippet of code that enumerates all devices on the bus and displays their IDs is as follows:</p> <div class="source"><pre>PROC_1W_SEARCH_RESET REPEAT F%=FN_1W_SEARCH(&amp;F0,ID):IF F% PRINT FN_1W_ID$(ID) UNTIL F%=FALSE</pre></div> <h3>Reading temperature sensors</h3> <p>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.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/bin/z88_1wire/ds18b20.jpg" class="lightbox" rel="external"><img src="http://benryves.com/bin/z88_1wire/ds18b20.thumb.jpg" alt="Photo of two DS18B20 sensors, one in a TO-92 package and the other in a cabled probe" width="400" height="400" /></a><br /><small>A DS18B20 temperature sensor in a TO-92 package and another in a cabled probe</small></div> <p>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 (&amp;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.</p> <p>Due to the large delay when waiting for the sensors to handle the "Convert T" command it is easiest to send the command to <em>all</em> devices on the network rather than to each one individually. This can be done by first sending the "Skip ROM" command (&amp;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 (&amp;44). The process to tell all devices to perform a temperature conversion is as follows:</p> <div class="source"><pre>IF FN_1W_RESET=FALSE PRINT "No devices found":END REM Start temperature conversion PROC_1W_PUT(&amp;CC):REM Skip ROM PROC_1W_PUT(&amp;44):REM Convert T T=TIME:IF INKEY(75)&gt;TRUE REPEAT:UNTIL TIME&gt;T+75:REM Delay 750ms</pre></div> <p><tt>INKEY(75)</tt> 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.</p> <p>After this, all of the devices on the network are enumerated as before:</p> <div class="source"><pre>REM Search for all temperature sensors on the bus and display their readings PROC_1W_SEARCH_RESET REPEAT F%=FN_1W_SEARCH(&amp;F0,ID) IF F% PROC_1W_PRINT_TEMP(ID) UNTIL F%=FALSE</pre></div> <p><tt>PROC_1W_PRINT_TEMP</tt> should check to see whether the device ID corresponds to a temperature sensor (its family code, the least-significant byte, should be &amp;28) and if so it should retrieve the temperature value and print it:</p> <div class="source"><pre>REM Print a single sensor's reading DEFPROC_1W_PRINT_TEMP(ID) LOCAL T IF ID?0&lt;&gt;&amp;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 @%=&amp;20409:PRINT MID$(FN_1W_ID$(ID),3,12);":",T;" deg C":@%=&amp;90A ENDPROC</pre></div> <br /> <tt>@%</tt> controls the way numbers are printed &ndash; 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.</p> <p><tt>FN_1W_READ_TEMP(ID)</tt> 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 (&amp;55) followed by the 64-bit device ID. After this the scratchpad RAM can be read by sending the "read scratchpad" command (&amp;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:</p> <div class="source"><pre>REM Retrieve a single sensor's reading DEFFN_1W_READ_TEMP(ID) LOCAL T IF FN_1W_RESET=FALSE =-999 PROC_1W_PUT(&amp;55):PROC_1W_PUTS(ID,8):REM Match ROM PROC_1W_PUT(&amp;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</pre></div> <p>The final line converts the reading to &deg;C. This is a signed 16-bit value stored in the first two bytes of the scratchpad memory. BBC BASIC's <tt>!</tt> 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&deg;C.</p> <p>A <a href="http://benryves.com/bin/z88_1wire/ONEWIRE.BBC">complete demo program</a> listing is shown below. Choosing option "3) Show DS18B20 temperatures" will show the temperatures of any connected DS18B20 temperature sensors.</p> <div class="source"><pre> 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 "&lt;Press any key&gt;"; 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%&gt;0 AND M%&lt;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(&amp;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(&amp;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(&amp;CC):REM Skip ROM 550 PROC_1W_PUT(&amp;44):REM Convert T 560 T=TIME:IF INKEY(75)&gt;TRUE REPEAT:UNTIL TIME&gt;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(&amp;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&lt;&gt;&amp;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 @%=&amp;20409:PRINT MID$(FN_1W_ID$(ID),3,12);":",T;" deg C":@%=&amp;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(&amp;55):PROC_1W_PUTS(ID,8):REM Match ROM 750 PROC_1W_PUT(&amp;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=&amp;E1:M_RXERXD=&amp;10 840 TXC=&amp;E4:M_TXCITX=&amp;08 850 SC=&amp;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,&amp;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&lt;&gt;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 &amp;F0 for all devices, &amp;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%&gt;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("&amp;"+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%)</pre></div> <h3>Temperature logger</h3> <p>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 <tt>PROC_1W_LOG_TEMP</tt> procedure that handles logging the data to a file instead of printing it on the display:</p> <div class="source"><pre>REM Log a single sensor's reading DEFPROC_1W_LOG_TEMP(ID) LOCAL T IF ID?0&lt;&gt;&amp;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</pre></div> <p>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.</p> <p>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 <tt>TIME$</tt> function into "YYYY-MM-DD&nbsp;hh:mm:ss" format. This is handled by the following three functions, <tt>FN_DATE</tt> (which extracts and reformats the date component into YYYY-MM-DD format), <tt>FN_TIME</tt> (which extracts the time component into hh:mm:ss format) and <tt>FN_DATETIME</tt> which glues the date and time back together with a space in the middle:</p> <div class="source"><pre>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%&gt;1 R$="-"+R$ IF C% R$=STR$(V%)+R$ IF V%&lt;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$)</pre></div> <p>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, <tt>OS_Off</tt>, which can be invoked from BASIC as follows:</p> <div class="source"><pre>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,(&amp;1FFE):PUSH HL RST &amp;20:DEFW &amp;EC06:REM OS_Off POP HL:LD SP,HL:RET:] ENDPROC DEFPROC_SWITCH_OFF:CALL switch_off:ENDPROC</pre></div> <p>As with the 1-Wire assembly routines you must first call an initialisation procedure (<tt>PROC_SWITCH_OFF_INIT</tt>) to assemble the routine before calling it with <tt>PROC_SWITCH_OFF</tt>. The actual <tt>OS_Off</tt> call is the <tt>RST&nbsp;&amp;20H:DEFW&nbsp;&amp;EC06</tt> 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 &ndash; 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.</p> <p>The <a href="http://benryves.com/bin/z88_1wire/TEMPLOG.BBC">complete temperature-logging program</a> is now shown below:</p> <div class="source"><pre> 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(&amp;CC):REM Skip ROM 110 PROC_1W_PUT(&amp;44):REM Convert T 120 T=TIME:IF INKEY(75)&gt;TRUE REPEAT:UNTIL TIME&gt;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(&amp;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&lt;&gt;&amp;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(&amp;55):PROC_1W_PUTS(ID,8):REM Match ROM 400 PROC_1W_PUT(&amp;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%&gt;1 R$="-"+R$ 510 IF C% R$=STR$(V%)+R$ IF V%&lt;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,(&amp;1FFE):PUSH HL 640 RST &amp;20:DEFW &amp;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=&amp;E1:M_RXERXD=&amp;10 740 TXC=&amp;E4:M_TXCITX=&amp;08 750 SC=&amp;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,&amp;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&lt;&gt;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 &amp;F0 for all devices, &amp;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%&gt;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("&amp;"+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%)</pre></div> <p>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 <tt>#BRUN"TEMPLOG.BBC"~E</tt> it will press □+B to switch to BASIC (<tt>#B</tt>), type in <tt>RUN"TEMPLOG"</tt> and then press Enter (<tt>~E</tt>).</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/bin/z88_1wire/templog-alarm.png" alt="Screenshot of the Z88 Alarm popdown being configured to run the task" width="640" height="64" /></div> <p>Setting up the alarm this way each time can be a bit tedious, so to make things easier <a href="http://benryves.com/bin/z88_1wire/TEMPLOG.CLI">here's a CLI file</a> that can be used to set up the alarm:</p> <div class="source"><pre>.;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</pre></div> <p>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 <tt>#A</tt> presses □+A to enter the Alarm pop-down, <tt>~R</tt>, <tt>~D</tt> or <tt>~U</tt> move the cursor right, down or up and where we need to type literal <tt>#</tt> or <tt>~</tt> signs they are doubled up (<tt>##</tt> or <tt>~~</tt>). 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.</p> <p>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.</p> <p>The temperature logs in the CSV files can be used to generate a chart like the following:</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/bin/z88_1wire/templog-chart.png" alt="Chart of the three temperature sensors logged over a 24-hour period" width="682" height="445" /></div> <p>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 &ndash; the sensor is near where I am sitting and my desktop computer, which is likely contributing some heat.</p> <h3>Conclusion</h3> <p>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.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/bin/z88_1wire/z88-demo-program.jpg" class="lightbox" rel="external"><img src="http://benryves.com/bin/z88_1wire/z88-demo-program.thumb.jpg" alt="Photo of a Z88 running connected to many 1-wire devices and running the demo program" width="768" height="432" /></a></div> <p>The files accompanying this post can be downloaded below:</p> <ul> <li><a href="http://benryves.com/bin/z88_1wire/ONEWIRE.BBC">ONEWIRE.BBC</a> &ndash; 1-Wire demonstration program.</li> <li><a href="http://benryves.com/bin/z88_1wire/TEMPLOG.BBC">TEMPLOG.BBC</a> and <a href="http://benryves.com/bin/z88_1wire/TEMPLOG.CLI">TEMPLOG.CLI</a> &ndash; Temperature logging program and CLI file to set up the alarm.</li> <li><a href="http://benryves.com/bin/z88_1wire/templogs.zip">templogs.zip</a> and <a href="http://benryves.com/bin/z88_1wire/templogs.xlsx">templogs.xlsx</a> &ndash; Sample data captured by the temperature logging program.</li> </ul> Sun, 17 Dec 2023 02:11:33 +0000 Adding 11KB of RAM to a CP/M 3 system with a single NAND gate chip http://benryves.com/journal/3763192 http://benryves.com/journal/3763192 <p>It's been quite a while since I posted about my <a href="http://benryves.com/journal/tags/Z80_computer">Z80 Computer</a> project. This is a home-made Z80 computer I built back in 2010 that features a 10MHz Z80 CPU with 64KB RAM that runs CP/M 3. It can drive an internal LCD, TV or VGA monitor at 320x240 (monochrome only) and unfortunately is a project I was never too happy with due to several compromises I had to make in its design &ndash; though at the time I was happy enough I got it to work at all! The video output was limited by both my choice to use an internal graphical LCD and the limitations of the dsPIC33F I chose to use to drive it and the software was all a bit half-baked. I could run the generic CP/M version of BBC BASIC on it, but this lacks graphics and sound support, for example.</p> <p>More recently my work on <a href="http://benryves.com/journal/tags/BBC_BASIC">adapting BBC BASIC to the Sega Master System</a> had reignited my interest in 8-bit programming, though that too was imperfect due to the limitations of the Master System's VDP. I was further encouraged by coming third in the <a href="https://www.reddit.com/r/retrobattlestations/comments/14hhq0l/retro_but_not_vintage_homebrew_z80_computer_that/" rel="external">"Retro not Vintage" competition on /r/retrobattlestations</a>, though I'm not sure I was quite worthy of a podium finish.</p> <p>With this in mind I started work on improving the computer. I replaced the existing dsPIC33F VDP with a new one based around a dsPIC33E. This newer microcontroller has 32KB of RAM and can run at up to 70 MIPS, a big upgrade from the previous 16KB RAM and 40 MIPS. This provides me with enough video RAM to store the largest BBC Micro screen mode frame buffer (20KB) as well as the necessary CPU grunt to look up pixel data from colour palettes and output it to the screen. I've implemented all eight of the standard BBC Micro screen modes, from the high-res 640x256 (in two colours) <tt>MODE&nbsp;0</tt> to the low-res 160x256 (in sixteen colours) <tt>MODE&nbsp;2</tt> along with the Teletext-compatible <tt>MODE&nbsp;7</tt>. This is all controlled via a BBC Micro-compatible <tt>VDU</tt> driver and the results all seem quite faithful with no real compromises.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/bin/z180/2023.08.29.Master.jpg" class="lightbox" rel="external"><img src="http://benryves.com/bin/z180/2023.08.29.Master.Thumb.jpg" alt="'The BBC Master Series Microcomputer' Welcome tape title screen" width="360" height="360" /></a> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <a href="http://benryves.com/bin/z180/2023.08.29.Ebony.jpg" class="lightbox" rel="external"><img src="http://benryves.com/bin/z180/2023.08.29.Ebony.Thumb.jpg" alt="'Ebony Castle' BBC Micro game" width="360" height="360" /></a><br /> <br /> <a href="http://benryves.com/bin/z180/2023.08.29.Spooks.jpg" class="lightbox" rel="external"><img src="http://benryves.com/bin/z180/2023.08.29.Spooks.Thumb.jpg" alt="'Spooks'n'Spirits' BBC Micro game" width="360" height="360" /></a> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <a href="http://benryves.com/bin/z180/2023.08.29.TechnoZone.jpg" class="lightbox" rel="external"><img src="http://benryves.com/bin/z180/2023.08.29.TechnoZone.Thumb.jpg" alt="'Techno Zone' joke advert from Digitiser" width="360" height="360" /></a></div> <p>There was even enough CPU power left over on the microcontroller to implement BBC Micro-compatible <tt>SOUND</tt> and <tt>ENVELOPE</tt>, and with <a href="https://github.com/jblang/bbcbasic-z80" rel="external">the source code for the CP/M version of BBC BASIC</a> having been released since I last worked on the project it made it much easier to add all of the graphics and sound routines into the version of BBC BASIC specific to my computer.</p> <p>To get an idea of what the computer is like to use, I recorded <a href="https://www.youtube.com/watch?v=ZAWAywcSFeY" rel="external">a little demo video here</a>. However, this is not really what I wanted to write about in this post &ndash; I wanted to cover an easy way to free up some RAM by implementing banked CP/M 3.</p> <h2>Non-banked versus banked CP/M</h2> <p>I chose CP/M 3 as the OS for my computer instead of CP/M 2 as I'm using an SD card for storage and CP/M 3 has native support for disk sector sizes that do not directly match the file record size and it will handle the blocking/unblocking for you (CP/M's file records are 128 bytes long, SD card sectors are 512 bytes long). One other nice feature of CP/M 3 is the existence of a "banked" version which allows it to run on systems with more than 64KB of RAM. As far as user programs are concerned they still run in a flat 64KB memory space, however the OS can move certain parts of itself as well as disk and directory buffers into a separate memory bank where they are only accessed when needed, freeing up space in the "transient program area" (TPA). As well as more memory for user programs the banked version provides a much improved line editor when typing at the command-line, password protection of files and more descriptive error messages.</p> <p>Naturally, when I read about this I thought it would be an obvious choice for my computer. As it is, I'm using a 128KB RAM chip but have tied A16 low as I didn't have any kind of MMU or bank-switching hardware setup (32KB and 128KB RAM chips are available in abundance, 64KB ones less so, and using a 128KB chip with the address line tied low involved a lot less soldering than two separate 32KB RAM chips). I did have an emulator where I could try to prototype the hardware changes to support a banked CP/M 3, however I was not able to get a banked version of the OS built and working so gave up &ndash; after all, I had a 49KB TPA, which seemed like it would be good enough.</p> <p>With the other improvements to the computer recently I thought it worth reinvestigating. I did a bit of hunting to see if I could find any recommendations for a simple setup but most of what I could find ended up being a lot more complicated than what I was really looking for. After a bit more experimentation I was able to end up with a banked version of CP/M running on my computer and all I needed was a single NAND gate chip.</p> <h2>Memory requirements for banked CP/M</h2> <p>The memory layout of banked CP/M is actually quite a bit simpler than a lot of the threads I could find online seemed to make out. All you really need is a shared common area at the top of memory that will always be accessible regardless of the current state of the selected bank, and memory below that which can be switched between multiple banks. When booting the computer bank 0 will be selected, so both the common (resident) and banked parts can be copied to memory, and then bank 1 will be swapped in to provide the large TPA. </p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/bin/z180/2023.08.29.Banked.png" alt="Memory layout of banked CP/M" width="640" height="433" /></div> <p>In my case, as I'm using a 128KB RAM chip, I will use A16 as the bank selection bit. When low this will provide access to the lower 64KB RAM on the chip, when high it will provide access to the upper 64KB RAM. To implement the common area at the top of memory, you then just need to check to see if the address is above the boundary between banked and common memory and if so to force A16 either high or low (it doesn't matter which, as long as it's consistent) so that when the address is in the common area the same bank will be accessed, regardless of the state of the bank selection bit.</p> <h2>Bank switching with simple logic</h2> <p>A simple way to implement a common area in upper memory is with AND (to detect the high address) and OR (to force the A16 high if it's a high address) logic, like this:</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/bin/z180/2023.08.29.BankSwitch.ANDOR.png" alt="Simple bank-switching hardware using a 4-input AND gate and a 2-input OR gate" width="400" height="120" /></div> <p>Here we use a 4-input AND gate to detect any memory address in the top 4KB of the chip (address lines A12 to A15 will go high at %1111000000000000 which gives a common region of $F000 to $FFFF). If that's the case, then the output of the 4-input AND gate will be high, which when ORed with the bank selection bit will force A16 high whenever we're in the common memory area. If we're below the common memory area then the value of the bank selection bit will pass through directly to A16, allowing us to bank switch the lower area of memory. Or, to summarise in a truth table:<br /> <table class="basic centred"> <thead> <tr> <th colspan="5">In</th> <th>Out</th> </tr> <tr> <th>A12</th> <th>A13</th> <th>A14</th> <th>A15</th> <th>BANK</th> <th>A16</th> </tr> </thead> <tbody> <tr> <th>1</th> <th>1</th> <th>1</th> <th>1</th> <td>x</td> <td>1</td> </tr> <tr> <th>0</th> <td>x</td> <td>x</td> <td>x</td> <th>0</th> <td>0</td> </tr> <tr> <th>0</th> <td>x</td> <td>x</td> <td>x</td> <th>1</th> <td>1</td> </tr> <tr> <td>x</td> <th>0</th> <td>x</td> <td>x</td> <th>0</th> <td>0</td> </tr> <tr> <td>x</td> <th>0</th> <td>x</td> <td>x</td> <th>1</th> <td>1</td> </tr> <tr> <td>x</td> <td>x</td> <th>0</th> <td>x</td> <th>0</th> <td>0</td> </tr> <tr> <td>x</td> <td>x</td> <th>0</th> <td>x</td> <th>1</th> <td>1</td> </tr> <tr> <td>x</td> <td>x</td> <td>x</td> <th>0</th> <th>0</th> <td>0</td> </tr> <tr> <td>x</td> <td>x</td> <td>x</td> <th>0</th> <th>1</th> <td>1</td> </tr> </tbody> </table></p> <p>However, it would be easier if we could implement this on a single chip. A 4x 2-input NAND gate chip (such as the SN74ALS00AN) should do the job when wired up as follows:</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/bin/z180/2023.08.29.BankSwitch.NAND.png" alt="Simple bank-switching hardware using a four 2-input NAND gates" width="500" height="120" /></div> <p>The truth table is a little different this time around:</p> <p><table class="basic centred"> <thead> <tr> <th colspan="4">In</th> <th>Out</th> </tr> <tr> <th>A13</th> <th>A14</th> <th>A15</th> <th>BANK</th> <th>A16</th> </tr> </thead> <tbody> <tr> <th>1</th> <th>1</th> <th>1</th> <td>x</td> <td>1</td> </tr> <tr> <th>0</th> <td>x</td> <td>x</td> <th>0</th> <td>1</td> </tr> <tr> <th>0</th> <td>x</td> <td>x</td> <th>1</th> <td>0</td> </tr> <tr> <td>x</td> <th>0</th> <td>x</td> <th>0</th> <td>1</td> </tr> <tr> <td>x</td> <th>0</th> <td>x</td> <th>1</th> <td>0</td> </tr> <tr> <td>x</td> <td>x</td> <th>0</th> <th>0</th> <td>1</td> </tr> <tr> <td>x</td> <td>x</td> <th>0</th> <th>1</th> <td>0</td> </tr> </tbody> </table> <br /> When accessing the banked region of memory A16 is the inverse of the bank selection bit. This doesn't matter, though, as long as there's a consistent mapping between logical addresses and the physical RAM addresses it will work even if it's "backwards". There's also one fewer address line, which means that the common area now runs from %1110000000000000 = $E000 to $FFFF, providing a common area of 8KB. In practice I didn't find this made a difference to the amount of memory available in the TPA; whether the common area was 4KB, 8KB or 16KB I was able to bring the TPA up to 60KB (from 49KB in the non-banked system), though it does eat into the amount of memory available on page 0 for disk and directory buffers. As I'm loading from an SD card (which is much faster than the floppy discs of yore) the reduced buffer space is less of a concern to me.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/bin/z180/2023.08.29.Mapper.jpg" class="lightbox" rel="external"><img src="http://benryves.com/bin/z180/2023.08.29.Mapper.Thumb.jpg" alt="Photo of a NAND gate chip installed in the computer" width="500" height="375" /></a></div> <p>Fortunately there was enough space inside the computer (and a single remaining pin on the I/O controller to act thas bank selection bit) to add the NAND chip and drive A16. At last I have access to 120KB of my 128KB RAM chip... but what about the software?</p> <h2>Building a banked version of CP/M</h2> <p>I will start with the assumption that you have been able to build a non-banked version of CP/M 3 and got that running on your computer, as there is a lot less that can go wrong when doing so. Once you've got that working there's not too much to add to your BIOS to make it support banking, however I did run into a few issues with missing files and some misinterpretation of how things should work until I was able to get it working.</p> <p>I used the <a href="http://www.cpm.z80.de/source.html" rel="external">"Developers Build Directory for CP/M 3" from <em>The Unofficial CP/M Web site</em></a> as my source for CP/M 3. This contains the <tt>GENCPM</tt> tool that will be used to generate the <tt>CPM3.SYS</tt> that will need to be loaded into memory by your boot loader. In my case I get my I/O controller to copy CP/M from the SD card into memory at boot &ndash; if you've already got the non-banked version of CP/M 3 booting then you'll be familiar with this, but do pay attention to table D-1 in the CP/M 3 system guide which points out the two parts of CP/M to load &ndash; the "resident" and "banked" portions. Both parts need to be loaded on a banked system, and both need to be loaded into page 0.</p> <p>To get that far you will need to have relocatable copies of your banked BIOS (<tt>BNKBIOS3.SPR</tt>) and the BDOS (<tt>RESBDOS3.SPR</tt> and <tt>BNKBDOS3.SPR</tt>) ready to be used by <tt>GENCPM</tt>. I couldn't find a ready-made copy of these BDOS modules, but you can build them using <tt>RMAC</tt> and <tt>LINK</tt> as shown below:</p> <div class="source"><pre>RMAC RESBDOS LINK RESBDOS3=RESBDOS[OS]</pre></div> <div class="source"><pre>PIP BNKBDOS3.ASM=CPMBDOS2.ASM,CONBDOS.ASM,BDOS30.ASM RMAC BNKBDOS3 LINK BNKBDOS3=BNKBDOS3[OS]</pre></div> <p>The banked BDOS source code is split between three different source files which need to be combined with <tt>PIP</tt> first, then can be built. For the sake of completeness, if you wanted to build the non-banked <tt>BDOS3.SPR</tt> you'd use a very similar set of commands, just with <tt>CPMBDOS1.ASM</tt> instead of <tt>CPMBDOS2.ASM</tt>:</p> <div class="source"><pre>PIP BDOS3.ASM=CPMBDOS1.ASM,CONBDOS.ASM,BDOS30.ASM RMAC BDOS3 LINK BDOS3=BDOS3[OS]</pre></div> <p>The other important ingredient is your banked BIOS, <tt>BNKBIOS3.SPR</tt>. I don't get on with 8080 syntax so I assemble my <tt>BIOS3.MAC</tt> with Microsoft's <tt>M80</tt> in Z80 mode (instead of <tt>RMAC</tt>).</p> <div class="source"><pre>RMAC SCB RMAC BIOSKRNL M80 =BIOS3 LINK BNKBIOS3[B]=BIOSKRNL,BIOS3,SCB</pre></div> <p>If you had previously edited <tt>BIOSKRNL.ASM</tt> to state <tt>banked&nbsp;equ&nbsp;false</tt> remember to change it to <tt>banked&nbsp;equ&nbsp;true</tt> as well!</p> <p>The only additions you should need in your BIOS are implementations of <tt>?xmove</tt> and <tt>?bank</tt>. <tt>?bank</tt> is an easy one, and just switches to the memory bank requested in the <tt>A</tt> register. In my case I handle that just by outputting <tt>A</tt> to the I/O port that handles bank switching:</p> <div class="source"><pre>; Select Memory Bank ; Entry Parameters: A=Memory Bank ; Returned Values: None ; You must preserve or restore all registers other than the ; accumulator, A, upon exit. ?bank: if banked out (bank$select),a ; change this for what your hardware requires endif ret</pre></div><br /> (To retain compatibility with my old banked BIOS I wrap the changes in an <tt>if&nbsp;banked</tt> condition &ndash; <tt>banked&nbsp;equ&nbsp;true</tt> appears earlier in the file).</p> <p><tt>?xmove</tt> is a little more complicated &ndash; this states that the subsequent <tt>?move</tt> operation (which copies <tt>BC</tt> bytes from <tt>DE</tt> to <tt>HL</tt>) should transfer data from one memory bank to another. Note that this only affects the next <tt>?move</tt> operation; if <tt>?move</tt> is called again afterwards without <tt>?xmove</tt> then it should perform a copy within the same bank as before.</p> <p>Fortunately the inter-bank copy is limited to 128 bytes so you can simply implement this by temporarily copying the data from one bank into a 128 byte buffer in common memory, then copying the data back to the destination bank. It's not exactly efficient, but it keeps the hardware simple.</p> <div class="source"><pre>; Memory-to-Memory Block Move ; Entry Parameters: HL=Destination address ; DE=Source address ; BC=Count ; Returned Values: HL and DE must point to ; next bytes following move operation ?move: ex de,hl ldir ex de,hl ret ; Set Banks for Following MOVE ; Entry Parameters: B=destination bank ; C=source bank ; Returned Values: None ?xmove: if banked ; Store the source/destination bank numbers ld (mov$src$b),bc ; Make sure that the next call to move (via ?mov vector) uses the banked move routine. ld bc,banked$move ld (?mov+1),bc ret banked$move: ; Select source bank ld a,(mov$src$b) call ?bank ; Swap registers from CP/M to Z80 conventions ex de,hl ; Preserve destination and length push de push bc ; Copy from source to buffer ld de,mov$buf ldir ; Recover length and destination, preserve source pop bc pop de push hl ; Select destination bank ld a,(mov$dst$b) call ?bank ; Copy from buffer to destination ld hl,mov$buf ldir ; Recover source pop hl ; Swap registers from Z80 to CP/M conventions ex de,hl ; Make sure that the next call to move (via ?mov vector) uses the regular move routine. ld bc,?move ld (?mov+1),bc ret mov$src$b: db 0 mov$dst$b: db 0 mov$buf: ds 128 else ; Unbanked ret endif</pre></div> <p>This implementation works by changing the <tt>?mov</tt> vector in the <tt>BIOSKRNL</tt> to point at our <tt>banked$move</tt> routine after a request to <tt>?xmove</tt>. Once we've carried out the banked move, the original <tt>?move</tt> routine is restored to the <tt>?mov</tt> vector.</p> <p>Once you have assembled and linked your <tt>BNKBIOS3.SPR</tt>, <tt>RESBDOS3.SPR</tt> and <tt>BNKBDOS3.SPR</tt> you can use <tt>GENCPM</tt> to create your new <tt>CPM3.SYS</tt>. You'll need to answer some questions differently to support the banked system:</p> <ul><li><strong>Bank switched memory?</strong> Y.</li><li><strong>Common memory base page?</strong> E0 (if using the NAND gate circuit above &ndash; our common area starts at $E000).</li><li><strong>Number of memory segments?</strong> 1 &ndash; we have three in total (bank 0, bank 1 and common) however bank 1 and common are not included in the segment table so should be ignored here.</li><li><strong>Memory segment table base, size, bank</strong>: 01, 90, 00 (we want to keep CP/M out of the "zero page" so start the segment from $0100, CP/M 3 starts at $9100 so we have $9100-$0100=$9000 as our size, the bank number is 0).</li></ul> <p>Before being prompted for the memory segment table <tt>GENCPM</tt> will display where CP/M 3 itself is using memory so you can use that to figure out how much free space you have on your bank zero for your segment definition. However, if you enter a value that is too large <tt>GENCPM</tt> will automatically reduce the size for you.</p> <p>After this you will be prompted to create disk and directory buffers for each of your disk definitions &ndash; pay attention to available space to get an idea of how many buffers you can create, but if in doubt just allocate a single buffer for each disk/directory as prompted as that will at least get you booted, then you can experiment with larger buffers later.</p> <p>I did intentionally start my segment from $0100 instead of $0000 and this is to avoid problems with interrupts and to keep the zero page free. My computer design uses interrupts to signal to the Z80 that keys are available (for example) instead of requiring it to constantly poll the I/O controller. However, I did find that if I interrupted the CPU (e.g. by pressing a key) when it had switched over to page 0 it would hang the computer as the ISR vector had been switched out from underneath it. My ISR is in common memory and I just make sure that when the computer boots it installs its interrupt vectors in every memory bank so that it doesn't matter which is currently swapped in, it'll always find its way to the common ISR. </p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/bin/z180/2023.08.29.60KTPA.jpg" class="lightbox" rel="external"><img src="http://benryves.com/bin/z180/2023.08.29.60KTPA.Thumb.jpg" alt="Photo of the computer's boot screen showing a 60KB TPA" width="500" height="375" /></a></div> <p>After making these changes I was greeted with a 60KB TPA instead of the previous 49KB TPA &ndash; 11KB of extra memory is well worth it, and the improved line editor in CP/M 3 is another nice bonus. I did think that implementing this was going to be a nightmare, but in the end I only needed one extra NAND gate and a few easy changes to the software.</p> <p><strong>Addendum (31st August 2023)</strong>: One other change you will need to implement is to support disk operations reading from or writing to specific memory banks. I forgot to mention this earlier as it's handled by the <tt>setbnk</tt> routine inside <tt>BIOSKRNL</tt>, and that routine stores the selected DMA bank number in the <tt>@dbnk</tt> variable. When your BIOS performs a disk read or write operation it will need to preserve the current bank number, switch to the bank number in <tt>@dbnk</tt>, carry out the read or write operation, then restore the previous bank number.</p> <p>In my case, disk I/O is handled by the AVR I/O controller where operations are set up by sending over the DMA address, sector and track numbers, drive index and then performing a read from either the "read" or "write" ports to initiate the I/O operation and retrieve the status. The only change required was to make sure that the bank number is also sent over before initiating the I/O request so the AVR knows which bank it should be accessing:</p> <div class="source"><pre>fd$copy$ptrs: ld hl,(@dma) ld a,l out (disk$dma$l),a ld a,h out (disk$dma$h),a ld hl,(@sect) ld a,l out (disk$sector$l),a ld a,h out (disk$sector$h),a ld hl,(@trk) ld a,l out (disk$track$l),a ld a,h out (disk$track$h),a ld a,(@adrv) out (disk$drive),a if banked ld a,(@dbnk) out (disk$dma$bank),a endif ret fd$write: call fd$copy$ptrs in a,(disk$write) ret fd$read: call fd$copy$ptrs in a,(disk$read) ret</div></pre></p> <p>I'm pretty sure I didn't forget anything else!</p> Wed, 30 Aug 2023 01:35:00 +0100 Updated TI-83 Plus BootExec with support for TI's "Silver Link" driver http://benryves.com/journal/3763190 http://benryves.com/journal/3763190 <p>The previous release of the TI-83 Plus BootExec program relied on temporarily replacing TI's Silver Link driver with WinUSB if you wanted to use the Silver Link USB cable. I've updated the program so it will try to use TI's driver if it's available, or WinUSB if not. This should help people who can't (or don't want to) temporarily replace TI's driver.</p> <p> The updated application can be downloaded, as before, from the same link: <a href="http://benryves.com/bin/ti83p-bootexec.zip">ti83p-bootexec.zip</a>.</p> Wed, 07 Jun 2023 01:32:26 +0100 Updated TI-83 Plus BootExec with USB "Silver Link" support http://benryves.com/journal/3763189 http://benryves.com/journal/3763189 <p>This is a quick update to the TI-83 Plus BootExec program described in <a href="http://benryves.com/journal/3763188">a previous journal entry</a>. The program now supports the USB "Silver Link" cable (as well as the serial "Black Link" it previously supported) though to access the USB device you do need to temporarily replace TI's supplied driver with a generic WinUSB one which can be organised with <a href="https://zadig.akeo.ie/">Zadig</a>.</p> <p>The updated application can be downloaded from the same link as before: <a href="http://benryves.com/bin/ti83p-bootexec.zip">ti83p-bootexec.zip</a>.</p> Sun, 04 Jun 2023 17:52:26 +0100 Unbricking a TI-83 Plus calculator with a link buffer overflow http://benryves.com/journal/3763188 http://benryves.com/journal/3763188 <p>A few years ago I started running into problems with my TI-83 Plus graphical calculator. I was unable to install applications &ndash; it would keep locking up when "defragmenting". In the end I attempted to reinstall the operating system to see if that would cure matters, but that failed too and in the process left the calculator in a state where it wouldn't boot at all. Switching it on you'd be presented with a screen prompting you to reinstall the OS:<br /> <div class="source"><pre>Waiting... Please install calculator software now.</pre></div><br /> If you tried to install the OS over the link port it would switch to a progress screen but then get permanently stuck at the 0% mark until you pulled a battery out.</p> <p>I eventually found a program called <a href="https://brandonw.net/calcstuff/overflow.zip" rel="external">Overflow</a> by Brandon Wilson which described similar symptoms and a possible cause &ndash; a corrupt certificate page. Considering the problems I'd been having with the flash ROM before attempting the OS reinstallation it seemed possible that my certificate page might have become corrupt and that was preventing me from reinstalling the OS.</p> <p>The Overflow program describes a technique whereby it can transfer a user-supplied program to the target calculator by sending a very large variable packet and taking advantage of a lack of bounds checking in the calculator's boot code. Unfortunately, I was unable to get it to work on my TI-83 Plus, in spite of many repeated attempts. I eventually bought a replacement calculator, though being a newer model and built to a much cheaper standard I was always a bit disappointed that my original calculator was lingering, bricked, in a drawer.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/ti83p-bootexec-repaired.jpg" class="lightbox"><img src="http://benryves.com/images/ti83p-bootexec-repaired.thumb.jpg" alt="Photo of the repaired calculator (right) next to the its temporary replacement (left)" width="600" height="600" /></a><br /> <small>Photo of the repaired calculator (right) next to the its temporary replacement (left) &ndash; note the missing ID on the repaired calculator.</small></div> <p>More recently I decided to revisit the problem, got a better understanding of just how the Overflow program worked and found a way to get it work on my original TI-83 Plus. The photo above shows the two working calculators I now have, though as I ended up having to erase the certificate page on the one on the right it now lacks an ID.</p> <h2>How Overflow works</h2> <p>The basic technique exploited here is that the TI-83 Plus boot code does not bounds-check the length of the link packet we're sending it, so by sending a very large packet we can overflow the intended buffer right up to user memory, send over a program we wish to execute, and then overwrite the Z80 stack with the address of our program so that when the link routines return it executes our program rather than returning to the boot code.</p> <p>Overflow satisfies this process by filling up the memory as described above, then sending some correcting data so that the checksum for the oversized packet is equal to zero, and then sending a constant stream of zeroes until the transfer fails. The last two bytes of a transfer are the checksum, and by previously correcting the packet's checksum to zero this means that the packet will be seen as valid.</p> <p>At this point the transmitting calculator detects the link error and tries to read back the acknowledgement from the receiving calculator, and all should be well.</p> <p>Unfortunately, the TI-83 Plus seems to be more fussy about how it handles linking errors and once the attempt to send too many zero bytes has failed it just displays an error message and switches off, rather than letting us receive the acknowledgement before executing our payload.</p> <p>Looking at the documentation for Overflow it seems to have been intended more for the TI-84 Plus series calculators, so it could be that they are more forgiving of the linking errors.<br /> <h2>Trial-and-error with zero padding</h2></p> <p>If the problem is that we're sending too many zero bytes, one option is to count how many zero bytes we can send successfully. Once the attempt has failed, we can then make sure that on our next attempt we only send just the right number of zero bytes (based on our previous count) and no more, then check for the acknowledgement from the receiving calculator. To my delight this strategy works well, and is provided by the application's <tt>-zeropad</tt> option.</p> <p>Unfortunately as over 30,000 zeroes need to be sent each time the exploit packet takes a long time to transmit and as we now need to do it twice this can really slow things down! Once a safe number is known this can be specified with <tt>-zeropad=&lt;count&gt;</tt> but it's still a time-consuming process.</p> <h2>Fixed-size packets for quicker transmission</h2> <p>The problem here is not knowing the size of the packet we're transmitting. The packet does start with a length parameter, however as the "number of bytes left to receive" counter is stored on the calculator's stack by the receiving routine we end up overwriting that with our exploit payload and the total number of bytes left to receive will end up depending on the particular stack level at the time.</p> <p>In my testing the variable ends up being stored on the stack at the same address ($FFC1 for normal transfers, $FFBF for ones where the flash was previously unlocked). Knowing this means that as we trample over the stack deploying our exploit we can at least make sure that we leave that value in the state it should be for the current point in the packet transfer.</p> <p>This is implemented in the program with the <tt>-fixed</tt> parameter, which executes much more quickly than the <tt>-zeropad</tt> one and only needs to run through once. It is however reliant on knowing exactly where on the stack the "number of bytes left to receive" variable is stored; if it's different from the two presets baked into the program it can be changed with <tt>-fixed=&lt;hex&nbsp;addr&gt;</tt>.</p> <h2>The program itself</h2> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/ti83p-bootexec-running.png" class="lightbox"><img src="http://benryves.com/images/ti83p-bootexec-running.thumb.png" alt="Screenshot of the running BootExec program" width="600" height="314" /></a></div> <p>In case it helps anyone else out, <a href="http://benryves.com/bin/ti83p-bootexec.zip">the program can be downloaded from this link</a>. It's a .NET application and requires a computer with a serial port and a "black link" compatible serial cable (I use a <a href="http://benryves.com/images/ti83p_serial_cable/" rel="external">home-made cable</a>), which I appreciate is not exactly the most modern solution but is what I have access to.</p> <p>It will allow you to transfer a standard "noshell" TI-83 Plus assembly program to the target calculator, with or without flash unlocked. As this is a potentially risky operation (especially with flash unlocked, which would allow you to completely brick the calculator by damaging the boot code) any such programs are left as an exercise to the user to be used at their own risk. The original Overflow program contains much more useful information, including a sample program that can erase the certificate page, though be warned that as written is is not designed for the TI-83 Plus and will erase the wrong page and so will need to be modified before use. This is only recommended as a last chance for calculators that are otherwise bricked and unusable!<br /> <HR> <strong>Update 4th June 2023:</strong> The program now supports the USB "Silver Link" cable, though you will need to temporarily replace TI's driver with a generic WinUSB driver using <a href="https://zadig.akeo.ie/" rel="external">Zadig</a>. The download link is the same as before.</p> <p><strong>Update 7th June 2023:</strong> The program will now try to use TI's driver for the "Silver Link" USB cable, if available. This avoids the need to temporarily replace it with the WinUSB driver.</p> Fri, 02 Jun 2023 16:53:13 +0100 Using a VDrive to access USB flash drives from a Cambridge Z88 http://benryves.com/journal/3763187 http://benryves.com/journal/3763187 <p>The VDrive is a handy module for electronic projects that need to access files on a USB flash drive. It's based around a USB host microcontroller and comes preinstalled with some firmware that provides control over the drive with simple commands sent via a serial connection (UART or SPI).</p> <p>A few years ago I started putting together some code to connect the module to my Cambridge Z88 computer. All I needed was a way to power the drive and a MAX232 chip to translate the computer's RS-232 interface to the VDrive's logic levels, and after around 150 lines of BBC BASIC I had a program that could show directory listings, let me browse folders, and fetch files from the USB drive to the Z88's file system.</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/products/vdrivez88/images/z88-and-vdrive.jpg" alt="Photo of the VDrive plugged into a Z88" width="640" height="480" /></div> <p>This worked well enough but was a bit clumsy. For example, to maintain good performance rather than alternate between reading a single byte from the drive and writing it to the local file system it's better to read and write larger chunks at a time. BBC BASIC doesn't provide a built-in way to do that, though you can read or write CR-terminated strings. When you read each part of the file this way you therefore need to decide whether the string you've just read is a certain length because you've reached a CR terminator (which isn't included in the read string), whether you've reached the end of the file, or whether the string buffer is full, and from that piece the file back together. I got this working quite well but it's still fundamentally an inelegant hack. Doing it properly would require some assembly code, and that would also be required for some other operations (such as properly transferring date and time modification information) that are otherwise not possible from pure BASIC.</p> <p>Fortunately, BBC BASIC has a built-in assembler and that makes integration of assembly code in BASIC programs quite a bit easier than it would otherwise be. However, as I considered the amount of assembly code required would be quite high, I thought it might be more sensible to just rewrite the program as a native Z88 popdown application.</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/products/vdrivez88/images/scr-fetch-from-drive.png" alt="Z88 screenshot of a dialog shown when fetching a file from the drive" width="640" height="64" /><br /><small> Status dialog shown when fetching a file from the drive</small></div> <p>This is what I ended up doing, and it can be downloaded from <a href="http://benryves.com/products/vdrivez88">its product page</a>. It was quite a lot of fun to learn my way around the Z88's OS &ndash; not just for things like file handling, date and time manipulation, and integration with menu and help system but for some of the challenges involved in writing Z80 code for a system that shares memory between multiple running applications (and the file system) rather than my usual environment of having a big block of contiguous RAM to do whatever I fancied in.</p> <p>The directory listing is the most obvious place where I had to rely on dynamic memory allocation. Each file or folder name being sent in a directory listing by the VDrive is allocated its own memory and I arranged the names together in a linked list that is sorted with an insertion sort.</p> <p>Being my first Z88 application it's not especially well written but I've been using it for a while now and it seems to work well enough so I've released it, both on <a href="http://benryves.com/products/vdrivez88">this very website</a> and on <a href="https://github.com/benryves/VDriveZ88" rel="external">GitHub</a>.</p> Sat, 13 May 2023 03:11:02 +0100 Take your TI-83 Plus online with a TIWiFiModem http://benryves.com/journal/3763186 http://benryves.com/journal/3763186 <p>One of the issues holding me back with my development of the Light Gun Commando project (aside from a lack of free time due to the day job) was running out of prototyping breadboards and the difficulty of swapping between different console adaptor boards for testing.</p> <p>Normally by this point I'd have started soldering together more permanent prototypes on little circuit boards, but I've been having a difficult time with the boards I've got in stock apparently being made of a metal that's impossible to solder to. They <em>were</em> very cheap, but for some reason the solder joints would end up coming out blobby, make poor connections, be prone to bridges and generally not "wetting" the pads at all. This makes hardware prototyping very frustrating and time-consuming, and though I'd tried different solder (no change), different temperatures (higher heats just meant the pads would unglue from the board more quickly), more flux (just more cleaning required afterwards) and other attempts to clean the boards before use (including light sanding) I wasn't getting very far.</p> <p>I eventually bought a set of new circuit boards from a more reputable seller but before cracking on with my light gun adaptors I thought I should try a more straightforward weekend project and I ended up building myself a <a href="https://github.com/mecparts/RetroWiFiModem/" rel="external">RetroWiFiModem</a>.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/retrowifimodem/retrowifimodem.jpg" class="lightbox"><img src="http://benryves.com/images/retrowifimodem/retrowifimodem.thumb.jpg" alt="Assembled RetroWiFiModem connected to a Z88 computer" width="600" height="450" /><br /><small>RetroWiFiModem connected to a Cambridge Z88 computer</small></a></div> <p>This is a device that looks like an old dial-up modem and though it does have an RS-232 serial port on the back to connect it to a computer it doesn't attach to a phone line but instead connects to a modern Wi-Fi network. You can send it Hayes-style AT commands and "dial out" to a domain name which will then open a Telnet (or raw socket) connection to the remote computer and allow you to exchange data. As long as your old computer has a terminal emulator on it you can use this to connect to and browse online services such as BBSes.</p> <p>I had a lot of fun building this and setting this up &ndash; especially as I can confirm that the circuit went together extremely easily on my new prototyping circuit boards &ndash; and it reminded me that I'd seen a terminal emulator program for the TI-83 Plus calculator around 20 years ago by the name of <a href="https://www.ticalc.org/archives/files/fileinfo/141/14141.html" rel="external">Telnet 83 Plus</a>. The documentation accompanying the program had lots of information in it about how to connect to a modem using the calculator's grey serial link cable which directly translates the calculator's link protocol to true 9600 baud RS-232, unlike the black cable which I owned which just uses the control lines to bit-bang the calculator's link protocol. As I never had the equipment for this the program only ever ended up being a curiosity to me, but having seen how well the RetroWiFiModem worked I thought it could be adapted for use on a calculator.</p> <p>To do this I wrote a simple implementation of the calculator's link protocol in a class that inherited from the Arduino's <tt>Stream</tt> class. This is the same class that the <tt>Serial</tt> class inherits from, so having done that all I needed to do was a find-and-replace of <tt>Serial.*</tt> in the original source code with <tt>tilp.*</tt> and I had a version of the RetroWiFiModem that worked when connected to a calculator. As I also wanted this version to be a little more pocket-sized I designed it around the cheap ESP-01 or ESP-01S modules, which lacks the pins to drive the status LEDs on the original version so this ended up being a slightly more slimmed-down version of the project. It still has all the networking features, though, and the end result is the TIWiFiModem:</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/retrowifimodem/tiwifimodem.jpg" class="lightbox"><img src="http://benryves.com/images/retrowifimodem/tiwifimodem.thumb.jpg" alt="TIWiFiModem connected to a TI-83 Plus calculator" width="600" height="450" /><br /><small>TIWiFiModem connected to a TI-83 Plus calculator</small></a></div> <p>To interact with the modem I was using Telnet 83 Plus however I'd encountered a few bugs with this program, including incompatibility with newer TI-83 Plus calculators with slower display drivers (resulting in a scrambled image on the LCD), a lack of overflow checking on the receive buffer that would cause it to truncate long transfers and the inability to type certain keys in uppercase. Fortunately the source code was included so I dusted off my Z80 assembler and fixed these issues, along with shaving a few thousand bytes off the program size, improved compatibility with some VT100 sequences, a mode that automatically keeps the cursor within the view of the screen and local echo. These changes, along with the firmware for the modem, can be found on <a href="https://github.com/benryves/TIWiFiModem/" rel="external">the TIWiFiModem Github page</a>.</p> <div class="html_center" style="text-align: center;"><a href="https://www.youtube.com/watch?v=eg_J6N9MSCY" rel="external"><img src="https://img.youtube.com/vi/eg_J6N9MSCY/maxresdefault.jpg" alt="Video thumbnail for demonstration of TIWiFiModem on YouTube" width="768" height="432" /></a></div> <p>If you'd like to see what the TIWiFiModem is all about before building one yourself, I put together a video demonstrating it which is embedded above.</p> Mon, 27 Mar 2023 20:38:24 +0100 Arcs, segments and sectors in BBC BASIC for the Sega Master System http://benryves.com/journal/3763174 http://benryves.com/journal/3763174 <h2>More musing on tape phases</h2> <p>I bought a few more Acorn-format BBC BASIC cassettes to test my adaptation of BBC BASIC to the Sega Master System with, and have found a few interesting oddities since <a href="http://benryves.com/journal/3763173">my last post</a>. In that post I made the assertion that the phase of the recorded signals on the tapes is at <a href="http://benryves.com/images/bbcbasic_sms/phases.png" class="lightbox">180&deg;, i.e. each wave cycle goes negative before it goes positive</a> (whereas a 0&deg; phase would go positive before it goes negative). This matched the documentation I'd read, the output of tools like <a href="https://www.8bitkick.cc/playuef.html" rel="external">PlayUEF</a> and my own tests with commercially-recorded tapes. With these new tapes I've found things are not quite so straightforward, though:</p> <ul><li>The "Welcome" tape for the BBC Master Series is recorded at 0&deg;. OK, maybe that's a BBC Master weirdness?</li> <li>One copy of the "Welcome" tape for the BBC Micro is recorded at the usual 180&deg;, hooray, we're back to normal!</li> <li>Another copy of the "Welcome" tape for the BBC Micro is recorded at 0&deg;. It's also a different colour, but otherwise has identical programs on it, maybe the difference comes from different duplication plants?</li> <li>Side A of Acornsoft's "Graphs and Charts" is recorded at 180&deg;, Side B of the same tape is recorded at 0&deg;. Flipping the tape flips the phase, I give up!</li></ul> <p>I am able to load the programs by changing the "phase" switch on my cassette recorder to "reverse", but not all cassette recorders have such a switch. There is a jumper setting in the tape interface circuit that the cassette loader software can check to control whether it starts timing the length of wave cycles on a falling (180&deg;, default) or rising (0&deg;) edge and so it can compensate for the reversed phase, but I'd rather see if I can find a way to automatically detect the phase and properly recover data without needing to rewind the tape, push a switch or type in a command, then trying again.</p> <p>A "1" bit is represented by two 2400Hz cycles and a "0" bit by a single 1200Hz cycle. Each byte has at least one "1" bit before it and always starts with a "0" start bit. In theory, then, a "0" bit with the correct phase should be represented by a wave cycle that's 2&times; the length of a preceding 2400Hz cycle, but if the phase is incorrect it'll only be 1.5&times; the length. At the moment the threshold to detect the difference between a 2400Hz and 1200Hz cycle is placed at 1.5&times;, maybe for the start bit it needs to be at 1.3&times; to detect the "0" bit instead, and if after that the wave is over 1.6&times; it's treated as a "normal" 180&deg; tape and loaded as normal, but if it's between 1.3&times; and 1.6&times; it's treated as a "reverse" 0&deg; tape and an extra rising edge is checked for before loading the tape with reverse phase.</p> <p>I'm not sure if that'll work and my quick initial tests didn't work very reliably so I'll need to do a bit more experimentation I think. I did encounter another oddity with formats beyond the reversed phase, though, and that's with Acornsoft's "Graphs and Charts". The tokenised BASIC programs on the tape would crash the loader or generate "Bad program" errors, and when I copied the tape to my PC and looked at the files there I could see that BBC BASIC for Windows refused to open them too. The problem is that instead of the usual <tt>&lt;CR&gt;&lt;FF&gt;</tt> terminator on each program, they all end with <tt>&lt;CR&gt;&lt;80&gt;</tt> instead. My loader tries to convert Acorn BBC BASIC format programs to the Z80 BBC BASIC format automatically during the load, but if it misses the terminator when stepping through the program it will either see the line as being zero bytes long (due to memory being cleared to 0) and loop infinitely when attempting to jump to the next line, or read a line length from uninitialised memory that causes it to advance to a line that doesn't start with the appropriate byte and so assume the program is not in Acorn format, skipping the conversion process and leaving a "Bad" program in memory.</p> <p>I made my loader more robust by appending a suitable dummy terminator to the loaded program; if it already was a valid program with a proper terminator then it makes no difference, but it otherwise prevents the convertor from dropping off the end of the program and allows me to load the programs from the "Graphs and Charts" tape.</p> <h2>Drawing arcs, segments and sectors in BBC BASIC</h2> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/clown-before.png" width="256" height="192" alt="Clown missing parts" /> &nbsp;&nbsp;&nbsp; <img src="http://benryves.com/images/bbcbasic_sms/clown-after.png" width="256" height="192" alt="Complete clown" /></div> <p>I hadn't implemented all of the <tt>PLOT</tt>ting routines that BBC BASIC can provide for drawing graphics on the screen, and a few of the programs on the BBC Master Series "Welcome" tape take advantage of the more advanced routines to draw circular arcs, segments and sectors. Attempting to run these programs produced results like the picture of the clown above with several parts of its face missing.</p> <p>I had been putting this off as I hadn't been able to think of a good way to implement drawing these shapes. After a bit more research online I came across <a href="http://www.crbond.com/papers/arcdraw.pdf" rel="external">a paper by C. Bond entitled <em>An Incremental Method for Drawing Circular Arcs Using Properties of Oriented Lines</em></a> which turned out to be ideal.</p> <p>The linked paper does a very good job of describing the technique so I would recommend reading it, but crucially it provides a solution that can be implemented easily in Z80 assembly with a few integer multiplications and additions. I was able to incorporate this into the existing circle tracing and filling code without too much effort, just an extra step in the "plot pixel" or "fill horizontal span" routines to clip against the lines that bound the arc, segment or sector.</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/ship-before.png" width="256" height="192" alt="Ship missing parts" /> &nbsp;&nbsp;&nbsp; <img src="http://benryves.com/images/bbcbasic_sms/ship-after.png" width="256" height="192" alt="Complete ship" /></div> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/acorn-before.png" width="256" height="192" alt="Acorn rendered poorly" /> &nbsp;&nbsp;&nbsp; <img src="http://benryves.com/images/bbcbasic_sms/acorn-after.png" width="256" height="192" alt="Acorn rendered correctly" /></div> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/welcome-before.png" width="256" height="192" alt="Welcome missing parts" /> &nbsp;&nbsp;&nbsp; <img src="http://benryves.com/images/bbcbasic_sms/welcome-after.png" width="256" height="192" alt="Welcome missing slightly fewer parts" /></div> <p>As you can no doubt see from the "Welcome" screen at the end there are still parts missing from the final image. This is because the program only draws each required letter once and then uses the block copying <tt>PLOT</tt> routines to duplicate the letter if it's needed again instead of rendering it again from scratch &ndash; hence the second "B" from "BBC" is missing, and "MASTER" becomes "MAS&nbsp;&nbsp;R" as the program is expecting to copy the "T" and "E" from "THE" in the line above.</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/welcome-complete.png" width="256" height="192" alt="Welcome in its complete form" /></div> <p>Disabling this optimisation within the BASIC program and forcing each letter to be drawn instead of copied does improve the results, though it still doesn't entirely fit on the screen due to the much lower resolution!</p> Tue, 12 Oct 2021 03:51:08 +0100 Tape cycle frequencies and phases, plus VDrive3 support for BBC BASIC on the Sega Master System http://benryves.com/journal/3763173 http://benryves.com/journal/3763173 <p>I have now moved the tape interface circuit described in <a href="http://benryves.com/journal/3763172">the previous entry</a> from its breadboard prototype to a neat enclosure where I am happy to report it mostly works as well as it did before.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/bbcbasic_sms/tape-interface-enclosure.jpg" class="lightbox"><img src="http://benryves.com/images/bbcbasic_sms/tape-interface-enclosure.thumb.jpg" width="500" height="375" alt="The tape interface installed in its enclosure." /></a></div> <p>I have spotted two issues, though. The first affects my small cassette recorder but not the large one, and is related to reading files from tape via the file IO routines (such as <tt>OPENIN</tt> then <tt>BGET#</tt> to retrieve a single byte, not from <tt>LOAD</tt>). Files are stored on tape in 256 byte blocks, and when the file is opened a whole 256 byte block is read from the tape and copied to the Master System's RAM. When the BASIC program requests a byte from the tape it is read from this local copy of the block instead, and when the read pointer reaches the end of the 256 byte block the next block is fetched from the tape and the local copy updated with this new data. The motor control comes in handy here, with the file system library stopping and starting the tape playback as desired.</p> <p>The problem I was having was that the first block would read fine, but when the time came to read the second block the tape started playing but the file system library never seemed to be able to detect any data &ndash; it would just work its way along to the end of the tape, never displaying any errors, but never reading any meaningful data either.</p> <p>The issue ended up being some code I'd added somewhat recently to automatically calibrate the threshold between a 1200Hz ("zero" bit) and a 2400Hz ("one" bit) tone. The code reads bits by counting the length of each wave cycle and comparing this to a threshold value - if it's longer, it's the 1200Hz "zero" bit tone, if it's shorter, it's the 2400Hz "one" bit tone. Before and after each file, and between each block, is a section of 2400Hz "carrier" tone. By detecting a large number of wave cycles that were all around the same length (say, within four length units of each other) you could assume that you were receiving the 2400Hz carrier tone and use that to calculate a suitable threshold for loading in data later.</p> <p>The main bug was that I was comparing the length of the wave cycle just received with the length of the <em>previous</em> wave cycle rather than the length of the <em>first</em> wave cycle. This becomes a problem when you start a tape that was paused in the middle of a carrier tone (e.g. between blocks) as the tape will take some time to come up to speed, during which time the period of each wave cycle will gradually decrease. As the reference length we're checking the current wave length against is changing over time it means we end up compensating for the tape's speed increasing and so end up accepting the slower (longer) wave cycle lengths as part of the calculation of the threshold between "zero" and "one" bit wave cycle lengths.</p> <p>As the wave cycle length threshold is now longer than it should be, all incoming wave cycles get interpreted as short ones and so it looks like we're just getting a stream of "one" bits - which looks like the carrier, so the code never starts trying to decode any blocks. My larger tape recorder comes up to speed much more quickly than the small one, or at least doesn't <em>audibly</em> ramp up in speed, and so isn't affected by this problem.</p> <p>Fortunately the fix is very easy, just check the length of wave cycles against the <em>first</em> wave received rather than the previous one. This way if the timings drift over time (as they would during the period that the tape is coming up to speed) then they'll eventually go out of the permitted range. Changing this fixed the issue.</p> <p>You may wonder why I'm calculating the threshold at runtime instead of just using a hard-coded value if the expected frequencies are a fixed 1200Hz and 2400Hz. My main intention was to be able to handle tapes that were running at the wrong speed due to a miscalibrated cassette recorder, but this does also open up the possibility to load from tapes at higher rates. Without any further adjustment my code can already load from audio generated at 2320 baud, i.e. with the two frequencies at 2320Hz and 4640Hz, without any further adjustment. This is a slightly annoying figure as 2400 baud would be the obvious target (being double the intended rate!) but at this speed the current code fails to latch on to the signal so I slowed the "tape" speed down until I found a value that worked. It's perhaps worth mentioning that the audio in this case was coming from <a href="https://www.8bitkick.cc/playuef.html" rel="external">PlayUEF</a> running on my phone rather than an analogue cassette tape with the <tt>BAUD</tt> (or <tt>LOW</tt>) parameter used to adjust the speed, I'm not sure that a 93% boost in loading speed would be achievable from a tape!</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/little-and-large-tape-recorders.jpg" width="640" height="360" alt="Little and large tape recorders." /></div> <p>I mentioned above that there were two issues. The slow speed-up issue affected the small cassette recorder, but the large cassette recorder had a new issue that only became apparerent after moving the circuit to its final enclosure. The Master System should be able to start and stop the cassette remotely, handled by a reed relay that is then plugged into the cassette recorder's "remote" socket. The Master System was able to start both tape recorders without any issues, but was unable to stop the large one &ndash; the "motor" status light would switch off, but the tape would continue playing. Tapping the relay sharply would be the only way to get it to switch off.</p> <p>I tried plugging in a 2.5mm TS plug into the side of the large tape recorder and used the very scientific approach of shorting its connections with a screwdriver to simulate the relay switching "on" and was surprised to see quite large sparks. I measured the DC current through the remote control switch when the cassette was running and it was only around 70mA at its highest &ndash; well below the 1A rating of the relay &ndash; however I reckon there is a very large inrush current when the motor kicks in that is sufficient to weld the reed switch contacts together, causing it to get stuck.</p> <p>Why didn't this affect the circuit on the breadboard? In that setup I was using a 3.5mm TS extension cable connected to a 3.5mm male-to-male adaptor cable which was then connected to a 3.5mm to a 2.5mm adaptor cable. My assumption is that all of the extra contact resistances were helping to limit the inrush current, preventing the contacts from welding together.</p> <p>It's very likely not the best sort of snubber circuit for this application but for the time being I've put a 10&Omega; resistor in series with the relay contacts, which limits the inrush current enough for the relay not to get stuck: </p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/tape-interface-with-remote-resistor.png" width="500" height="400" alt="Tape interface circuit for the Sega Master System with added 10&Omega; resistor on the relay contacts to limit inrush current." /></div> <p>One matter that I've also been testing is the phase of the audio signal. As far as I can tell Acorn's tape format assumes a 180&deg; phase; that is to say that each wave cycle starts at 180&deg; and runs to 180&deg;+360&deg;=540&deg; for a complete cycle rather than starting at 0&deg; and running to 360&deg;. The result of this is that instead of the signal starting at an amplitude of 0, then going positive before negative (as you'd expect for a typical sine wave) the signal goes negative first:</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/phases.png" width="256" height="128" alt="0&deg; and 180&deg; phase." /></div> <p>I have confirmed this by looking at the output of tools like PlayUEF as well as looking at the signals coming directly off commercially pre-recorded tapes with BBC Micro software on them, after first confirming that my sound card doesn't invert the signal by briefly connecting its input to a positive DC power supply and seeing that the received signal goes positive when that happens. Here's an example of a commercially-released tape, where you can see that after the high-frequency cycles in the first longer wave (a "zero" bit, acting as the start bit, starting at around 12.7065) starts low then goes high:</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/phase-commercial.png" width="500" height="178" alt="Plot of the signal from a commercially-released tape showing the 180&deg; phase." /></div> <p>I then generated a test tone that alternates between 0 and 1 bits, i.e. one complete 1200Hz cycle then two complete 2400Hz cycles with the appropriate 180&deg; phase:</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/phase-test-wave.png" width="500" height="178" alt="Plot of the test signal showing the 180&deg; phase." /></div> <p>This pattern should make it easy to spot the phase of recordings. I first tried running the test tone straight back into my PC to ensure that the phase was not being reversed by the PC, and got the same signal back in compared to to what I played out, as you'd expect. I then tried recording the signal to tape with my Grundig recorder, and found something interesting when playing it back:</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/phase-test-grundig.png" width="500" height="178" alt="Plot of the test signal recovered from the Grundig tape recorder." /></div> <p>The phase there is very clearly reversed &ndash; the signal I played into the tape recorder goes low before it goes high, whereas the signal I've recovered goes high before it goes low. I repeated the test with the Alba tape recorder:</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/phase-test-alba.png" width="500" height="178" alt="Plot of the test signal recovered from the Alba tape recorder." /></div> <p>This shows the same thing &ndash; the phase on the tape is at 0&deg; even though the test signal was at 180&deg;. I also have a Sony digital voice recorder (an ICD-BX140) that I've been using as well as a tape recorder, so I tried the test with that too:</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/phase-test-sony.png" width="500" height="178" alt="Plot of the test signal recovered from the Sony digital voice recorder." /></div> <p>Apart from the much lower recording level this once again shows the same thing &ndash; the signal phase is inverted when recorded.</p> <p>I had previously encountered an issue where I had tried to make a tape from a UEF image by playing the audio generated by <a href="https://www.8bitkick.cc/playuef.html" rel="external">PlayUEF</a> into a tape recorder. Attempting to load the data directly from PlayUEF worked fine, but the tape recording of the same signal wouldn't load. My Grundig tape recorder has a phase reversal switch, and setting it to "reverse" would allow me to load the tape created by recording the output of PlayUEF. However, in that reverse setting, I lost the ability to load from my commercially-recorded tapes. I think the above tests indicate why this is the case &ndash; the recorders all invert the signal when creating the tape recording. I find it particularly interesting that the Sony digital voice recorder does the same thing!</p> <p>I did check to see if the phase switch on the Grundig tape recorder had any influence on the recording, but it doesn't &ndash; whether it's set to "normal" or "reverse" the recording to tape still ends up inverted. The phase switch only appears to affect playback.</p> <p>PlayUEF does have an option to set the output phase to 0&deg; instead of its default 180&deg;. By doing that and recording its output to tape I was able to load the programs directly from either of my tape recorders.</p> <p>Of course, it would be nice if the loader was able to handle either phase, and in practice I've found it does sometimes handle the "incorrect" phase quite well but it does seem to be more a lot more error prone &ndash; some programs load without any problem, others report problems with some blocks and others fail to be detected at all! I think I'll see if there's a way to reliably detect the phase and switch to the appropriate handler, but for the time being I've added a crude workaround &ndash; pulling pin 1 of the controller port (the d-pad "Up" input) to ground tells the loader to assume the phase is inverted. As I hope this won't be necessary in future I've left this as a jumper in my current tape interface circuit (visible below in the top left corner) rather than add a switch!</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/bbcbasic_sms/tape-interface-circuit.jpg" class="lightbox"><img src="http://benryves.com/images/bbcbasic_sms/tape-interface-circuit.thumb.jpg" width="400" height="400" alt="The tape interface circuit board." /></a></div> <p>A lot of the work has been focused on the tape loader, but tapes are not the most practical storage media these days. Finding a working tape recorder is a challenge in its own right, and a lot of the old tapes are tricky to work with as they've become sticky with age. In an attempt to be a bit more modern I've started integrating support for the <a href="https://ftdichip.com/products/vdrive3/" rel="external">VDrive3</a> to BBC BASIC. This is an inexpensive module that lets you access files on a USB mass storage device via simple commands sent over a serial interface. I'd previously used <a href="http://benryves.com/journal/2967911">the VMusic2 device in another project</a> and that uses the same firmware commands so I already had a little experience with it (the VMusic is pretty much a VDrive with an MP3 decoder bolted on top). I already had some serial routines written so it was a reasonably easy job to plumb in some code to handle <tt>LOAD</tt>ing and <tt>SAVE</tt>ing programs, and from a hardware perspective all you need is a cable to connect the module directly to the Master System's controller port &ndash; the VDrive3 uses 3.3V signalling levels but its I/O lines are 5V tolerant.</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/vdrive3.jpg" width="500" height="375" alt="The VDrive3 alongside the tape interface and RS-232 modules." /></div> <p>One notable advantage of the VDrive3 over the tape interface is that it allows for proper random-access of files rather than just sequential access. I've been working on implementing all of the required BASIC statements to support this (<tt>OPENIN</tt>, <tt>OPENOUT</tt>, <tt>OPENUP</tt>, <tt>BGET#</tt>, <tt>BPUT#</tt>, <tt>PTR#</tt>, <tt>EXT#</tt>, <tt>EOF#</tt>, <tt>CLOSE#</tt> etc). For a bit of fun I wrote a <a href="http://benryves.com/bin/bbcbasic/programs/SHOWPIC.BBCS" rel="external">BASIC program</a> that loads an image file from disk (or tape) and displays it on screen. I needed to add user-defined character support to the new reduced-resolution <tt>MODE&nbsp;2</tt> to handle this (something that was not implemented before due to a lack of VRAM) but with that in place it's working pretty well.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/bbcbasic_sms/duck.jpg" class="lightbox"><img src="http://benryves.com/images/bbcbasic_sms/duck.thumb.jpg" width="400" height="400" alt="Loading a picture of a duck from a USB drive." /></a></div> <p>It's not fast, taking 2m45s to load from from tape and 1m19s from a USB flash drive, but it's a fun diversion!</p> Wed, 06 Oct 2021 03:58:15 +0100 Refining the tape interface for BBC BASIC on the Sega Master System http://benryves.com/journal/3763172 http://benryves.com/journal/3763172 <p>It's been quite a while since my last post but work has continued with the version of BBC BASIC (Z80) for the Sega Master System. Most of the features I have been working have not been particularly exciting to write about on their own, but here are some of the notable changes:</p> <ul><li>All VDU code (for text and graphics output) has been moved to a separate ROM bank, freeing up around 16KB of extra ROM space. It makes calling the code slightly more complicated but in the previous post I mentioned that I had less than 100 bytes of free ROM space so this was definitely required.</li> <li>Simplified the video mode driver interface. Each video mode has its own driver code which exposes a number of different functions. Previously, each function had its own vector in RAM which was fast but consumed a lot of precious work RAM. Now only a couple of functions (which need to be called frequently and as quickly as possible) are vectored, and all of the other functions are called via a single generic function with a parameter for which specific operation to carry out (e.g. "reset the graphics viewport", "change the text colour").</li> <li>Rewrote the triangle filling routine and line drawing routine to ensure that the same pixels are covered whether you fill a triangle or trace its outline &ndash; previously there would be some stray pixels that leaked outside the perimeter of a filled triangle.</li> <li>Changed the output resolution for most video modes to 240&times;192 with a logical to physical pixel scale factor of 5&#x2153; for a total logical resolution of 1280&times;1024, the same as the BBC Micro &ndash; graphics programs designed for the BBC Micro now properly fit on the screen.</li> <li>Changed <tt>MODE&nbsp;2</tt> to use a smaller resolution of 160&times;128 with a pixel scale factor of 8. Unfortunately, there is not enough video RAM to have a unique pattern in VRAM for every character position on-screen when using all 16 colours (as in mode 2) at the higher 240&times;192 resolution. The new lower resolution avoids screen corruption that previously occurred when running out of VRAM:</li></ul> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/mode2-island-240x192.png" width="256" height="192" alt="Corrupt MODE 2 graphics" /> &nbsp;&nbsp;&nbsp; <img src="http://benryves.com/images/bbcbasic_sms/mode2-island-160x128.png" width="256" height="192" alt="Fixed but letterboxed MODE 2 graphics" /><br /> <small>MODE 2 graphics at the full-screen 240&times;192 and reduced 160&times;128 resolutions</small></div> <ul><li>Added support for scrolling the text viewport down as well as up, this is used by the line editor which now allows very long lines to be edited in small viewports by scrolling and repainting as necessary.</li> <li>Reworked the keyboard input routines for greater reliability and performance, including proper mapping of key codes in <tt>*FX&nbsp;4,1</tt> and <tt>*FX&nbsp;4,2</tt> and removal of "clever" (but flawed) interrupt-based detection of when a key is available (that had a habit of dropping keys and resulted in worse performance than just periodically polling the keyboard anyway).</li></ul> <p>The most significant changes have been all to do with loading and saving from tape, though. Since my previous post I have been doing a lot of work testing and refining both the hardware and software of the tape cassette interface. The hardware has now been tested with a couple of different tape recorders, as well as a pocket digital recorder, my mobile phone and my PC's sound card. The loader is much more robust and if a single block fails to load from the tape you can usually just rewind the tape a short way and it will try again, no need to rewind back to the start and start from scratch. I was previously counting the duration of half-wave cycles from the tape and comparing these to some fixed thresholds baked into the code to determine the difference between a "0" and a "1", I've now changed this to measure full-wave cycles (which aren't affected by any duty-cycle shifts in the recordings) and to calibrate the thresholds during the initial carrier tone, which allows the code to compensate for tape recorders that aren't quite running at the correct speed. On a more practical basis I've also added the ability to save to tape, motor control via a reed relay (so the tape will automatically stop after a file is loaded), and support for opening files on tape for reading or writing and accessing them on a byte-by-byte basis. As tapes are sequential access there are some limitations to this (for example, you can only advance the read pointer later in the file, you can't seek backwards) but it works quite well otherwise.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/bbcbasic_sms/tape-equipment.jpg" class="lightbox"><img src="http://benryves.com/images/bbcbasic_sms/tape-equipment.thumb.jpg" width="400" height="400" alt="A selection of tapes and a tape data recorder." /></a></div> <p>Of course, to test all of this I needed a tape recorder! I purchased the above Grundig DCR 001 data cassette recorder as well as some blank cassettes and a couple of commercially-prerecorded ones to do so. The DCR 001 has a few nice features that make it great for loading programs:</p> <ul><li>A dedicated "data" mode that sets the output level at an appropriate fixed level &ndash; no need to guess about finding a suitable volume level.</li> <li>An optional "monitor" in data mode that lets you listen to the data on the tape as it's loading.</li> <li>A "phase" switch that can be used to reverse the phase output, to compensate for the case where the data has been recorded with a 180&deg; phase shift.</li> <li>An "Automatic Programme Detecting System" that lets you skip ahead to the next program or back to the previous one by pressing Rewind or Fast Forward when Play is pressed &ndash; this stops when it detects a silent gap between recorded sections of data on the tape.</li></ul> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/tape-interface.png" width="500" height="400" alt="Tape interface circuit for the Sega Master System." /></div> <p>Above is the cassette interface circuit I came up with. It's quite simple and has a notable lack of analogue sections (no op-amp chips here!) &ndash; it uses a NOT gate with a 10K&Omega; negative feedback resistor to turn the analogue signal from the tape recorder's earphone output into a clean digital signal that can be fed into the Sega Master System. I've tested this with an SN74LS04N, SN74HCU04N and SN74F04N and all worked well, however for best results I'd recommend the SN74LS04N. The filtering capacitors clean up some high-frequency pulses/glitches that may otherwise end up on the output; this is not something I found was much of a problem with a cassette recorder or the audio output from my phone or PC, but a pocket digital voice recorder I have seems to produce glitchy high-frequency pulses in its output (I'm not sure if that's an artefact of its DAC or in the MP3 compression it uses for its voice clips) and those capacitors clean up the signal enough to be able to read it reliably.</p> <p>The remote motor control relay is a reed relay, and must be a low current device to avoid overloading the Master System's controller power supply and to also allow it to be driven directly by the hex inverter chip. I found the EDR201A0500 perfect for this role!</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/bbcbasic_sms/alba-dirty.jpg" class="lightbox"><img src="http://benryves.com/images/bbcbasic_sms/alba-dirty.thumb.jpg" width="500" height="370" alt="A grubby ALBA tape recorder." /></a></div> <p>Being able to save and load back programs on one cassette recorder is fair enough, but to really be sure my hardware and software are working as they should I thought I should see if I was able to save a program to tape on one tape recorder and then load the same program back on a different tape recorder. To this end I bought the above cassette recorder. It was clear from the listing that the record button and counter reset button were damaged, and the whole thing was quite grubby, but I thought it worth a punt.</p> <p>Getting inside was very easy as all but one of the screws was missing, and it was clear that someone had got there before me as the damaged plastic parts (including two of the main screw posts as well as the snapped-off parts of the record button and counter reset mechanism) had been removed at some point. After giving the tape path a very thorough clean and replacing one of the drive belts that was turning into a tarry mess I was able to get it to play a tape, so I took everything out of the enclosure to give that a good scrub and prepared to fix the damaged plastic parts.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/bbcbasic_sms/alba-record-button.jpg" class="lightbox"><img src="http://benryves.com/images/bbcbasic_sms/alba-record-button.thumb.jpg" width="400" height="450" alt="Repairing the record button." /></a></div> <p>By tracing the arm from one of the other buttons onto a piece of scrap ABS plastic I was able to make a replacement arm for the damaged record button, which I then glued on with cyanoacrylate and some two-part epoxy resin to fill in the gaps. The result is not especially pretty but it does the job and my blobby repair is not going to be visible unless you take the recorder apart.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/bbcbasic_sms/alba-counter.jpg" class="lightbox"><img src="http://benryves.com/images/bbcbasic_sms/alba-counter.thumb.jpg" width="500" height="370" alt="Repairing the counter reset button." /></a></div> <p>The counter reset button was a bit more fiddly; the reset mechanism seems to rely on the "n" shaped bit of plastic shown above in the top left photo, and though it looked like something used to be attached to it that had snapped off I wasn't sure what it would have originally looked like. I cut up some more scraps of ABS sheet to make up a new counter reset button which seems to work pretty well, though! </p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/bbcbasic_sms/alba-refurbished.jpg" class="lightbox"><img src="http://benryves.com/images/bbcbasic_sms/alba-refurbished.thumb.jpg" width="500" height="370" alt="A somewhat refurbished ALBA tape recorder." /></a></div> <p>The final addition was a couple of new knobs for the tone and volume sliders &ndash; these are cheap generic parts and they don't quite line up with the markings (the original sliders have off-centre indicators) but overall I think this turned out pretty well, and my main concern &ndash; being able to save a program to tape on one machine and then load it back from another &ndash; has been thoroughly tested now and I'm happy with the results. My next task is to transplant the circuit from its current breadboard layout to a neat project box!</p> Tue, 28 Sep 2021 02:23:47 +0100 Loading BASIC programs from cassette tape to the Sega Master System http://benryves.com/journal/3763171 http://benryves.com/journal/3763171 <p>After I posted about the pattern filling modes on Twitter I was alerted to the <a href="https://www.bbcmicrobot.com/" rel="external">BBC Micro Bot</a> website which hosts a gallery of programs that produce impressive graphical output from very short BASIC programs (short enough to fit in a Tweet!)</p> <p>I tried a few of them out but unfortunately ran into problems with a lot of them that use various tricks to reduce the original program text length by embedding non-ASCII characters directly into the body of the program. My usual approach to prepare programs was either to copy and paste them into an emulator (my emulator strips out non-ASCII characters on pasting, as these can't be mapped to keystrokes) or to save them to a file and transfer that serially, and my own editor's tokeniser and BBC BASIC for Windows both had a habit of mangling the non-ASCII characters.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/bbcbasic_sms/bbcmicrobot-shell.jpg" class="lightbox"><img src="http://benryves.com/images/bbcbasic_sms/bbcmicrobot-shell.thumb.jpg" width="400" height="400" alt="TV showing a picture of a shell generated from a BASIC program" /></a></div> <p>The BBC Micro Bot website does not provide a way to download the BASIC programs directly but does provide a share link that allows you to export a disk image or play back the program from a virtual tape cassette. I don't have a a disk drive attached to my Master System, but the tape option seemed promising...</p> <h3>The basic cassette tape interface</h3> <p>My initial approach to loading programs into BASIC from external storage was to connect my Cambridge Z88 computer running <em>PC Link 2</em> or <em>EazyLink</em> to the Master System with an RS-232 serial cable. This works well for me, but isn't very practical for others who might not already own a Z88, and I do want this to be an easy project for people to replicate!</p> <p>Being able to load from tape would significantly lower the barrier to entry, if the cassette tape interface circuit can be constructed easily. You wouldn't even need an actual tape player; the drive belts have gone in mine (I tried to make a test recording on mine and it just unspooled the tape into the bowels of the machine) so for the time being I've been testing loading from <a href="https://www.8bitkick.cc/playuef.html" rel="external">PlayUEF</a>, the web-based UEF player that's also found on the BBC Micro Bot website. This can be run from a browser and it works well on my mobile phone and makes loading programs from a web link an absolute doddle. Perfect!</p> <p>The challenge, then, is how to get that audio data into the Master System in the first place and whether it's got enough grunt to be able to decode it in software. Using the same tape format as the BBC Micro seemed like it would be a good choice so I read <a href="http://beebwiki.mdfs.net/Acorn_cassette_format" rel="external">this article on the Acorn cassette format on BeebWiki</a>. The highest frequency audio signal is a sine wave at 2400Hz. As we'll need to handle both halves of each cycle we'll need to sample the signal at least 4800 times per second. As the Master System has a CPU clock of around 3.58MHz, that gives us 3.58&times;10<sup>6</sup>/4800&#x2248;756 CPU cycles for each half of the cycle which should be more than sufficient.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/bbcbasic_sms/tape-interface-prototype.jpg" class="lightbox"><img src="http://benryves.com/images/bbcbasic_sms/tape-interface-prototype.thumb.jpg" width="500" height="375" alt="Breadboard with the prototype tape interface on it" /></a></div> <p>We still need to get the audio signal into the Master System somehow! I did a bit of digging online and couldn't find too much information about suitable electrical interfaces. My best lead was the circuit diagram for the BBC Micro's tape interface, however that requires several op-amps and I only have a single LM741 in my parts bin, which isn't going to do a very good job on a single 5V supply. In the short term I've used the circuit above which is a slightly modified version of <a href="https://electronics.stackexchange.com/questions/270344/s-pdif-to-ttl-converter" rel="external">this common circuit used to convert S/PDIF signals to TTL levels</a> using an unbuffered hex inverter as an amplifier. It's not the most stable circuit and has a habit of oscillating when not fed an input signal but the +5V pullup on the <em>output</em> from the console inhibits this (putting a pull-up or pull-down on any of the inputs changes the mark:space ratio of the pulses). That a load on the output affects what's happening on the input is definitely not a good indication of a properly-functioning circuit, but it'll have to do for now.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/bbcbasic_sms/tape-interface-logic-analyser.jpg" class="lightbox"><img src="http://benryves.com/images/bbcbasic_sms/tape-interface-logic-analyser.thumb.jpg" width="600" height="338" alt="Logic analyser trace of the audio signal converted to square pulses" /></a></div> <p>I don't know how well it'll respond to a real tape cassette player, but the signal from my PC's sound card or my mobile phone produces very clean square pulses so it should be enough to start experimenting with until I can come up with more stable design.</p> <h3>Decoding the tape signal in software</h3> <p>To decode the signal we'll need to know whether the tape is outputting silence, a 1200Hz tone or a 2400Hz tone. I'm handling this with a simple routine that samples the current level of the input, then loops around waiting for the input level to change state, counting the number of times it takes to go around the loop before the transition. If the counter overflows before the state changes then it's assumed that the tape is outputting silence, but if the state changes before that we can check the value the counter reached to see if it got to a small value (a high frequency signal from the tape) or a large value (a low frequency signal from the tape).</p> <p>To determine a suitable value for the thresholds I wrote a test program that simply called this routine in a loop 256 times, storing its results in memory, before dumping the results to the screen once all 256 values had been found. By playing a section of the tape in the middle of a data block (so there would be a good mixture of 1200Hz and 2400Hz tones) I got the rather pleasing result that 1200Hz tones appeared to make the counter reach $20 (32) and 1200Hz tones made the counter reach $10 (16), so I put the threshold value between them at 24 as an initial test.</p> <p>Once we know the frequency of the tape signal at a particular point we can use this to determine the current bit &ndash; a "0" bit is a complete 1200Hz cycle (so we'd see two instances of the counter reaching &gt;16) and a "1" bit is two compete 2400Hz cycles (so we'd see four instances of the counter &lt;16). Knowing the bit on its own is not very useful, though, so I added byte decoding which waits for a start bit (0), receives and stores 8 data bits, then checks for a stop bit (1). I could then dump this data to the screen:</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/bbcbasic_sms/tape-data-dump.jpg" class="lightbox"><img src="http://benryves.com/images/bbcbasic_sms/tape-data-dump.thumb.jpg" width="400" height="400" alt="A data dump from the tape to the screen" /></a></div> <p>At this point the data isn't completely correctly decoded, but it always produces the same results on every run-through which to my mind was a good sign. The text displayed at the bottom of the screen shows some recognisable fragments of the original BASIC program (as the program is tokenised the keywords are missing). The main problem I was having was down to synchronisation; as the article on BeebWiki states "an odd number of 2400Hz cycles can and does occur" and these occasional cycles were throwing me out of sync. The fix was to add an extra check when handling 2400Hz; even though four 2400Hz half-waves are expected, if a 1200Hz half-wave is detected instead it's assumed that we've gone out of sync and the bit should be decoded as a complete 1200Hz pulse instead. After making this change more recognisable text started coming through, so I was able to move on to decoding the data blocks. <br /> <h3>Decoding data blocks on tape</h3></p> <p>Files on tape are broken up into multiple data blocks, each with a header describing the block (e.g. file name, block number, data length) followed by a variable amount of data (up to 256 bytes) containing the data itself. To load a file from tape each incoming block has to be handled. If the block number is 0 then that's the start of the file, so the filename is checked. If this matches the supplied filename (or the supplied filename was the empty string <tt>""</tt>) then the loader switches from "Searching" to "Loading" mode and the data we just received is stored in memory. From that point on each incoming block is appended, as long as the block name and number match what we expect (i.e. the same filename as before but the block number should be the previous block number + 1). This continues until the "end of file" block flag is set. A CRC-16 of the received data is also checked after each block to ensure that the data has been received successfully. If there are any errors (the block name/number doesn't match what was expected, or the CRC-16 check fails) then an error message is printed and you can rewind the tape a bit to try receiving the block again.</p> <p>This doesn't sound too complicated, but I've not had too much luck implementing this successfully. My code is currently a bit of a mess and doesn't properly maintain the "Loading" or "Searching" states which means that if an error occurs at any point then it can get a bit confused trying to resolve it (to the point where it's easier to just abort the load and start again from the beginning). However, the tape block issues are not the only problem... <br /> <h3>Conflicting BASIC program formats</h3></p> <p>The programs I'm trying to load are designed for the BBC Micro, where programs are stored in the Acorn or "Wilson" (after Sophie Wilson) format. However, BBC BASIC (Z80) was written by Richard Russell and therefore uses the "Russell" format. Both have a lot in common, fortunately but the way each line is stored in memory differs (this is covered in more detail in <a href="http://www.beebwiki.mdfs.net/Program_format" rel="external">the "Program format" article on BeebWiki</a>). Fortunately the formats are similar enough that it's possible to convert them reasonably trivially in-memory.</p> <p>My initial plan was to leave the loaded program as it is, and to provide a star command (e.g. <tt>*FCONVERT</tt>, named after the <a href="http://www.bbcbasic.co.uk/bbcbasic/manual/annexe.html" rel="external">FCONVERT.BBC program</a>) to convert from the "Wilson" format to the "Russell" format. Unfortunately, after loading the file BBC BASIC (Z80) checks the program and applies its own fixes to it, notably writing the appropriate terminator on the end of the program. As far as I can tell it does this by following the program along, line by line, until it finds what it believes is the end. In the "Russell" format the first byte of the line is its length, and in the "Wilson" format it's a carriage return (value 13). What I think this means is that BBC BASIC (Z80) thinks that the second line of an Acorn program should occur 13 bytes in, and as this isn't likely to be the case it ends up writing its terminator 13 bytes in to end the first line. This prevents me from fixing loaded programs after the fact, as they've already had part of their contents overwritten.</p> <p>Instead, I've added a check to the program after I've loaded it but before I've returned control to BBC BASIC. If I am able to read the program from start to end in "Wilson" format then I apply the conversion to "Russell" format automatically. In practice this means that both file formats load correctly, but I don't particularly like the way that if you <tt>LOAD</tt> a "Wilson"-format file then <tt>SAVE</tt> it back it'll be saved as a "Russell"-format file.</p> <p>There are still some differences in the formats and the way they're tokenised but usually that can be fixed by loading the problematic lines into the line editor (<tt>*EDIT</tt>) and pressing Return without making any changes. This forces them to be retokenised and has fixed some of the issues I've run into.</p> <p>Another issue is that "Wilson" format BASIC files support a line number 0, but "Russell" format ones do not; the file will be loaded and can be run, but if you try to <tt>LIST</tt> it then it'll stop when it reaches line 0 (which is usually the first one!) so programs appear to be empty. This is not something I think can (or indeed <em>should</em>) be fixed automatically but if necessary you can change the line number for the first line from 0 to 1 with <tt>?(PAGE+1)=1</tt> so there is at least one solution.</p> <div class="html_center" style="text-align: center;"><a href="https://www.youtube.com/watch?v=XlTgatmvI9o" rel="external"><img src="https://i.ytimg.com/vi/XlTgatmvI9o/maxresdefault.jpg" alt="" width="600" height="340" /></a></div> <p>The video above shows a couple of programs being loaded from the BBC Micro Bot website onto the Master System. Even with the terrible audio to digital interface it gets the job done! The <tt>7905=7905</tt> messages at the end of each block during loading show the CRC-16 calculated for the received data followed by the expected CRC-16, as they match it indicates the data is getting through successfully.</p> <h3>Emulating the tape interface</h3> <p>As alluded to above my current loading system works well enough when everything is OK but it gets a bit confused when there's a fault. I never really planned out how to handle this and the code's a bit of a spaghetti mess in places so I drew out a flowchart of a more robust loader, which itself is also a bit of a mess but hopefully one that will make loading tapes more user-friendly and robust.</p> <p>Unfortunately, doing any work on this interface has been very tedious as every time I make a change I need to try it on real hardware &ndash; I don't know of any Master System emulators that include tape support! I've been using my own <a href="https://github.com/benryves/Cogwheel">Cogwheel</a> emulator throughout which is itself not a very good emulator (there are no proper debugging tools, for example) but I can at least bolt on weird features I need like PS/2 keyboard emulation, so I thought it would be a good use of my time to add tape interface emulation. I started by writing a <a href="http://electrem.emuunlim.com/UEFSpecs.htm" rel="external">UEF (Unified Emulator Format)</a> parser that could turn a UEF image file into a 4800 baud bitstream that could be fed into the Master System's controller port. For some reason my files end up being slightly different lengths to the sound files generated by <a href="https://www.8bitkick.cc/playuef.html" rel="external">PlayUEF</a> &ndash; nothing major, but a handful of seconds over the length of a roughly 7 minute file. I tried loading the generated bitstream as a 4800Hz sample rate file into Audacity and it didn't quite line up with the audio file generated by PlayUEF either, but after poring over the specifications for the UEF format I was unable to figure out where the discrepancy lay.</p> <p>Undeterred I pressed ahead and added this simple cassette player to the emulator, which happily seems to work well in spite of the reported timing differences:</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/bbcbasic_sms/cogwheel-tape-loader.png" class="lightbox"><img src="http://benryves.com/images/bbcbasic_sms/cogwheel-tape-loader.thumb.jpg" width="600" height="386" alt="Cassette recorder interface in the Cogwheel emulator" /></a></div> <p>One thing that's notable there is that the program <tt>CIRCLE2</tt> appears in the catalogue on-screen. When I first added the cassette interface emulation it was missing, but it would not be detected on real hardware either, which made me quite happy! Being able to more rapidly make and test changes to the tape loader code I was able to experiment and found that the tape loader seemed to miss the synchronisation byte ($2A) sent at the start of block headers on the <tt>CIRCLE2</tt> program. This seemed to be due to going out of sync with the bit stream in the period between detecting the carrier tone and the first start bit, so I changed the code to demand exactly two 1200Hz half-waves as the start bit rather than using my more lax code that decodes the contents of the main bit stream that allows for extra 2400Hz pulses. This ensures that the start bit is always properly synchronised, and after making this change <tt>CIRCLE2</tt> appeared in the catalogue as it should. I then reflashed the ROM chip on the cartridge and tried on real hardware, and that now picks up <tt>CIRCLE2</tt>. Being able to fix a problem that appears on hardware by replicating and resolving the same issue in software emulation justifies the few hours I spent adding the cassette tape emulation!</p> <p>Unfortunately, I'm down to under a hundred bytes of free program space on the 32KB ROM I've been developing with, and I still have a lot of features I'd like to add (including the more resilient tape loader). I've been trying to put this moment off as long as possible as I'm going to have to use bank switching to map in additional memory banks, and that means a lot of difficult decisions about what code lives on which ROM pages to minimise the amount of switching that's required. Conventionally slot 2 is used as the slot to map different banks into but at the moment that's where I've mapped in the cartridge RAM to extend BASIC's memory. On the plus side very little of my code needs direct access to BASIC's memory (I mostly use my own data storage or the stack which lives at the top end of memory, far above slot 2) so this would seem like a good fit but I currently allocate storage space in BASIC's free memory for some operations (e.g. as temporary space when copying data around the screen during scrolling operations) so I'll need to change that code to use memory allocated elsewhere. It's not the most glamourous part of the project, but it'll need to be done if I am to add all the features I'd like.</p> Mon, 16 Aug 2021 01:35:32 +0100 Patterned fill modes for the Master System version of BBC BASIC http://benryves.com/journal/3763170 http://benryves.com/journal/3763170 <p>One of the features I was quite happy with in the TI-83 Plus version of BBC BASIC was the dithered fills used to provide some semblance of different colours. Sixteen different patterns were provided between black and white:</p> <div class="html_center" style="text-align: center;"><img alt="2009.01.21.01.gif" src="http://benryves.com/bin/bbcbasic/2009.01.21.01.gif" style="width:192px;image-rendering:crisp-edges;" /></div> <p>As well as baking in the sixteen different dithered patterns I added a command that let you use your own pattern tile in a very hacky method (you'd allocate the memory for the pattern via <tt>DIM</tt> and then pass the address of that memory to <tt>GCOLPAT</tt> &ndash; the extra "PAT" statement after <tt>GCOL</tt> is what did the trick).</p> <p>What I didn't realise at the time is that the BBC Micro also had support for patterned fills via its Graphics Extension ROM and a much more natural way to interact with it &ndash; <tt>GCOL&nbsp;16,0</tt>, <tt>GCOL&nbsp;32,0</tt>, <tt>GCOL&nbsp;48,0</tt> and <tt>GCOL&nbsp;64,0</tt> let you select one of four pattern slots, and you can redefine the patterns with <tt>VDU&nbsp;23,2,&hellip;</tt> to <tt>VDU&nbsp;23,5,&hellip;</tt> followed by 8 bytes of pattern data, similar to how user-defined characters are already programmed.</p> <p>Armed with the Graphics Extension ROM's user manual and sample programs I went ahead and implemented these patterned fills in my four- and sixteen-colour modes. Here's the output of the "shades" program, which runs in a four-colour mode and generates dithered fill patterns for the triangle to simulate different shades:</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/shades-1.png" width="256" height="192" alt="Screenshot showing a triangle with dithered fill patterns to simulate additional colour shades" /> &nbsp;&nbsp;&nbsp; <img src="http://benryves.com/images/bbcbasic_sms/shades-2.png" width="256" height="192" alt="Screenshot showing a triangle with dithered fill patterns to simulate additional colour shades" /></div> <p>Depending on the number of colours of the screen mode you're currently in the 8&times;8 bitmap used as a fill pattern is interpreted differently; in the 16-colour mode it represents a 2&times;8 pattern that is repeated four times horizontally, in 4-colour mode it represents a 4&times;8 pattern that is repeated twice and in 2-colour modes it's a complete 8&times;8 pattern with no repetition. I like the idea of these 8&times;8 patterns so added a new two-colour video mode, mostly based on the existing four-colour mode but with the palette limited to two colours and the revised rules on interpreting fill patterns. This produces results like the following from the "pattern" demo program:</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/pattern-1.png" width="256" height="192" alt="Screenshot of several different fill patterns in the new two-colour mode" /> &nbsp;&nbsp;&nbsp; <img src="http://benryves.com/images/bbcbasic_sms/pattern-2.png" width="256" height="192" alt="Screenshot of several different fill patterns in the new two-colour mode" /></div> <p>I've also implemented <tt>VDU&nbsp;23,11,&hellip;</tt> (it resets the four patterns to their initial values) and <tt>VDU&nbsp;23,12,&hellip;</tt> to <tt>VDU&nbsp;23,15,&hellip;</tt> &ndash; these are helper commands that let you populate a fill pattern from a 2&times;4 list of colour values that is then repeated across the specified pattern number regardless of the current screen mode and without you needing to perform the bitwise arithmetic to pack the different colour values into memory properly. You may have also noticed that the four patterns are spaced sixteen <tt>GCOL</tt> numbers apart from each other (16, 32, 48, 64) &ndash; this is because you can use these pattern fills in conjuction with the <a href="http://benryves.com/journal/3763169">logical plotting modes</a>, e.g. <tt>GCOL 16+1,0</tt> will <tt>OR</tt> the pattern over the existing screen contents.</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/patterned-text.png" width="256" height="192" alt="Screenshot of text with a pattered fill applied" /></div> <p>These pattern fills apply to all graphics operations, so if I break out of the shaded triangle program above when it's settled on a particularly garish palette and switch to outputting text as graphics with <tt>VDU&nbsp;5</tt> I get results like the above. I'm not sure how practical or useful that would be but it does at least show how flexible the graphics plotting system is.</p> Tue, 10 Aug 2021 23:05:42 +0100 Better plotting modes, text anywhere on the screen and double-height characters for Sega Master System BBC BASIC http://benryves.com/journal/3763169 http://benryves.com/journal/3763169 <p>I've made quite a few changes to the way colours and plotting modes are handled in the Sega Master System version of BBC BASIC; previously this was handled on a per-graphics-mode driver basis, but this resulted in a lot of duplicated code and some inconsistencies. The new code is a bit more straightforward but also adds quite a few new features, most obviously different plotting modes:</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/colour-blend-or.png" width="256" height="192" alt="Screenshot showing three colours blended together with an OR plotting mode" /><BR > <small>Plotting with the OR mode</small></div> <p>The above screenshot shows the result of filling three circles with the OR plotting mode. The red circle is colour 9, the blue one is colour 10 and the green one is colour 12. Where the colours overlap they are ORed together when drawing, so for example red OR blue is 9 OR 10 which is 11, the colour code for magenta, hence the colour between the two is magenta. Where all three colours mix, that's 9 OR 10 OR 12 which is 15, the colour code for white.</p> <p>It's the <em>logical</em> colours (the palette indices) that are combined by these plotting operations, rather than the RGB values, it just happens that the stock palette lines up neatly with the RGB values!</p> <p>Another addition I've made is support for <tt>VDU&nbsp;5</tt>. In normal operation, text sent to the display is displayed at the text cursor position. In <tt>VDU&nbsp;5</tt> mode the text is instead drawn at the graphics cursor position, which allows text to be drawn anywhere on the screen, outside the confines of its usual grid. It also takes into account the graphics plotting modes mentioned above and so you can blend text with other graphics operations. Here's an example:</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/wavetext.png" width="256" height="192" alt="Screenshot showing text drawn in a wavy fashion on the screen" /></div> <p>The somewhat blurry text has been drawn in two passes slightly offset from one another, once with colour 1 and again with colour 2, ORed together so that where the text overlaps you get colour 3, which is white. The program for this is as follows:<br /> <div class="source"><pre> 10 REM Draw wavy text in a watery style. 20 MODE 2 30 REM Set up a suitably watery palette. 40 COLOUR 0,0,0,64 50 COLOUR 1,0,64,128 60 COLOUR 2,0,128,255 70 COLOUR 3,255,255,255 80 REM Draw text at the graphics cursor position. 90 VDU 5 100 REM How many lines of text will we draw? 110 RESTORE 120 L%=-1 130 REPEAT READ L$ 140 L%=L%+1 150 UNTIL L$="" 160 REM Draw in two passes, slightly offset, in OR mode for a blurry effect 170 FOR C%=1 TO 2 180 GCOL 1,C% 190 RESTORE 200 Y%=(960+L%*50)/2 210 REPEAT READ L$ 220 IF LEN(L$)>0 AND L$&lt;&gt;"-" PROCWAVES(L$, C%*4, Y%-C%*4) 230 Y%=Y%-50 240 UNTIL L$="" 250 NEXT C% 260 REM Restore normal text operation. 270 VDU 4 280 REM Wait for a key then exit. 290 REPEAT UNTIL INKEY(0)=TRUE 300 REPEAT UNTIL INKEY(0)&lt;&gt;TRUE 310 END 320 REM Draw some wavy text at X%,Y% 330 DEFPROCWAVES(TEXT$,X%,Y%) 340 LOCAL I% 350 X%=X%+(1280-(LEN(TEXT$))*35)/2 360 FOR I%=1 TO LEN(TEXT$) 370 MOVE X%,Y%+20*SIN(X%/80+Y%/160) 380 X%=X%+35 390 PRINT MID$(TEXT$,I%,1); 400 NEXT I% 410 ENDPROC 420 REM Text strings to display. 430 DATA "VDU 5 can be used to place text" 440 DATA "anywhere you like on the screen," 450 DATA "not just aligned to the main" 460 DATA "text grid." 470 DATA "-" 480 DATA "This is fun, isn't it?" 490 DATA</pre></div><br /> When you're finished you can use <tt>VDU&nbsp;4</tt> to return to the normal text mode. In the video modes where I've had enough free VRAM to spare for it I've also added support for user-defined characters. You can send 23, then a character number, then eight bytes of pixel data for the 8&times;8 character bitmap to the VDU and then use that data for subsequent text operations (the characters from &amp;80..&amp;FF can be redefined). These user-defined characters can also be used as 8&times;8 sprites when combined with <tt>VDU&nbsp;5</tt>.</p> <p>I'm having quite a lot of fun writing my own test programs but it's also been useful to be able to run existing software as a test. I've tried to improve my sound handling and it now sounds a bit more faithful to the BBC Micro (though the Master System is a slower machine, so it can't quite keep up with more adventurous pieces of music).</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/bbc-disc-system.png" width="256" height="192" alt="Screenshot showing the BBC Disc System Welcome screen" /> &nbsp;&nbsp;&nbsp; <img src="http://benryves.com/images/bbcbasic_sms/bones.png" width="256" height="192" alt="Screenshot showing the 'Bones' demo" /><BR > <small>The <em>BBC Disc System</em> "Welcome" screen and the <em>Bones</em> demo</small></div> <p>With the new graphics plotting modes and <tt>VDU&nbsp;5</tt> support the BBC Disc System "Welcome" program runs, for example, though it looks a bit rough around the edges due to mismatches in the video modes. <em>Bones</em> also runs, however the mismatch is even more severe here and half way through the skeleton turns red as it's running in the four-colour video mode and <tt>COLOUR&nbsp;1</tt> is mapped to red, not the white in the monochrome mode the demo is expecting. The misalignment comes from differences in the number of rows and columns of text that are available; the demo mixes graphics commands (which are mapped reasonably closely to the BBC Micro's screen) and text; if the demo expects the screen to be 40 columns wide and the best I can offer with this hardware is 32 columns, it won't line up! Of course, the BASIC program can be tweaked to get it working properly, but it's fun to see just how close it gets without any tweaking...</p> <p>One recurring issue, however, is with programs that expect to be able to use Teletext features. The BBC Micro's default video mode used a Teletext character generator, which allows for bright colourful text and some limited graphics support in very little memory. I had previously mapped this mode to the TMS9918A text mode, which allows for 40 characters in 24 rows, just one row short of the BBC Micro's 40&times;25. It's a fast mode and is good for program editing due to the large number of text characters on screen at a time (all other modes are 32&times;24) however it is very limited - there's no support for colour, for example, other than the global foreground and background colours.</p> <p>To provide some colour support I loaded the character set into memory twice, once normally and once inverted, so via the <tt>COLOUR</tt> statements it's possible to inject some variety into this mode. The BBC Micro's Teletext <tt>MODE&nbsp;7</tt> doesn't support the <tt>COLOUR</tt> statement, it relies on embedding special control codes in character positions on the screen that affects the rendering of the rest of the text in the line &ndash; for example, a command might change the colour, or cause the rest of the line to flash. As I can't possibly support such codes, I just ignore them, and the programs run fine but look a bit bland.</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/duplicated-text.png" width="256" height="192" alt="Screenshot showing double-height text being duplicated" /></div> <p>The main exception to this, I've found, is the double-height text mode. You can switch this on within a line by storing the value 141 in a character cell and then off again with 140 afterwards. This will only display the top halves of the characters, so to display the bottom halves you need to output the same content on the line below. A <em>lot</em> of BBC Micro programs seems to take advantage of this feature, so most programs I'd tried ended up having doubled-up text like you can see in the screenshot above.</p> <p>The character set I'm using is only 96 characters long (32-127) but I had avoided investigating this as I assumed I'd need to triple the number of characters - the regular-height ones, plus the top and bottom halves of the doubled characters. 96&times;3=288 which is more than the 256 characters we have available, so I didn't think this would fit.</p> <p>However, I did think that maybe some characters shared parts - for example, the top halves of the colon and semicolon are the same, and the bottom halves of the semicolon and comma are the same, so I wrote a quick program to count how many unique patterns would be needed and found that it would only take 225 patterns in total, well within our 256 pattern budget!</p> <p>I set up a new video mode based on the original TMS9918 text mode. This generates these 129 additional patterns for the double-height characters when initialised, and also checks the nametable when outputting characters to see if there's a "double height" control character in the same line to determine whether it should pick one of the stretched characters (picking a top or bottom half depending on whether there are an even or odd number of lines above it that also contain the double height control character).</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/double-height-text.png" width="256" height="192" alt="Screenshot showing double-height text being stretched" /></div> <p>This new mode does lose the inverted text, but I think the double-height text will be more useful in practice &ndash; or at least is better at making programs look less broken!</p> Mon, 09 Aug 2021 03:57:23 +0100 A four colour any-pixel-addressable video mode for the Master System http://benryves.com/journal/3763168 http://benryves.com/journal/3763168 <p>The video display processor in the Sega Master System is derived from the TMS9918A and provides a number of different screen modes. These modes are mostly character-based and generate the picture that is displayed on your TV based on two blocks of data in video RAM: the name table, which specifies which character appears in each cell on the screen and the pattern generator which contains the pixel data for each character.</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/text.png" width="256" height="192" alt="Example of TMS9918A Text output" /><BR > <small>TMS9918A "Text" mode</small></div> <p>The "Text" mode is probably the most straightforward example. Only two colours can be displayed on the screen; a foreground and a background colour, both selected from a fixed palette of 15 colours. The name table is 40 cells wide and 24 tall. Each cell, or text position, is a single byte that refers to one of 256 patterns in the pattern generator, so the complete name table is 960 bytes long. Each character is displayed as 6&times;8 pixels but the data in the pattern generator is eight pixels wide, and each row is stored as a single byte with a set bit selecting the foreground colour and a cleared bit selecting the background colour (the two least-significant bits are ignored). Each character pattern therefore takes up 8 bytes of VRAM, or 2,048 bytes (2KB) for a complete set of 256.</p> <p>The "Graphics I" mode changes the character size to be the full 8&times;8 pixels at the expense of the number of characters that can be displayed per line; this is reduced from 40 to 32, so the name table is now 32&times;24 and 768 bytes long. The pattern generator is the same size as before and you can still only display 256 unique patterns on-screen at a time, but there is at least some provision for colour. The top five bits of the character/pattern number are used as an index into a 32-byte colour table, where each colour table entry contains two four-bit values corresponding to the foreground and background colours for the pattern. This means that patterns 0-7 share the same two colours, patterns 8-15 have the next set of two colours, 16-23 after that and so on; 256 patterns, in groups of eight with a different pair of colours assigned to each of those 32 groups.</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/cones.png" width="256" height="192" alt="Example of TMS9918A Graphics II output" /><BR > <small>TMS9918A "Graphics II" mode</small></div> <p>Fortunately, the TMS9918A is a revised version of the original TMS9918 which adds the much more useful "Graphics II" mode. The name table is the same as before, 32&times;24 cells of one byte each, and the patterns are still 8&times;8 pixel bitmaps. However the pattern generator table is now three times the size, storing 768 unique 8&times;8 patterns in 6KB. To be able to address all 768 patterns when you can only store values from 0-255 in the name table the screen is divided into three sections; the top third references the first 2KB of the pattern generator (patterns 0-255), the middle third references the second 2KB of the pattern generator (patterns 256-511) and the bottom third references the last 2KB of the pattern generator (patterns 512-767). This allows every single character position in the nametable to reference a unique pattern, and therefore have a bitmap that covers the entire screen area.</p> <p>The colour table has also been significantly improved. Instead of one pair of colours for each group of eight patterns, each pattern now gets its own table of colour pairs, one per row. This doesn't allow every pixel on the screen to have its own colour, but you can at least give each 8&times;1 region its own pair of colours. This does lead to attribute clash when drawing; you can see this in the screenshot of the cones above, where the colour from some lines bleeds into the colour set for nearby lines if they happen to pass through the same 8&times;1 pixel block.</p> <p>Very few Master System games use the TMS9918A video modes (<em>F16 Fighting Falcon</em> seems to be about the only concrete example), instead using a new video mode added specifically for the Master System. This has a number of nice new features (such as hardware scrolling and two user-definable 16-colour palettes instead of a fixed 15-colour one), but it's the name table and pattern generator that's of main interest. Instead of having a separate bitmap for the pattern and its colour table, each pattern is now a 32 byte object where each 8 pixel row is encoded as four bytes, one byte for each bit of the four-bit colour palette index. That is, if you wanted to store the top row of a pattern where the leftmost pixel was set to palette index 1, the rightmost pixel was set to palette index 15 and everything else was left as 0 it would be stored like this:<br /> <div class="source"><pre>.db %10000001 .db %00000001 .db %00000001 .db %00000001</pre></div><br /> The second row would then follow with another four bytes, all the way down to the bottom row for the complete 32 byte pattern. This scheme allows you to set each pixel in every pattern to any of the sixteen colours per palette, so if you could fill the screen with such patterns then it would be possible to set any pixel on the screen to any of sixteen colours, solving the attribute clash problem. Unfortunately, to do so would take 768 patterns (32&times;24), and if each pattern is 32 bytes then that would take up 24KB of video RAM. The Master System only has 16KB of video RAM, which would limit you to 512 patterns, so it's not possible to fill the screen with unique patterns. In practice you can't even have 512 patterns, as you still need to store the name table in video RAM, and as each name table entry has been inflated to two bytes (which does at least let you index pattern numbers above 255 without splitting the screen into thirds as per the Graphics II scheme) that leaves even less room for patterns.</p> <p>In practice you are limited to 448 patterns, which is not enough to cover the screen. As you will generally start from a blank screen and then add to it, the name table could be filled with blanks by default and then as you draw over the screen of it new patterns could be allocated as required. This is how the graphics currently work in the 16-colour Master System video mode, and it allows for colourful drawings with no colour clash as long as you don't fill too much of the screen.</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/sphere.png" width="256" height="192" alt="Example of 16-colour Master System output" /><BR > <small>Cautious use of the 16-colour Master System mode</small></div> <p>That's the mode the sphere above is rendered in; you can see there's fine detail at its North pole without becoming subject to colour clash and it doesn't fill so much of the screen that we ran out of unique patterns to allocate. As it would be very bad to run out of patterns when attempting to draw text the character set (the 96 characters from 32-127) are permanently allocated as unique patterns which further reduces us to 352 tiles for graphics output.</p> <p>It's possible to constrain the graphics viewport via <a href="http://www.bbcbasic.co.uk/bbcwin/manual/bbcwin8.html#vdu24" rel="external"><tt>VDU&nbsp;24</tt></a> to a smaller region which would ensure you never needed more than 352 tiles, as long as your new viewport was properly aligned to the 8&times;8 grid. For example, 22&times;16=352 which means that you could have a 176&times;128 pixel graphics window that you could draw to without risking running out of patterns.</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/clash-graphics-ii.png" width="256" height="192" alt="Screenshot of colour clash in Graphics II" /> &nbsp;&nbsp;&nbsp; <img src="http://benryves.com/images/bbcbasic_sms/clash-mode4.png" width="256" height="192" alt="Screenshot of running out of patterns in Master System mode" /><br /> <small>Colour clash in Graphics II (left) versus running out of tiles in Master System Mode (right)</small></div> <p>The two screenshots above show the trade-off between colour clash in Graphics II and corruption due to running out of patterns in the 16-colour Master System graphics mode, both caused by running this program:<br /> <div class="source"><pre> 10 INPUT "Mode",M% 20 MODE M% 30 FOR I%=0 TO 47 40 GCOL 0,I% 50 X%=(I%/47)*1279 60 Y%=(I%/47)*959 70 MOVE X%,0 80 DRAW 1279,Y% 90 DRAW 1279-X%,959 100 DRAW 0,959-Y% 110 DRAW X%,0 120 NEXT I% 130 REPEAT:UNTIL INKEY(0)=-1 140 REPEAT:UNTIL INKEY(0)&lt;&gt;-1</pre></div><br /> Having to worry about your pattern budget isn't really in the spirit of fun and experimentation. If we're up against hardware and memory limitations I'd rather have a slightly less capable video mode that does at least generate correct output however you use it.</p> <p>It is possible to completely avoid colour clash if you use the Graphics II mode and simply never use any colour commands, but only having two colours is a bit drab. With a bit of tinkering I came up with another graphics mode that is still using the Master System mode, but reduces the number of colours down to 4. Quite fittingly, a lot of BBC Micro screen modes only supported four physical colours, so I set this new mode to use the same black/red/yellow/white palette by default:</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/no-clash-mode4.png" width="256" height="192" alt="Example of 4-colour Master System output" /><BR > <small>Four colours on-screen at once, no clash and no corruption</small></div> <p>In fact, the palette is the whole key to this solution. You may recall that the Master System mode supports two 16-colour palettes. Usually one is assigned as the "background" palette and the other as the "sprite" palette, however each entry in the name table contains a flag that lets you specify which palette the selected pattern should be shown with. In this new video mode, the top half of the screen is assigned to patterns 0-383 using the "background" palette, and the bottom half of the screen is assigned to the same set of patterns 0-383 but to use the "sprite" palette. The palette is then filled with four colours, like this: </p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/reduced-palette.png" width="416" height="80" alt="Illustration of the reduced colour palette" /><BR > <small>Using all 32 palette entries to display four colours on screen simultaneously</small></div> <p>The top row is the "background" palette and shows the four desired colours repeating in order four times. The bottom row is the "sprite" palette, and shows that each of the four colours is stretched to fill four consecutive palette entries. This use of repetition effectively turns two bits in each pattern's bitplanes into "don't care" values, depending on which palette is selected.</p> <p>For example, assume we're in a pattern assigned to the "background" (top) palette and we want to display red. This is colour %01, but as the red entry is repeated we could use use any of colours %0001, %0101, %1001 or %1101 and they'll all come out red &ndash; we don't care about the top two bits.</p> <p>Alternatively, we could be in a pattern assigned to the "sprite" (bottom) palette and want to display yellow. This is colour %10, but it fills the entire palette block from %1000 to %1011, so again we don't care what the lower two bits are, just what the top two bits are.</p> <p>Using this somewhat redundant palette, we can pack two different four-colour patterns for different parts of the screen into a single sixteen-colour pattern. It's a shame that a halving in bit count ends up quartering the number of colours but I think it's an acceptable solution considering the hardware limitations.</p> Wed, 04 Aug 2021 22:53:46 +0100 Better drawing, editing, memory and emulation for BBC BASIC on the Sega Master System http://benryves.com/journal/3763167 http://benryves.com/journal/3763167 <p>I have continued to work on the Sega Master System version of BBC BASIC, and it's feeling much more like a practical version of BASIC than something that was just holding together to run one specific program!</p> <p>One of the key improvements is to standardise the handling of the different graphics modes. Previously I was using a coordinate system where (0,0) is in the top left corner and all drawing operations were carried out using physical device coordinates (so the screen was treated as being 256 pixels wide and 192 pixels tall). I have now moved the origin (0,0) into the bottom left corner with the Y axis pointing up and scale down all coordinates by 5, effectively making the screen 1280 logical units wide and 960 units tall. This isn't 100% compliant, as the BBC Micro and other versions of BASIC treat the screen as being 1024 units tall, but dividing by 5&#x2153; is considerably trickier and it would result in the graphics being squished further vertically, so I think using a logical resolution of 1280&times;960 is an acceptable compromise. I've added some <tt>VDU</tt> statements to allow you to move the graphics origin as well as define custom graphics and text viewports, so you can control where on the screen graphics and text appear and how they are clipped/scroll.</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/mandelbaum.png" width="256" height="192" alt="Screenshot of the Mandelbaum program output" /><BR > <small>The output of the "Mandelbaum" program following these changes</small></div> <p>I have also changed the default palette to more closely match the one used by the BBC Micro and other versions of BBC BASIC. This isn't too difficult when using the Master System's "Mode 4" as that has a user-definable palette, but the legacy TMS9918A modes have a fixed palette so I've tried to match the default palette as sensibly as I can to the TMS9918A palette. It's possible to change the logical to physical palette mappings under Master System "Mode 4" via a <tt>VDU</tt> command which writes directly to the CRAM (you can either remap one of the 16 logical entries to one of the stock 16 colours, or supply an RGB value directy to select additional colours) which allows for neat tricks like palette cycling, but the TMS9918A modes currently only let you change the current text/drawing colour, not amend the palette as that's fixed in hardware.</p> <p>I've also added filled/outlined circle and axis-aligned ellipse <tt>PLOT</tt> commands using some code written by Darren "qarnos" Cubitt which I originally used in the TI-83 Plus version of BBC BASIC. This code is very capable and fully accepted 16-bit coordinates for its inputs, however it was also originally designed to output to a 96&times;64 pixel screen so the final plotting was done with 8-bit coordinates ranging from -128..127. Fortunately the Master System's screen also fits in 8-bit coordinates at 256 pixels wide but that's not quite enough information as you also need to be able to tell if a particular point is off-screen (less than zero or greater than 255); simply clipping it against those boundaries will result in a vertical smear on the left or right edge of the screen when drawing outlines. Fortunately I was able to figure out how to modify his code to add some extra clipping status flags to ensure that ellipses were clipped and displayed correctly on any part of the screen.</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/filled-circles.png" width="256" height="192" alt="Screenshot showing filled circles" /> &nbsp;&nbsp;&nbsp; <img src="http://benryves.com/images/bbcbasic_sms/cones.png" width="256" height="192" alt="Screenshot showing filled cones" /><br /> <small>Filled circles and filled/outlined ellipses make these drawings possible</small></div> <p>The only graphics operation exposed by the mode-specific drivers before was a simple "set pixel" routine. This is fine for lines but quite slow for filling shapes so graphics mode drivers can now supply a "fill horizontal span" routine for faster shape-filling. If the routine is left unimplemented a stub routine is provided that fills the span using the "set pixel" routine.</p> <p>I also added a rectangle-filling <tt>PLOT</tt> command, which is perhaps not the most exciting-sounding graphics operation but it is used to clear the screen so it is at least useful. More interesting is a triangle-filling routine, something I've never enjoyed writing!</p> <p>Usually I get very bogged down in the idea that the pixels affected when you fill a triangle should exactly fit within the pixels that are outlined by drawing lines between each of the triangle's points, no more and no less. This can be a bit difficult when the triangle is filled by tracing its edges from top to bottom and drawing horizontal spans between them. If the edge is "steep" (its overall height is greater than or equal to its width) then this isn't too bad, as there's only one X coordinate for each Y coordinate where a pixel would have been plotted. However, when the edge is "shallow" (its overall width is greater than its width) there are going to be certain Y coordinates where the line drawn would have had multiple pixels plotted. In that case, where is the boundary of the horizontal span?</p> <p>The cop-out answer I've used in the past has been to set up three buffers the total height of the screen and to "draw" the three lines first using the same line-drawing algorithm as the line <tt>PLOT</tt>ting command, keeping track of the minimum and maximum X coordinate for each Y coordinate. When it's time to fill the triangle the minimum and maximum X coordinate for each edge can be determined based on the current Y coordinate and a span drawn between them for perfect triangles. On the TI-83 Plus this takes up four bytes per line (minimum and maximum 16-bit values) for a 64 pixel tall screen, with three buffers for the three lines that comes to 4&times;64&times;3=768 bytes, pretty bad. On the Sega Master System that would be 4&times;192&times;3=2304 bytes, totally unacceptable on a machine with only 8KB total work RAM!</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/sphere.png" width="256" height="192" alt="Screenshot of a 3D sphere rendered by BBC BASIC" /><br /> <small>Each face of this 3D sphere is filled by two triangles.</small></div> <p>I've instead simply done my best to interpolate from one X coordinate to the other when working my way down the triangle and filling scanlines, doing a bit of extra pre-incrementing and fudging of initial error values depending on whether it's the top half or bottom half of the triangle. My test was to draw the outline of a triangle in bright white and then to fill a dark blue triangle over the top, if any bright white pixels were visible around the outside this indicated a gap. I mostly got it filled, but I then tried my test program on a BBC Micro emulator and found the BBC Micro exhibited similar gaps so I don't think I'm doing too badly! The above screenshot of the 3D sphere was rendered using this triangle-filling code.</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/pront.png" width="256" height="192" alt="Screenshot showing buggy BASIC code with a PRONT instead of a PRINT statement" /></div> <p>I've also been working on improving the line editor. This is called by BASIC when it's asking for a line of text input from you. Previously I'd only implemented adding characters to the end of the line and pressing backspace to remove characters from the end of the line; if you'd typed in a long piece of code and made a mistake at the start you'd need to backspace all the way to the mistake, correct it, then re-type the rest of the line. Now you can use the cursor keys (or home/end) to move around within the line, insert or overwrite new characters (toggled by pressing the insert key on the keyboard) at the cursor position or backspace/delete characters mid-line as required. It sounds like a small thing but it was quite a lot of code to get right and makes a big difference for usability!</p> <p>Another feature I've added to aid modifying lines of code is the <tt>*EDIT</tt> command. This takes a line number as an argument and then brings up the program line you've requested in the line editor, ready for editing. The way this works is a bit sneaky, as it's not natively implemented by BBC BASIC! The trick that makes it work is the ability to override two routines, <tt>OSLINE</tt> (which BASIC calls when it wants to display the line editor) and <tt>OSWRCH</tt> (which BASIC calls when it wants to output a character).</p> <p>When a valid <tt>*EDIT&nbsp;&lt;line&gt;</tt> command is entered, <tt>OSLINE</tt> is overridden with the first custom routine. This routine doesn't ask for a line of input, but instead copies <tt>L.&lt;line&gt;</tt> to the input buffer, overrides <tt>OSWRCH</tt> with a routine that captures data to RAM, overrides <tt>OSLINE</tt> with a second custom routine, then returns. BASIC therefore thinks you've typed in a <tt>LIST</tt> statement so it dutifully starts outputting the line via <tt>OSWRCH</tt>, but this has been overridden to write the characters to RAM rather than the screen. When it's done this BASIC then calls <tt>OSLINE</tt> again for the next line of input, which brings up the second custom <tt>OSLINE</tt> handler. This pulls the data from RAM previously captured by the <tt>OSWRCH</tt> handler, copies it to the <tt>OSLINE</tt> buffer, and dispays it on-screen as if you'd just typed it in. It then restores the original <tt>OSLINE</tt> and <tt>OSWRCH</tt> handlers before jumping back into the stock <tt>OSLINE</tt> routine, so you can continue editing the line that you'd requested via <tt>*EDIT</tt>.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/bbcbasic_sms/monopoly-cartridge.jpg" class="lightbox"><img src="http://benryves.com/images/bbcbasic_sms/monopoly-cartridge.thumb.jpg" width="500" height="375" alt="A modified Monopoly cartridge, used to run BBC BASIC with an 8KB RAM expansion." /></a></div> <p>All of this hopefully makes entering programs via the keyboard less cumbersome. Of course, not having to type in programs in full every time you wanted to run them would be even better, and an attempt to give BASIC more memory provides another way to load programs in a somewhat roundabout manner.</p> <p>The photograph above shows the modified <em>Monopoly</em> cartridge that I'm now using to test BBC BASIC on real hardware instead of <a href="http://benryves.com/journal/3763154">the modified <em>After Burner</em> cartridge</a> I was using before. The advantage of <em>Monopoly</em> is that it has an additional 8KB RAM on board, which is used to save games in progress. Sega's cartridge mapper allows for on-cartridge RAM to be mapped into the address range $8000..$BFFF, immediately below the main work RAM which is at $C000..$DFFF. If present, then, BASIC's memory range (which runs from <tt>PAGE</tt> to <tt>HIMEM</tt>) can be extended by enabling the save RAM and moving <tt>PAGE</tt> down.</p> <p>$8000..$BFFF is a 16KB range, though, and I mentioned that <em>Monopoly</em> has an 8KB RAM. This is true, and what it means is that the 8KB cartridge RAM is accessible from $8000..$9FFF but is then repeated ("mirrored") from $A000..$BFFF. The cartridge RAM detection therefore has to check two things: firstly that there is RAM in the first place (which can be verified by modifying memory at $8000 and seeing if those values stick) and how big it is (which can be checked by writing to $8000 and seeing if that has also modified the data at $8000+&lt;RAM size&gt;). Once presence of any RAM has been determined during startup, RAM mirroring is checked in 1KB, 2KB, 4KB and 8KB offsets. If any RAM mirroring is detected, then it's assumed the RAM is the size that was being checked at the time, however if not it's assumed that the RAM is the full 16KB. At this point, <tt>PAGE</tt> (which is $C000 in a stock machine with no cartridge RAM) is moved backwards by the size of the detected cartridge RAM, e.g. to $A000 for an 8KB RAM and $8000 for a 16KB RAM. This results in 16KB or 24KB total available to BBC BASIC, a considerable upgrade from the plain 8KB work RAM!</p> <p>An added bonus of this is that when you type in programs they grow upwards in memory from <tt>PAGE</tt>. As <tt>PAGE</tt> now starts within your cartridge memory, and that cartridge memory retains its contents courtesy of a backup battery, it means that if your entered program is smaller than the size of your cartridge memory you can restore it when you switch the console back on by typing <tt>OLD</tt>.</p> <p>That's very well for life on real hardware, but I continue to do most of my development testing in an emulator. I'm having to use my own emulator as there aren't any other Master System emulators that also include a PS/2 keyboard emulator, but I did end up running into a very weird bug. Certain trigonometric functions in BBC BASIC were producing very wrong values, resulting in very odd-looking output. </p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/images/bbcbasic_sms/wonky-cube.png" width="256" height="192" alt="Screenshot showing wonkily-rendered cube" /><img src="http://benryves.com/images/bbcbasic_sms/wonky-mandelbaum.png" width="256" height="192" alt="Screenshot showing wonkily-rendered Mandelbaum set" /><img src="http://benryves.com/images/bbcbasic_sms/wonky-sphere.png" width="256" height="192" alt="Screenshot showing wonkily-plotted sphere" /><br /> <small>These shapes are on the wonk</small></div> <p>Even though the Z80 emulator at the heart of the program passed ZEXDOC (an instruction tester that checks documented functionality) I remembered that it had failed some aspect of ZEXALL which checks the undocumented flags too. I re-ran ZEXALL and found the problem was with my implementation of the <tt>bit</tt> instruction, so I worked on fixing that including emulation of the Z80's internal temporary <tt>memptr</tt>/<tt>WZ</tt> register to ensure that <tt>bit&nbsp;n,(hl)</tt> set bits 3 and 5 of the flag register appropriately. ZEXALL now passes, but unsurprisingly it didn't fix my problem (as I didn't really think that BBC BASIC would rely on undocumented functionality!)</p> <p>I ended up isolating certain exact values that when plugged into <tt>SIN()</tt> or <tt>COS()</tt> would produce incorrect results. I then dug out my old CP/M emulator and tried plugging those values into the generic CP/M version of BBC BASIC (which has none of my code in it!) and that produced the same, incorrect, results confirming that the issue was definitely in my Z80 emulation and not in some flaw of the BBC BASIC port.</p> <p>After tracing through the code and dumping out debug values at certain points and seeing where it differed to a known good reference emulator I found that the fault occurred in <tt>FMUL</tt>, and from there I found that at some point the carry flag (which is either rotated or added to registers with <tt>RR</tt> or <tt>ADC</tt> instructions) was being lost. <tt>RR</tt> looked fine but digging into my implementation for the 16-bit <tt>ADC</tt> I found the culprit: the code is the same for <tt>ADD</tt> and <tt>ADC</tt>, but in <tt>ADC</tt> if the carry flag is set then the second operand is incremented by one first. This produces the correct numeric answer, but if the second operand was $FFFF on entry to the function then adding a carry of 1 to it would cause it to overflow back to 0. As this happens before any of the rest of the calculations are made it means that the final value of the carry flag was calculated based on op1+0 instead of op1+$10000, hence the loss of the carry flag.</p> <p>Fortunately, fixing this fixed the wonky output demonstrated above and I feel slightly more confident in my emulation! Now I can continue working on BBC BASIC, I've had an idea for a screen mode that has a reduced number of colours but will hopefully let you draw anywhere on the screen without running out of tiles causing odd corruption (as happens in the usual 16-colour mode 4) and without attribute clash (as happens in the TMS9918A Graphics II mode)...</p> Tue, 03 Aug 2021 20:00:03 +0100 Running BBC BASIC on the Sega Master System http://benryves.com/journal/3763166 http://benryves.com/journal/3763166 <p>I've recently been spending some time finding a way to run BBC BASIC on the Sega Master System, inspired by <a href="https://www.reddit.com/r/retrobattlestations/comments/oevqbq/contest_basic_month_6_the_mandelbaum_set/">BASIC Month 6: The Mandelbaum Set</a> on the RetroBattlestations Reddit community.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/bbcbasic_sms/mandelbaum.jpg" class="lightbox"><img src="http://benryves.com/images/bbcbasic_sms/mandelbaum.thumb.jpg" width="600" height="450" alt="Photograph of the final setup with a Sega Master System running BBC BASIC and a Z88 computer acting as file store." /></a></div> <p>When this month's program was first announced I tried running it on my only unarguably retrobattlestation, my Cambridge Z88, but the screen's low (vertical) resolution <a href="https://twitter.com/benryves/status/1412577944237711363" rel="external">didn't do the program much justice</a>.</p> <p>I thought this gave me two options:</p> <ol><li>Control some external piece of retro tech to produce higher-resolution output (e.g. a printer or a plotter.</li><li>Port a BASIC interpreter to another retro system with a higher resolution display and run the program on that.</li></ol> <p>Unfortunately, I don't own any old printers or plotters (or a Logo-like turtle!) so option 2 seemed my best option. I had some experience adapting <a href="http://www.bbcbasic.co.uk/bbcbasic/z80basic.html" rel="external">Richard Russell's BBC BASIC (Z80)</a> to run on the <a href="http://benryves.com/products/bbcbasic" rel="external">TI-83 Plus calculator</a> so I thought I should pick the Sega Master System as that also has a Z80 CPU in it. Here are some rough specs:</p> <ul><li>3.58(ish)MHz Z80 CPU.</li><li>8KB work RAM.</li><li>TMS9918A-derived VDP for video with 16KB dedicated VRAM accessed via I/O port.</li><li>SN76489-derived PSG for sound.</li><li>Two controller ports with six input pins and two pins that could be configured as inputs or outputs.</li><li>Software loaded from ROM cartridge or card slot with a small BIOS ROM that detects whether a cartridge or card is inserted (and if not, runs its own built-in game).</li></ul> <p>There is some precedence to this endeavour with the computer version of Sega's SG-1000, the SC-3000, which came with a keyboard and had BASIC ROM cartridges. </p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/bbcbasic_sms/adaptors.jpg" class="lightbox"><img src="http://benryves.com/images/bbcbasic_sms/adaptors.thumb.jpg" width="400" height="400" alt="An RS-232 serial adaptor and PS/2 keyboard adaptor for the Master System." /></a></div> <p>I wanted to try to keep this project as retro as possible, so no modern microcontrollers as I've been accused of cheating by using them in the past. I did have to make a couple of adaptors to allow me to plug in a keyboard and to give the Master System a serial port to load or save programs over &ndash; more about these later!</p> <h3>Loading BASIC onto the Master System</h3> <p>The first problem was getting BASIC onto the Master System at all. As my Master System doesn't have BASIC in ROM (it comes with <em>Hang-On</em>, which is perhaps more fun but less likely to handle the Mandelbaum set) I'd need to load the program onto a cartridge or card. Master System cartridges usually contain a mapper circuit to handle bank switching in the lower 48KB of the Z80's address space (the upper 16KB contains the 8KB work RAM, appearing twice) which is normally integrated directly into the ROM chip for the game, however there are a handful of games that have separate mapper chips and ROM chips and the ROM chips that Sega used have a pinout that is extremely close to that used by common EEPROMs (e.g. 29F010, <br /> 49F040) with only a couple of pins needing to be swapped around. <em>After Burner</em> is one such cartridge, so <a href="http://benryves.com/journal/3763154" rel="external">I've modified an old copy to let me plug in flash memory chips that I can program with the version of BASIC I'm working on</a>. A switch at the top of the cartridge lets me switch back to the original pin configuration if I want to play the original copy of <em>After Burner</em>.</p> <h3>Typing commands into BASIC</h3> <p>With a flashable cartridge to hand I was able to assemble a version of Richard Russell's BBC BASIC (Z80) with some simple code stubs in place to direct text output to the screen. Output is useful but only half the story, we still need to be send commands to BASIC!</p> <p>The Master System doesn't have its own keyboard, so I'd need to find some way to interface one to it. I have previously used PS/2 keyboards in a number of projects, as they are pretty simple to deal with. Electrically, they use two open collector I/O lines for bidirectional data transfer (one as clock, one as data), and fortunately each controller port of the Master System has two pins that can be configured as outputs which can therefore be used to interface with a keyboard (either left as inputs and pulled weakly high in their idle state, or driven low as outputs for their active state).</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/bbcbasic_sms/keyboard-adaptor.jpg" class="lightbox"><img src="http://benryves.com/images/bbcbasic_sms/keyboard-adaptor.thumb.jpg" width="400" height="400" alt="Showing the internal wiring of the PS/2 keyboard adaptor and controller passthrough." /></a></div> <p>It acts as a pass-through cable so you can still have a regular controller plugged in when using the keyboard.</p> <p>The two pins on the Master System control port that can act as outputs are TH and TR; TH is normally used by light guns to latch the horizontal counter in the video chip so is unused with normal controllers so it's no great loss here, but TR is the right action button (marked "2") so by using this pass-through adaptor you do unfortunately lose one of the controller buttons. However, you do gain a hundred or so keyboard keys, so I don't think it's too bad a compromise...</p> <p>For the software side of things I adapted my <a href="http://benryves.com/projects/emerson" rel="external">Emerson AT device library</a>, previously written for the TI-83 Plus calculators, to the Sega Master System hardware. This library handles the low-level AT device protocol and also translates the raw keyboard scancode values to the corresponding characters.</p> <h3>Saving and loading programs</h3> <p>At this point I was able to type in BASIC programs and run them on the Master System, which was pretty neat! However, I was still working on adding new features (e.g. drawing commands for graphics) and having to type in the entire Mandelbaum program after every change was going to get pretty exhausting. I could bake it into the ROM but that seemed like cheating, so I thought I should try to find a way to load the file from an external source. A floppy disk or tape cassette would seem authentically retro but adding a floppy drive controller to the Master System would be a fairly complicated task and I don't have a suitable data cassette recorder to even attempt loading from tape so I thought some sort of file store accessible over a serial port would be a good option.</p> <p>The Z88 has a serial port and can act as a remotely-controlled file store when running the PC Link software (with the protocol <a href="https://cambridgez88.jira.com/wiki/spaces/ZP/pages/75956356/PC+Link+II" rel="external">documented here</a>. This seemed like a good choice, if not for the fact that the Sega Master System doesn't have a serial port of its own. To get around this, I added one, using a MAX232 chip to adapt the Master System's 5V logic levels to RS-232 compatible ones so I could plug in a null modem cable from the Z88 or my PC without accidentally frying the Master System with -12V.</p> <div class="html_center" style="text-align: center;"><a href="http://benryves.com/images/bbcbasic_sms/rs232-adaptor.jpg" class="lightbox"><img src="http://benryves.com/images/bbcbasic_sms/rs232-adaptor.thumb.jpg" width="400" height="400" alt="Showing the insides of the RS-232 serial port adaptor." /></a></div> <p>I wrote some code that bit-bangs the serial data over the controller port lines using the timing loop code I wrote for a previous BASIC Month (Crisps Tunes) to support rates between 19200 and 300 baud. 19200 baud is somewhat unreliable but that's OK because the Z88's 19200 baud is unreliable too, so the default 9600 baud speed does a good job. RTS/CTS handshaking has to be implemented, as there is no hardware serial support on the Master System and it needs to be actively polling the port to receive any data. In doing so I noticed one awkward fact about my PC's serial port - if you de-assert RTS it will continue sending data until its buffer is empty, presumably only checking the RTS line when it's about to top up the buffer. In practice this means that even if I change RTS virtually as soon as the start bit for the first byte is received, the PC will continue to send up to 256 bytes before stopping. To get around this I added a serial receive buffer that immediately checks for the next byte even after asserting RTS, and this seems to have done the trick.</p> <p>The protocol used by PC Link requires acknowledgement after every single byte so is very slow but at least it's reliable. I plumbed the PC Link code into BASIC's <tt>LOAD</tt> and <tt>SAVE</tt> which makes loading and saving programs as transparent and easy as if you had a floppy disk in the system instead!</p> <h3>Pressed for size</h3> <p>The Master System has 8KB of RAM. Of this, 16 bytes are mapped to special hardware functions and BBC BASIC reserves 768 bytes for itself, so we're already down to 7,408 bytes. I initially reserved 256 bytes for my own needs (display settings, VDU command buffer, serial port status, keyboard status etc) bringing it down to 7,152 bytes. The 16KB of display memory is not directly accessible to the CPU, so it can't be used for additional work RAM, and having to access it indirectly via I/O ports is very slow but I can't afford to mirror parts of it in RAM for speed.</p> <p>Initially the Mandelbaum program ran well enough by stripping out comments, but I then added sound support (with eight 13-byte envelope definitions, and four channels with their own state, copy of their active envelope and a command queue for 32 bytes per channel, adding around another 140 bytes of memory usage) and the program stopped running with a "No room" error during execution (performing a square root operation, of all things!) so I guess the tolerances were very tight. I went and combine more lines of code into single lines and replaced two-letter variable names with single-letter ones (no, really) and it was able to run again but I don't think 8KB is a particularly comfortable amount of RAM for a BASIC computer! <br /> <h3>Further design considerations</h3></p> <p>The version of the BASIC host interface used here is very much a work-in-progress. I would need to extend it considerably to be useful, including:</p> <ul><li>Fuller support of different VDU commands, e.g. redefining character shapes, changing the text and graphics viewports, better colour handling (differentiating between logical palettes and physical palettes).</li> <li>Better support of other modes (so far only TMS9918A "Text" and "Graphics II" modes are used, there is a Master System-specific "mode 4" but that lacks graphics support and only takes advantage of hardware scrolling for extremely fast program <tt>LIST</tt>ing).</li> <li>Implementation of more graphics commands - so far only <tt>PLOT 4</tt> (<tt>MOVE</tt>) and <tt>PLOT 5</tt> (<tt>DRAW</tt>) are implemented. They also use a non-standard coordinate system with (0,0) in the top left of the screen and a screen resolution of 256x192, whereas for standardisation with other BBC BASIC implementations this should move (0,0) to the bottom left and use a logical resolution of 1280x1024 or similar.</li> <li>Support of other file systems rather than rely on a Z88 running PC Link, e.g. using I²C EEPROMs (as they use two open collector pins, so could be plugged into a controller port via a passive adaptor).</li> <li>Support of extra RAM, either integrated directly on a custom cartridge or using the battery-backed SRAM supported by some other cartridge types.</li> <li>Native controller support via BASIC <tt>ADVAL</tt> command (at the moment you can access the controller ports directly with <tt>GET(&amp;DC)</tt>)</li></ul> <p>This is before even getting into adding anything machine-specific (e.g. to take advantage of scrolling tilemaps or hardware sprites), but getting the BASICs down (and consistent!) is a very important starting point. Consistency is useful, after all I was able to get the original program running under BBC BASIC with very minimal changes to it.</p> <p>But, in the short term, I have at least succeeded in what I set out to do, which was to run the Mandelbaum program on a retro "computer" wholly unsuited to it!</p> <div class="html_center" style="text-align: center;"><a href="https://www.youtube.com/watch?v=4MhnyWq-OpA" rel="external"><img src="https://i.ytimg.com/vi/4MhnyWq-OpA/maxresdefault.jpg" alt="" width="600" height="340" /></a></div> <p>The above video provides another demonstration of the setup &ndash; playing the <em>Cold Tea</em> music demo, albeit with a heavily stripped-down version of the visuals as the Master System doesn't have anything that can match the capabilities of the BBC Micro's teletext mode 7.</p> Thu, 22 Jul 2021 16:54:00 +0100 A parallel port and a demonstration of the Z80 computer http://benryves.com/journal/3701592 http://benryves.com/journal/3701592 <p>The last piece of hardware to add to the computer was a parallel port. These have eight data lines and nine assorted control and status lines. My last two 8-bit I/O expanders provide sixteen of these seventeen lines, and the final one was provided by the DS1307 real-time clock chip which happily has a spare pin on it that can be used as an output.</p> <div class="html_center" style="text-align: center;"><img src="http://benryves.com/bin/z180/2010.08.19.01.Parallel.Port.IO.Expanders.jpg" width="320" height="240" alt="Parallel port I/O expanders" /> <img src="http://benryves.com/bin/z180/2010.08.19.02.Parallel.Port.Connector.jpg" width="320" height="240" alt="Parallel port connector" /></div> <p>This parallel port can be used to print from the computer. Some software has printing capabilities built in (such as the text editor VEDIT Plus), but by pressing Ctrl+P in CP/M any text sent to the display will be simultaneously sent to the printer.</p> <p>I also needed to mount the LCD inside the case. I bought a plastic strip to try to make a nice frame for it, but couldn't cut it accurately enough by hand so have had to make do with merely sticking the LCD behind a rectangular hole cut in the aluminium. It's not the neatest arrangement and doesn't protect the LCD from scratches but is better than nothing.</p> <p>To demonstrate the computer's hardware and software, I recorded a video:</p> <div class="html_center" style="text-align: center;"><a href="http://www.youtube.com/watch?v=DQ3vQW2Fs_w"><img src="http://benryves.com/bin/z180/2010.09.04.01.Video.Thumbnail.jpg" width="425" height="343" alt="Demonstration video thumbnail" /><small><br /> Watch video on YouTube</small></a></div> <p>I'm not desperately happy with the way it came out; I really need to find a better microphone and the angle of the sun and variable weather when I made the video threw the white balance off. On the plus side, I did find out how to capture crisp black and white video with my TV capture card; I connected the composite video output from the computer to the luma pins on the S-video input on the capture card, then dropped the saturation to zero in VirtualDub. For some reason this produces great quality video, in comparison to the composite input which produces a fuzzy mess — there shouldn't really be any difference with a black and white signal (regular television sets don't have any problems).</p> Sun, 05 Sep 2010 12:27:03 +0100