Jump to content
  • Advertisement
  • entries
    222
  • comments
    606
  • views
    593005

About this blog

Z80 and C#-related shenanigans - now with added electronics.

Entries in this blog

 

Nibbles and Logo

Work on BBC BASIC has slowed down quite a bit, with only minor features being added. A *FONT command lets you output large or font sized text to the graphics cursor position regardless of the current MODE:


10 MODE 3
20 VDU 5
30 MOVE 0,255 : PRINT "Small"
40 *FONT LARGE
50 MOVE 0,227 : PRINT "Large"
60 VDU 4
70 PRINT TAB(0,3) "Small (VDU 4)"

Another new command is the dangerous *GBUF that can - when used correctly - let you switch the location of the graphics buffer. You can simulate greyscale by quickly flickering between two different images on the LCD, which is where this command may come in use.


Snake/Nibbles is a fun game and an easy one to write, so here's a simple implementation that features variable speeds and mazes. The game runs quickly on a 6MHz TI-83+, which I'm happy with. And yes, I know I'm terrible at it. [wink]

One thing I've always been pretty bad at is writing language parsers resulting in poor performance and bugs. I've started writing a primitive Logo interpreter in C# to try and improve my skills in this area. So far it supports a handful of the basic language features and statements:
(No, no turtle graphics yet [wink]). There's no support for infix operators yet. The BBC BASIC ROM manual describes the top-down parsing method it uses to evaluate expressions so I'm going to attempt to reimplement that.

One issue I've already run into are the parenthesis rules: for example the sum function outside parentheses only allows two arguments, but inside parentheses works until the closing parenthesis:
I'm not sure whether a "this statement was preceded by an opening parenthesis" flag would be sufficient.

benryves

benryves

 

Extending BBC BASIC

BBC BASIC may have originated with the 8-bit home computer era, but it's still being updated and its most up-to-date incarnation - BBC BASIC for Windows - has a wealth of features that are unavailable on the Z80 version.

The BBC BASIC graphics API is primarily accessed via the multi-purpose PLOT statement. PLOT is followed by three arguments - the type of graphics operation being carried out followed by an X and a Y coordinate. For example, to draw a line between (20,30) and (100,120) you could do this:

PLOT 4,20,30 : REM Move graphics cursor to (20,30)
PLOT 5,100,120 : REM Draw a line to (100,120)

This results in needing to remember a lot of different plot codes (there is a logic to how they are formed but I still need to consult a list of codes from time to time). All implementations of BBC BASIC feature two helper statements to aid the user:
MOVE x,y (equivalent to PLOT 4,x,y) DRAW x,y (equivalent to PLOT 5,x,y) More recent versions, such as BBC BASIC for Windows, also implements the following helper statements, amongst others:
CIRCLE x,y,r (equivalent to MOVE x,y : PLOT 145,r,0) ELLIPSE x,y,w,h (equivalent to MOVE x,y : PLOT 0,w,0 : PLOT 193,0,h) FILL x,y (equivalent to PLOT 133,x,y) RECTANGLE FILL x,y,w,h (equivalent to MOVE x,y : PLOT 97,w,h) This is all very well, but the BBC BASIC (Z80) interpreter is a sealed box as far as I am concerned. I can ask it to perform tasks for me ("evaluate this expression") and it can ask me to perform tasks for it ("output this character to the display") but I can't modify its behaviour.

Or, so I thought - until I read through the copy of BASIC ROM User Guide that a friend had rescued and sent to me. It has a section on adding statements, which it achieves by using a clever - but simple - trick.

When BBC BASIC encounters a statement it doesn't recognise it triggers the Mistake error. On the BBC Micro the error handler is vectored, meaning that it loads the address of the error handling routine from RAM first instead of jumping to a fixed address. This allows the user to override the normal error handler, detect the Mistake condition and try and parse the erroneous statement themselves. If they can't handle the statement either control is passed back to BBC BASIC's usual error handler, otherwise the error condition is cleared and execution continues as normal.

BBC BASIC (Z80) follows the same procedure but with one major difference - the error handler routine is not vectored. Unfortunately, the only practical workaround I can think of is to patch the interpreter's error handler routine directly. Richard Russell somehow managed to add support for additional commands to the Z88 version via a patch that runs from RAM, but I haven't been able to work out how he managed to do that yet.

The first series of additional statements I added were the graphics helper statements listed above, WAIT (which pauses execution for a certain number of centi-seconds) and SWAP which exchanges the contents of two variables. These are all relatively simple statements to implement as they do not affect the state of BBC BASIC in any other way; they perform a single, simple task then exit.

One of the more useful additions to more recent versions of BBC BASIC is the WHILE...ENDWHILE loop structure. A limitation of BBC BASIC (Z80)'s statement blocks is that their contents must be executed at least once, hence IF statements must fit on one line, multi-line procedures or functions should be placed at the end of the file after an END statement and REPEAT...UNTIL loops - where the looping conditional is at the end of the block, rather than the start - are provided. If a WHILE condition evaluates to FALSE, control needs to resume at the matching ENDWHILE. This is an interesting technical challenge, as it needs to handle nested WHILE...ENDWHILE stataments when searching through the code to find the terminating ENDWHILE, but appears to work pretty well now.

Another useful recent addition is EXIT (in three variations - EXIT FOR, EXIT REPEAT and EXIT WHILE) which breaks out of a loop structure. This has the same technical challenges as the WHILE...ENDWHILE loop structure (searching for the matching loop terminator) with the additional difficulty of unwinding the stack to the correct position.

By combining WHILE loops and EXIT WHILE you can simulate multi-line IF statement blocks, so

IF THEN

becomes

WHILE

EXIT WHILE:ENDWHILE

These additions are not without their downsides. Most of the statements supported natively by BBC BASIC (Z80) are represented by single-byte tokens, whereas these extensions are stored as ASCII text. This makes them take up more room in the source file and slower to execute (searching for and handling strings is a much more complex operation than searching for bytes). Using them makes your programs incompatible with other versions of BBC BASIC (Z80). I personally feel that these disadvantages are far outweighed by the advantage of easier to read code, however.


To round the entry off, have a fractal. [smile]

benryves

benryves

 

BBC BASIC for the TI-83+/TI-84+ beta release

Work commitments have prevented me from doing much on my own projects recently, but zipping up a few files to get BBC BASIC tested is not a time-consuming process so I've started to release test builds.


Download latest version
The documentation is available online. It's generated by a little tool I hacked together to turn a MediaWiki database into a CHM file.

I've had a few issues with the TI-84+ hardware, such as LCD corruption, difficulty in getting key presses to register and crashes when USB devices are unplugged. I think I've fixed the LCD and key issues by dropping the CPU speed down to 6MHz when the full 15MHz is not required, but am still stumped by the USB issues. Unfortunately documentation on the USB hardware is rather thin on the ground and I don't own a TI-84+ for testing.

The package comes with a few demo programs from the CP/M release and a few I cobbled together myself. Most recently I've tried putting together a few little graphics demos.


GFXDEMO.bbc
There's still a fair amount of work to go on this project (especially optimising - some of the code is extremely inefficient) but it feels nice to have something out there for people to try. [smile]

benryves

benryves

 

C# emu2413

This is fairly embarrassing; somebody sent me an email that was flagged as spam which I accidentally deleted. So if you sent me an email and I haven't replied, I'm not deliberately being rude; could you send it again? [embarrass]

After encountering strange crashes (not .NET exceptions, full out crashes) with emu2413 I decided to port it to straight C# instead from its existing C incarnation (emu2413.h.cs and emu2413.c.cs). Even though the original was macro-heavy it was relatively simple to port, and so there's no dependency on an unmanaged DLL to generate FM sound any more. However, the C# version is significantly slower (Cogwheel now takes about 50% extra CPU time when FM sound is enabled), possibly due to many extraneous method calls that were macros in the original.

However, the emulator still crashes when FM sound is enabled. And I have no idea why, as it only happens in Release mode and outside the IDE. The Debug build works fine inside and outside the IDE, and Release mode works fine when run within the IDE. [sad]

benryves

benryves

 

Controller input updates to Cogwheel

I hope you all had a good Christmas and New Year period!

I received an Xbox 360 controller for Christmas, so have done a bit of work on Cogwheel to add support for it. (You can download a copy of the latest version 1.0.2.0 with SlimDX here).

The first issue to deal with was the D-pad on the Xbox 360 controller. When treated as a conventional joystick or DirectInput device the D-pad state is returned via the point-of-view (POV) hat. The joystick input source class couldn't raise events generated by the POV hat so support for that had to be added. This now allows other controllers that used the POV hat for slightly bizarre reasons (eg the faceplate buttons on the PlayStation controller when using PPJoy) to work too.

The second issue was the slightly odd way that the Xbox 360's DirectInput driver returns the state of the triggers - as a single axis, with one trigger moving the axis in one direction, the other trigger moving it in the other. You cannot differentiate between both triggers being held and both being released, as both states return 0. To get around this, I've added support for XInput devices, where all buttons and triggers operate independently.

The Xbox 360 controller now shows up twice in the UI - once as an XInput device and again as a conventional joystick. Fortunately, you can check if a device is an XInput device by the presence of IG_ in its device ID. Here's some C# code that can be used to check with a joystick is an XInput device or not.

using System.Globalization;
using System.Management;
using System.Text.RegularExpressions;

namespace CogwheelSlimDX.JoystickInput {

///
/// Provides methods for retrieving the state from a joystick.
///
public class Joystick {

/* ... */

///
/// Gets the vendor identifier of the .
///
public ushort VendorId { get; private set; }

///
/// Gets the product identifier of the .
///
public ushort ProductId { get; private set; }

/* ... */

///
/// Determines whether the device is an XInput device or not. Returns true if it is, false if it isn't.
///
public bool IsXInputDevice {
get {
var ParseIds = new Regex(@"([VP])ID_([\da-fA-F]{4})"); // Used to grab the VID/PID components from the device ID string.

// Iterate over all PNP devices.
using (var QueryPnp = new ManagementObjectSearcher(@"\\.\root\cimv2", string.Format("Select * FROM Win32_PNPEntity"), new EnumerationOptions() { BlockSize = 20 })) {
foreach (var PnpDevice in QueryPnp.Get()) {

// Check if the DeviceId contains the tell-tale "IG_".
var DeviceId = (string)PnpDevice.Properties["DeviceID"].Value;
if (DeviceId.Contains("IG_")) {

// Check the VID/PID components against the joystick's.
var Ids = ParseIds.Matches(DeviceId);
if (Ids.Count == 2) {
ushort? VId = null, PId = null;
foreach (Match M in Ids) {
ushort Value = ushort.Parse(M.Groups[2].Value, NumberStyles.HexNumber);
switch (M.Groups[1].Value) {
case "V": VId = Value; break;
case "P": PId = Value; break;
}
}
if (VId.HasValue && this.VendorId == VId && PId.HasValue && this.ProductId == PId) return true;
}
}
}
}
return false;
}
}

/* ... */
}
}

