Close

Propeller based serial terminal device with SD card as program storage.

A project log for MKHBC-8-Rx

Generic purpose MOS-6502 based computer.

marekMarek 05/16/2017 at 07:080 Comments

This little computer ultimately is going to be a standalone system. I am working towards this goal to hopefully one day cut the cord and get rid of the need for a PC as a host computer / cross-development platform. In the recent months I have been working on sort of an off-shot project that one day will help me complete this plan. I employed Parallax Propeller chip to build a serial terminal device for this computer. My requirements were as follows:

* Dumb terminal with some subset of ANSI functionality with a keyboard (PC or self-made) and at least 80 columns x 25 rows display.

* Mass storage (SD card) that would allow to transfer files to host computer (not PC, the MKHBC-8-R2) and save output from computer coming via serial port to a file on SD card.

This serial terminal device is coming together nicely. It is not the last word though. In the final version I would like to have a Propeller based I/O card which serves keyboard input and display output, maybe some sound and joystick / paddle ports as well which interfaces with MKHBC-8-R2 via CPU or I/O bus instead of slow serial port. This serial terminal is sort of an intermediate step allowing me to get more familiar with Propeller chip.

So far I was able to put together hardware and code to:

* Connect to MKHBC-8-R2 via serial (RS-232) with 9600 baud speed.

* Display hi-res text on VGA display.

* Interface my home-brew retro computer keyboard (TI99/4A + 8052 micro as controller, serial interface similar to PS2) to Propeller and write my driver code. I also have a variant for PC keyboard, both presenting identical API interface, so all I need to do is load different driver to cog if I want to use a different keyboard, other code does not change.

* Interface micro SD card to the Propeller and write Terminal Menu code which aids in sending contents of the text file from SD card to the serial port or dumping memory of MKHBC-8-R2 system to a file on SD card in the form of monitor memory write commands. Thus I can prepare file with monitor memory write commands on a PC or MKHBC system, store on SD card and then send them to the MKHBC-8-R2 system - a primitive way of of loading or saving data, but effective.

* Implemented few ANSI terminal functions, like clearing screen or positioning cursor at specified column and row.

I will soon post the code to github and update project with a link to it.

On this screenshot I just uploaded Tiny Basic from SD card, ran it, then uploaded BASIC program listing from SD card effectively entering BASIC program into the memory just as I would do it from keyboard, then listed it and ran it.

This is how the startup screen looks like. The 'mkhbc>' string is a prompt from the MKHBC-8-R2 computer received via serial port.

My development system. I am using Quick Start board from Parallax with Human Interface shield on top for VGA and keyboard interfacing. To the right you see TI99/4A keyboard with 8052 controller (not visible, tucked in under the keyboard) connected to the Human Interface board. PC is the host for SPIN program development. The serial port interface is put together on the breadboard on the left of the Parallax board (just 3 resistors). The VGA monitor connected to the Human Interface shield is the one on the left that you can only see a corner of with the green glow.

This is the Parallax Propeller USB Project board. This board will ultimately host the serial terminal device. I don't use it for code development for the serial terminal project because of the problem with powering keyboard from the USB port. To test code I needed to disconnect the USB programming cable and connect external power supply. For this reason I had to write the program to EPROM each time I made new build iteration if I needed to test it with the keyboard. Therefore I was afraid I was going to wear down the EPROM prematurely by frequent writing and brick the project board.

On the project area I added serial interface, 5V power supply, keyboard mini-din socket, various headers and connections to VGA and micro SD which has a socket on the bottom side of the board. I may also add some sound generating hardware there (probably some simple stuff, few resistors and socket as the actual sound will be generated by software in the Propeller).

The actual MKHBC-8-R2 computer as it looks now. CPU/ROM/RAM card is on the left, UART to the right and RTC and banked RAM is still on the breadboard.

Keyboard interface is the same as on Parallax Human Interface Board. On my project board I just assembled the keyboard part, leaving the mouse connector off.

This is the serial interface circuit. Pretty simple and it works:

