The Propeller spin code used to drive the design for test purposes has been written years ago, for a different project:
However, it could be repurposed here with only minimal changes. That was possible because:
- V99X8 VDPs are truly backward compatible with TMS9918
- No special 99X8 modes are being used
- No extended registers are being used (only single address line is used)
Parallax Propeller is a very powerful chip - it contains 8 32-bit CPUs that can control 32-bit I/O pins. This allows direct interfacing with legacy chips in speed ranges below 10MHz or so. Beside VDPs, for example I was able to drive a Am9511 FPU too.
This project has only 2 files:
This is the VDP driver. It is interfacing the physical pins and drives them as if the VDP is on a bus of a microcomputer.
CON
'Signal Propeller pin VDP pin ( == F18A pins)
nRESET = 27'12' 34 == pull low for reset
MODE = 26'11' 13 == memory/register mode
nCSW = 25'10' 14 == write to register or VDP memory
nCSR = 24'9' ' 15 == read from register or VDP memory
nINT = 23'8' 16 == input always, activated after each scan line if enabled
CD0 = 7' 24 == MSB (to keep with "reverse" TMS99XX family documentation)
CD1 = 6' 23
CD2 = 5' 22
CD3 = 4' 21
CD4 = 3' 20
CD5 = 2' 19
CD6 = 1' 18
CD7 = 0' 17 == LSB
'VSS 12 == GND
'VCC 33 == +5V
Programming the Propeller has many interesting aspects, one of the most important ones is how to make multiple CPUs ("cogs") work in parallel. Each cog can drive own pins, but when the cog is stopped, those pins are "released". To ensure the pins toward VDP are constantly driven, a cog is initialized and then kept in a "dead loop".
The public "Start" method communicates the shared memory (described later) and after some housekeeping kicks off the _vdpProcess() routine in a new cog.
PUB Start(plCommandBuffer, initialMode, useInterrupt, enableTracing) : success
longfill(@stack, 0, STACK_LEN)
skipTrace := true
if (enableTracing)
pst.Start(115_200)
pst.Clear
skipTrace := false
Stop
plCommand := plCommandBuffer
longfill(@spriteSpeed, 0, 32)
colorGraphicsForeAndBack := byte[@GoodContrastColorsTable]
_prompt(String("Press any key to continue with TMS9918 object start using command buffer at "), plCommand)
lockCommandBuffer := locknew
if (lockCommandBuffer == -1)
_logError(String("No locks available to start object!"))
return false
else
cogCurrent := cognew(_vdpProcess(initialMode, useInterrupt), @stack)
if (cogCurrent == -1)
_logError(String("No cogs available to start object!"))
lockret(lockCommandBuffer~)
return false
waitcnt((clkfreq * 1) + cnt)
_logTrace(String("TMS9918 object launched into cog "), cogCurrent, String(" using lock "), lockCommandBuffer, String(" at clkfreq "), clkfreq, 0)
return true
The cog now runs the routine until it exists or other cog kills it from outside. The _vdpProcess() does the following:
- initialized the pins (input / output)
- fills the video memory (clears 16k)
- sets initial video mode
After that, it goes into an infinite loop of watching for a command and its parameters, and if received executes them. This is very similar to Window message processing paradigm: as long as the window exists, it has a "message pump" that accepts commands sent to it and execute them (one can even say that cog is the "hWnd").
The commands are "longs" (32-bit) values written to common RAM memory area. This is again similar to Windows CMD, lParam and wParam mechanism, but to simplify, the number of parameters here are flexible based on the command:
PRI _vdpProcess(initialMode, useInterrupt) |i, y, timer
_logTrace(String("TMS9918 object starting in cog "), cogId, String(" using lock "), lockCommandBuffer, String(" at clkfreq "), clkfreq, 0)
nextCharRow := 0
nextCharCol := 0
if (useInterrupt)
vdpAccessWindow := ((((clkfreq / 60) * (262 - 192)) / 262) * 95) / 100 'see table 3.3 in TMS9918 documentation (we have 70 scan lines every 1/60s)
else
vdpAccessWindow := clkfreq / 60
_logTrace(String("Initial mode is "), initialMode, String(" use interrupt is "), useInterrupt, String(" vdp access clock cycles is "), vdpAccessWindow, 0)
outa[nReset .. CD7]~~ 'set all to 1 (inactive)
dira[nReset .. CD7]~ 'set all to input first
dira[nReset .. nCSR]~~ 'these are always outputs
_vdpReset
_setReg(1, reg[1] & %1011_1111) 'blank screen
lastStatus := _readStatus
_fillVdpMem(0, 16 * 1024, 170, 0) '10101010 pattern
'this is the first command that will be executed
long[plCommand][0] := CMD_SETMODE
long[plCommand][1] := initialMode
displayMode := initialMode
longfill(@lastSpritePositionUpdateCnt, cnt, 32)
repeat 'keep executing commands until cog is stopped
repeat until not lockset(lockCommandBuffer) 'wait for the free lock (don't execute while command buffer is updated)
'update position of even numbered sprites according to their speed, if set
_updateSpritePositions(0)
timer := cnt
case LONG[plCommand]
CMD_SETSPRITEMODE:
_setSpriteMode(long[plCommand][1] & %0000_0011)
'_logCommand(String("CMD_SETSPRITEMODE in mode "), _interval(cnt, timer))
... (OTHER COMMANDS)
This mechanism could allow:
- FIFO buffering of commands (the driver component can work async and "stuff" commands to some preset depth and continue processing while the VDP cog executes)
- Multiple cogs can interface independently with various chips - with enough pins, 2 VDPs can be driven independently etc.
Let's see how a sample command is executed, for example drawing a circle:
CMD_DRAWCIRCLE:
_drawCircle(long[plCommand][1], long[plCommand][2], long[plCommand][3], long[plCommand][4])
'_logCommand(String("CMD_DRAWCIRCLE in mode "), _interval(cnt, timer))
Circle takes 4 parameters which are the coordinates of the center, radius, and color (which can be 0 or 1 in hi-res, or 0-3 in multicolor modes)
PRI _drawCircle(xc, yc, radius, color) |x, y, x2, y2, r2, x2m, pixCount
'_logTrace(String("Drawing circle in color "), color, String(" at "), xc << 16 | yc , String(" with radius "), radius, 8)
if (radius < 1)
return 0
pixCount := 0
x := radius
y := 0
r2 := radius * radius
x2 := r2
y2 := 0
repeat while (y =< x)
pixCount += _drawPixel(xc + x, yc + y, color)
pixCount += _drawPixel(xc + x, yc - y, color)
pixCount += _drawPixel(xc - x, yc + y, color)
pixCount += _drawPixel(xc - x, yc - y, color)
pixCount += _drawPixel(xc + y, yc + x, color)
pixCount += _drawPixel(xc + y, yc - x, color)
pixCount += _drawPixel(xc - y, yc + x, color)
pixCount += _drawPixel(xc - y, yc - x, color)
y2 := y2 + y + y + 1
y++
x2m := x2 - x - x + 1
if (_circleError(x2m, y2, r2) < _circleError(x2, y2, r2))
x--
x2 := x2m
On the bottom of the execution stack are the routines that drive the VDP signals in order to write command or data, or read status or data, including generating a reset:
{{ interfacing with VDP chip }}
PRI _readStatus
return _vdpRead(1)
PRI _vdpRead(modeVal)
if (modeVal == 0) 'only wait if reading from vdp memory, not status reg
_waitForScan
outa[MODE] := modeVal 'set mode
outa[nCSW]~~ 'write inactive
outa[nCSR]~~ 'read inactive
dira[CD0 .. CD7]~ 'data bus is input
outa[nCSR]~ 'pulse nCSR
result := ina[CD0 .. CD7]
outa[nCSR]~~
PRI _vdpWrite(byteVal, modeVal)
if (modeVal == 0) 'only wait if writing to vdp memory, not register
_waitForScan
outa[MODE] := modeVal 'set mode
outa[nCSW]~~ 'write inactive
outa[nCSR]~~ 'read inactive
dira[CD0 .. CD7]~~ 'data bus is output
outa[CD0 .. CD7] := byteVal
outa[nCSW]~ 'delay
outa[nCSW]~~
PRI _vdpReset
outa[nReset]~
waitcnt((clkfreq / 2) + cnt) '500ms
outa[nReset]~~
In Propeller parlance, this is the "top level" object code, that is started up at boot time. Its purpose is to exercise various modes and options of the VDP to show its working on the screen. As a parameter, it takes the state of 4 switches on the Propeller demo board to either run all the demos or generate test picture to adjust the colors (== screwdriver and potentiometers!) or timings (== switches on FPGA board):
PUB Main | mode, rnd, switches
waitcnt((clkfreq * 4) + cnt) 'wait 4s before start
if vdp.Start(@CommandBuffer, vdp#GRAPHICS1, false, true)
repeat true
'read switches and if color is TRANSPARENT (== 0) continue with demo, otherwise show solid color screen
dira[13..10]~ 'set as input
switches := ina[13..10]
if (switches < 8)
vdp.Trace(String("Switches are in COLOR (< 8) mode, displaying 8 vertical color bars for calibration "), switches)
vdp.SetMode(vdp#MULTICOLOR)
_colorfulBlocks(byte[@ColorPalette8][switches])
else
if (switches > 8)
vdp.Trace(String("Switches are in TICK (> 8) mode, tick lines (every 8 pixels) "), switches)
vdp.SetMode(vdp#GRAPHICS2)
_tickLines(byte[@ColorPalette8][switches - 8])
else
vdp.Trace(String("Switches are in DEMO (== 8) mode, all running demos "), switches)
repeat mode from vdp#TEXT to vdp#GRAPHICS1
(demo cases)
Here is for example a demo that generated 8 sprites and sets them wandering across the screen in various directions:
PRI _spriteDemo(char, waitSecs) |dx, dy, i, rnd
vdp.SetSpriteMode(vdp#SPRITESIZE_16X16 | vdp#SPRITEMAGNIFICATION_2X)
repeat i from 0 to 7
vdp.GenerateSpritePatternFromChar(@SpriteTestPattern16, char + i, 32)
vdp.SetSpritePattern(i * 4, @SpriteTestPattern16, 32)
vdp.SetSprite(i, vdp#SPRITEMASK_SETPATTERN | vdp#SPRITEMASK_SETCOLOR | vdp#SPRITEMASK_SETX | vdp#SPRITEMASK_SETY, i * 4, vdp.SpriteHPixelCount / 2 - 16, vdp.SpriteVPixelCount / 2 - 16, 15 - i)
'give speed vectors to sprites and let send them off autonomously
vdp.SetSprite(0, vdp#SPRITEMASK_VX | vdp#SPRITEMASK_VY, 0, 1, 0, 0)
vdp.SetSprite(1, vdp#SPRITEMASK_VX | vdp#SPRITEMASK_VY, 0, 1, -1, 0)
vdp.SetSprite(2, vdp#SPRITEMASK_VX | vdp#SPRITEMASK_VY, 0, 0, -1, 0)
vdp.SetSprite(3, vdp#SPRITEMASK_VX | vdp#SPRITEMASK_VY, 0, -1, -1, 0)
vdp.SetSprite(4, vdp#SPRITEMASK_VX | vdp#SPRITEMASK_VY, 0, -1, 0, 0)
vdp.SetSprite(5, vdp#SPRITEMASK_VX | vdp#SPRITEMASK_VY, 0, -1, 1, 0)
vdp.SetSprite(6, vdp#SPRITEMASK_VX | vdp#SPRITEMASK_VY, 0, 0, 1, 0)
vdp.SetSprite(7, vdp#SPRITEMASK_VX | vdp#SPRITEMASK_VY, 0, 1, 1, 0)
repeat waitSecs
vdp.WaitASecond
It is interesting to note that vdp.SetSprite() function is executed by the "current cog", not the one driving the VDP. But the execution is really just preparing the command and parameters to be written to common RAM (all cogs share common RAM, accessed on round-robin basis), after which the SetSprite() function exists. The VDP cog then reads the command from common RAM and drives the sprite across the screen:
PRI _setSprite(spriteId, mask, patternId, x, y, color) |spriteAttributeAddress
spriteAttributeAddress := SpriteAttributeTable + (spriteId << 2)
_copyFromVdpMem(spriteAttributeAddress, @SpriteBuff, 4)
'_logSprite(String("Sprite before "), spriteAttributeAddress, @SpriteBuff)
if (mask & SPRITEMASK_SETY)
byte[@SpriteBuff][0] := y
else
if (mask & SPRITEMASK_DY)
byte[@SpriteBuff][0] += y
else
if (mask & SPRITEMASK_VY)
byte[@spriteSpeed + (spriteId << 1)][1] := y
if (mask & SPRITEMASK_SETX)
byte[@SpriteBuff][1] := x
else
if (mask & SPRITEMASK_DX)
byte[@SpriteBuff][1] += x
else
if (mask & SPRITEMASK_VX)
byte[@spriteSpeed + (spriteId << 1)][0] := x
if (mask & SPRITEMASK_SETPATTERN)
byte[@SpriteBuff][2] := patternId
if (mask & SPRITEMASK_SETCOLOR)
byte[@SpriteBuff][3] := (byte[@SpriteBuff][3] & $F0) | (color & $0F)
'_logSprite(String("Sprite after "), spriteAttributeAddress, @SpriteBuff)
_copyToVdpMem(spriteAttributeAddress, @SpriteBuff, 4)
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.