When the joysticks are enumerated they are only added to the input manager if they are not XInput devices.


To round up the entry, here's a screenshot of a minesweeper clone I've been working on in BBC BASIC.


You can view/download the code here and it will run in the shareware version of BBC BASIC for Windows. The code has been deliberately uglified (cramming multiple statements onto a single line, few comments, trimmed whitespace) to try and keep it within the shareware version's 8KB limit as this is a good limit to keep in mind for the TI-83+ version too.

benryves

benryves

 

Sega Master System emulation in Silverlight

I've had to quickly learn Silverlight for work recently, which has been an interesting experience. I've had to write new code, which is fine but doesn't really excite me as far as Silverlight is concerned - it doesn't really matter which language new code is developed in, as long as it gets the job done.

What does interest me more is that Silverlight is ".NET in your browser", and I'm a big fan of .NET technology with a handful of .NET-based projects under my belt. Silverlight therefore gives me the opportunity to run these projects within the browser, which is a fun idea. [smile]


Click to launch emulator in new window.
To this end, I've turned Cogwheel, a Sega 8-bit system emulator, into a Silverlight application. It took about an hour and a half, which was not as bad as I'd expected! (Skip to the bottom for instructions for the demo).

Raster graphics

Silverlight's raster graphics support is somewhat lacking. You can display raster graphics in Image elements, but - as far as I can see - that's about it. If you wish to generate and display images dynamically via primitive pixel-pushing, you're out of luck as far as Silverlight's class library is concerned.

Thankfully, Ian Griffiths has developed a class named PngGenerator that can speedily encode a PNG from an array of Colors that can then be displayed in an Image. Cogwheel's rasteriser returns pixel data as an array of integers so there's a small amount of overhead to convert these but other than that it's easy to push pixels, albeit in a fairly roundabout manner.

Render loop

The render loop is based around an empty Storyboard that invokes an Action every time it completes then restarts itself.

using System;
using System.Windows;
using System.Windows.Media.Animation;

namespace Cogwheel.Silverlight {

public static class RenderLoop {

public static void AttachRenderLoop(this FrameworkElement c, Action update) {
var Board = new Storyboard();
c.Resources.Add("RenderLoop", Board);
Board.Completed += (sender, e) => {
if (update != null) update();
Board.Begin();
};
Board.Begin();
}

public static void DetachRenderLoop(this FrameworkElement c) {
var Board = (Storyboard)c.Resources["RenderLoop"];
Board.Stop();
c.Resources.Remove("RenderLoop");
}

}
}

I'm not sure if this is the best way to do it, but it works well enough and is easy to use - just grab any FrameworkElement (in my case the Page UserControl) and call AttachRenderLoop:

private void UserControl_Loaded(object sender, RoutedEventArgs e) {
this.UserControlRoot.AttachRenderLoop(() => {
/* Update/render loop in here. */
});
}

Missing .NET framework class library features

This is the big one; Silverlight does not cover the entire .NET framework class library, and so bits of it are missing. Fortunately this can be resolved, the difficulty depending on how you want the functionality of the original app to be affected.

Missing types you're not interested in.

These are the easiest to deal with, and this includes attributes and interfaces that the existing code uses that you're not especially interested in. For example, Cogwheel uses some of .NET's serialisation features for save states - a feature I wasn't intending on implementing in the Silverlight version. The [Serializable] and [NonSerialized] attributes are not available in Silverlight, nor is the IDeserializationCallback interface. To get the project to compile some dummy types were created.

namespace System {

class SerializableAttribute : Attribute { }
class NonSerializedAttribute : Attribute { }

interface IDeserializationCallback {
void OnDeserialization(object sender);
}

}

Missing types or methods that you don't mind partially losing.

Cogwheel features some zip file handling code that uses System.IO.Compression.DeflateStream, a class not available in Silverlight. Rather than remove the zip classes entirely (which would require modifications to other files that relied on them) it was easier to use conditional compilation to skip over the DeflateStream where required.

switch (this.Method) {
case CompressionMethod.Store:
CompressingStream = CompressedStream;
break;

#if !SILVERLIGHT
case CompressionMethod.Deflate:
CompressingStream = new DeflateStream(CompressedStream, CompressionMode.Compress, true);
break;
#endif

default:
throw new NotSupportedException();
}

Missing instance methods.

C# 3.0 adds support for extension methods - user-defined methods that can be used to extend the functionality of existing classes that you cannot modify directly. Silverlight is missing a number of instance methods on certain classes, such as string.ToLowerInvariant();. By using extension methods the missing methods can be restored.

namespace System {

public static class Extensions {
public static string ToLowerInvariant(this string s) { return s.ToLower(CultureInfo.InvariantCulture); }
public static string ToUpperInvariant(this string s) { return s.ToUpper(CultureInfo.InvariantCulture); }
}

}

Missing static methods.

These are the most work to fix as extension methods only work on instance methods, not static methods. This requires a change at the place the method is called as well as the code for the method itself.

I've got around this by creating new static classes with Ex appended to the name then using using to alias the types. For example, Silverlight lacks the Array.ConvertAll method.

namespace System {

static class ArrayEx {
public static TOut[] ConvertAll(TIn[] input, Func fn) {
TOut[] result = new TOut[input.Length];
for (int i = 0; i result = fn(input);
}
return result;
}
}

}

First, a replacement method is written with Ex appended to the class name. Secondly, any file that contains a reference to the method has this added to the top:

#if SILVERLIGHT
using ArrayEx = System.ArrayEx;
#else
using System.IO.Compression;
using ArrayEx = System.Array;
#endif

Finally, anywhere in the code that calls Array.ConvertAll is modified to call ArrayEx.ConvertAll instead. When compiling for Silverlight it calls the new routine, otherwise it calls the regular Array.Convert.

Demo

The links below launch the emulator with the selected ROM image.
Sega Tween (Project page). Fire Track (Project page). To run your own ROM image, click on the folder image in the bottom-right corner of the browser window to bring up a standard open file dialog.

Zip files are not handled correctly, but if you type *.* into the filename box, right-click a zip file, pick Open, then select the ROM from inside that it should work (it does on Vista at any rate).

The cursor keys act as you'd expect; Ctrl or Z is button 1/Start; Alt, Shift or X is 2 (Alt brings up the menu in IE). Space is Pause if you use an SMS ROM and Start if you use a Game Gear ROM. Keys don't work at all in Opera for some reason, but they should work fine in IE 8 and Firefox 3. You may need to click on the application first!

Issues

There are a number of issues I have yet to address. Performance is an obvious one; it's a little choppy even with 100% usage of one the cores on a Core 2 Duo. Sound is missing, and I'm not sure what Opera's doing with keys. Other than that, I thought it was a fun experiment. [smile]

Once I've tidied it up a bit I'll merge the source code with the existing source repository.

benryves

benryves

 

Virtual screen resolutions for BBC Micro compatibility

The BBC Micro had a virtual resolution of 1280x1024, meaning that if you drew a circle centred on (1280/2,1024/2) it would appear in the middle of the screen regardless of its pixel resolution. On top of that, (0,0) was in the bottom-left hand corner of the screen with the Y axis pointing upwards.


Thus far I'd been using the slightly more intuitive fixed resolution of 96x64 with (0,0) in the top-left hand corner with the Y axis pointing downwards. This means that any graphics program for the TI-83+ version appears upside down and squashed into the bottom-left corner when run on the BBC Micro.

I have worked on attempting to remedy this. 1280x1024 does not divide cleanly into 96x64, so I've used a constant scale factor of 16 on both axes resulting in a virtual resolution of 1536x1024. (Flipping the Y axis is easy enough). This means that programs drawing shapes (lines, circles, triangles, rectangles, parallelograms) will run on both BBC Micro "compatible" versions of BBC BASIC and the TI-83+ and produce roughly the same results.


As this may not be to everyone's tastes the two options may be independently controlled with two new star commands;

*YAXIS UP|DOWN *GSCALE ON|OFF
The default is to have *YAXIS UP and *GSCALE ON. To revert to the old behaviour you could specify *YAXIS DOWN and *GSCALE OFF.

I have also been working on implementing the OS call OSGBPB. This call allows you to read/write multiple bytes of data in one go, so is much faster than using the byte-at-a-time BGET# and BPUT# commands. It also provides a way to enumerate filenames, a feature I have yet to implement but one that should be useful (eg to search for data files for your own program without having the filename hard-coded or having to prompt the user).

benryves

benryves

 

Three sides good, four sides bad.

Work on the TI-83+/TI-84+ port of BBC BASIC continues bit-by-bit.


Source code of TRIANGLE demo on the left.
I've added triangle filling (left) and, by extension, parallelogram filling (right) PLOT commands. The triangle filler is a little sluggish, tracing each edge of the triangle using 16-bit arithmetic, but it seems fairly robust. I am trying to focus on robustness over speed for the moment, but it would seem easy enough to add a special-case triangle edge tracer if both ends of the edge can fit into 8-bit coordinates (all inputs to plot commands use 16-bit coordinates).

The parallelogram on the right is specified with three coordinates, and the fourth point's position is calculated with point3-point2+point1. As parallelograms are drawn as two triangles there's an overdraw bug when they are drawn in an inverting plotting mode.


This needs to be fixed.
The BBC Micro OS exposed certain routines in the &FF80..&FFFF address range. These routines carried out a wide variety of tasks, from outputting a byte to the VDU to reading a line of input or changing the keyboard's auto-repeat rate. BBC BASIC (Z80) lets the person implementing the host interface catch these special-case calls, so I've started adding support for them. A friend very kindly donated copies of three BBC Micro books, including the advanced user guide (documenting, amongst other things, the OS routines) and the BASIC ROM manual.

As an example, if you write the value 16 to the VDU it clears the text viewport. BBC BASIC has the CLS keyword for this, but you can also send values directly to the VDU with the VDU statement, like this: VDU 16. This in turn calls the BBC Micro's OSWRCH routine, located at &FFEE, with the accumulator A containing the value to send to the VDU. An alternative method to clear the text viewport would therefore be A%=16:CALL &FFEE.

Now, you may well be wondering how this is of any use, given the existance of a perfectly good CLS statement (or, failing that, the VDU statement). The usefulness becomes apparent when you remember that BBC BASIC has an inline assembler. There is no CLS or VDU instruction in Z80 assembly, but by providing an OSWRCH routine you can interact with the host interface and so clear the screen from an assembly code routine; in this case [LD A,16:CALL &FFEE:] (square brackets delimit assembly code).