Code (not all, just main cog):
{{

  Homebrew Serial Keyboard + VGA Terminal (80x40) and SD card.
  Marek Karcz (C) 2016, 2017. All rights reserved.
  Free for personal and educational use.  

  Serial keyboard consists of matrix retro keyboard (TI 99/4A)
  and AT89S52 controller + open collector clock and data output,
  pretty much like a PS/2 keyboard, but the protocol is different.
  See documentation in serkb_recv object.

  This configuration uses P8X32A QuickStart Board + Human Interface Board
  from Parallax INC.

  Terminal will also work with PC keyboard and Keyboard object.
  
}}
CON
  _clkmode = xtal1 + pll16x 'Use low crystal gain, wind up 16x
  _xinfreq = 5_000_000 'External 5 MHz crystal on XI & XO
  ''_CLKFREQ = 80_000_000

  SDA_pin = 26  'keyboard port on human interface board
  SCL_pin = 27

  'SDA_pin = 24  'keyboard port on human interface board
  'SCL_pin = 25  

  SerialTx_pin = 5
  SerialRx_pin = 7

  SerialBaud = 9600
  SerialMode = %0011

  MAX_col = scr#cols - 1
  MAX_row = scr#rows - 1

  chrs = scr#cols * scr#rows 

  'MAX_col = 31
  'MAX_row = 14

  CRSBLDEL = 3000

  BKSPC = 8
  BKSPC_PC = $C8
  CTRL_H_PC = $268
  NUMLOCK_PC = $DF
  NL = $0D
  CR = $0A
  CTRL_C = 3             ' CTRL-C from TI99/4A keyboard driver
  CTRL_C_PC = $263       ' CTRL-C from PS/2 keyboard driver
  CTRL_Z = 26
  CTRL_Z_PC = $27A
  SPC = $20
  ESC_PC = $CB
  ESC = $1B
  F1_PC = $D0
  CTRL_Q_PC = $271
  CTRL_Q = $11

  ' Micro SD connections
  
  CS  = 3       ' Propeller Pin 3 - Set up these pins to match the Parallax Micro SD Card adapter connections.
  DI  = 2       ' Propeller Pin 2 - For additional information, download and refer to the Parallax PDF file for the Micro SD Adapter.                        
  CLK = 1       ' Propeller Pin 1 - The pins shown here are the correct pin numbers for my Micro SD Card adapter from Parallax                               
  D0  = 0       ' Propeller Pin 0 - In addition to these pins, make the power connections as shown in the following comment block.  

OBJ
  scr     : "vga_hires_text_mk"
  serkb   : "serkb_recv"             '  serkb_recv object is interchangeable
  'serkb   : "keyboard"               ' with keyboard object - no more code
                                     '  changes are required, just swap them here
  rs232   : "FullDuplexSerial_mk"
  'serial  : "Parallax Serial Terminal"
  'Num    : "numbers"

  sdfat   : "fsrw"                     '  r/w file system
  streng  : "ASCII0_STREngine_1"       ' string library

DAT
  str01     BYTE "Serial Keyboard + VGA Terminal (80x40).",NL,0
  strFmVer  BYTE "Firmware version 2.0.",NL,0
  strCpr01  BYTE "Copyright (C) by Marek Karcz 2016,2017.",NL,0
  strCpr02  BYTE "All rights reserved.",NL,0
  str01_1   BYTE NL,"Press (at any time):",NL,NL,0
  str02     BYTE "   CTRL-C to Clear Screen,",NL,0
  str03     BYTE "   CTRL-H to Backspace/Delete,",NL,0
  str04     BYTE "   CTRL-Z to open Terminal Menu,",NL,0
  str05     BYTE "   CTRL-Q (F1) to see this help.",NL,0
  strSdFnd  BYTE NL,"SD card found. Open Terminal Menu to mount.",NL,0
  strNoSd   BYTE NL,"ERROR: There is no SD card.",NL,0
  strSpaces BYTE "                                ",0
  rdcmd     BYTE "r ",0

VAR  
  long  col, row
  long  rcv, key, prevkey
  'long  crsct
  'sync long - written to -1 by VGA driver after each screen refresh
  long  sync
  'screen buffer - could be bytes, but longs allow more efficient scrolling
  long  screen[chrs/4]
  'row colors
  word  colors[MAX_row+1]
  'cursor control bytes
  byte  cx0, cy0, cm0, cx1, cy1, cm1
  byte  fname[13]
  byte  staddr[5], endaddr[5]
  byte  memrdcmd[16], buf[80]
  long  sdcard_found
  
