2021-03-14 17:46:52 +00:00
|
|
|
package izapple2
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"image"
|
|
|
|
"image/color"
|
2021-03-14 21:38:41 +00:00
|
|
|
"time"
|
2021-03-14 17:46:52 +00:00
|
|
|
|
|
|
|
"github.com/ivanizag/izapple2/component"
|
|
|
|
)
|
|
|
|
|
|
|
|
/*
|
|
|
|
Videx 80 columns card for the Apple II+
|
|
|
|
|
|
|
|
See:
|
|
|
|
https://mirrors.apple2.org.za/Apple%20II%20Documentation%20Project/Interface%20Cards/80%20Column%20Cards/Videx%20Videoterm/Manuals/Videx%20Videoterm%20-%20Installation%20and%20Operation%20Manual.pdf
|
|
|
|
http://bitsavers.trailing-edge.com/components/motorola/_dataSheets/6845.pdf
|
|
|
|
https://glasstty.com/?p=660
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
// CardVidex represents a Videx compatible 80 column card
|
|
|
|
type CardVidex struct {
|
|
|
|
cardBase
|
|
|
|
mc6845 component.MC6845
|
|
|
|
sramPage uint8
|
|
|
|
sram [0x800]uint8
|
|
|
|
upperROM memoryHandler
|
|
|
|
charGen []uint8
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewCardVidex creates a new CardVidex
|
|
|
|
func NewCardVidex() *CardVidex {
|
|
|
|
var c CardVidex
|
|
|
|
c.name = "Videx 80 col Card"
|
|
|
|
|
|
|
|
// The C800 area has ROM and RAM
|
|
|
|
c.loadRomFromResource("<internal>/Videx Videoterm ROM 2.4.bin")
|
|
|
|
c.upperROM = c.romC8xx
|
|
|
|
c.romC8xx = &c
|
|
|
|
|
|
|
|
// The resource should be internal and never fail
|
|
|
|
c.loadCharacterMap("<internal>/80ColumnP110.BIN")
|
|
|
|
return &c
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *CardVidex) loadCharacterMap(filename string) error {
|
2022-01-28 18:25:52 +00:00
|
|
|
bytes, _, err := LoadResource(filename)
|
2021-03-14 17:46:52 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
size := len(bytes)
|
|
|
|
if size < 0x800 {
|
2021-08-05 19:12:52 +00:00
|
|
|
return errors.New("character ROM size not supported for Videx")
|
2021-03-14 17:46:52 +00:00
|
|
|
}
|
|
|
|
c.charGen = bytes
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *CardVidex) assign(a *Apple2, slot int) {
|
2021-05-09 17:48:54 +00:00
|
|
|
|
|
|
|
// TODO: use addCardSoftSwitches()
|
2021-03-14 17:46:52 +00:00
|
|
|
for i := uint8(0x0); i <= 0xf; i++ {
|
|
|
|
// Bit 0 goes to the RS pin of the MC6548. It controls
|
|
|
|
// whether a register is being accesed or the contents
|
|
|
|
// of the register is being accessed
|
|
|
|
rsPin := (i & 1) == 1
|
|
|
|
|
|
|
|
// Bits 2 and 3 determine which page will be selected
|
|
|
|
sramPage := i >> 2
|
|
|
|
|
|
|
|
ssName := fmt.Sprintf("VIDEXPAGE%v", sramPage)
|
|
|
|
if rsPin {
|
|
|
|
ssName += "REG"
|
|
|
|
} else {
|
|
|
|
ssName += "ADDRESS"
|
|
|
|
}
|
|
|
|
|
2022-08-05 17:43:17 +00:00
|
|
|
c.addCardSoftSwitchR(i, func() uint8 {
|
2021-03-14 17:46:52 +00:00
|
|
|
c.sramPage = sramPage
|
|
|
|
return c.mc6845.Read(rsPin)
|
|
|
|
}, ssName+"R")
|
2022-08-05 17:43:17 +00:00
|
|
|
c.addCardSoftSwitchW(i, func(value uint8) {
|
2021-03-14 17:46:52 +00:00
|
|
|
c.sramPage = sramPage
|
|
|
|
c.mc6845.Write(rsPin, value)
|
|
|
|
}, ssName+"W")
|
|
|
|
}
|
|
|
|
|
|
|
|
c.cardBase.assign(a, slot)
|
|
|
|
}
|
|
|
|
|
|
|
|
const videxRomLimit = uint16(0xcc00)
|
|
|
|
const videxSramLimit = uint16(0xce00)
|
|
|
|
const videxSramMask = uint16(0x01ff)
|
|
|
|
|
|
|
|
func (c *CardVidex) peek(address uint16) uint8 {
|
|
|
|
if address < videxRomLimit {
|
|
|
|
return c.upperROM.peek(address)
|
|
|
|
} else if address < videxSramLimit {
|
|
|
|
return c.sram[address&videxSramMask+uint16(c.sramPage)*0x200]
|
|
|
|
}
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *CardVidex) poke(address uint16, value uint8) {
|
|
|
|
if address >= videxRomLimit && address < videxSramLimit {
|
|
|
|
c.sram[address&videxSramMask+uint16(c.sramPage)*0x200] = value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *CardVidex) setBase(base uint16) {
|
|
|
|
// Nothing
|
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
videxCharWidth = uint8(8)
|
|
|
|
)
|
|
|
|
|
|
|
|
func (c *CardVidex) buildImage(light color.Color) *image.RGBA {
|
|
|
|
params := c.mc6845.ImageData()
|
|
|
|
width, height := params.DisplayedWidthHeight(videxCharWidth)
|
|
|
|
if (width == 0) || (height == 0) {
|
|
|
|
// No image available
|
|
|
|
size := image.Rect(0, 0, 3, 3)
|
|
|
|
img := image.NewRGBA(size)
|
|
|
|
img.Set(1, 1, color.White)
|
|
|
|
return img
|
|
|
|
}
|
2021-03-14 21:38:41 +00:00
|
|
|
ms := time.Now().Nanosecond() / (1000 * 1000) // Host time, used for the cursoR blink
|
2021-03-14 17:46:52 +00:00
|
|
|
|
|
|
|
size := image.Rect(0, 0, width, height)
|
|
|
|
img := image.NewRGBA(size)
|
|
|
|
|
|
|
|
params.IterateScreen(func(address uint16, charLine uint8,
|
2021-03-14 21:38:41 +00:00
|
|
|
cursorMode uint8, displayEnable bool,
|
2021-03-14 17:46:52 +00:00
|
|
|
column uint8, y int) {
|
|
|
|
|
|
|
|
bits := uint8(0)
|
|
|
|
if displayEnable {
|
|
|
|
char := c.sram[address&0x7ff]
|
|
|
|
bits = c.charGen[(uint16(char&0x7f)<<4)+uint16(charLine)]
|
2021-03-14 21:38:41 +00:00
|
|
|
isCursor := false
|
|
|
|
switch cursorMode {
|
|
|
|
case component.MC6845CursorFixed:
|
|
|
|
isCursor = true
|
|
|
|
case component.MC6845CursorSlow:
|
|
|
|
// It should be 533ms (32/60, 32 screen refreshes)
|
|
|
|
// Let's make a 2 blinks per second
|
|
|
|
isCursor = ms/2 > 1000/4
|
|
|
|
case component.MC6845CursorFast:
|
|
|
|
// It should be 266ms (32/60, 16 screen refreshes)
|
|
|
|
// Let's make a 4 blinks per second
|
|
|
|
isCursor = ms/4 > 1000/8
|
|
|
|
}
|
|
|
|
if isCursor {
|
2021-03-14 17:46:52 +00:00
|
|
|
bits = ^bits
|
|
|
|
}
|
|
|
|
if char >= 128 {
|
2021-03-14 21:38:41 +00:00
|
|
|
// Inverse
|
2021-03-14 17:46:52 +00:00
|
|
|
bits = ^bits
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
x := int(column) * int(videxCharWidth)
|
|
|
|
|
|
|
|
for i := 0; i < int(videxCharWidth); i++ {
|
|
|
|
pixel := (bits & 0x80) != 0
|
|
|
|
if pixel {
|
|
|
|
img.Set(x, y, light)
|
|
|
|
} else {
|
|
|
|
img.Set(x, y, color.Black)
|
|
|
|
}
|
|
|
|
bits <<= 1
|
|
|
|
x++
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return img
|
|
|
|
}
|