Sadly, there is a limitiation in the TI-83+/TI-84+ hardware that prevents this from working seamlessly. The memory in the range &C000..&FFFF, where these routines reside, is mapped to RAM page 0. RAM page 0 has a form of execution protection applied to it, so if the Z80's program counter wanders onto RAM page 0 the hardware triggers a Z80 reset. To this end all of these OS calls are relocated to &4080..&40FF, so in the Z80 assembly snippet above you would CALL &40EE instead. This only applies to CALLs made from assembly code - the BASIC CALL statement traps calls to the &FF80..&FFFF so they can be redirected seamlessly to retain compatibility with other versions of BBC BASIC (Z80).

After all this work I noticed that the host interface was crashing in certain situations, especially when writing to a variable-sized file in a loop. This is the sort of bug that is tricky to fix; sometimes it would crash instantly, sometimes it would write the first 5KB of the file fine then crash.

It turned out to be a bug in the interrupt service routine (ISR). In this application the ISR is used to handle a number of tasks such as trapping the On key being pressed to set the Escape condition or to increment the TIME counter (as well as other time-related features such as the keyboard auto-repeat or cursor flash). On the TI-83+, which doesn't have a real-time clock, it also calls a RTC.Tick function approximately once per second to update the (very inaccurate) software real-time clock. To call this function it uses the BCALL OS routine. It appears that if the BCALL routine was used from an ISR when the TI-OS was in the process of enlarging a file it would crash. Removing the call to RTC.Tick appears to have fixed the bug entirely.


It is possible to put BBC BASIC into an infinite loop if you make a mistake in your error handler. In the above example program the error handler in line 10 fails to bail out on an error condition, running back into line 20 that itself triggers a division by zero error. You cannot break out of the loop by pressing On as that works by triggering an error (error 17, Escape). To improve safety I've added a feature whereby holding the On key down for about 5 seconds causes BBC BASIC to restart. This loses the program that was previously loaded in RAM, but you can retrieve with the OLD statement.

I've also rewritten all of the "star" commands. These are commands, usually prefixed with an asterisk, that are intended to be passed to the OS. As the TI-83+/TI-84+ does not have an especially useful command-line driven interface (most of the UI is menu-driven) I've implemented this part myself, basing its commands on ones provided by the BBC Micro OS. For example, *SAVE can be used to save a block of memory to a file, or *CAT (aliased to *DIR and *.) can be used to show a list of files.


In the case of *CAT I've added a pattern-matching feature that lets you use ? and * as wildcards to limit the files shown.

After noticing that *COPY took three seconds to copy an 860 byte file I optimised some of the file routines to handle block operations more efficiently. Reading and writing single bytes at a time is still rather sluggish, but I'm not sure that there's much I can really do about that.


Finally, for a bit of fun I noticed a forum post enquiring about writing assembly programs on the calculator. Here's a program that assembles a regular Ion program using BBC BASIC's assembler.

benryves

benryves

 

BBC BASIC's improved filling, *EXEC and Lights Out

Progress on the TI-83+/TI-84+ port of BBC BASIC continues - I'm hoping to get a beta release out soon. [smile]


I've done quite a lot of work on the graphics features. Every shape that is plotted can be set to either the foreground colour, background colour or to invert the pixels it covers. This wasn't implemented properly (everything was always drawn in the foreground colour) which has been corrected.

The first image in the above group shows the flood-filler in action, filling inside and outside a triangle. The second image demonstrates the ellipse drawing and filling code by qarnos. It had a small amount of overdraw, which is not normally a problem, but in an inverting plot mode drawing a pixel twice causes it to reset to its original value. This ends up leaving gaps in the circle. Fortunately he was able to give me a lot of help in fixing it. [smile]

The third image demonstrates a non-standard feature I've added - being able to set your own fill patterns. The GCOL statement lets you set the foreground or background colour, and for values between black and white a dithered fill pattern is substituted instead. GCOLPAT takes a pointer to an 8x8 pixel fill pattern and subsequent fill operations will use that instead; passing FALSE (0) to GCOLPAT or setting a colour normally via GCOL reverts to the standard dither fills.

I've also done a small amount of benchmarking. There's a sample program in the TI-83+ guidebook that draws a Sierpinski triangle.


On a regular 6MHz TI-83+, the TI program takes 7 minutes and 8 seconds to run. A direct translation to BBC BASIC executes in 2 minutes and 21 seconds, and a simplified version executes in 1 minute and 56 seconds.


I'm also trying to improve the number of OS-level "star" commands. Above is a demonstration of *EXEC which reads console input from a text file. A file is opened for output using OPENOUT, some text is written into it using PRINT#, and then it it *EXECuted. This is one possible way of converting a text file into a BBC BASIC program.


Finally, I'm trying to write a game as an example program. The above screenshot shows an incomplete clone of the Lights Out game by Tiger Electronics.

benryves

benryves

 

TIME$ to resume work on TI-83+ BBC BASIC

It's been a while since I worked on the TI-83+ calculator port of BBC BASIC, and due to a relatively modular design some of the new features I'd been working on for the Z80 computer project version could be easily transferred across.

The first addition to the calculator port is the TIME$ keyword, which lets you get or set the system time.


That's all very well and good, but only the TI-84+ calculator has real-time clock hardware - the TI-83+ doesn't have any sort of accurate timekeeping to speak of. Rather than display an error when TIME$ is used I opted to use an inaccurate software-based clock. It uses the TI-83+'s timer interrupts (roughly 118Hz) to update the date and time about once a second. The clock is reset to Mon,01 Jan 2001.00:00:00 every time BBC BASIC is restarted and keeps abysmal time, but software designed to use the clock will at least run.

I have been transferring and amending documentation from Richard Russell's website to a private installation of MediaWiki. There are about 120 entries so far; having documentation puts me much closer to being able to make a release.

I have also fixed a handful of bugs. One that had me tearing my hair out was something like this:

250 DEF PROC_someproc(a,b)
260 a=a*PI
270 ENDPROC

The program kept displaying a No such variable error on line 260. Well, a is clearly defined, and retyping the procedure in another program worked, so what was the problem here? I thought that maybe one of the graphics calls or similar was corrupting some important memory or modifying a register it shouldn't. It turns out that the problem lay in the Windows-based tokeniser - it was not picking up PI as a token, for starters, and was storing the ASCII string "PI" instead. On top of that, it was treating anything after a * as a star command, which aren't tokenised either. (Star commands, such as *REFRESH, are passed directly to the host interface or OS). Retyping the problematic lines caused BBC BASIC to retokenise them, which was why I couldn't replicate the problem in other programs. By fixing the tokeniser, everything started working again.

The source code for the analogue clock program is listed below.

10 *REFRESH OFF
20 VDU 29,48;32;
30 GCOL 0,128
40 REPEAT
50 t$=TIME$
60 hour%=VAL(MID$(t$,17,2))MOD12
70 min%=VAL(MID$(t$,20,2))
80 sec%=VAL(MID$(t$,23,2))
90 sec=sec%/60
100 min=(min%+sec)/60
110 hour=(hour%+min)/12
120 CLG
130 GCOL 0,127
140 MOVE 0,0
150 PLOT 153,31,0
160 GCOL 0,0
170 FOR h=1TO12
180 hA= h/6*PI
190 hX=30*SIN(hA)
200 hY=30*COS(hA)
210 MOVE hX,hY
220 DRAW hX*0.9,hY*0.9
230 NEXT h
240 PROC_drawHand(sec,30)
250 PROC_drawHand(min,24)
260 PROC_drawHand(hour,16)
270 *REFRESH
280 UNTIL INKEY(0)-1
290 *REFRESH ON
300 END
310 DEF PROC_drawHand(pos,length)
320 MOVE 0,0
330 pos=pos*2*PI
340 DRAW length*SIN(pos),-length*COS(pos)
350 ENDPROC

I translated the tokeniser source code to PHP so that by pointing a browser to file.bbcs for a known file.bbc the highlighted, detokenised source code is served as HTML instead. Hurrah for mod_rewrite, and if you're using IIS Ionic's Isapi Rewrite Filter performs a similar job using the same syntax.

benryves

benryves

 

Z80 computer - Lines, cubes and inverted text

I've made a few additions to the operating system for the computer. The Console module, which handles text input and output, now supports "coloured" text - that is you can set the text foreground and background colours to either black or white. This functionality is exposed via the BBC BASIC COLOUR statement. If you pass a value between 0 and 127 this sets the foreground colour (0..63 is white, 64..127 is black) and if you pass a value between 128 and 255 this sets the background colour (128..191 is white, 192..255 is black).


The image on the right also demonstrates another addition - you can set the text viewport to occupy a partial area of the display. This is most useful when coupled with the ability to define graphics viewports, which I have yet to add.

That said, I have started writing the Graphics module. So far all it can do is draw clipped lines, and this functionality is exposed via BBC BASIC's MOVE and DRAW statements. MOVE sets the graphics cursor position - DRAW also moves the graphics cursor, but also draws a line between the new position and the previously visited one.


I cannot use drawing code I've written for the TI-83+ version due to differences in the LCD hardware and the way that buffers are laid out. The popular way to lay out graphics buffers on the TI-83+ is as follows:


Each grey block represents 8 pixels - one byte in LCD memory represents 8 pixels grouped horizontally. The leftmost bit in each 8-pixel group is the most significant bit of each byte. The data is stored in the buffer so that each row of the LCD is represented by 12 consecutive bytes. This left-to-right, top-to-bottom arrangement should seem sensible to anyone who has worked with a linear framebuffer. However, due to the way that the LCD I'm using is arranged, I'm using the following buffer layout:


The LCD hardware groups pixels vertically, but when you write a byte to it its internal address pointer moves right. Furthermore, the most significant bit of each byte written is at the bottom of each group. This may sound a little confusing, but actually works out as more efficient. Writing text is easy; I'm using a 4x8 pixel font, so all I need to do is set the LCD's internal address counter correctly then write out four bytes, one for each column of the text (other sensible font sizes for the display, such as 6x8 or 8x8 are just as easy to display).

Another example of improved efficiency is if you deal with pixel-plotting routines. Each pixel on the display can be addressed by a buffer offset and an eight-bit mask to "select" the particular pixel in an eight-pixel group. With this arrangement, moving the pixel left or right is easy; simply increment or decrement the buffer offset by one. Moving the pixel up or down is a case of rotating the mask in the desired direction. If the rotation moves the pixel mask from one 8-pixel group to another (which only happens every eight pixels) the buffer offset needs to be moved by 128 in the correct direction to shunt it up or down.

On the TI-83+, moving the pixel up or down requires moving the buffer offset up or down by 12; moving the pixel left or right is a rotation as before with a simple buffer offset increment or decrement to move between 8-pixel groups.