PUB Main | i

  sdcard_found := -1
  prevkey := 0
  'crsct := CRSBLDEL
  col := 0
  row := 0
  'Num.init
  'serial.Start(115200)
  rs232.Start(SerialRx_pin, SerialTx_pin, SerialMode, SerialBaud)
  serkb.Start(SDA_pin, SCL_pin)  ' Start the serial keyboard object
  scr.start(16, @screen, @colors, @cx0, @sync)
  'set up colors, clear screen
  repeat i from 0 to MAX_row
    colors[i] := %%0100_1310
  repeat i from 0 to chrs - 1
    screen.byte[i] := $20      
  ScrStr(@str01)
  ScrStr(@strFmVer)
  ScrStr(@strCpr01)
  ScrStr(@strCpr02)
  HelpInfo
  waitcnt(cnt + clkfreq)
  MountSD(FALSE)
  'sdcard_found := \sdfat.mount_explicit(D0, CLK, DI, CS) ' Here we call the 'mount' method using the 4 pins described in the 'CON' section.
  if sdcard_found => 0
    ScrStr(@strSdFnd)
    UnmountSD(FALSE) 
  rs232.RxFlush
  repeat
    key := serkb.Key
    ' conversions related to used driver (ti99/4a a.k.a. serkb_recv or ps/2 a.k.a. keyboard)
    if key > 0
      if key == CR
        key := NL
      if key == CTRL_C_PC
        key := CTRL_C
      if key == CTRL_Z_PC
        key := CTRL_Z
      if key == BKSPC_PC or key == CTRL_H_PC
        key := BKSPC
      if key == NUMLOCK_PC
        key := 0
      if key == ESC_PC
        key := ESC
      if key == CTRL_Q_PC or key == F1_PC
        key := CTRL_Q
    if key > 0
      rs232.Tx(key & $ff)
    'rcv := rs232.RxCheck
    rcv := rs232.RxTime(20)
    if rcv => 0
      'serial.Str(STRING("Received character from RS232:"))
      'serial.Dec(rcv)
      'serial.NewLine
      PrnChar(rcv & $ff)
      prevkey := rcv & $ff
    Cursor

PUB UnmountSD(verbose)
  if verbose == TRUE   
    PrnChar(NL)
  if sdcard_found => 0
    sdfat.unmount
    sdcard_found := -1
    if verbose == TRUE
      ScrStr(String("SD card has been unmounted successfully.",NL))
  else
    if verbose == TRUE
      ScrStr(String("ERROR: Nothing to unmount. (already unmounted?)",NL))

PUB MountSD(verbose)
  if verbose == TRUE
    PrnChar(NL)
  if sdcard_found < 0
    sdcard_found := \sdfat.mount_explicit(D0, CLK, DI, CS) ' Here we call the 'mount' method using the 4 pins described in the 'CON' section.
    if verbose == TRUE
      if sdcard_found => 0
        ScrStr(String("SD card has been mounted successfully.",NL))
      else
        ScrStr(String("ERROR: Unable to mount SD card, error code="))
        ScrStr(streng.integerToHexadecimal(sdcard_found, 8))
        PrnChar(NL)
  else
    if verbose == TRUE
      ScrStr(String("ERROR: Nothing to mount. (already mounted?)",NL))         

PUB HelpInfo
  StrOut(@str01_1)
  StrOut(@str02)
  StrOut(@str03)
  StrOut(@str04)
  StrOut(@str05)
  Cursor     

PUB ScrStr(strptr)
  repeat StrSize(strptr)
    PrnChar(byte[strptr++])

PUB ScrOut(c)
  screen.byte[row*(MAX_col+1) + col] := c

PUB StrOut(strptr)
  repeat StrSize(strptr)
    if byte[strptr] == NL
      col := 0
      IncRow
      strptr++
    else
      ScrOut(byte[strptr++])
      IncCol

PUB Cursor
  cx0 := col
  cy0 := row
  cm0 := %010
  ' uncomment code below to enable own cursor implementation
  ' and comment code above
  {{
  crsct--
  if crsct > CRSBLDEL / 2
    ScrOut("|")
  else
    ScrOut("_")
  if crsct == 0
    crsct := CRSBLDEL
  }}

PUB IncRow
  row := row + 1
  if row > MAX_row
    row := MAX_row
    ByteMove(@screen, @screen+MAX_col+1, chrs-MAX_col-1)
    ByteFill(@screen+chrs-MAX_col-1, 32, MAX_col+1)

PUB IncCol
  col := col + 1
  if col > MAX_col
    col := 0
    IncRow

PUB DecCol
  if col > 0
    col := col - 1

PUB ReadSerialAndPrint(rdw)
  repeat
    'rcv := rs232.RxCheck
    rcv := rs232.RxTime(rdw)
    if rcv < 0
      Quit
    PrnChar(rcv & $ff)