In Z80 assembly incrementing or decrementing a 16-bit pointer by one is a single instruction taking 6 clock cycles; moving it by a larger offset takes at least 21 clock cycles, 42 if you include backing the temporary register such an operation would take.

What may be interesting to see is how well a raycaster would work on a system that has video memory arranged into columns.

Without wishing to be typecast as that programmer who loves spinning cubes, I also wrote a cube-spinning demo to test the line drawing routines as well as some integer arithmetic routines I've added (the Z80 can't multiply or divide, so these operations need to be implemented in software).


Click for video (1.86MB WMV)
It runs fairly smoothly (bearing in mind the 2MHz clock speed). The second half of the video has the Z80 running at 10MHz; it actually seems quite stable even though the LCD is being accessed at nearly five times its maximum speed (the system did need to be reset a few times until it worked without garbling the display).

benryves

benryves

 

Fixed and scaled CHIP-8/SCHIP interpreter

The CHIP-8/SCHIP interpreter now seems happy enough to run games, though the lack of settings to control how fast or slow they run makes things rather interesting.


First of all, I've hacked together a painfully simple read-only file system. Each file is prefixed with a 13-byte header; 8 bytes for the filename (padded with spaces), 3 bytes for the extension (padded with spaces) and two bytes for the file size. The above file listing can be generated by typing *. at the BASIC prompt.

I've written a new sprite drawing routine that scales sprites up to double size when in CHIP-8 mode; this allows CHIP-8 games to fill the entire screen. Unlike the existing sprite code, which I've retained for SCHIP games, it runs entirely from ROM; the existing sprite code has to be copied to RAM as it uses some horrible self-modifying code tricks. I should probably rewrite that bit next. [smile]

As for the bug I mentioned in the last post, it was because of this:
; --- snip ---

; Group 9:
; * 9XY0 - Skips the next instruction if VX doesn't equal VY.
InstructionGroup.9
call GetRegisterX
ld b,a
call GetRegisterY
cp b
jp nz,SkipNextInstruction

; Group A:
; * ANNN - Sets I to the address NNN.
InstructionGroup.A
call GetLiteralNNN
ld (DataPointer),hl
jp ExecutedInstruction

; --- snip ---

If an instruction in the form 9XY0 is executed and VX == VY, rather than jumping to ExecutedInstruction the code runs on and executes the instruction as if it had been an ANNN as well, which ended up destroying the data pointer. Adding a jp ExecutedInstruction after the jp nz,SkipNextInstruction fixed the bug.

One other advantage of the zoomed sprites is that "half-pixel" scrolls also work correctly:


...not that I've seen any game that uses them.






The last two screenshots show two versions of the game Blinky, one as a regular CHIP-8 program and the other taking advantages of the SCHIP extensions.

benryves

benryves

 

64KB RAM and a CHIP-8/SCHIP interpreter

The only major hardware modification since last time is the addition of another 32KB SRAM.


Click to toggle labels
This appears as two 16KB pages in the $4000..$7FFF slot. Currently only the first page is used for OS variables and scratch space, freeing up the upper 32KB entirely for BBC BASIC's use.

One other minor hardware addition is support for a dual-coloured LED on the control port. This LED will be used to signify file access - reads by a green LED and writes by a red LED. As such I haven't implemented a proper file system, but typing SAVE "FILE" or LOAD "FILE" at the prompt will transfer data between the Z80 RAM and a 24LC256 32KB EEPROM. The routines do not pay attention to any file name specified - the first two bytes on the EEPROM indicate the file size, and the rest of the EEPROM is the file. I think some sort of simplified version of FAT may work well, as the EEPROM has a natural page size of 64 bytes which could be used in place of clusters.


Adding the second 32KB SRAM required soldering wires to the underside of the stripboard, not something I'd recommend!
As I have not yet added any graphical commands to BBC BASIC, and as porting assembly programs to this hardware is going to be a bit of a pain until I decide on the way the OS is going to work, I decided to try and port Vinegar to the system. Vinegar is a CHIP-8 and SCHIP interpreter - CHIP-8 programs being simple bytecode and so relatively simple to interpret.


The code I had written was difficult to port, however, being inefficiently and messily written, so I ended up rewriting all of it apart from the sprite drawing routines. The TI-83+ LCD follows the usual trend of storing 8 horizontal pixels in each byte of video memory. The LCD I have stores 8 vertical pixels in each byte of video memory, which means that each 8x8 pixel block in memory needs to be rotated by 90? before being sent to the LCD hardware. This is understandably very slow, and not helped by the Z80 only running at 2MHz. To further complicate issues, games rely on two 60Hz timers, and I have no timing hardware. The current version of the interpreter has some bugs, but is good enough to run some SCHIP programs.


Click for video (1.16MB XviD MPEG-4 AVI)
CHIP-8 programs are displayed squashed in the top-left hand corner, as they're designed to run in a 64x32 video mode unlike SCHIP's 128x64 (happily, the resolution of the LCD) - typically, the one thing I really did need to fix for the new hardware, the sprite code, is the only thing I copied over. In reality, CHIP-8 graphics would need to be scaled up to fit the screen. Working out a way of getting the system to operate at 10MHz would really be a welcome upgrade!

benryves

benryves

 

Times, backlights and off-page calls

Dates, times and backlights

I'm using a DS1307 real-time clock to provide the computer with real-time date and time functions. It's a great little chip - all it needs is power, two lines for I2C communications, a 32768Hz crystal between two pins and a back-up battery to keep it ticking when main power is removed and it's happy. That accounts for seven pins; the last remaining pin can be used as a one-bit output (you can set it to a high or low state in software) or it can be configured to output a square wave at 1Hz, ~4kHz, ~8kHz or ~32kHz.


BBC BASIC can access the clock via the TIME$ pseudo-variable. This string variable returns the date and time in the format Sun,14 Sep 2008.15:20:00, and you can set the clock by assigning to the variable. When setting the clock you can specify either the date, the time, or both. Parsing the string has been an interesting exercise in Z80 programming, as it's not something I've ever attempted without regular expressions before!


The only hardware modification since last time is a very poorly implemented software control of the backlight. The fifth bit of the control port specifies whether the backlight is on or off, and it can be toggled with the *BACKLIGHT command. I say "poorly implemented" as the transistor driver I'm using to interface the hardware port with the backlight LEDs results in a much dimmer backlight than when I had the LEDs hooked up directly to the power supply (on the positive side, at least the 5V regulator's heatsink is cool enough to touch - the backlight draws a lot of current).

Calling off-page functions

Now that I have access to all eight 16KB "pages" that make up the 128KB OS ROM, it may help to explain how one can use all of this memory. After all, if page 1 is swapped in and you wish to call a function on page 2, a regular Z80 call isn't going to work as you need to swap page 2 before calling the function then swap page 1 back in afterwards.

The trick is to exploit the way that the Z80 handles calling subroutines. There is a 16-bit register, PC, which stores the address of the next instruction to execute. When you call a subroutine, the Z80 pushes PC onto the stack then sets PC to the address of the subroutine. When you return from a subroutine (via the ret instruction) the Z80 simply pops the value it previously pushed onto the stack and copies this back to PC. Instead of calling the target subroutine directly, you call a special handler that is available on every page. Following your call is 16-bit identifier for the off-page function you wish to call. This handler then (prematurely) pops off the return address from the stack, reads the 16-bit value that follows it (which is the indentifier of the function you wish to call), looks up the page and address of the target function, swaps in the correct page and calls it as normal. When the function returns, the handler then swaps back the calling page and jumps back to the return address.