PUB SDDir | n
  if sdcard_found < 0
    ScrStr(@strNoSd)
    return
  PrnChar(NL)
  PrnChar(NL)
  ScrStr(String("Directory:",NL))
  PrnChar(NL)
  sdfat.opendir
  repeat
    n := sdfat.nextfile(@fname)
    if n < 0
      Quit
    ScrStr(@fname)
    PrnChar(NL)

PUB GetStr(pstr, size) | n, q
  n := 0
  q := FALSE
  repeat until q == TRUE
    key := serkb.Key
    if key > 0
      if key == CR
        key := NL
      if key == CTRL_C_PC
        key := CTRL_C
      if key == CTRL_Z_PC
        key := CTRL_Z
      if key == BKSPC_PC
        key := BKSPC
      if key == NUMLOCK_PC
        key := 0                    
      case key
        NL:    byte[pstr+n] := 0
               q := TRUE
        BKSPC: if n > 0
                 n := n - 1
                 byte[pstr+n] := 0
        OTHER: if key > 0 and n < size-1
                 byte[pstr+n] := key
                 n := n + 1
                 byte[pstr+n] := 0
      col := 0
      ScrStr(@strSpaces)
      ScrOut(SPC)
      col := 0
      ScrStr(pstr)
    Cursor    

PUB EnterFileName '| n, q
  PrnChar(NL)
  ScrStr(String("Enter file name:",NL))
  PrnChar(NL)
  Cursor
  GetStr(@fname, 13)

PUB SendFileFromSD2Serial | n, q
  if sdcard_found < 0
    ScrStr(@strNoSd)
    return
  SDDir
  EnterFileName
  PrnChar(NL)
  ReadSerialAndPrint(0)
  ScrStr(String("*** Loading file ***", NL))
  waitcnt(cnt + clkfreq)
  sdfat.popen(@fname, "r")
  key := NL
  rs232.Tx(key & $ff)
  ReadSerialAndPrint(0)
  repeat
    key := sdfat.pgetc
    if key < 0
      Quit
    if key == NL
      key := 0
    if key == CR
      key := NL
    if key > 0      
      rs232.Tx(key & $ff)
      waitcnt(cnt + clkfreq/250)
    if key == NL
      waitcnt(cnt + clkfreq/6)
    ReadSerialAndPrint(0)
  ReadSerialAndPrint(0)
  PrnChar(NL)
  sdfat.pclose

PUB SaveMemory2FileSD | m, adrbeg, adrend, curaddr
  if sdcard_found < 0
    ScrStr(@strNoSd)
    return
  EnterFileName
  PrnChar(NL)
  ScrStr(String("Enter start address (hex DDDD, e.g: 0400):",NL))
  PrnChar(NL)
  Cursor
  GetStr(@staddr, 5)
  adrbeg := streng.hexadecimalToInteger(@staddr)
  PrnChar(NL)
  ScrStr(String("Enter end address (hex DDDD, e.g: 1000):",NL))
  PrnChar(NL)
  Cursor
  GetStr(@endaddr, 5)
  adrend := streng.hexadecimalToInteger(@endaddr)
  PrnChar(NL)
  Cursor
  if adrend - adrbeg < 15
    ScrStr(String("Address range must be greater than 14 bytes.",NL))
    return
  curaddr := adrbeg
  sdfat.popen(@fname, "w")
  repeat
    memrdcmd[0] := 0
    streng.stringCopy(@memrdcmd, @rdcmd)
    streng.stringConcatenate(@memrdcmd, @staddr)
    streng.stringConcatenate(@memrdcmd, String("-"))
    curaddr := curaddr + 16
    streng.stringConcatenate(@memrdcmd, streng.integerToHexadecimal(curaddr-1, 4))
    streng.stringConcatenate(@memrdcmd, String(" ",NL))
    ' Send the memory read command
    streng.stringToLowerCase(@memrdcmd)
    ScrStr(String("Command: "))
    ScrStr(@memrdcmd)    
    rs232.str(@memrdcmd)
    ' Read response from memory read command and save it to file
    ' line by line
    m := 0
    repeat
      rcv := rs232.RxTime(20)
      if rcv < 0
        Quit
      if rcv <> NL
        buf[m++] := rcv
        if m > 79
          m := 0
        buf[m] := 0
      else
        buf[m++] := NL
        if m > 79
          m := 0
        buf[m++] := CR
        if m > 79
          m := 0                
        buf[m] := 0
        if buf[0] == "w"
          sdfat.pputs(@buf)
          ScrStr(String("buf="))
          ScrStr(@buf)
        m := 0            
    ' End of address range
    if curaddr > adrend
      Quit
    streng.stringCopy(@staddr,  streng.integerToHexadecimal(curaddr, 4))

  sdfat.pclose
  PrnChar(NL)
  ScrStr(String("File saved.",NL))