The Z80 has a series of rst instructions that call fixed addresses within the first 256 bytes of memory. These instructions are useful as they're small (one byte vs three bytes for a regular call) and fast, so I'm using rst $28 to call the off-page call handler (for no other reason than it's the same as the handler on the TI-83+).

As an example, let's say you had this function call at address $2B00:
$2B00: rst $28
$2B01: .dw $30F0
$2B03: ; We'd return here.


When the Z80 executed that rst $28 it would push $2B01 (address of the next instruction) to the stack then jump to $28. The handler at $28 would do something like this:
pop hl ; hl is a 16-bit register and would now contain $2B01
ld e,(hl) ; Read "e" from address pointed to by hl, now equals $F0
inc hl ; hl = $2B02
ld d,(hl) ; Read "d" from address pointed to by hl, now equals $30
inc hl ; hl = $2B03 ("real" return address)
push hl ; push hl back on the stack so when we return from here we end up in the correct place.

Now, de is $30F0 - this is the identifier of the function we're calling. In my case, the identifier points to a function table on page 0. Each entry in the table is three bytes - one byte for the page index and two bytes for the address of the function on the that page. We'd need to do something like this: in a,(Page) ; Read the current page into A.
push af ; Push A and F to the stack for later retrieval.
and ~7 ; Mask out the lower three bits of the address.
out (Page),a ; Sets current ROM page to 0.
ex de,hl ; Exchanges de and hl, so hl now points to the function identifier.
or (hl) ; ORs contents of memory at (hl) (ie, page number) with a, to set the target page.
inc hl
ld e,(hl) ; e = LSB of target address
inc hl
ld d,(hl) ; d = MSB of target address
ex de,hl ; hl = target address.
out (Page),a ; Swaps in the correct page.
At this point, the correct page is swapped in and hl points to the address of the function to call. All we need to do now is call it! ld de,ReturnFromHandler ; Address to return to.
push de ; Store on stack.
jp (hl) ; Set pc = hl.
ReturnFromHandler
; Swap back the original page which was pushed earlier...
pop af
out (Page),a
ret ; ...and return to the calling page!

A further advantage of using rst $28 to replace call is that both are the same size, so the assembler can check if you're calling an address on the same page or a different one and insert the regular (and much faster) Z80 call in places where you don't need to swap the page.


Click for video (578KB XviD MPEG-4 AVI)
Finally, the obligatory video, this time showing a clock that toggles the backlight once a second.

benryves

benryves

 

Bank-Switching Memory and I2C

Cheers for the comments. [smile] As EasilyConfused pointed out, I have done calculator programming in the past, which makes this much easier - learning Z80 assembly to program a calculator influenced the choice of CPU in this computer, and porting BBC BASIC to the calculator showed that with a minimal amount of code to sit between it and the hardware you'd have a decent operating system with very little work. And if a Terminator-related name is good enough for the UK military, it should be good enough for this project...

The I/O board from a few posts ago has undergone a few revisions:


Both PS/2 ports are now fully wired up, though only the lower one is currently used by the OS for keyboard input. I will need to adjust the AT protocol routines (the AT protocol is used to control both keyboard and mouse) to support multiple physical ports, as it was adapted from code I wrote for the TI-83+ calculator and as such only supports one device at a time. The mouse position will be polled by calling ADVAL(axis%), which on the BBC Micro would return the joystick position (axis% specifies the type of information to retrieve from the mouse - a value of 0 returns the buttons as a bitfield, 1 returns the movement in the X axis, 2 the movement in the Y axis and 3 the amount the scrollwheel has been scrolled).

At the very bottom of the circuit board is another 8-bit latch. This is for the (currently) write-only control port. The three least significant bits specify the current ROM page (one of eight 16KB ROM pages can be swapped in for a total of 128KB) and the next bit specifies one of two 16KB RAM pages accessible in the $4000..$7FFF address range. One of the other bits will be used to switch the LCD backlight on and off in software, one more may be connected to a buzzer, and I'm sure I can find some use for the last two. As it's write-only, its current state needs to be stored in RAM so that you can change bits of it (eg when changing the ROM page you wouldn't want to change the backlight status at the same time; you'd need to retrieve the current state and mask in the bits you wish to preserve). This is obviously an ugly hack, and I'm hoping I'll be able to use some of the space to the right of the latch IC on the circuit board to add the other latch to allow the port to be read as well (an I/O port needs two latches - an output latch that takes data from the data bus and outputs it to external hardware, and an input latch that takes data from external hardware and puts it back on the data bus).

The first test of the new ROM paging hardware was to display a simple animation. Assuming 1KB on each ROM page was taken up by the animation playback program, that leaves 15KB per ROM page. A frame (128x64 pixels) is 1KB, so that's 15 frames per page, or 120 frames total. I converted a clip from Pink Floyd's Arnold Layne music video to a suitable format and wrote a playback routine that could run from RAM. When the computer booted it would copy the player to RAM and run it from there as it could then run uninterrupted when different ROM pages were swapped in to read the frame data.


Click for video (526KB XviD MPEG-4 AVI)
An animation like this is a useful test, as if the ROM paging didn't work properly (simulated by holding the three ROM page selection lines low) the software would still run, but would just loop the first 15 frames (or play chunks of 15 frames out of sequence) instead of crashing.

Another addition to the circuit above is the cluster of discrete transistors and resistors under the lowest PS/2 port. This is the same sort of pair of open-collector I/O data lines that drive each PS/2 port, except that the two data lines are fed out of the I/O board and back to the breadboard that's currently sitting between the memory board and the I/O board to these two simple 8-pin chips:


This is the I2C bus, a simple, low-speed, two-wire bus that will allow other components be easily connected to the computer. The I2C protocol is implemented in software. The two chips in the above image are a DS1307 real-time clock (foreground, with quartz crystal) which I hope to use for timing purposes and a 24LC256 32Kx8 EEPROM which I hope to use for file storage. I would need to have some way of accessing the I2C bus externally (to plug in EEPROMs as removable storage) as well as supporting the internal devices.

I haven't yet done any work on supporting I2C devices properly, but I have added I2C bus emulation to the emulator I'm using to develop the OS. BBC BASIC will pass commands prefixed with a *STAR to the operating system, so I've added a *I2CPROBE command that will hammer through all available addresses and list any devices that acknowledge a write request.


$A0 is the EEPROM and $D0 is the clock.

I think I may have dug myself into a hole for CPU timing. I mentioned that I will need to drop the CPU clock to 2MHz when accessing the LCD; unfortunately, switching between 2MHz and 10MHz doesn't seem to work very well. I can run the system relatively stably at either speed (though at 10MHz data sent to the LCD is occasionally corrupted) but if I try and switch dynamically (eg switching from the 10MHz to the 2MHz clock when /IORQ goes low to indicate an I/O request) the system locks up. My assumption is that during time it takes the logic gates that perform the 10MHz/2MHz switch to properly settle into their new state (which is in the tens of nanoseconds) the clock signal stutters a little, effectively producing a clock signal (albeit a brief one) well over 10MHz. I don't have an oscilloscope to verify this, however. [sad]

benryves

benryves

 

Running BBC BASIC on a home-built computer

This computer needs a name - I'd welcome any suggestions!
I have built a circuit on another piece of stripboard that will handle memory, clock signal generation and the Z80 itself.

A few posts ago I was wondering about how I'd partition memory. To date I've been using a very simple circuit where the lower 32KB of addressable memory is mapped to ROM and the upper 32KB is mapped to RAM. As my ROM chip is 128KB and I have two 32KB RAM chips, this seems a bit wasteful.

The memory layout I'm now using is quite simple: the upper 32KB is still mapped to RAM. However, only the first 16KB is mapped to ROM, and the three most significant bits of the ROM chip's address lines are connected to a device on the I/O board so that one of its eight 16KB "pages" can be swapped in. The next 16KB will be mapped to RAM, and the most significant bit of the RAM chip's address is connected to the same device on the I/O board so one of its two 16KB "pages" can be swapped in.


For more information, see the Wikipedia article on bank switching. There is a potential problem here; the Z80 uses particular fixed addresses for certain operations. The three most obvious ones are $0000 (jumped to on reset), $0038 (address of maskable interrupt handler) and $0066 (address of non-maskable interrupt handler). As which 16KB bank switched in at power-on is effectively random, the easy way around this problem is to ensure that the first 256 bytes or so of every ROM page has the same code assembled on it. This means that whichever page is swapped in on boot doesn't matter, as the same common boot code is available on each page.

The assembled memory board looks like this:


Click to toggle labels
I have only attached one of the 32KB RAM chips. The wiring was becoming a bit of a nightmare (I think I'll need to solder to the track side of the stripboard to fit in that other RAM chip) so for the moment the system can only access the fixed 32KB RAM. I haven't yet added the device on the I/O board to handle bank switching, so for the moment the ROM is permanently configured to access the first 16KB page by pulling the its three externally controllable address lines low.

That said, this machine does genuinely run BBC BASIC (the last system only ran a mockup with a dummy header at the top of the screen). I've done quite a bit of work on the OS in the emulator and it works pretty well there, and with a minor adjustment to cram it onto a single 16KB page it works well on hardware too.


Click for video (664KB XviD MPEG-4 AVI)
The row of chips along the bottom of the memory board are responsible for generating the clock signals that drive the computer. If this looks needlessly complex, that's because it can run at either 10MHz or 2MHz and generates the E signal for LCD access. The CPU needs to drop to 2MHz when accessing the LCD (the LCD driver can't keep up, otherwise) so I'll probably end up connecting the input for this 2MHz/10MHz switch to the LCD chip enable pins so that normally the system runs at 10MHz but drops to 2MHz when accessing the LCD. Allowing the user to drop to 2MHz to save power is an appealing idea, however...

benryves

benryves

 

2MHz should be enough for anyone

LCD Timing
Last time I discussed the hardware I mentioned I had LCD timing issues. I have finally resolved them, but this has been the most time consuming part of the project so far.

The first thing to sort out was the LCD's E pin. Once you have set up the LCD's input pins to a state where they're ready to read or write data, you need to drive this pin high. I had had some success by holding it high permanently and relying on the Z80 to set all the other to the right state at roughly the same moment, but this was inaccurate and resulted in occasional display glitching.

Consulting the datasheet, it appears that once the input pins are ready E needs to be held low for at least 450nS and then needs to be driven high for at least 450nS. Hmm. During an I/O request (and once the Z80 has prepared the address and data bus) there's a delay of 1 clock cycle, then /IORQ is held low for about 2.5 CPU cycles. That is the window of opportunity. I have connected a binary counter to the Z80's clock signal and take the least significant bit of the output - every clock cycle this output toggles between a low and a low, effectively halving the CPU clock rate. I then connect the counter's reset pin (which overrides the clock input and forces it to output zero) to /IORQ. The result is that when the Z80 is not accessing hardware the counter is held in its reset state, and E is held low. When the Z80 holds /IORQ low, the counter starts up and outputs a zero for one CPU cycle, outputs a one for the next CPU cycle, then outputs a zero for the next half clock cycle at which point /IORQ goes high again and it is back to zero anyway. This is exactly what's needed!

This also allows us to calculate the maximum CPU clock rate. If we are generous and allow E to be low for 500nS then high for 500nS, that gives us a CPU clock rate of 1/500nS=2MHz.

Anyhow, that's one problem resolved, but there was still one nasty bug. When reading from the LCD it would occasionally end up writing to the area that was being read or, in worse cases, the Z80 would "crash". The LCD has a R/#W pin that is held high when reading and held low when writing. I had connected it directly to the Z80's /WR pin, which is high normally and low when writing. The problem here is "normally" as the LCD was expecting to be read even when the Z80 wasn't requesting a /RD. When being read, the LCD expects to put something onto the data bus, and it appeared that it kept thinking that it needed to put something on the bus when it wasn't needed. This caused fighting with the other chips that were trying to put their own values on the data bus, hence the crashes as the Z80 received invalid data.

The answer was very easy; simply connect the Z80's /RD pin to the LCD's R/#W pin via a NOT gate. In the default state the pin is held low (LCD expects a write and leaves the data bus alone), and only goes high during a /RD. The LCD interface is now very robust.

CPU Clock
Above I mention the calculation for the maximum clock rate. Rather than use the 555 for timing, I switched to using a 10MHz crystal resonator oscillator. I'm using the serial resonant circuit from z80.info with a 74F04 hex inverter (second circuit down). Fortunately the counter chips I have are decade counters (ie, designed to count from 0 to 9) made up of a /2 and a /5 section. I can connect a 10MHz oscillator to the /5 section and use the output of that to drive the CPU. In the final design I'd like to add a "hardware control port" with a bit that would let the programmer choose 2MHz or 10MHz mode by setting or resetting a particular output bit (other control bits would include switching the LCD backlight on or off and a buzzer for beeping sound output).

PS/2 Ports
As a friend pointed out, the 8-bit open-collector I/O port (which will drive two PS/2 ports, the I2C bus and TI calculator link port) had a flaw - there was no resistor on the base of the output transistors. The result is that if the output latch tries to drive the transistor base high, the transistor switches on and shorts the output of the latch to ground. This was clearly a problem in the design as the LCD backlight dimmed when trying to output to these ports as they drew a excessive amount of current when effectively short-circuited. A 22K resistor between the output latch and base of the transistor fixed the problem.


In the above photo, I've also added two 100K resistors to hold the output high when floating, but only to the foreground PS/2 port for the time being. I don't think I'll be controlling a mouse yet!

Revised OS
With a little modification of the Emerson PS/2 library I've got a basic keyboard driver up and running on the hardware. All the OS does for the moment is check for keys and display them on the screen. It's currently hard-coded to the UK layout, as I haven't yet decided how I'm going to handle storage and by keeping the layout in ROM it at least frees up a few hundred bytes of RAM that would otherwise need to be there for the scancode translation tables.


Click for video (1.76MB WMV)

benryves

benryves

 

Remapped IO.DLL

A common problem with software that uses the parallel port is that is hard-coded to use particular port addresses, such as 0x378 for LPT1. This is all well and good on older machines that have integrated or ISA ports that can be assigned this base address, but newer machines with PCI cards don't get any choice over which port address range is assigned to the parallel port.


Programming an AM29F010B with a PCI parallel port.
The supporting software for the Willem programmer has this problem, and so far I've been using an old XP laptop with a flaky WiFi adapter to program chips. This has been rather painful, understandably!

Fortunately, in Windows NT Microsoft forbade us from accessing hardware ports directly when in user-mode. This probably doesn't sound very fortunate, but the result is that various libraries for accessing the I/O ports using an embedded kernel-mode driver sprang up. Whilst this still isn't perfect (you need to run the calling app with Administrator privileges) it does mean that apps that access the parallel port will have one of these libraries kicking around as a DLL in their installation directory. The Willem programmer software is no exception; it uses IO.DLL.

The trick, then, is to simply replace the IO.DLL bundled with the app with a custom one that performs the same task, but redirects port writes in the LPT1 range (0x378~0x37F) to a user-specified base address (in my case, 0xCCD8). I couldn't get IO.DLL itself to work from within a DLL, and using it meant that you'd have to also rename the existing IO.DLL to something else, so I used Inpout32.dll instead to access the ports from the new DLL.

IO.DLL provides many helper functions, Inpout32.dll only offers two - Inp32() and Out32(). Fortunately, the Willem software doesn't seem to use any of the helper functions, and only uses PortIn() and PortOut(). (It does also use IsDriverInstalled(), but I've hard coded that to always return -1). The sample C++ interface code doesn't check the return value of GetProcAddress(), so the software still initialises, but will crash with an access violation if it tries to use any of the unsupported functions.