PUB ListFileSD
  if sdcard_found < 0
    ScrStr(@strNoSd)
    return
  EnterFileName
  PrnChar(NL)
  sdfat.popen(@fname, "r")
  repeat
    rcv := sdfat.pgetc
    if rcv < 0
      Quit
    PrnChar(rcv & $ff)
  sdfat.pclose  
                                  
PUB TermMenu | q
  q := FALSE
  MountSD(TRUE)  ' SD card us mounted only while in Terminal Menu
  repeat until q == TRUE
    ScrStr(String("Terminal Menu:",NL))
    PrnChar(NL)  
    ScrStr(String("   1 - Send file from SD card to serial port.",NL))
    ScrStr(String("   2 - Save memory write commands to file on SD card.",NL))
    ScrStr(String("   3 - SD card directory.",NL))
    ScrStr(String("   4 - List file contents.",NL))
    ScrStr(String("   5 - Unmount SD card.",NL))
    ScrStr(String("   6 - Mount SD card.",NL))
    ScrStr(String("   Q - Exit Menu",NL))
    PrnChar(NL)
    ScrStr(String("Your selection ? "))
    repeat
      key := serkb.Key
      if key > 0
        case key
          "1" :  SendFileFromSD2Serial
                 q := TRUE
          "2" :  SaveMemory2FileSD
                 q := TRUE
          "3" :  SDDir
          "4" :  ListFileSD
          "5" :  UnmountSD(TRUE)
          "6" :  MountSD(TRUE)
          "q" :  ScrStr(String("Quit.",NL))
                 q := TRUE
          OTHER: ScrStr(String("Unknown menu option.",NL))
        PrnChar(NL)
        Quit
      Cursor
  ScrOut(SPC)
  UnmountSD(TRUE)  ' SD card is only mounted while in Terminal Menu

PUB ClrScr
  ByteFill(@screen, SPC, chrs) 
  col := 0
  row := 0

PUB IsDigit(c)
  if c => "0" and c =< "9"
    return TRUE

  return FALSE

' Read digits from rs232, convert to number and return value.
' Th las read non-digit character code is remembered in key variable.
'  
PUB RdNumSer(chars) : numval | c, ba
  numval := -1
  ba := chars
  byte[chars] := 0
  c := rs232.RxTime(20)
  if IsDigit(c) == TRUE
    repeat while IsDigit(c) == TRUE
      byte[chars++] := c
      byte[chars] := 0
      c := rs232.RxTime(20)
  key := c
  numval := streng.decimalToInteger(ba)                             

PUB PrnChar(c) | n, lcol, lrow
  if c == "[" and prevkey == ESC
    lrow := RdNumSer(@buf)
    if lrow => 0
      case key
      
        "J"      : case lrow
                     0 :  ' write code here to clear from cursor to end of screen
                          return
                     1 :  ' write code here to clear from cursor to begin of screen
                          return
                     2 :
                          'StrOut(String("[CLS]"))
                          ClrScr
                          return
                          
                     OTHER : return
                     
        ";"      : 'StrOut(String("[;]"))
                   lcol := RdNumSer(@buf)
                   if lcol => 0
                     if key == "H" or key == "f"
                       col := lcol - 1
                       row := lrow - 1
                       if col < 0
                         col := 0
                       if row < 0
                         row := 0
                   return
                          
        OTHER    : return
  if c == NL
    'ScrOut(SPC) 'uncomment only if own cursor impl. is used
    col := 0
    IncRow
  if c == CTRL_C 'CTRL-C, Clear Screen
    rs232.Tx(BKSPC) 'Send backspace to delete control character
    ClrScr
    rs232.Tx(NL) 'Send NL to serial
  if c == CTRL_Q
    HelpInfo
    rs232.Tx(NL) 'Send NL to serial    
  if c == BKSPC
    ScrOut(SPC)
    DecCol
  if c == CTRL_Z 'CTRL-Z, Menu
    rs232.Tx(BKSPC) 'Send backspace to delete control character
    ScrOut(SPC)
    col := 0
    IncRow
    IncRow
    TermMenu
    IncRow
    rs232.Tx(NL) 'Send NL to serial    
  if c > 31 and c < 127
    ScrOut(c)
    IncCol
    {{
  if c == ESC
    StrOut(String("[ESC]"))
    }}
  Cursor
   

Discussions