With some help from ibutsu to get around the C function name mangling problem (resolved by adding a simple .def file), you can simply extract three files into the Willem software directory (new io.dll, inpout32.dll for port access and io.ini for the user-specified port address) and the software appears to work fine! (I haven't tested all chip modes, as I only have one type of flash memory chip and an I2C serial EEPROM, but they work).

Download DLLs and source or documentation.

benryves

benryves

 

Z80 computer with a primitive I/O board

A computer needs some way of interacting with the outside world via input and output devices. It's about time, then, that the Z80 computer project acquires a section dedicated to I/O.


The Z80 differentiates between memory and I/O devices, though both share the data bus and the address bus. You can control I/O devices using the in (input) and out (output) instructions. When you input or output you must specify a device address and a value or target register. For example,

in a,($20) ; Read a value from device $20 and store it in A.
ld a,123
out ($40),a ; Output 123 to device $40.


When you write to a device, the following happens:
The address bus is set to the address of the device to output to. The data bus is set to the value to be written to the device. The /IORQ and /WR pins on the Z80 go low. The device processes the written data. The /IORQ and /WR pins on the Z80 go high.
Reading from the device is very similar:
The address bus is set to the address of the device to read from. The /IORQ and /RD pins on the Z80 go low. The device puts the value to read onto the data bus. The Z80 reads the value on the data bus. The /IORQ and /RD pins on the Z80 go high.
(Accessing memory is a similar procedure, except with the /IORQ pin replaced by the /MREQ pin).

Interfacing I/O devices to a Z80 CPU should be rather straightforwards, then. I am using a 74HCT138N 3-to-8 line inverting decoder to handle the address bus input and /IORQ signal. This IC has three address inputs and 8 outputs. If the address input is %000, output 0 is low and all the other pins are high; if the address input is %001 output 1 is low and all the others are high; if the input is %010 output 2 is low and all the others are high (and so on and so forth). /IORQ is connected to another input on the chip, /E1, which causes all of the pins to go high when it is high regardless of the address input.

What does this mean in practice? Well, most devices have a "chip enable" or "chip select" input pin. When this input is active the device performs its function, but when the input is not active the device is deactivated and doesn't respond to any other inputs or output anything. By connecting each output of the 3-to-8 decoder to a particular device's chip enable pin I can ensure that each device is only activated when its address is specified on the address bus and the /IORQ pin on the Z80 is low.

I have connected the Z80's A5-A7 to A0-A2 on the 3-to-8 decoder. This means that the first device has a base address of $00, the second $20, the third $40 and so on at increments of $20. This might sound a little odd, but has a reason. Some devices, such as the LCD, have sub-addresses of their own. In the case of the LCD, it has a pin that specifies whether you're dealing with an instruction (such as a command to switch the display on or off or read the LCD status) or some data (which forms part of the picture on the LCD). By attaching this pin directly to the Z80's A0 and the LCD's chip select pin to output 1 from 3-to-8 decoder you end up with an LCD instruction port at $20 and an LCD data port at $21.

An LCD is all well and good, but we'll need to take input from the user. To accomplish this, I'm going to supply two PS/2 ports and implement the AT protocol (as used by PS/2 keyboards and pointing devices) in software. Each device only requires two open-collector data lines (data and clock), so a single I/O device that provides eight I/O lines would be useful.

The design I'm going for uses two 74AC373 octal transparent latches. When the latch enable input pin is held high whatever value is on the input passes through to the corresponding output. When the latch enable pin goes low, the last value that was latched at the input is still output. These particular latches also have an output enable pin that can be used to disable the outputs and let them "float" (ie, other devices can then drive that particular connection high or low as required). In this instance, one latch has its output enable pin activated so that it always outputs the last value written to it and has its latch enable pin connected to the Z80's /WR pin. The other latch has its latch enable pin activated so that it always outputs the values at its input and has its output enable pin connected to the Z80's /RD pin.


Click for legible schematic
The transistors on each output are used to provide open-collector outputs. When the base of the transistor is held low, the transistor is "switched off" and its output floats, and so can be driven by external circuitry. When the base of the transistor is held high, it switches on and effectively connects the output to ground. A pull-up resistor ensures that the pin has a high signal when not connected to anything. This arrangement is useful as each pin can be driven low by either device and so works as an input or an output (for a real-world example, an AT keyboard usually outputs a clock signal on one line to the host when sending data, but if the host pulls the clock line low it can inhibit communication and the keyboard buffers the data to send instead).


Click to toggle labels
Rather than build the circuit on breadboard, I went straight to stripboard. The above photo shows an incomplete version of the output board. Only one PS/2 port is wired up at all! The pin header to the left is to connect the LCD to. The coloured wires at the extreme left connect this I/O board to the rest of the computer.


Click to toggle labels
I have modified the Z80 board I was using last time to add support for RAM. The 3-to-8 decoder in the bottom right is used to partition the address space into two 32KB regions. The lower 32KB is mapped to ROM, and the upper 32KB is mapped to RAM. This wastes 75% of the ROM chip (it's a 128KB chip) but without a more complex memory management unit this will have to do for the moment. The most significant bit of the address bus, A15, is fed into the 3-to-8 decoder along with the /MREQ pin.

The test software is a Z80 program that displays an animation on the LCD using 20 frames (1KB per frame) stored in ROM.


Click for video (943KB WMV)
The Z80 is still not breaking MHz speeds, but there are problems here. I have not interfaced the LCD correctly, as its timing patterns for reading and writing data are quite different to those used by the Z80. Bizarrely, holding the E pin on the LCD permanently high appears to work 99% of the time, even though the datasheet indicates that it should be used to clock data in or out. The result is glitches in the data sent to the LCD, usually on the left hand side (the left hand side of the display has a propensity to believe it's been sent the "switch off" command). I'm not sure I'll be able to remedy this situation. Judging by the datasheet it looks like the LCD does its stuff when the E pin goes from a low to high state (the Z80 does everything when /IORQ goes low), so maybe simply inverting /IORQ and pumping it into E will do the trick.

benryves

benryves

 

Z80 Light-flasher

Now armed with a flash programmer, I thought it about time to try and build a Z80-based system.


Click for video (829KB WMV)
Not much to look at, and it doesn't do much either. The large IC in the bottom-left, prominently marked Z, is the Z80 itself. To its left is a 555, generating a ~220Hz clock signal (yes, Hz, not MHz or even kHz). Above the Z80 is another large chip - this is the 128KB flash ROM. The eight parallel wires between them are the address bus - only A0 to A7 are connected. This only lets the Z80 address 256 bytes, but that should be enough for testing.

To the right of the flash ROM is an octal latch. This is used to provide an 8-bit output port for the system, which is connected to the LEDs to its right. As the latch's latch enable pin is active high, unlike everything else in the system (which is active low - ie, it does something when you drive it low) I have to put a NOT gate - the final black IC to the right of the Z80 - between it and the Z80's /WR (write) pin. I do not do any address decoding or even check the /IORQ pin, so any value written to to any hardware device or memory address will end up on the LED display. Not that that really matters, as there is a conspicuous lack of RAM in the system!

The large physical size and tedium of wiring even such a primitive system as this makes me wonder whether it's worth jumping straight to stripboard for subsequent hardware revisions...

For the curious, the program running on the Z80 is as follows.
.for p = 0 to 7
.defpage p, kb(16), $0000
.loop
.emptyfill $FF

.page 0

im 1
di

-- ld hl,LightSequence
ld b,LightSequenceEnd-LightSequence
- ld a,(hl)
out (0),a
inc hl
djnz -
jr --

LightSequence
.db %00000001
.db %00000010
.db %00000100
.db %00001000
.db %00010000
.db %00100000
.db %01000000
.db %10000000
.db %01000000
.db %00100000
.db %00010000
.db %00001000
.db %00000100
.db %00000010
.db %00000001
.db %00000010
.db %00000100
.db %00001000
.db %00010000
.db %00100000
.db %01000000
.db %10000000
.db %01000000
.db %00100000
.db %00010000
.db %00001000
.db %00000100
.db %00000010
.db %00000001
.db %00000011
.db %00000111
.db %00001111
.db %00011111
.db %00111111
.db %01111111
.db %11111111
.db %11111111
.db %00000000
.db %11111111
.db %00000000
.db %11111111
.db %00000000
.db %11111111
.db %00000000
.db %11111111
.db %00000000
.db %11111111
.db %00000000
.db %11111111
.db %11111110
.db %11111100
.db %11111000
.db %11110000
.db %11100000
.db %11000000
.db %10000000
.db %00000000
.db %10000000
.db %11000000
.db %11100000
.db %11110000
.db %01111000
.db %00111100
.db %00011110
.db %00001111
.db %10000111
.db %11000011
.db %11100001
.db %11110000
.db %01111000
.db %00111100
.db %00011110
.db %00001111
.db %10000111
.db %11000011
.db %11100001
.db %11110000
.db %01111000
.db %00111100
.db %00011110
.db %00001111
.db %00000111
.db %00000011
.db %00000001
.db %00000000
LightSequenceEnd

.echoln strformat("Size: {0} bytes", $)

benryves

benryves

 

Emulators and neatened wiring

I've decided to switch to a regular 10MHz Z80 rather than a Z180, given the difficulty of using an SDIP 64. I now have a DIP 40 Z80 ready for use, but as I don't have the programmer for the Flash chip (which will hold the OS) there's not much I can do with it physically. I have therefore cobbled together a basic emulator to help develop some of the software beforehand.


To cut hardware costs I'm going to try and handle input in software. One bit of hardware I'm planning on having is an eight-bit open collector I/O port. Open collector pins float high in their reset state, and any device connected to the pin can drive it low. AT devices (keyboard and mouse) use this type of electrical connection, as does the I2C bus and the TI calculator link port. I can use up the eight pins easily - two pins per AT device (keyboard and mouse) makes four, two pins for the I2C bus and two pins for a TI calculator link port.

The I2C bus I mentioned above is a simple way to enhance the computer once built. There will be one device permanently attached to the bus, a DS1307 real-time clock, which will be used to provide time-keeping functions for the OS as well as generating periodic interrupts (the chip could be configured to trigger an interrupt 100 times a second, useful for timing game logic). I could then leave empty space on the circuit board to add other I2C devices over time, or have a socket on the case that could be used to plug in additional I2C modules.

Now that I have some more tools, namely a desoldering pump, I tidied up the horrible hack job I'd done on the graphical LCD (replacing the multiple wires with a single pin header).


Yes, still the PICAXE here, but I'm using its 256 byte EEPROM to store a 32x64 pixel image of Sonic that is repeated four times horizontally.

I'm still not sure what I'm doing with regards to memory or storage. I'm still working on the simple assumption that ROM is 32KB ($0000..$7FFF) and RAM is 32KB ($8000..$FFFF) but this wastes a lot of memory and isn't very flexible at all. I've planned a bank-switching MMU, but as this will require at least four registers to store what appears in each of the four 16KB windows it will end up being physically very large and painful to wire.

As for storage, I have no idea. I have some 32KB I2C EEPROMs, but 32KB isn't exactly very large. Alternatively, I have an old 512MB SD card, and could try talking to it over bit-banged SPI. (SD cards use 3.3V, though, which complicates matters - not to mention that bit-banged SPI is going to be a little sluggish). I also have a USB module which can talk to USB mass storage devices over a serial connection, so maybe I should add a UART to the project. Adding a fully-blown USB module (which also plays WMA, MP3 and MIDI files) to such an otherwise low-tech computer feels like heresy, though.

benryves

benryves

 

Experimenting with a 32KB RAM

The next component I thought I'd experiment with is the RAM. The project is an analogue recorder - a circuit that samples an analogue input periodically and saves it in RAM, and can be configured to play the recorded signal back afterwards.


Yes, it says plating.

A single RAM chip offers 32K with an eight-bit word size. This requires fifteen lines to address it, A0..A14. The PICAXE-28X1 that is to control the circuit does not have enough output pins to be able drive this address bus and a data bus (to transfer values to and from RAM) and a still have enough pins left over to control the various components. To get around this, two octal (eight-bit) latches are used to drive the address lines, A0..A7 from one chip and A8..A14 from another. The inputs to these latches are connected to the data bus (PortC on the PICAXE), and two pins on the PICAXE are set aside to trigger the latch enable pins on either latch.

What this means in practice is that if you wished to change the current address to $1234 you would put $34 on the data bus and trigger the latch that corresponds to the least significant byte of the address, then put $12 on the data bus and trigger the latch that corresponds to the most significant byte of the address.


Any hobbyist can have wire insulation in any color that he wants so long as it is black.
A 10K potentiometer provides the required analogue input and an LED provides the output. The switch on the left is used to change between recording and playback modes. The large chip at the top is the RAM, the two small ones in the middle are the octal latches and the medium one on the right is the PICAXE-28X1.

As only 15 lines are needed to address 32KB, the most significant bit of the address bus is wired to the /WE pin of the RAM chip. This pin determines whether we're writing to (low) or reading from (high) the chip. This effectively means that addresses $0000..$7FFF are used when writing and addresses $8000..$FFFF are used when reading.

The only remaining connections to the RAM chip required are chip enable (/CE) and output enable (/OE). When chip enable is low, the RAM chip can be accessed; when high, it ignores all input. When not in use we therefore make sure that chip enable is high. When output enable is low, the RAM chip puts the value at the current address onto the data bus, so we need to pull this low when reading but make sure it's left high most of the time so that the RAM chip doesn't interfere with other devices trying to put a value on the data bus.

The code for the test program is as follows:
; Pins:
Symbol RamChipDisable = 4
Symbol RamOutputDisable = 5
Symbol AddressLatch0 = 6
Symbol AddressLatch1 = 7

; Registers:
Symbol RamValue = B0
Symbol RamAddress = W1 ; B3:B2
Symbol RamAddressLow = B2
Symbol RamAddressHigh = B3
Symbol RamPointer = W2 ; B5:B4

Symbol RecordingLength = W3 ; B7:B6

Boot:
Let DirsC = $00
High RamChipDisable
High RamOutputDisable
Low AddressLatch0
Low AddressLatch1
Let RamPointer = 0
SetFreq M8

Main:

StartPlaying:
SerTxd ("Started playing: ", #RecordingLength, " bytes", CR, LF)
Let RamPointer = 0
Pause 100
PlayingLoop:
If PortA Pin1 = 1 Then StartRecording

; Read stored value from RAM.
Let RamAddress = RamPointer
GoSub ReadRam

; Set LED brightness to stored value.
Let W4 = RamValue * 4
HPwm PwmSingle, PwmHHHH, %0100, 255, W4

; Increment playback pointer and loop if hit end.
Let RamPointer = RamPointer + 1
If RamPointer = RecordingLength Then
RamPointer = 0
EndIf

; Loop back.
GoTo PlayingLoop


StartRecording:
SerTxd ("Started recording...", CR, LF)
Let RecordingLength = 0
Pause 100
RecordingLoop:
If PortA Pin1 = 0 Then StartPlaying

; Read value from ADC.
ReadAdc 0, RamValue

; Set LED brightness to read value.
Let W4 = RamValue * 4
HPwm PwmSingle, PwmHHHH, %0100, 255, W4

; Store value read from ADC into RAM.
Let RamAddress = RecordingLength
GoSub WriteRam

; Increment record pointer.
Let RecordingLength = RecordingLength + 1

GoTo RecordingLoop

WriteRam:
; Set up address bus:
Let DirsC = $FF
Let RamAddressHigh = RamAddressHigh & %01111111
Let PinsC = RamAddressHigh
High AddressLatch1 : Low AddressLatch1
Let PinsC = RamAddressLow
High AddressLatch0 : Low AddressLatch0
; Set up data bus and write:
Let PinsC = RamValue
Low RamChipDisable
High RamChipDisable
Let DirsC = $00
Return

ReadRam:
; Set up address bus:
Let DirsC = $FF
Let RamAddressHigh = RamAddressHigh | %10000000
Let PinsC = RamAddressHigh
High AddressLatch1 : Low AddressLatch1
Let PinsC = RamAddressLow
High AddressLatch0 : Low AddressLatch0
; Set up data bus and read:
Let DirsC = $00
Low RamOutputDisable
Low RamChipDisable
Let RamValue = Pins
High RamChipDisable
High RamOutputDisable
Return

As before, there's a simple video of the circuit in action.


798KB WMV

benryves

benryves

 

Back to Hardware

I enjoy dabbling with low-level programming, but have never actually built a computer to run these programs. I think it's time to correct that, and as the BBC BASIC project has required me to develop an almost complete Z80 OS (the only thing that's left for the TI-OS to do is manage files) I thought a Z80 computer would be a good start.

The planned specs are (as a starting point): 10 MHz Z80180 CPU; 64KB RAM (2 32Kx8 SRAM chips); 128KB Flash ROM; Graphical LCD; Simple joypad input; Keyboard input (AT using either software AT routines or dedicated microcontroller). The first spanner in the works is the Z80180, as I didn't read the datasheet closely enough and it's in a DIP 64 package with 0.07" pin spacing instead of the standard 0.1" pin spacing. I'll need to find some way of constructing an adapter so I can use it with my breadboards and stripboard. [smile]

In the meantime, I've concentrated on the graphical LCD. I picked a 128x64 backlit graphical LCD for the princely sum of GBP16. It's very easy to control - you hook up it up to a 8-bit data bus to transfer image data and instructions and a handful of control pins to indicate what you're doing on that bus (reading or writing, whether you're sending an instruction or some image data, that sort of thing) and that's it - the only supporting circuitry it requires is a 10K potentiometer to act as a contrast control and power for the display and backlight.


To experiment with the LCD, I'm using a PICAXE-28X1 microcontroller, programmed in BASIC. There isn't much space to store graphics, so I'm using a 32 character font (at eight bytes per character, that takes up all 256 bytes of free EEPROM space!)

; LCD data bus should be connected to port C.

Symbol LcdRegisterSelection = 0 ; D/I : 4
Symbol LcdReadWrite = 1 ; R/W : 5
Symbol LcdStartEnable = 2 ; E : 6
Symbol LcdChipSelect1 = 3 ; CS1 : 15
Symbol LcdChipSelect2 = 4 ; CS2 : 16
Symbol LcdReset = 5 ; /RST : 17

; Storage for console state variables.

Symbol ConsoleX = B10
Symbol ConsoleY = B11
Symbol ConsoleChar = B12


GoSub LcdInit ; Initialise LCD.
B0 = %00111111 : GoSub LcdWriteInstruction ; Switch LCD on.

GoSub LcdClear ; Clear LCD

; Write the obligatory message to the LCD.

ConsoleX = 0 : ConsoleY = 0

ConsoleChar = $08 : GoSub LcdPutChar ; H
ConsoleChar = $05 : GoSub LcdPutChar ; E
ConsoleChar = $0C : GoSub LcdPutChar ; L
ConsoleChar = $0C : GoSub LcdPutChar ; L
ConsoleChar = $0F : GoSub LcdPutChar ; O
ConsoleChar = $1D : GoSub LcdPutChar ; ,
ConsoleChar = $00 : GoSub LcdPutChar ;
ConsoleChar = $17 : GoSub LcdPutChar ; W
ConsoleChar = $0F : GoSub LcdPutChar ; O
ConsoleChar = $12 : GoSub LcdPutChar ; R
ConsoleChar = $0C : GoSub LcdPutChar ; L
ConsoleChar = $04 : GoSub LcdPutChar ; D
ConsoleChar = $1B : GoSub LcdPutChar ; !

Pause 2000

B2 = 0
MainLoop:
B2 = B2 - 1
B0 = B2
GoSub LcdGotoZ
Pause 30
GoTo MainLoop

LcdInit:
DirsC = $00 ; Set data bus to input.
High LcdStartEnable ; We're not writing anything.
High LcdChipSelect1
High LcdChipSelect2
Low LcdReset
Pause 500
High LcdReset
Pause 500
Return

LcdWriteInstruction:
Low LcdReadWrite
DirsC = $FF ; Data bus = output.
PinsC = B0 ; Set data bus state.
Low LcdRegisterSelection ; Instruction, not data.
Low LcdStartEnable
High LcdStartEnable
DirsC = $00 ; Leave data bus floating.
Return

LcdWriteData:
Low LcdReadWrite
DirsC = $FF ; Data bus = output.
PinsC = B0 ; Set data bus state.
High LcdRegisterSelection ; Data, not instruction.
Low LcdStartEnable
High LcdStartEnable
DirsC = $00 ; Leave data bus floating.
Return

LcdGotoX:
B0 = B0 And 7
B0 = B0 + %10111000
GoTo LcdWriteInstruction

LcdGotoY:
B0 = B0 And 63
B0 = B0 + %01000000
GoTo LcdWriteInstruction

LcdGotoZ:
B0 = B0 And 63
B0 = B0 + %11000000
GoTo LcdWriteInstruction

LcdClear:
For B2 = 0 To 7
B0 = B2
GoSub LcdGotoX
B0 = 0
GoSub LcdGotoY
B0 = 0
For B3 = 0 To 63
GoSub LcdWriteData
Next
Next B2
Return

LcdPutMap:
B1 = B0 * 8
For B2 = 0 To 7
Read B1, B0
GoSub LcdWriteData
B1 = B1 + 1
Next B2
Return

LcdPutChar:
B0 = ConsoleY
GoSub LcdGotoX
B0 = ConsoleX * 8
If B0 64 Then
Low LcdChipSelect2
Else
Low LcdChipSelect1
B0 = B0 - 64
EndIf
GoSub LcdGotoY
B0 = ConsoleChar
GoSub LcdPutMap
High LcdChipSelect1
High LcdChipSelect2
ConsoleX = ConsoleX + 1
If ConsoleX = 16 Then
ConsoleX = 0
ConsoleY = ConsoleY + 1
If ConsoleY = 8 Then
ConsoleY = 0
EndIf
EndIf
Return

; Font
EEPROM $00,($00,$00,$00,$00,$00,$00,$00,$00,$7E,$7F,$09,$09,$7F,$7E,$00,$00)
EEPROM $10,($7F,$7F,$49,$49,$7F,$36,$00,$00,$3E,$7F,$41,$41,$63,$22,$00,$00)
EEPROM $20,($7F,$7F,$41,$63,$3E,$1C,$00,$00,$7F,$7F,$49,$49,$49,$41,$00,$00)
EEPROM $30,($7F,$7F,$09,$09,$09,$01,$00,$00,$3E,$7F,$41,$49,$7B,$3A,$00,$00)
EEPROM $40,($7F,$7F,$08,$08,$7F,$7F,$00,$00,$41,$41,$7F,$7F,$41,$41,$00,$00)
EEPROM $50,($20,$61,$41,$7F,$3F,$01,$00,$00,$7F,$7F,$1C,$36,$63,$41,$00,$00)
EEPROM $60,($7F,$7F,$40,$40,$40,$40,$00,$00,$7F,$7F,$06,$1C,$06,$7F,$7F,$00)
EEPROM $70,($7F,$7F,$0C,$18,$7F,$7F,$00,$00,$3E,$7F,$41,$41,$7F,$3E,$00,$00)
EEPROM $80,($7F,$7F,$09,$09,$0F,$06,$00,$00,$3E,$7F,$41,$31,$6F,$5E,$00,$00)
EEPROM $90,($7F,$7F,$09,$19,$7F,$66,$00,$00,$26,$6F,$49,$49,$7B,$32,$00,$00)
EEPROM $A0,($01,$01,$7F,$7F,$01,$01,$00,$00,$3F,$7F,$40,$40,$7F,$3F,$00,$00)
EEPROM $B0,($1F,$3F,$60,$60,$3F,$1F,$00,$00,$7F,$7F,$30,$1C,$30,$7F,$7F,$00)
EEPROM $C0,($63,$77,$1C,$1C,$77,$63,$00,$00,$07,$0F,$78,$78,$0F,$07,$00,$00)
EEPROM $D0,($61,$71,$59,$4D,$47,$43,$00,$00,$00,$00,$5F,$5F,$00,$00,$00,$00)
EEPROM $E0,($02,$03,$59,$5D,$07,$02,$00,$00,$00,$80,$E0,$60,$00,$00,$00,$00)
EEPROM $F0,($00,$00,$60,$60,$00,$00,$00,$00,$07,$07,$00,$07,$07,$00,$00,$00)

The code isn't very robust - it doesn't check the state of the LCD's busy flag as I'm assuming that a 4MHz PIC running an interpreted BASIC is too slow to manage to write another byte to the LCD driver before it has finished processing the last one.

The font was generated from the following image (it's the BBC Micro font):


It's rotated through 90? as, unlike the LCD driver in the TI-83+, each byte written outputs 8 pixels vertically, with the least significant at the top. (On the TI-83+, each byte written outputs 8 pixels horizontally, with the most significant bit on the left). More interestingly, this graphical LCD is made up of two 64x64 regions next to eachother, and by controlling two chip select pins you can control whether each byte written updates the left side, the right side, neither or both. I'm entirely sure how I could use this, though, other than not-very-exciting tricks like clearing the LCD extra-fast.


924KB WMV
Finally, here's a video of the LCD test in action. It's not very speedy, but will hopefully pick up some speed once I figure out how I'm going to use that Z80180 CPU. [smile]

benryves

benryves

 

Graphical text, BASIC tokeniser and flood-filling

I've got a fairly hackish "graphical text" mode set up (enabled with VDU 5, disabled with VDU 4) that causes all text that is sent to the console to be drawn using the current graphics mode (at the graphics cursor position, using the graphics colour and logical plotting mode and graphics viewport). This allows text to be drawn at any position on-screen, but is (understandably) a bit slower and doesn't let you do some of the things you may be used to (such as scrolling text, copy-key editing and the like).


I've also done some work on a tool to convert files from the PC to use in BBC BASIC. It takes the form of a Notepad-like text editor:


BBC BASIC programs are stored in a tokenised format (usually .bbc files on a PC) and need to be wrapped into a .8xp for transferring to the calculator. The editor above can open .8xp, .bbc and .txt directly, and will save to .8xp.

The detokeniser can be passed a number of settings, which can be used to (for example) generate HTML output, like this. The indentation is generated by the detokeniser (leading/trailing whitespace is stripped by the tokeniser). The tool can also be used to directly convert binaries into .8xp files if need be.


I've been doing a little work on a flood-filling algorithm. (PLOT 128-135, 136-143). The above images show its progress; on the left is the first version (which can only fill in black). There is a hole in the bottom-left of the shape, so the leaking is intentional. It also stops one pixel away from the screen boundary -- this too is intentional (it clips against the viewport). The second version, in the middle, plugs the leak and applies a pattern (which will be a dither pattern in BBC BASIC) to the filled area. On the right is the third version, which will fill over black or white pixels with a pattern.

The main filling algorithm needs a 764 byte buffer for the node queue and three 16-bit pointer variables to manage the queue. I've rounded the queue size up to 768 bytes, so it fits neatly on one of the RAM areas designed to store a bitmap of the display.

The problem is filling with a pattern. The way I currently do this is to back up the current screen image to a second 768-byte buffer, fill in black as normal, then compare the two buffers to work out which bits have been filled and use those as a mask to overlay the dither pattern. This is quite a lot of RAM, just to flood-fill an image!

For those who are interested, I'm using the "practical" implementation of a flood fill algorithm from Wikipedia.

benryves

benryves

 

Text viewports and sprites

Back to work on the TI-83 Plus port of BBC BASIC! To complement the graphics viewport I've added support for text viewports -- this lets you define the area the text console uses. The following VDU commands are now supported:
VDU 24,;;;;
Define a graphics viewport. VDU 28,,,,
Define a text viewport. VDU 26
Reset both viewports to their default settings (full screen). VDU 29,;;
Defines the graphics origin.

The above screenshots defines the graphics viewport to fill the left hand side of the screen and shunts the text viewport over to the right half, using the following code:VDU 24,0;0;47;63;
VDU 28,12,0,23,9
VDU 29,24;32;I've also added simple sprite drawing to BBC BASIC's PLOT command. PLOT usually takes a shape type and two coordinates, but for sprites (shapes 208..215) I've added an extra parameter - the address of the sprite data to use.


10 DIM ball 7
20 ball?0=&3C
30 ball?1=&5E
40 ball?2=&8F
50 ball?3=&DF
60 ball?4=&FF
70 ball?5=&FF
80 ball?6=&7E
90 ball?7=&3C
100 *REFRESH OFF
110 REPEAT
120 CLG
130 T=TIME/100
140 FOR P=0 TO 5
150 A=P/3*PI+T
160 X=16*SIN(A)+44
170 Y=16*COS(A)+28
180 PLOT 213,X,Y,ball
190 NEXT
200 *REFRESH
210 UNTIL INKEY(0)-1
220 *REFRESH ON

The above code allocates 8 bytes of memory (DIM ball 7) then copies the sprite data to it by use of the ? indirection operator. This is a little laborious, so in reality you'd probably store your sprites in a binary file external to the main program, and might load them like this:
10 ball%=FN_loadSprite("SPRITES",0)
20 face%=FN_loadSprite("SPRITES",1)
30 *REFRESH OFF
40 REPEAT
50 CLG
60 T=TIME/100
70 FOR P=0 TO 5
80 A=P/3*PI+T
90 X=16*SIN(A)+44
100 Y=16*COS(A)+28
110 PLOT 213,X,Y,ball%
120 NEXT
130 PLOT 213,44,28,face%
140 *REFRESH
150 UNTIL INKEY(0)-1
160 *REFRESH ON
170 END
180 DEF FN_loadSprite(f$,i%)
190 fh%=OPENIN(f$)
200 PTR#fh%=i%*8
210 DIM spr 7
220 FOR j%=0 TO 7
230 spr?j%=BGET#fh%
240 NEXT j%
250 CLOSE#fh%
260 =spr
270 ENDPROC

(Note FN_loadSprite() at the end of the program). The result is the following:


Next up: drawing text at the graphics cursor position (as sprites).

benryves

benryves

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!