Portfolio

Table of Contents

Introduction

Here I have compiled a subset of the things I have worked on throughout my life. It is certainly not comprehensive and many projects are unfinished. I have included things I have worked on both in school and on my own time.

This Website

I write this site using Emacs org-mode, making good use of its fantastic export capabilities. My workflow is a little different from how emacs is configured by default, however. org-publish is sort of set up for someone who uses a single computer. My workflow involves a plethora of devices. I actively edit this site on my Macbook, and my desktop running Arch. I can foresee myself also editing it on my desktop under Windows, or on other laptops running whatever operating system I decide to run at that time.

Because of this workflow, I wanted to be able to contain all the information related to this website, including export information, inside of a single directory tree. This immediately presents challenges, because org-publish makes use of several global variables which add state to its export functions. The main variable of concern, however, is org-publish-project-alist. This variable is intended to contain all projects that you ever work on. In theory, there are benefits to this, but in practice, it just means my config files for the website are spread all over my computer.

To solve this, I wrote some elisp which is bound to SPC m P l. Simply put, it looks for a file named export-config.el in the root directory of your project. This file defines a variable called org-publish-project-local-alist, which does the same thing as org-publish-project-alist, but it’s local (real complex stuff, I know).

There is one problem though. In the local alist, I can’t use absolute paths, because that would sort of defeat the purpose of being able to clone the git repo anywhere and work on it. I don’t need absolute paths, because all the files I reference in export-config.el are relative to the website root directory. But when the publishing function runs, it gets confused with the relative paths (especially when using #+include: ... in my files.)

At this point, I pull an India Jones with these two variables and swap them before calling the org-publish-all function. This publishes the entire website with a local configuration.

I moved the elisp I wrote for this modification into a separate file which is loaded during startup for emacs. This file is shown below.

  1: ;;; org-publish-local.el -*- lexical-binding: t; -*-
  2: ;;;
  3: ;;; PROGRAMMER: Ethan Smith
  4: ;;;
  5: ;;; DATE: 5 June 2023
  6: ;;;
  7: ;;; DESCRIPTION: this file is an extention to org-publish. Project export
  8: ;;; configuration is placed in a file caled export-config.el. this file must set
  9: ;;; a variable called ~org-publish-local-project-alist~. it should be an alist
 10: ;;; in the same format as ~org-publish-project-alist~, for details see its
 11: ;;; documentation. The only difference between these two variables is that the
 12: ;;; local one may use relative paths for its base directories, and the export
 13: ;;; local command will resolve those paths.
 14: ;;;
 15: ;;; BINDINGS:
 16: ;;; "SPC m P l"  org-publish-local-project
 17: 
 18: (add-hook
 19:  'org-mode-hook
 20:  (lambda ()
 21:    (evil-define-key '(normal visual) org-mode-map (kbd "SPC m P l") #'org-publish-local-project)))
 22: 
 23: ;;; NOTE: every file in a project is loaded into a buffer, and made current in
 24: ;;; the org-publish-to function. That function tries to use existing buffers if
 25: ;;; available, otherwise it will just create a new one, temporarily.
 26: ;;;
 27: ;;; If a buffer already exists, but its path is screwed up, that will screw up
 28: ;;; the path of its includes.
 29: 
 30: ;; state variable for local publish function
 31: (setq org-publish-local-root nil)
 32: (defun org-publish-local-project (project-root)
 33:   "command to publish projects not listed in ~org-publish-project-alist~.
 34: 
 35: This requrires the project alist to be defined as
 36: org-publish-local-alist in a file called export-config.el in the
 37: project root directory. When this command is called for the first
 38: time during a session, the user will be prompted to select the
 39: root directory for their project. This directory is remembered
 40: between calls"
 41:   (interactive (list (if org-publish-local-root org-publish-local-root
 42:                        (setq org-publish-local-root
 43:                              (read-directory-name "Select project root: " )))))
 44: 
 45:   (let ((config-file (concat project-root "export-config.el")))
 46:     (when (file-exists-p config-file)
 47:       ;; file exists, so get the code from it
 48:       (require 'ox-publish)
 49:       (load config-file nil nil t)
 50:       (let (tmp-alist
 51:             (org-publish-use-timestamps-flag nil)
 52:             (buffer-directory (file-name-directory (buffer-file-name)))) ; force all files to be published
 53: 
 54:         (setq org-publish-local-alist
 55:               (map 'list
 56:                    (lambda (project)
 57:                      (cons (car project)
 58:                            (publish-local-fix-base-dir org-publish-local-root
 59:                                                            (cdr project))))
 60:                    org-publish-local-alist))
 61: 
 62:         ;; the org-publish-expand-project function requires that
 63:         ;; org-publish-project-alist contain all projects. save the current
 64:         ;; value of org-publish-project-alist, and restore it after this
 65:         ;; function is ran.
 66:         (cl-rotatef org-publish-local-alist org-publish-project-alist)
 67: 
 68:         ;; publish all files in the project. NOTE: this might not actually work.
 69:         ;; (cd org-publish-local-root)
 70:         (org-publish-all t)
 71:         ;; (cd buffer-directory)
 72: 
 73:         ;; restore project-alist variable
 74:         (cl-rotatef org-publish-local-alist org-publish-project-alist)))))
 75: 
 76: (defun publish-local-fix-base-dir (root-dir plist)
 77:   "updates :base-directory in project plist to be a subdirectory of root-dir.
 78: 
 79: if :base-directory is absolute, then then this function simply
 80: returns a copy of project. when :base-directory is relative, that
 81: key is updated in a copy of project, which is returned.
 82: 
 83: root-dir is the directory where export-config.el is located. It
 84: should be in the form of /foo/bar/
 85: 
 86: project is the project plist"
 87: ;;;
 88: ;;; org-publish-fix-base-dir
 89: ;;;
 90: 
 91:   (let ((new-plist (copy-tree plist))
 92:         relative-dir)
 93:     (dolist (key '(:base-directory :publishing-directory) new-plist)
 94:       (setf relative-dir (plist-get plist key))
 95:       (cond ((f-relative-p root-dir)
 96:              (error "root-dir must be absolute: %S" root-dir))
 97:             ;; don't return error if base-directory is nil
 98:             ((and relative-dir
 99:                   (not (directory-name-p relative-dir)))
100:              (error "%S must be a directory. (did you mean  %S?)"
101:                     relative-dir
102:                     (concat relative-dir "/"))))
103: 
104:       ;; update key with either expanded or replaced filename
105:       (setf new-plist
106:             (if (and relative-dir
107:                      (f-relative-p relative-dir))
108:                 (plist-put (copy-tree new-plist) key
109:                            (expand-file-name relative-dir root-dir))
110:               (copy-tree new-plist))))))
111: 
112: ;; this function is no longer used in this file, but I like it so much, I can't
113: ;; bring myself to delete it.
114: (defun alist-update-key (key value alist)
115:   "returns a new alist with the value at key updated.
116: 
117: The whole structure of the alist is copied over (ie copy-tree vs
118: copy-list). the result is an alist with key removed, and a new
119: element with key pushed to the front of the alist. "
120: ;;;
121: ;;; alist-update-key
122: ;;;
123:   (cons (list key value)
124:         (assq-delete-all key (copy-tree alist))))

Courses at Penn State

There were a few classes in that I learned anything of substance during college.

Computer Engineering 472

The coures webpage has a lot of information about the course. Professor Choi updates the course from year to year, so the curriculum posted there probably isn’t exactly what I did.

We spent the semester programming the HC12 Microcontroller in Assembly. We learned about many different programming techniques specific to embedded systems:

  • Peripherals
  • Memory access/management
  • Interrupts
  • Using the stack

We were assigned homework every week, which consisted of writing various routines. It was incredibly important to complete these routines because they would be used in subsequent assignments. Many hard lessons about assembly were learned in this class.

Some notable applications we made included a prefix notation calculator and a command line memory editor. I remember my excitement when I punched in the address for the command prompt and changed the symbol from a ’>’ to ’$’!

I have included the code from the first and last homework we completed in this class to show the progression of program complexity.

Homework 1

 1: ;***********************************
 2: ;*
 3: ;* Title: StarFill (in Memory lane)
 4: ;*
 5: ;* Objective: COMPEN472 Homework 1
 6: ;*
 7: ;* Revision: V2
 8: ;*
 9: ;* Date: 27 Aug 2022
10: ;*
11: ;* Programmer: Ethan Smith
12: ;*
13: ;* Company: The Pennsylvania State University
14: ;* Electrical Engineering and Computer Science
15: ;*
16: ;* Algorithm: Simple while-loop demo of HCS12 assembly program
17: ;*
18: ;* Register use: A accumulator: character data to be filled
19: ;*               B accumulator: counter, number of filled locations
20: ;*               X register: memory address pointer
21: ;*
22: ;* Memory Use: RAM Locations from $3000 to $30C9
23: ;*
24: ;* Input: Parameters hard coded in the program
25: ;*
26: ;* Output: Data filled in memory locations, from $3000 to $30C9
27: ;*
28: ;* Observation: This program is designed for instruction purpose.
29: ;* This program can be used as a 'loop' template
30: ;*
31: ;* Note: This is a good example of program comments
32: ;* All Homework programs MUST have comments similar
33: ;* to this Homework 1 Program. So, please use this
34: ;* comment format for all your subsequent CMPEN 472
35: ;* Homework programs.
36: ;*
37: ;* Adding more explanations and comments help you and
38: ;* others to understand your program later.
39: ;*
40: ;* Comments: This program is developed and simulated using CodeWarrior
41: ;* development software
42: ;*
43: ;*********************************************************
44: ;* Parameter Declearation Section
45: ;*
46: ;* Export Symbols
47:         XDEF        Entry   ; export 'pgstart' symbol
48:         ABSENTRY    Entry   ; for assembly entry point
49: ;* Symbols and Macros
50: PORTA   equ         $0000   ; i/o port addresses
51: PORTB   equ         $0001
52: DDRA    equ         $0002
53: DDRB    equ         $0003
54: ;*********************************************************
55: ;* Data Section
56: ;*
57:         org     $3000   ; reserved memory starting address
58: here    DS.B    $CA     ; 202 memory locations reserved
59: count   DC.B    $CA     ; constant. star count = 202
60: ;*********************************************************
61: ;* Program Section
62: ;*
63:         org     $3100   ; program start address, in RAM
64: Entry   ldaa    #'*'    ; load '*' into accumulator A
65:         ldab    count   ; load star counter into B
66:         ldx     #here   ; load address pointer into X
67: loop    staa    0, x    ; put a star
68:         inx             ; point to next location
69:         decb            ; decrease counter
70:         bne     loop    ; if not done, repeat
71: done    bra     done    ; task finished
72:                         ;  do nothing
73: 
74: ;*
75: ;* Add any subroutines here
76: ;*
77: 
78:         END             ; last line of a file

Homework 11

   1: ;;; Title:          Analog signal decoder/transmitter and generator
   2: ;;;
   3: ;;; Objective:      CMPEN 472 Homework 11
   4: ;;;
   5: ;;; Revision:       V3.2  for CodeWarrior 5.2 Debugger Simulation
   6: ;;;
   7: ;;; Date:           30 November 2022
   8: ;;;
   9: ;;; Programmer:     Ethan Smith
  10: ;;;
  11: ;;; Company:        The Pennsylvania State University
  12: ;;;                 Department of Computer Science and Engineering
  13: ;;;
  14: ;;; Program:        Generates waves (saw, triangle, and square), outputting them over serial.
  15: ;;;                 a timer runs in the background
  16: ;;;
  17: ;;; Algorithm:      Command line interface. The user is presented with a command
  18: ;;;                 prompt on which they can type.
  19: ;;;
  20: ;;;                 when the user hits enter, their input is validated.
  21: ;;;                 if the input is valid, then it is parsed and evaluated.
  22: ;;;                 otherwise, the program returns an error.
  23: ;;;
  24: ;;;                 the clock is run using the RTI interrupt, which increments a counter.
  25: ;;;                 this counter is checked periodically, and when it is 400 (set lower for
  26: ;;;                 sim purposes), 1 second has elapsed, and the seconds/minutes counters are
  27: ;;;                 also updated.
  28: ;;;
  29: ;;;                 the waves are generated using the oc5 interrupt. Depending on the type of wave
  30: ;;;                 desired, there are 3 separate ISRs which will be set in the ISR jump vector when
  31: ;;;                 the appropriate command is run.
  32: ;;;
  33: ;;;                 the ADC command creates another interrupt routine on oc5 which
  34: ;;;                 reads from the ADC channel 7, and transmits the data to the
  35: ;;;                 terminal
  36: ;;;
  37: ;;; Register use:   Various usages depending on the subroutine.
  38: ;;;                 typically, X is used as input buffer pointer.
  39: ;;;                 D is typically used as the data pointer.
  40: ;;;
  41: ;;;                 each subroutine defines stack variables which are used throughout
  42: ;;;                 the subroutine.
  43: ;;;
  44: ;;; Memory use:     RAM Locations from $3000 for data,
  45: ;;;                 RAM Locations after data for program
  46: ;;;
  47: ;;;                 RAM Locations from program to 4100 for SP
  48: ;;;
  49: ;;;
  50: ;;; Observation:    Clock which counts up and loops around to 0:00 after 9:59
  51: ;;;                 can be set via tty s command.
  52: ;;;
  53: ;;;                 clock can be stopped with the quit command
  54: ;;;
  55: ;;;                 wave generator works as expected, with the caveat that the clock is slowed down
  56: ;;;                 while the wave is being generated, because the simulator slows down while printing.
  57: ;;;
  58: ;;;                 adc command works as expected. ADC channel 7 is sampled at
  59: ;;;                 8000Hz. it returns 2048 samples worth of data over the
  60: ;;;                 serial output.
  61: ;;;
  62: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  63: ;;; Parameter Declearation Section
  64: ;;;
  65: ;;; Export Symbols
  66:             XDEF        pstart       ; export 'pstart' symbol
  67:             ABSENTRY    pstart       ; for assembly entry point
  68: 
  69: ;;; Symbols and Macros
  70: PORTA       EQU         $0000        ; i/o port A addresses
  71: DDRA        EQU         $0002
  72: PORTB       EQU         $0001        ; i/o port B addresses
  73: DDRB        EQU         $0003
  74: 
  75: SCIBDH      EQU         $00C8        ; Serial port (SCI) Baud Register H
  76: SCIBDL      EQU         $00C9        ; Serial port (SCI) Baud Register L
  77: SCICR2      EQU         $00CB        ; Serial port (SCI) Control Register 2
  78: SCISR1      EQU         $00CC        ; Serial port (SCI) Status Register 1
  79: SCIDRL      EQU         $00CF        ; Serial port (SCI) Data Register
  80: 
  81: TIOS        EQU         $0040        ; Timer Input Capture (IC) or Output Compare (OC) select
  82: TIE         EQU         $004C        ; Timer interrupt enable register
  83: TCNTH       EQU         $0044        ; Timer free runing main counter
  84: TSCR1       EQU         $0046        ; Timer system control 1
  85: TSCR2       EQU         $004D        ; Timer system control 2
  86: TFLG1       EQU         $004E        ; Timer interrupt flag 1
  87: TC5H        EQU         $005A        ; Timer channel 5 register
  88: 
  89: CRGFLG      EQU         $0037        ; Clock and Reset Generator Flags
  90: CRGINT      EQU         $0038        ; Clock and Reset Generator Interrupts
  91: RTICTL      EQU         $003B        ; Real Time Interrupt Control
  92: 
  93: ATDCTL2     EQU  $0082               ; Analog-to-Digital Converter (ADC) registers
  94: ATDCTL3     EQU  $0083
  95: ATDCTL4     EQU  $0084
  96: ATDCTL5     EQU  $0085
  97: ATDSTAT0    EQU  $0086
  98: ATDDR0H     EQU  $0090
  99: ATDDR0L     EQU  $0091
 100: ATDDR7H     EQU  $009e
 101: ATDDR7L     EQU  $009f
 102: 
 103: BS          equ         $08          ; backspace character
 104: CR          equ         $0d          ; carriage return, ASCII 'Return' key
 105: LF          equ         $0a          ; line feed, ASCII 'next line' character
 106: NULL        equ         $00          ; null terminator
 107: 
 108: DATAmax     equ         2048         ; Data count maximum, 1024 constant
 109: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 110: 
 111: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 112: ;;; Interrupt Vector Section
 113: ;;; org     $3FF0               ; RTI interrupt vector setup for CSM-128 board
 114:             org     $FFF0               ; RTI interrupt vector setup for the simulator
 115:             DC.W    rtiisr              ; place the address of the ISR at this location
 116: 
 117:             ORG     $FFE4       ; Timer channel 5 interrupt vector setup, on simulator
 118: oc5isr      DC.W    $0000
 119: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 120: 
 121: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 122: ;;; Data Section: address used [ $3000 to $30FF ] RAM memory
 123:             ORG         $3000        ; Reserved RAM memory starting address
 124:                                      ;   for Data for CMPEN 472 class
 125: 
 126: ;;; convenience string for sending newlines to terminal
 127: NEWLINE     DC.B         CR, LF, NULL
 128: 
 129: ;;; the menu which is printed when the program first starts
 130: MENU        DC.B   "Wave Generation Program", CR, LF
 131:             DC.B   "use 'gw' to generate a sawtooth wave", CR, LF
 132:             DC.B   "use 'gt' to generate a triangle wave", CR, LF
 133:             DC.B   "use 'gq' to generate a square wave", CR, LF
 134:             DC.B   "use 's M:SS' to set the current time", CR, LF
 135:             DC.B   "use 'q' to quit the program and stop the clock", CR, LF, NULL
 136: 
 137: PROMPT      DC.B   "HW11> ", NULL
 138: QUIT_MSG    DC.B   "Stopping Clock...", CR, LF
 139:             DC.B   "Typewriter Program Started...", CR, LF, NULL
 140: 
 141: BUFF_ERR    DC.B   "Buffer Error", CR, LF, NULL
 142: FMT_ERR     DC.B   "Format Error: proper format is 's M:SS'. M=[0,9] SS=[00,59]", CR, LF, NULL
 143: FMT_ERR2    DC.B   "Format Error: you can only generate sawtooth (gw) triangle (gt) or square (gq) waves", CR, LF, NULL
 144: CMD_ERR     DC.B   "Command Error: valid commands are", CR, LF
 145:             DC.B   "                            's'  : set time", CR, LF
 146:             DC.B   "                            'gw' : sawtooth wave generation", CR, LF
 147:             DC.B   "                            'qt' : triangle wave generation", CR, LF
 148:             DC.B   "                            'gq' : square wave generation", CR, LF
 149:             DC.B   "                            'adc': get and print analog wave", CR, LF
 150:             DC.B   "                            'q'  : quit", CR, LF, NULL
 151: 
 152: msg3        DC.B   "> Be sure to start saving Terminal data: open Output file = RxData3.txt", CR, LF, NULL
 153: msg4        DC.B   "> press any key to continue...", CR, LF, CR, LF, NULL
 154: msg5        DC.B   "> Done!  Close Output file.", CR, LF, NULL
 155: msg6        DC.B   "> Ready for next data transmission.", CR, LF, NULL
 156: 
 157: ctr125u     DS.W   1            ; 16bit interrupt counter for 125 uSec. of time
 158: 
 159: BUF         DS.B   6            ; character buffer for a 16bit number in decimal ASCII
 160: CTR         DS.B   1            ; character buffer fill count
 161: 
 162: ;;; ADC variables
 163: ATDdone     DS.B   1
 164: 
 165: ;;; input and output string buffers
 166: buffer    DS      30      ; 30 bytes for user input
 167: 
 168: ;;; global variables for the current time
 169: timem       DS.B    1           ; minute
 170: times       DS.B    1           ; second
 171: 
 172: ctr2p5m     DS.W    1           ; interrupt counter for 2.5msec
 173: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 174: 
 175: 
 176: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 177: ;;; Program Section: address used [ end of DATA to $3FFF ] RAM memory
 178: 
 179: ;;; char* buffer;
 180: ;;;
 181: ;;; while(true) {
 182: ;;;     update_leds();
 183: ;;;     print("Clock> ");
 184: ;;;     read_line(buffer);  // get user input
 185: ;;;     eval_input(buffer); // send user input to eval subroutine
 186: ;;; }
 187: pstart
 188:     lds     #$4100              ; initialize stack pointer
 189: 
 190:     ldaa    #%11111111          ; set all PORTA/B bits as output
 191:     staa    DDRB                ; |
 192:     staa    DDRA                ; |
 193: 
 194:     ldaa    #%00000000          ; clear PORTA/B
 195:     staa    PORTB               ; |
 196:     staa    PORTA               ; |
 197: 
 198:     ldaa    #$0C         ; Enable SCI port Tx and Rx units
 199:     staa    SCICR2       ; disable SCI interrupts
 200: 
 201:     ldd     #$0001       ; Set SCI Baud Register = $0001 => 1.5M baud at 24MHz (for simulation)
 202: ;   ldd     #$0002       ; Set SCI Baud Register = $0002 => 750K baud at 24MHz
 203: ;   ldd     #$000D       ; Set SCI Baud Register = $000D => 115200 baud at 24MHz
 204: ;   ldd     #$009C       ; Set SCI Baud Register = $009C => 9600 baud at 24MHz
 205:     std     SCIBDH       ; SCI port baud rate change
 206: 
 207:     ;; RTI ISR configuration
 208:     bset   RTICTL,%00011001 ; set RTI: dev=10*(2**10)=2.555msec for C128 board
 209:                             ;      4MHz quartz oscillator clock
 210:     bset   CRGINT,%10000000 ; enable RTI interrupt
 211:     bset   CRGFLG,%10000000 ; clear RTI IF (Interrupt Flag)
 212: 
 213:     ;;; ATD initialization
 214:     ldaa    #%11000000       ; Turn ON ADC, clear flags, Disable ATD interrupt
 215:     staa    ATDCTL2
 216:     ldaa    #%00001000       ; Single conversion per sequence, no FIFO
 217:     staa    ATDCTL3
 218:     ldaa    #%10000111       ; 8bit, ADCLK=24MHz/16=1.5MHz, sampling time=2*(1/ADCLK)
 219:     staa    ATDCTL4          ; for SIMULATION
 220: 
 221:     ldx    #0
 222:     stx    ctr2p5m          ; initialize interrupt counter with 0.
 223:     cli                     ; enable interrupt, global
 224: 
 225:     ;; print the menu
 226:     ldx     #MENU
 227:     jsr     print
 228: 
 229:     ldx     #PROMPT
 230:     jsr     print
 231: 
 232:     ;; get user input and evaluate the command
 233:     ldx     #buffer             ; load input_buffer
 234: 
 235: looop
 236:     jsr     update_LEDs      ; make sure clock LEDs are updated
 237:     ;; prompt the user for a command
 238:     jsr     getchar             ; type writer - check the key board
 239:     cmpa    #$00                ;  if nothing typed, keep checking
 240:     beq     looop
 241: 
 242:     cmpa    #CR                 ; was an enter typed?
 243:     beq     on_enter            ; |
 244: 
 245:     cmpa    #BS                 ; | was a backspace typed?
 246:     beq     on_backspace        ; |
 247: 
 248:     cpx     #buffer+30          ; is there enough space in the input buffer?
 249:     bls     append              ;  if yes, append input to input buffer
 250: 
 251:     ;; there is not enough space in the buffer, clear it (by reseting X)
 252:     ;;  and report an error
 253: clr_buf
 254:     ldx     #NEWLINE            ; print a newline
 255:     jsr     print               ; |
 256: 
 257:     ldx     #BUFF_ERR           ; print 'invalid command'
 258:     jsr     print               ; |
 259: 
 260:     ldx     #NEWLINE            ; print a newline
 261:     jsr     print               ; |
 262: 
 263:     ldx     #PROMPT
 264:     jsr     print
 265: 
 266:     ldx     #buffer             ; reset input_buffer
 267:     bra     looop               ; re-enter loop
 268: 
 269: 
 270: append
 271:     staa    1,X+                ; store the the typed character, and move the pointer
 272:     clr     X                   ; null terminate
 273: 
 274:     jsr     putchar             ; display typed character in terminal
 275:     bra     looop               ; get next character
 276: 
 277: on_enter
 278:     ldx     #buffer             ; set input for evaluate_cmd
 279: 
 280:     ldaa    X                   ; if the user just pressed enter, without typing anything
 281:     cmpa    #NULL               ; just print a new prompt (don't call evaluate)
 282:     beq     blank_line          ; |
 283: 
 284:     ;; the user actually typed something
 285:     jsr     eval                ; check the command
 286: 
 287: blank_line
 288:     ldx     #NEWLINE            ; print a newline
 289:     jsr     print               ; |
 290: 
 291:     ldx     #PROMPT
 292:     jsr     print
 293: 
 294:     ldx     #buffer             ; reset input_buffer
 295:     clr     X                   ; NULL terminate beginning of input buffer to clear it.
 296: 
 297:     bra     looop               ; get next command
 298: 
 299: on_backspace
 300:     cpx     #buffer             ; ensure that there are characters to delete
 301:     beq     looop               ; if there is nothing in the buffer, return to looop
 302: 
 303:     clr     1,-X                ; delete the last character, move pointer
 304:     pshx                        ; store this value on stack so we can use X to print stuff
 305: 
 306:     ldaa    #LF                 ; move cursor to beginning of line
 307:     jsr     putchar             ; |
 308: 
 309:     ldx     #PROMPT             ; print as new prompt
 310:     jsr     print               ; |
 311: 
 312:     ldx     #buffer             ; print the input buffer
 313:     jsr     print               ; |
 314: 
 315:     pulx                        ; restore current location of cursor in buffer
 316: 
 317:     bra     looop
 318: 
 319: ;;; subroutine section below
 320: 
 321: ;;; eval
 322: ;;;
 323: ;;; Program:    validates input and executes the command
 324: ;;;
 325: ;;; Input:      X Register: buffer
 326: ;;;
 327: ;;; Algorithm:
 328: ;;;     checks if command is s or q. if not, then exit to command error.
 329: ;;;
 330: ;;;     if command is s:
 331: ;;;         is the rest of the command in this format: M:SS? (where M is [0,9], S is [00,59])
 332: ;;;         if so, then set the minutes and seconds variables.
 333: ;;;         else, exit format error.
 334: ;;;
 335: ;;;     if command is q:
 336: ;;;         check and make the buffer only contains q
 337: ;;;             if not, exit command error
 338: ;;;             else, print quit message, enter typewriter program
 339: eval
 340:     pshx
 341:     pshy
 342:     psha
 343: 
 344:     ldaa    1,X+                ; A = command (every command is just one character)
 345:     cmpa    #'s'                ; does command == 's'?
 346:     beq     eval_set_time       ; if so, validate and execute it.
 347: 
 348:     cmpa    #'g'                ; does command == 'g'?
 349:     beq     eval_gen_sig        ; if so, validate and execute it.
 350: 
 351:     cmpa    #'a'                ; does command == 'a'?
 352:     lbeq     eval_adc            ; if so, see if it is 'adc'
 353: 
 354:     cmpa    #'q'                ; does command == 'q'?
 355:     lbeq     eval_quit           ; if so, validate and execute it.
 356: 
 357:     ;; the command didn't match anything, so exit failure
 358:     lbra     eval_exit_cmd_error
 359: 
 360: eval_set_time
 361:     ldaa    1,X+                ; is the input in the format of "s M:SS"?
 362:     cmpa    #' '                ;    if not, jump to exit_error
 363:     lbne    eval_exit_error      ;    else, continue
 364:                                 ; |
 365:     ldaa    1,X+                ; |
 366:     jsr     is_dig              ; |
 367:     lbne     eval_exit_error     ; |
 368:                                 ; |
 369:     ldaa    1,X+                ; |
 370:     cmpa    #':'                ; |
 371:     lbne     eval_exit_error     ; |
 372:                                 ; |
 373:     ldaa    1,X+                ; |
 374:     jsr     is_dig              ; |
 375:     lbne     eval_exit_error     ; |
 376:                                 ; |
 377:     ldaa    1,X+                ; |
 378:     jsr     is_dig              ; |
 379:     lbne     eval_exit_error     ; |
 380:                                 ; |
 381:     ldaa    1,X+                ; |
 382:     cmpa    #NULL               ; |
 383:     lbne     eval_exit_error     ; |
 384: 
 385:     ;; at this point, we know the buffer is properly formatted
 386:     ldx     3,SP                ; reset buffer pointer
 387:     leax    4,X                 ; load address of seconds number
 388:     jsr     atoi                ; convert number at address from ascii to integer
 389: 
 390:     cpd     #59                 ; if seconds > 59 then:
 391:     lbhi     eval_exit_error     ; invalid time input, exit error
 392: 
 393:     stab    times               ; store lower 8 bits of output to seconds variable
 394: 
 395:     ;; store the minutes now that we know the seconds are valid
 396:     ldx     3,SP                ; reset buffer pointer
 397:     leax    2,X                 ; load address of first digit
 398:     ldaa    X                   ; A = first digit
 399:     suba    #$30                ; Convert A from ascii to integer
 400: 
 401:     staa    timem               ; store to minutes variable
 402: 
 403:     lbra     eval_exit           ; exit subroutine
 404: 
 405: eval_gen_sig
 406:     ldaa    1,X+
 407:     cmpa    #'w'
 408:     beq     eval_sig_valid
 409:     cmpa    #'t'
 410:     beq     eval_sig_valid
 411:     cmpa    #'q'
 412:     beq     eval_sig_valid
 413:     lbra    eval_exit_error2
 414: 
 415: eval_sig_valid
 416:     ldaa    1,X-                ; move pointer back to the wave specifier
 417:     cmpa    #NULL
 418:     lbne    eval_exit_error2    ; exit error
 419:     tfr     X,Y                 ; Move X to Y, so X can be used for print
 420: 
 421:     ldx     #NEWLINE
 422:     jsr     print
 423:     ldx     #msg3
 424:     jsr     print
 425:     ldx     #msg4
 426:     jsr     print
 427: 
 428: eval_sig_get_key
 429:     jsr     update_LEDs
 430:     jsr     getchar
 431:     cmpa    #NULL
 432:     beq     eval_sig_get_key
 433: 
 434:     ldx     #0                  ; reset counter
 435:     stx     ctr125u
 436: 
 437:     ldaa    Y
 438:     jsr     StartTimer5oc
 439: 
 440: loop2048
 441:     jsr     update_LEDs      ; make sure clock LEDs are updated
 442:     ldd     ctr125u
 443:     cpd     #DATAmax         ; 1024 bytes will be sent, the receiver at Windows PC
 444:     bhs     loopTxON         ;   will only take 2048 bytes.
 445:     bra     loop2048         ; set Terminal Cache Size to 10000 lines, update from 1000 lines
 446: 
 447: loopTxON
 448:     LDAA    #%00000000
 449:     STAA    TIE               ; disable OC5 interrupt
 450: 
 451:     ldx     #NEWLINE
 452:     jsr     print
 453:     jsr     print
 454: 
 455:     ldx     #msg5            ; print '> Done!  Close Output file.'
 456:     jsr     print
 457: 
 458:     ldx     #msg6            ; print '> Ready for next data transmission'
 459:     jsr     print
 460: 
 461:     ldx     #NEWLINE
 462:     jsr     print
 463: 
 464:     bra     eval_exit
 465: 
 466: eval_adc
 467:     ldaa    1,X+
 468:     cmpa    #'d'
 469:     bne     eval_exit_cmd_error
 470: 
 471:     ldaa    1,X+
 472:     cmpa    #'c'
 473:     bne     eval_exit_cmd_error
 474: 
 475:     ldaa    1,X+
 476:     cmpa    #NULL
 477:     bne     eval_exit_cmd_error
 478: 
 479:     ldx     #NEWLINE
 480:     jsr     print
 481:     jsr     print
 482: 
 483: eval_adc_get_key
 484:     jsr     update_LEDs
 485:     jsr     getchar
 486:     cmpa    #NULL
 487:     beq     eval_adc_get_key
 488: 
 489:     ldx     #0                  ; reset counter
 490:     stx     ctr125u
 491: 
 492:     ldx     #NEWLINE
 493:     jsr     print
 494: 
 495:     ldaa    #'a'
 496:     jsr     StartTimer5oc
 497:     bra     loop2048
 498: 
 499: eval_quit
 500:     ldaa    X
 501:     cmpa    #NULL
 502:     bne     eval_exit_cmd_error
 503: 
 504:     sei                         ; disable interrupts, thus stopping the clock
 505: 
 506:     ldx     #NEWLINE            ; print quit message
 507:     jsr     print               ; |
 508:     ldx     #QUIT_MSG           ; |
 509:     jsr     print               ; |
 510: 
 511:     jsr     typewriter          ; enter typewriter program
 512: 
 513: eval_exit_error
 514:     ldx     #NEWLINE            ; print a newline, then the error message
 515:     jsr     print               ; |
 516:     ldx     #FMT_ERR            ; |
 517:     jsr     print               ; |
 518:     bra     eval_exit           ; |
 519: 
 520: eval_exit_error2
 521:     ldx     #NEWLINE            ; print a newline, then the error message
 522:     jsr     print               ; |
 523:     ldx     #FMT_ERR2           ; |
 524:     jsr     print               ; |
 525:     bra     eval_exit           ; |
 526: 
 527: eval_exit_cmd_error
 528:     ldx     #NEWLINE            ; print a newline, then the error message
 529:     jsr     print               ; |
 530:     ldx     #CMD_ERR            ; |
 531:     jsr     print               ; |
 532: 
 533: eval_exit
 534:     pula
 535:     puly
 536:     pulx
 537: 
 538:     rts
 539: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 540: 
 541: ;***********Timer OC5 interrupt service routine***************
 542: oc5isr_saw
 543:     ldd     #3000              ; 125usec with (24MHz/1 clock)
 544:     addd    TC5H               ;    for next interrupt
 545:     std     TC5H               ;
 546:     bset    TFLG1,%00100000    ; clear timer CH6 interrupt flag, not needed if fast clear enabled
 547:     ldd     ctr125u
 548:     ldx     ctr125u
 549:     inx                        ; update OC5 (125usec) interrupt counter
 550:     stx     ctr125u
 551:     clra                       ;   print ctr125u, only the last byte
 552:     jsr     pnum10             ;   to make the file RxData3.txt with exactly 1024 data
 553:     RTI
 554: ;***********end of Timer OC5 interrupt service routine********
 555: 
 556: ;Timer OC5 interrupt service routine***************
 557: oc5isr_tri
 558:     ldd     #3000              ; 125usec with (24MHz/1 clock)
 559:     addd    TC5H               ;    for next interrupt
 560:     std     TC5H               ;
 561:     bset    TFLG1,%00100000    ; clear timer CH6 interrupt flag, not needed if fast clear enabled
 562:     ldd     ctr125u
 563:     ldx     ctr125u
 564:     inx                        ; update OC5 (125usec) interrupt counter
 565:     stx     ctr125u
 566: 
 567: 
 568:     anda    #%00000001          ; if lsb is 1, countdown
 569:     bne     tri_countdown       ; else, countup
 570:     bra     tri_countup
 571: 
 572: tri_countdown
 573:     ldaa    #$FF                ; do 255 - B
 574:     sba                         ; result is stored in A,
 575:     tab                         ; so move it to B
 576:     bra     tri_exit
 577: 
 578: tri_countup
 579: 
 580: tri_exit
 581:     clra
 582:     jsr   pnum10             ;   to make the file RxData3.txt with exactly 1024 data
 583:     rti
 584: ;***********end of Timer OC5 interrupt service routine********
 585: 
 586: ;Timer OC5 interrupt service routine***************
 587: oc5isr_square
 588:     ldd     #3000              ; 125usec with (24MHz/1 clock)
 589:     addd    TC5H               ;    for next interrupt
 590:     std     TC5H               ;
 591:     bset    TFLG1,%00100000    ; clear timer CH6 interrupt flag, not needed if fast clear enabled
 592:     ldd     ctr125u
 593:     ldx     ctr125u
 594:     inx                        ; update OC5 (125usec) interrupt counter
 595:     stx     ctr125u
 596: 
 597:     anda    #%00000001          ; if lsb is 1, hi
 598:     bne     square_hi           ; else, low
 599:     bra     square_lo
 600: 
 601: square_hi
 602:     ldab    #$FF
 603:     bra     square_exit
 604: square_lo
 605:     ldab    #0
 606: 
 607: square_exit
 608:     clra                     ;   print ctr125u, only the last byte
 609:     jsr     pnum10             ;   to make the file RxData3.txt with exactly 1024 data
 610:     RTI
 611: ;***********end of Timer OC5 interrupt service routine********
 612: 
 613: ;Timer OC5 interrupt service routine***************
 614: oc5isr_adc
 615:     ldd     #3000              ; 125usec with (24MHz/1 clock)
 616:     addd    TC5H               ;    for next interrupt
 617:     std     TC5H               ;
 618:     bset    TFLG1,%00100000    ; clear timer CH6 interrupt flag, not needed if fast clear enabled
 619:     ldx     ctr125u
 620:     inx                        ; update OC5 (125usec) interrupt counter
 621:     stx     ctr125u
 622: 
 623: ;adcwait
 624: ;    ldaa    ATDSTAT0            ; if necessary, wait until conversion is done.
 625: ;    anda    #%10000000
 626: ;    beq     adcwait
 627: 
 628:     ldab    ATDDR0L            ; get number from adc
 629:     clra
 630:     jsr     pnum10             ; print number from adc
 631: 
 632:     ;; start another conversion
 633:     ldaa    #%10000111       ; right justified, unsigned, single conversion,
 634:     staa    ATDCTL5          ; single channel, CHANNEL 7, start the conversion
 635:     RTI
 636: ;***********end of Timer OC5 interrupt service routine********
 637: 
 638: ;***************StartTimer5oc************************
 639: ;* Program: Start the timer interrupt, timer channel 6 output compare
 640: ;* Input:   A Register: ascii code for type of signal
 641: ;           Constants - channel 6 output compare, 125usec at 24MHz
 642: ;* Output:  None, only the timer interrupt
 643: ;* Registers modified: D used and CCR modified
 644: ;* Algorithm:
 645: ;             initialize TIOS, TIE, TSCR1, TSCR2, TC2H, and TFLG1
 646: ;**********************************************
 647: StartTimer5oc
 648:     pshd
 649:     pshx
 650: 
 651:     ;; set appropriate interrupt vector
 652:     cmpa    #'w'
 653:     beq     saw5oc
 654:     cmpa    #'t'
 655:     beq     triangle5oc
 656:     cmpa    #'q'
 657:     beq     square5oc
 658:     cmpa    #'a'
 659:     beq     adc5oc
 660: 
 661: saw5oc
 662:     ldx     #oc5isr_saw
 663:     bra     store5oc
 664: triangle5oc
 665:     ldx     #oc5isr_tri
 666:     bra     store5oc
 667: square5oc
 668:     ldx     #oc5isr_square
 669:     bra     store5oc
 670: adc5oc
 671:     ldx     #oc5isr_adc
 672: 
 673: 
 674: store5oc
 675:     stx     oc5isr
 676: 
 677:     ldaa    #%00100000
 678:     staa    TIOS              ; set CH5 Output Compare
 679:     staa    TIE               ; set CH5 interrupt Enable
 680:     ldaa    #%10000000        ; enable timer, Fast Flag Clear not set
 681:     staa    TSCR1
 682:     ldaa    #%00000000        ; TOI Off, TCRE Off, TCLK = BCLK/1
 683:     staa    TSCR2             ;   not needed if started from reset
 684: 
 685:     ldd     #3000            ; 125usec with (24MHz/1 clock)
 686:     addd    TCNTH            ;    for first interrupt
 687:     std     TC5H             ;
 688: 
 689:     bset    TFLG1,%00100000   ; initial Timer CH5 interrupt flag Clear, not needed if fast clear set
 690:     ldaa    #%00100000
 691:     staa    TIE               ; set CH5 interrupt Enable
 692: 
 693:     pulx
 694:     puld
 695:     rts
 696: ;***************end of StartTimer2oc*****************
 697: 
 698: 
 699: ;***********pnum10***************************
 700: ;* Program: print a word (16bit) in decimal to SCI port
 701: ;* Input:   Register D contains a 16 bit number to print in decimal number
 702: ;* Output:  decimal number printed on the terminal connected to SCI port
 703: ;*
 704: ;* Registers modified: CCR
 705: ;* Algorithm:
 706: ;     Keep divide number by 10 and keep the remainders
 707: ;     Then send it out to SCI port
 708: ;  Need memory location for counter CTR and buffer BUF(6 byte max)
 709: ;**********************************************
 710: pnum10          pshd                   ;Save registers
 711:                 pshx
 712:                 pshy
 713:                 clr     CTR            ; clear character count of an 8 bit number
 714: 
 715:                 ldy     #BUF
 716: pnum10p1        ldx     #10
 717:                 idiv
 718:                 beq     pnum10p2
 719:                 stab    1,y+
 720:                 inc     CTR
 721:                 tfr     x,d
 722:                 bra     pnum10p1
 723: 
 724: pnum10p2        stab    1,y+
 725:                 inc     CTR
 726: ;--------------------------------------
 727: 
 728: pnum10p3        ldaa    #$30
 729:                 adda    1,-y
 730:                 jsr     putchar
 731:                 dec     CTR
 732:                 bne     pnum10p3
 733:                 ldx     #NEWLINE
 734:                 jsr     print
 735:                 puly
 736:                 pulx
 737:                 puld
 738:                 rts
 739: ;***********end of pnum10********************
 740: 
 741: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 742: ;;; typewriter
 743: ;;; Program:    simple echo program which sends user input back to the terminal
 744: ;;;
 745: ;;; Input:      None
 746: ;;; Output:     None, doesn't ever return to main loop
 747: ;;; Algorithm:
 748: ;;;     gets character from terminal
 749: ;;;     sends character back to terminal.
 750: ;;;     if newline is recieved, a linefeed is also sent to terminal
 751: typewriter
 752:     jsr     getchar
 753:     jsr     putchar
 754: 
 755:     cmpa    #CR
 756:     bne     typewriter
 757: 
 758: typewriter_onenter
 759:     ldaa    #LF
 760:     jsr     putchar
 761:     bra     typewriter
 762: 
 763: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 764: ;;; update_LEDs
 765: ;;; Program:    convert integer to binary coded decimal, and update PORTA/PORTB with minutes and seconds.
 766: ;;;
 767: ;;; Input:      None. uses global minutes and seconds counters
 768: ;;; Ouput:      Modifies PORTA and PORTB
 769: ;;; Algorithm:
 770: ;;;     values are stored as integers. minutes is easy, just store it at PORTA
 771: ;;;
 772: ;;;     for PORTB, first it is divided by 10. the remainder is the 1's place,
 773: ;;;     quotient is the 10's place.
 774: ;;;
 775: ;;;     the 1's digit is stored in the temp variable. then the 10's digit is shifted.
 776: ;;;     they are or'ed together
 777: ;;;     the result is stored in PORTB
 778: ;;;
 779: ;;;     NOTE: A temp variable isn't needed, but if PORTB is used directly, then the simulator
 780: ;;;     will flicker the seconds 10's place digit (this wouldn't be visible in real life).
 781: ;;;     a temp variable is used soley for the simulator.
 782: update_LEDs
 783:     pshd
 784:     pshx
 785:     clr     1,-SP               ; temp variable
 786: 
 787:     ;; does the time need updated?
 788:     ldd     ctr2p5m             ; check the 2.5ms counter, has 1000ms elapsed?
 789:     cpd     #150                ; | (ie, counter == 400)
 790:     bls     update_LEDs_skip    ; | (for sim purposes, this value is set to 150)
 791: 
 792:     ;; the counter
 793:     clr     ctr2p5m             ; clear (BOTH BYTES) of the counter
 794:     clr     ctr2p5m+1           ; |
 795:     inc     times               ; increment the seconds counter
 796: 
 797:     ldaa    times               ; is the seconds counter == 60?
 798:     cmpa    #60                 ; |
 799:     blo     update_LEDs_skip    ; |
 800: 
 801:     ;; seconds counter == 60
 802:     clr     times               ; reset the seconds counter
 803:     inc     timem               ; increment the minutes counter
 804: 
 805:     ldaa    timem               ; is the minutes counter == 10?
 806:     cmpa    #10                 ; |
 807:     blo     update_LEDs_skip    ; |
 808: 
 809:     ;; minutes counter == 10
 810:     clr     timem               ; reset minutes counter back to 0
 811: 
 812: update_LEDs_skip
 813: 
 814:     ;; do first divide (for 1's digit)
 815:     ldab    times               ; set dividend (total num of seconds)
 816:     clra                        ; ensure A is zero for divide
 817:     ldx     #10                 ; set divisor
 818:     idiv
 819: 
 820:     stab    SP                  ; stash 1's digit on temp variable
 821: 
 822:     ;; 10's digit is now in X register
 823:     tfr     X,D                 ; move from X to D (low-4 will be in B register)
 824: 
 825:     lslb                        ; move digit to upper four bits
 826:     lslb                        ; |
 827:     lslb                        ; |
 828:     lslb                        ; |
 829: 
 830:     orab    SP                  ; combine 10's and 1's in PORTB
 831:     stab    PORTB               ; store value to PORTB
 832: 
 833:     ;; send result out to ports
 834:     ldaa    timem
 835:     staa    PORTA
 836: 
 837:     leas    1,SP                ; pop off temp variable
 838:     pulx
 839:     puld
 840:     rts
 841: 
 842: ;;; atoi
 843: ;;;
 844: ;;; Program:    converts a string to an integer (if possible)
 845: ;;;
 846: ;;; Input:      X Register: pointer to string
 847: ;;;
 848: ;;; Output:     D Register: integer representation of string
 849: ;;;             modifies err_flag and err_data
 850: ;;;
 851: ;;; Algorithm:
 852: ;;;     converts digits up to first non-number character.
 853: ;;;      e.g. atoi("12hey there") -> 12
 854: ;;;
 855: ;;;     if the first character is a non-number, D is set to zero
 856: ;;;      and err_flag and err_data are set accordingly
 857: ;;;      e.g. atoi("afs12") -> 0 (ERROR)
 858: ;;;
 859: ;;;     Stack Layout:
 860: ;;;         SP + 8: return address
 861: ;;;         SP + 6: X reg
 862: ;;;         SP + 5: len var
 863: ;;;         SP + 4: pow var
 864: ;;;         SP + 2: tmp var
 865: ;;;         SP + 0: accum var
 866: ;;;
 867: ;;;     // find length of number
 868: ;;;     len = 0 ; length accumulator
 869: ;;;     while is_digit((X++)*):
 870: ;;;         len++;
 871: ;;;
 872: ;;;     // add up each digit
 873: ;;;     index = len; // len is now index for X
 874: ;;;     accum = A; store A on the stack
 875: ;;;     pow = 0; // power variable
 876: ;;;     B = 1 ; // B is now the Power Accumulator
 877: ;;;     X = index+X ; we are going through this number backwards
 878: ;;;     while index > 0:
 879: ;;;         index--;
 880: ;;;         pow = index;
 881: ;;;         D = X[-index];
 882: ;;;
 883: ;;;         if pow == 0:
 884: ;;;             accum += D
 885: ;;;
 886: ;;;         tmp = D
 887: ;;;         while pow > 0:
 888: ;;;             tmp += tmp << 3
 889: ;;;             tmp += tmp << 1
 890: ;;;             pow--;
 891: ;;;
 892: ;;;         accum += tmp
 893: ;;;
 894: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 895: atoi
 896:     pshy
 897:     pshx
 898:     clr     1,-SP               ; len var / index var
 899:     clr     1,-SP               ; pow var
 900:     clr     1,-SP               ; tmp var
 901:     clr     1,-SP               ; |
 902:     clr     1,-SP               ; accum var
 903:     clr     1,-SP               ; |
 904: 
 905: atoi_getlen
 906:     ldaa    1,X+                ; get next character
 907:     jsr     is_dig              ; break if character != number
 908:     bne     atoi_getlen_exit    ; |
 909: 
 910:     inc     5,SP                ; increment length
 911:     bra     atoi_getlen         ; continue
 912: 
 913: atoi_getlen_exit
 914:     ldx     6,SP                ; reload x from stack
 915:     dex                         ; keep X from being incremented prematurely.
 916: atoi_accumulate
 917:     ldaa    5,SP                ; if index == 0, break
 918:     cmpa    #0                  ; |
 919:     beq     atoi_exit           ; |
 920:     dec     5,SP                ; decrement index
 921:     deca                        ; decrement register to track
 922:     inx                         ; decrement pointer
 923: 
 924:     movb    5,SP, 4,SP          ; pow = index
 925: 
 926:     cmpa    #0                  ; if pow > 0, skip special handling
 927:     bne     atoi_calc_power     ;
 928: 
 929:     clra                        ; clear A register
 930:     ldab     X                  ; load lower 8 bits to B
 931:     subb    #$30                 ; convert from ascii
 932:     addd    SP                  ; D += accum
 933:     std     SP                  ; accum = D
 934:     bra     atoi_exit           ; at this point, loop is finished
 935: 
 936:     ;; because of the previous if statement, we know that index and power
 937:     ;; will always be at least 1
 938: atoi_calc_power
 939:     clra
 940:     ldab    X                   ; load lower 8 bits to B
 941:     subb    #$30                ; convert from ascii
 942: 
 943:     std     2,SP                ; store in tmp
 944: atoi_calc_power_lp
 945:     ldaa    4,SP                ; if pow = 0, break
 946:     cmpa    #0                  ; |
 947:     beq     atoi_calc_power_exit; |
 948:     dec     4,SP                ; decrement pow var
 949: 
 950:     ;; prepare for first shift
 951:     ldd     2,SP                ; D = tmp
 952:     tfr     D,Y                 ; stash D in Y
 953: 
 954:     lsld                        ; D *= 8
 955:     lsld                        ; |
 956:     lsld                        ; |
 957: 
 958:     std     2,SP                ; tmp = D
 959: 
 960:     ;; prepare for shift again
 961:     tfr     Y,D                 ; retrieve D from Y
 962: 
 963:     lsld                        ; D *= 2
 964: 
 965:     addd    2,SP                ; tmp += D
 966:     std     2,SP                  ; |
 967: 
 968:     bra     atoi_calc_power_lp
 969: 
 970: atoi_calc_power_exit
 971:     ldd     2,SP                ; load tmp into D
 972:     addd    SP                  ; add tmp to accum
 973:     std     SP                  ; |
 974:     bra     atoi_accumulate
 975: 
 976: atoi_exit
 977:     ldd     SP                  ; D = accumulator
 978:     leas    6,SP
 979:     pulx
 980:     puly
 981:     rts
 982: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 983: 
 984: ;;; is_dig
 985: ;;;
 986: ;;; Program:    determines if the value in A is an ascii digit
 987: ;;;
 988: ;;; Input:      A register: Ascii test value
 989: ;;;
 990: ;;; Output:     Modifies Zero bit in CCR
 991: ;;;
 992: ;;; Algorithm:
 993: ;;;     if A < '0' or A > '9':
 994: ;;;         return false
 995: ;;;     else:
 996: ;;;         return true
 997: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 998: is_dig
 999:     cmpa    #'0'                ; if A < '0' or A > '9' return false
1000:     blo     is_dig_false        ; |
1001:     cmpa    #'9'                ; |
1002:     bhi     is_dig_false        ; |
1003: 
1004: is_dig_true
1005:     orcc    #%00000100          ; set Z bit in CCR
1006:     rts
1007: 
1008: is_dig_false
1009:     andcc   #%11111011          ; clear Z bit in CCR
1010:     rts
1011: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
1012: 
1013: ;;; print
1014: ;;;
1015: ;;; Program: Output character string to SCI port, print message
1016: ;;; Input:   Register X points to ASCII characters in memory
1017: ;;; Output:  message printed on the terminal connected to SCI port
1018: ;;;
1019: ;;; Registers modified: CCR
1020: ;;; Algorithm:
1021: ;;;     Pick up 1 byte from memory where X register is pointing
1022: ;;;     Send it out to SCI port
1023: ;;;     Update X register to point to the next byte
1024: ;;;     Repeat until the byte data $00 is encountered
1025: ;;;       (String is terminated with NULL=$00)
1026: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
1027: print
1028:     psha                   ;Save registers
1029:     pshx
1030: printmsgloop
1031:     ldaa    1,X+           ;pick up an ASCII character from string
1032:                             ;   pointed by X register
1033:                             ;then update the X register to point to
1034:                             ;   the next byte
1035:     cmpa    #NULL
1036:     beq     printmsgdone   ;end of strint yet?
1037:     jsr     putchar        ;if not, print character and do next
1038:     bra     printmsgloop
1039: 
1040: printmsgdone
1041:     pulx
1042:     pula
1043:     rts
1044: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
1045: 
1046: 
1047: 
1048: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
1049: ;;; putchar
1050: ;;;
1051: ;;; Program: Send one character to SCI port, terminal
1052: ;;; Input:   Accumulator A contains an ASCII character, 8bit
1053: ;;; Output:  Send one character to SCI port, terminal
1054: ;;; Registers modified: CCR
1055: ;;; Algorithm:
1056: ;;;    Wait for transmit buffer become empty
1057: ;;;      Transmit buffer empty is indicated by TDRE bit
1058: ;;;      TDRE = 1 : empty - Transmit Data Register Empty, ready to transmit
1059: ;;;      TDRE = 0 : not empty, transmission in progress
1060: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
1061: putchar
1062:     brclr SCISR1,#%10000000,putchar   ; wait for transmit buffer empty
1063:     staa  SCIDRL                      ; send a character
1064:     rts
1065: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
1066: 
1067: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
1068: ;;; getchar
1069: ;;;
1070: ;;; Program: Input one character from SCI port (terminal/keyboard)
1071: ;;;             if a character is received, other wise return NULL
1072: ;;; Input:   none
1073: ;;; Output:  Accumulator A containing the received ASCII character
1074: ;;;          if a character is received.
1075: ;;;          Otherwise Accumulator A will contain a NULL character, $00.
1076: ;;; Registers modified: CCR
1077: ;;; Algorithm:
1078: ;;;    Check for receive buffer become full
1079: ;;;      Receive buffer full is indicated by RDRF bit
1080: ;;;      RDRF = 1 : full - Receive Data Register Full, 1 byte received
1081: ;;;      RDRF = 0 : not full, 0 byte received
1082: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
1083: getchar
1084:     brclr SCISR1,#%00100000,getchar7
1085:     ldaa  SCIDRL
1086:     rts
1087: getchar7
1088:     clra
1089:     rts
1090: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
1091: 
1092: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
1093: rtiisr
1094:     bset    CRGFLG,%10000000 ; clear RTI Interrupt Flag - for the next one
1095:     ldx     ctr2p5m          ; every time the RTI occur, increase
1096:     inx                      ;    the 16bit interrupt count
1097:     stx     ctr2p5m
1098:     rti
1099: 
1100: ;OPTIONAL
1101: ;more variable/data section below
1102: ; this is after the program code section
1103: ; of the RAM.  RAM ends at $3FFF
1104: ; in MC9S12C128 chip
1105: 
1106:                END               ; this is end of assembly source file
1107:                                  ; lines below are ignored - not assembled/compiled

Computer Engineering 473

car_demo.jpg

This course was also taught by Professor Choi, the course webpage can be found here. We spent the semester designing and building an autonomous robotic car from scratch, using a Raspberry Pi 4. We started by learning how to access the GPIO registers on the Pi in memory, using the mmap C function. From that point on, continued to iterate on the design, constantly adding new features.

Ring Buffer Implmentation

This course heavily incorporated multithreaded programming and interthread communication. Most of my peers used the professor’s FIFO implementation, but I had already made a ring-buffer implementation by the time he distributed his solution.

ringbuf.h
 1: #ifndef RINGBUF_H
 2: #define RINGBUF_H
 3: 
 4: #include <stdlib.h>
 5: 
 6: /* Lock-Free thread messaging.
 7:  *
 8:  * ring-buffer has designated producers and consumers. the read-head points to
 9:  * the location of the next insertion, and the read-head points to the location
10:  * before the next read.
11:  *
12:  * every time an element is inserted into the ringbuffer, the write_head is
13:  * incremented.
14: **/
15: 
16: struct RingBuffer {
17:     char* _buffer;
18:     size_t length;
19:     size_t element_size;
20: 
21:     int write_head;
22:     int read_head;
23: };
24: 
25: /// wrapper struct. This struct is what is passed to the producer thread.
26: /// the ringbuffer field is not meant to be directly accessed.
27: struct Producer {
28:     struct RingBuffer* _ringbuf;
29: };
30: 
31: /// wrapper struct. This struct is what is passed to the consumer thread.
32: /// the ringbuffer field is not meant to be directly accessed.
33: struct Consumer {
34:     struct RingBuffer* _ringbuf;
35: };
36: 
37: struct RingBuffer ringbuf_new(size_t len, size_t element_size);
38: void ringbuf_del(struct RingBuffer* ringbuf);
39: 
40: int ringbuf_next_index(const struct RingBuffer* rb, int index);
41: void ringbuf_print(struct RingBuffer* rb);
42: 
43: struct Producer ringbuf_make_producer(struct RingBuffer* ringbuf);
44: 
45: /// pushes value into the ringbuffer returns 0 on success.
46: /// may fail if the ring buffer is full (ie, write-head runs into read-head).
47: /// returns -1 on fail
48: int producer_push(struct Producer* producer, void* val);
49: 
50: struct Consumer ringbuf_make_consumer(struct RingBuffer* ringbuf);
51: 
52: void* consumer_pop(struct Consumer* consumer);
53: void* consumer_peek(struct Consumer* consumer);
54: 
55: #endif // RINGBUF_H
ringbuf.c
  1: #include "ringbuf.h"
  2: #include <stdlib.h>
  3: #include <string.h>
  4: #include <stdio.h>
  5: 
  6: /// allocates/creates a new ringbuffer
  7: ///
  8: /// len must be at least 3
  9: struct RingBuffer ringbuf_new(size_t len, size_t element_size) {
 10:     struct RingBuffer rb;
 11: 
 12:     // allocate space for the ring-buffer
 13:     rb._buffer = malloc(len * element_size);
 14:     rb.length = len;
 15:     rb.element_size = element_size;
 16: 
 17:     rb.write_head = 1;
 18:     rb.read_head = 0;
 19: 
 20:     return rb;
 21: }
 22: 
 23: // debug function, prints the ring-buffer
 24: void ringbuf_print(struct RingBuffer* rb) {
 25:     printf("ringbuf: < ");
 26:     for (char* c = rb->_buffer; c <= &rb->_buffer[rb->length - 1]; c++) {
 27:         printf("%d ", *c);
 28:     }
 29: 
 30:     printf(">\n");
 31: }
 32: /// frees the ringbuffer zeros it's fields
 33: void ringbuf_del(struct RingBuffer *rb) {
 34:     free((void *)rb->_buffer);
 35:     rb->element_size = 0;
 36:     rb->length = 0;
 37:     rb->_buffer = NULL;
 38:     rb->write_head = 0;
 39:     rb->read_head = 0;
 40: }
 41: 
 42: int ringbuf_next_index(const struct RingBuffer* rb, int index) {
 43:     int new = index + 1;
 44:     if (new >= rb->length) {
 45:         return 0;
 46:     } else {
 47:         return new;
 48:     }
 49: }
 50: 
 51: struct Producer ringbuf_make_producer(struct RingBuffer *rb) {
 52:     struct Producer prod;
 53: 
 54:     prod._ringbuf = rb;
 55: 
 56:     return prod;
 57: }
 58: 
 59: int producer_push(struct Producer* producer, void* val) {
 60:     // get an easy handle to the ring-buffer
 61:     struct RingBuffer rb = *producer->_ringbuf;
 62: 
 63:     // ringbuffer is full, need to wait until consumer increments the read head.
 64:     if (ringbuf_next_index(&rb, rb.write_head) == rb.read_head) {
 65:         return -1;
 66:     }
 67: 
 68:     // copy the data into the ring-buffer
 69:     char* dest = rb._buffer + rb.write_head*rb.element_size;
 70:     memcpy(dest, val, rb.element_size);
 71: 
 72:     // wrap the write head around if it is past the upper bound
 73:     producer->_ringbuf->write_head = ringbuf_next_index(&rb, rb.write_head);
 74: 
 75:     return 0;
 76: }
 77: 
 78: struct Consumer ringbuf_make_consumer(struct RingBuffer *rb) {
 79:     struct Consumer con;
 80: 
 81:     con._ringbuf = rb;
 82: 
 83:     return con;
 84: }
 85: 
 86: // increments the read head and returns the value.
 87: // returns NULL if buffer is empty
 88: void* consumer_pop(struct Consumer* consumer) {
 89:     void* val = consumer_peek(consumer);
 90: 
 91:     if (val == NULL) {
 92:         return NULL;
 93:     }
 94: 
 95:     // get an easy handle to the ring-buffer
 96:     struct RingBuffer rb = *consumer->_ringbuf;
 97: 
 98:     // wrap the write head around if it is past the upper bound
 99:     consumer->_ringbuf->read_head = ringbuf_next_index(&rb, rb.read_head);
100:     return val;
101: }
102: 
103: // returns the value after the read head.
104: // returns NULL if buffer is empty
105: void* consumer_peek(struct Consumer* consumer) {
106:     // get an easy handle to the ring-buffer
107:     struct RingBuffer rb = *consumer->_ringbuf;
108: 
109:     if (ringbuf_next_index(&rb, rb.read_head) == rb.write_head) {
110:         return NULL;
111:     }
112: 
113:     return rb._buffer + ringbuf_next_index(&rb, rb.read_head)*rb.element_size;
114: }

Curses Asynchronous User Interface (for debugging)

One of the problems I faced while debugging this program was data visibility. I often needed to see the state of many variables across many threads. This would clog up the standard output during debugging, which prevented me from being able to understand everything going on in my code. I decided to create a graphical user interface using the ncurses API to help format all the data better.

Since ncurses is not an async library, this presented some challenges for me, since I could no longer simply use print statements for debugging. I had to create a custom API for the display so that it could be controlled asynchronously from many threads. This API ended up being a source of frustration for me, however, since my implementation had data races/unsafe pointer utilization which I didn’t discover until later in the semester.

This problem can be found on line 148 of gui.c. As you can see here, I am storing the pointer of the command or data to be written to the screen in a queue, which will be processed at some point in the future. This is a problem, because if that pointer is changed before the GUI prints the data at its address, then it will start printing garbage and mess up the whole graphical interface.

gui.h
  1: #ifndef GUI_H_
  2: #define GUI_H_
  3: 
  4: #include <ncurses.h>
  5: #include <stdlib.h>
  6: #include <string.h>
  7: #include <ringbuf.h>
  8: 
  9: #define COMMAND_PROMPT "hw11> "
 10: 
 11: // GUI data structure. This structure holds all the data that the GUI needs in
 12: // order to operate. it contains WINDOW* fields which refer to ncurses windows.
 13: // the help and status windows have additional "master" windows. The master
 14: // windows are a little larger than the regular window, and contain the border
 15: // and window title.
 16: //
 17: // there are strings associated with the title of the status and help windows.
 18: // This string contains additional title data. For each window the title will
 19: // read as follows:
 20: //  "[window: window_title]"
 21: //  where "window" is either "help" or "status".
 22: //
 23: // To add text to the status or help windows, one should use the wprintw
 24: // function provided by ncurses.
 25: //
 26: // the cmd window is used for command input. the "line_mode" option allows line
 27: // mode to be turned on/off. When line mode is off, only one character will ever
 28: // be entered onto the command line. with line mode on, successive key presses
 29: // will append the typed character onto the command line.
 30: typedef struct Gui {
 31:     WINDOW *cmd;
 32:     bool line_mode;
 33: 
 34:     WINDOW *help;
 35:     WINDOW *status;
 36: 
 37:     WINDOW *help_master;
 38:     WINDOW *status_master;
 39: 
 40:     int status_x, status_y;
 41:     int help_x, help_y;
 42: 
 43:     char* status_title;
 44:     char* help_title;
 45: } Gui;
 46: 
 47: // allocate and initialize the GUI structure. This data must be free'ed at the
 48: // end of the program using the gui_del function.
 49: Gui gui_init();
 50: 
 51: // frees data allocated by the GUI free function, and ends ncurses instance
 52: void gui_del(Gui* gui);
 53: 
 54: // gets input from the command line, returning the character. this is a blocking
 55: // function. if non-blocking behavior is desired, a separate thread must be used.
 56: char get_input(Gui* wins);
 57: 
 58: // Changes the title of the status window. You cannot just replace the string in
 59: // the structure, because the title/border isn't updated very often. this
 60: // function updates the relative fields in the GUI structure, and then refreshes
 61: // the window to display the changes
 62: void status_set_title(Gui* wins, char* title);
 63: 
 64: // clears the contents of the window. this function updates the relavant fields
 65: // in the gui structure, and then refreshes the window to reflect those changes.
 66: void status_clear(Gui* wins);
 67: 
 68: // Changes the title of the status window. You cannot just replace the string in
 69: // the structure, because the title/border isn't updated very often. this
 70: // function updates the relative fields in the GUI structure, and then refreshes
 71: // the window to display the changes
 72: void help_set_title(Gui* wins, char* title);
 73: 
 74: // clears the contents of the window. this function updates the relavant fields
 75: // in the gui structure, and then refreshes the window to reflect those changes.
 76: void help_clear(Gui* wins);
 77: 
 78: void wprintw_file(WINDOW* win, char* filename);
 79: 
 80: // command which is used to print data to the GUI. Commands that print stuff may take
 81: // either a c string (char*), or if the c-string is NULL, then the subsequent command
 82: // entries will be DATA packets, each of which contain the c member of the union.
 83: // this allows data to be copied over rather than referenced.
 84: typedef struct GuiCommand {
 85:     enum GuiControl {
 86:         PRINT_HELP,
 87:         PRINT_FILE_HELP,
 88:         PRINT_STATUS,
 89:         TERMINAL_PRINT,
 90:         TERMINAL_ENTER,
 91:         TERMINAL_RETURN,
 92:         SET_TITLE_HELP,
 93:         SET_TITLE_STATUS,
 94:         CLEAR_HELP,
 95:         CLEAR_STATUS,
 96:         REFRESH,
 97:         CMD_ECHO_ON,
 98:         CMD_ECHO_OFF,
 99:         GUI_EXIT,
100: 
101:         DATA
102:     } cmd;
103: 
104:     union {
105:         char* data;
106:         char c;
107:     };
108: 
109: } GuiCommand;
110: 
111: struct GuiThreadParams {
112:     Producer* input_rb;
113:     Consumer* command_rb;
114: };
115: 
116: // helper function to fascilitate printing and otherwise controlling the GUI
117: // from other threads
118: //
119: // for the window refresh command, an optional string can be provided to refresh
120: // specific windows.
121: //
122: //   's' = status
123: //   'h' = help
124: //   'c' = command
125: //   'S' = status border
126: //   'H' = help border
127: //
128: // multiple windows can be refreshed in the same command string, for example,
129: // all the following are valid:
130: //
131: //   "shc"
132: //   "Ss"
133: //   "SH"
134: //
135: // one thing to note is that all three of the following will refresh all of the
136: // windows in one command:
137: //
138: //   "shcSH"
139: //   ""
140: //   NULL
141: //
142: void gui_command(Producer* gui_cmd, enum GuiControl ctrl, char* data);
143: 
144: // neat function to package and send a GUI command, however, this function will
145: // copy the data in the data string instead of passing a pointer.
146: void gui_print_cmd(Producer* gui_cmd, enum GuiControl ctrl, char* data);
147: 
148: 
149: 
150: void gui_thread(struct GuiThreadParams* params);
151: 
152: #endif // GUI_H_
gui.c
  1: #include <curses.h>
  2: #include <fcntl.h>
  3: #include <gui.h>
  4: #include <ncurses.h>
  5: #include <stdio.h>
  6: #include <string.h>
  7: #include <pthread.h>
  8: #include <termios.h>
  9: #include <unistd.h>
 10: 
 11: Gui gui_init() {
 12:     Gui wins;
 13:     #define CMD_WIN_HEIGHT 3
 14: 
 15:     // set terminal settings
 16:     initscr();
 17:     raw();
 18:     refresh();
 19: 
 20:     int xpos, ypos, width, height;
 21:     // place cmd window at bottom of screen
 22:     height = CMD_WIN_HEIGHT;
 23:     width = COLS;
 24:     ypos = (LINES - height);
 25:     xpos = 0;
 26: 
 27:     wins.cmd = newwin(height, width, ypos, xpos);
 28:     box(wins.cmd, 0,0);
 29:     mvwprintw(wins.cmd, 0, 2, "[Command Input]");
 30:     mvwprintw(wins.cmd, 1, 1, COMMAND_PROMPT);
 31: 
 32:     // place help at top-left, taking half the screen
 33:     height = LINES - CMD_WIN_HEIGHT;
 34:     width = COLS / 2;
 35:     ypos = 0;
 36:     xpos = 0;
 37: 
 38:     wins.help_master = newwin(height, width, ypos, xpos);
 39:     wins.help = newwin(height-2, width-2, ypos+1, xpos+1);
 40:     help_set_title(&wins, "Default");
 41: 
 42: 
 43:     // place status at top-right, taking half the screen
 44:     height = LINES - CMD_WIN_HEIGHT;
 45:     width = COLS / 2;
 46:     ypos = 0;
 47:     xpos = COLS / 2;
 48: 
 49:     wins.status_master = newwin(height, width, ypos, xpos);
 50:     wins.status = newwin(height-2, width-2, ypos+1, xpos+1);
 51:     status_set_title(&wins, "Default");
 52: 
 53: 
 54:     wrefresh(wins.cmd);
 55:     wrefresh(wins.help_master);
 56:     wrefresh(wins.help);
 57:     wrefresh(wins.status_master);
 58:     wrefresh(wins.status);
 59: 
 60:     return wins;
 61: }
 62: 
 63: void gui_del(Gui* gui) {
 64:     delwin(gui->cmd);
 65:     delwin(gui->status);
 66:     delwin(gui->help);
 67: 
 68:     clear();
 69:     refresh();
 70: 
 71:     endwin();
 72: }
 73: 
 74: void window_title_helper(WINDOW* win, char* winname, char* title) {
 75:     // redraw the border.
 76:     box(win, 0,0);
 77: 
 78:     mvwprintw(win, 0, 2, "[");
 79: 
 80:     // make the window name bold
 81:     wattron(win, A_BOLD);
 82:     mvwprintw(win, 0, 3, "%s", winname);
 83:     wattroff(win, A_BOLD);
 84: 
 85:     // print the custom name. it shouldn't be bold.
 86:     mvwprintw(win, 0, 3+strlen(winname), ": %s]", title);
 87: }
 88: 
 89: char get_input(Gui* wins) {
 90:     mvwprintw(wins->cmd, 1, 1, COMMAND_PROMPT);
 91:     wrefresh(wins->cmd);
 92:     char c = mvwgetch(wins->cmd, 1, strlen(COMMAND_PROMPT) + 1);
 93:     wrefresh(wins->cmd);
 94:     return c;
 95: }
 96: 
 97: void status_set_title(Gui* wins, char *title) {
 98:     window_title_helper(wins->status_master, "Status", title);
 99:     wins->status_title = title;
100: 
101:     wrefresh(wins->status_master);
102: }
103: void status_clear(Gui* wins) {
104:     wclear(wins->status);
105: 
106:     wrefresh(wins->status);
107: 
108:     wins->status_x = 0;
109:     wins->status_y = 0;
110: }
111: void help_set_title(Gui* wins, char* title) {
112:     window_title_helper(wins->help_master, "Help", title);
113:     wins->help_title = title;
114: 
115:     wrefresh(wins->help_master);
116: }
117: 
118: void help_clear(Gui* wins) {
119:     wclear(wins->help);
120:     wrefresh(wins->help);
121: 
122:     wins->help_x = 0;
123:     wins->help_y = 0;
124: }
125: 
126: void wprintw_file(WINDOW* win, char* filename) {
127:     FILE* file = fopen(filename, "r");
128: 
129:     // attempt to open file, print error if fail
130:     if (file == NULL) {
131:         wprintw(win, "No help file found at: \"%s\"", filename);
132:         return;
133:     }
134: 
135:     // read the help text file
136:     char buf[513] = {0};
137:     int bytes_read;
138:     while ((bytes_read = fread(buf, sizeof(char), 512, file)) != 0) {
139:         // null terminate the position after the last byte.
140:         buf[bytes_read] = '\0';
141:         wprintw(win, "%s", buf);
142:     }
143: 
144:     fclose(file);
145: }
146: 
147: // neat function to package and send a GUI command
148: void gui_command(Producer* gui_cmd, enum GuiControl ctrl, char* data) {
149:     // if the ringbuf is full, wait until it isn't
150:     while (producer_push(gui_cmd, &(GuiCommand){ctrl, data}) == -1);
151: }
152: 
153: // neat function to package and send a GUI command, however, this function will
154: // copy the data in the data string instead of passing a pointer.
155: void gui_print_cmd(Producer* gui_cmd, enum GuiControl ctrl, char* data) {
156:     while (producer_push(gui_cmd, &(GuiCommand){ctrl, NULL}) == -1);
157: 
158:     while (*data != '\0') {
159:         GuiCommand cmd;
160:         cmd.cmd = DATA;
161:         cmd.c = *(data++); // assign and increment
162: 
163:         // if the ringbuf is full, wait until it isn't.
164:         while (producer_push(gui_cmd, &cmd) == -1);
165:     }
166: }
167: 
168: void get_string_helper(Consumer* command_rb, void (*print)(char*)) {
169:     char data[512] = {};
170:     char* data_p = data;
171:     GuiCommand* cmd;
172:     while (true) {
173:         cmd = consumer_peek(command_rb);
174: 
175:         // keep checking for more data
176:         if (cmd == NULL) {
177:             continue;
178:         // if the next command isn't data, end the loop.
179:         } else if (cmd->cmd != DATA) {
180:             break;
181:         }
182: 
183:         consumer_pop(command_rb);
184: 
185:         // set and increment the data
186:         *(data_p++) = cmd->c;
187: 
188:         if (data_p >= data+511) {
189:             *data_p = '\0'; // ensure string is null-terminated
190:             (*print)(data); // print the string using the provided function
191:             data_p = data; // reset the pointer
192:         }
193:     }
194: 
195:     // print the remaining data in the the buffer
196:     *data_p = '\0'; // ensure the string is null-terminated
197:     (*print)(data); // print the string using the provided function
198: 
199:     return;
200: }
201: 
202: void gui_thread(struct GuiThreadParams* params) {
203:     Producer* input_rb = params->input_rb;
204:     Consumer* command_rb = params->command_rb;
205: 
206:     Gui gui = gui_init();
207:     nodelay(gui.cmd, true);
208: 
209:     FILE* elog = fopen("gui_error_log.txt", "w");
210: 
211:     // get the total elapsed time from the monotonic epoch (arbitrary)
212:     struct timespec elapsed_time;
213:     clock_gettime(CLOCK_MONOTONIC, &elapsed_time);
214: 
215:     // ms precision
216:     long last_refresh = elapsed_time.tv_sec*1000 + elapsed_time.tv_nsec / 1000000;
217: 
218:     // this limits the amount of times a refresh can be conducted per second.
219:     // there are 5 windows, each window has a field.
220:     bool refresh_scheduled[5] = {false};
221:     bool exit = false;
222:     bool term_mode = false;
223:     while (!exit) {
224:         struct GuiCommand* cmd = consumer_pop(command_rb);
225: 
226:         if (cmd != NULL) {
227:             bool throw_cmd = term_mode && (cmd->cmd == REFRESH
228:                                            || cmd->cmd == PRINT_HELP
229:                                            || cmd->cmd == PRINT_FILE_HELP
230:                                            || cmd->cmd == PRINT_STATUS
231:                                            || cmd->cmd == SET_TITLE_HELP
232:                                            || cmd->cmd == SET_TITLE_STATUS
233:                                            || cmd->cmd == CLEAR_HELP
234:                                            || cmd->cmd == CLEAR_STATUS
235:                                            || cmd->cmd == REFRESH
236:                                            || cmd->cmd == CMD_ECHO_ON
237:                                            || cmd->cmd == CMD_ECHO_OFF);
238: 
239:             // if any of the above commands are recieved in terminal mode,
240:             // the command must be thrown out, because they are attempting to
241:             // modify a context which doesn't exist.
242:             if (throw_cmd) {
243:                 continue;
244:             }
245: 
246:             switch (cmd->cmd) {
247:             case PRINT_HELP :
248:             {
249:                 void print (char* data) {
250:                     mvwprintw(gui.help, gui.help_y, gui.help_x, "%s", data);
251:                 }
252: 
253:                 if (cmd->data == NULL) {
254:                     get_string_helper(command_rb, &print);
255:                 } else {
256:                     mvwprintw(gui.help, gui.help_y, gui.help_x, "%s", cmd->data);
257:                 }
258: 
259:                 getyx(gui.help, gui.help_y, gui.help_x);
260:                 break;
261:             }
262:             case PRINT_FILE_HELP :
263:                 wprintw_file(gui.help, cmd->data);
264:                 break;
265:             case PRINT_STATUS :
266:             {
267:                 void print (char* data) {
268:                     mvwprintw(gui.status, gui.status_y, gui.status_x, "%s", data);
269:                 }
270: 
271:                 if (cmd->data == NULL) {
272:                     get_string_helper(command_rb, &print);
273:                 } else {
274:                     mvwprintw(gui.status, gui.status_y, gui.status_x, "%s", cmd->data);
275:                 }
276: 
277:                 //fprintf(elog, "printing to status...\n");
278: 
279:                 getyx(gui.status, gui.status_y, gui.status_x);
280:                 break;
281:             }
282:             case TERMINAL_ENTER:
283:             {
284:                 // stop the fancy GUI, and
285:                 gui_del(&gui);
286:                 term_mode = true;
287: 
288:                 break;
289:             }
290:             case TERMINAL_PRINT :
291:             {
292:                 void print (char* data) {
293:                     fprintf(stdout, "%s", data);
294:                 }
295: 
296:                 if (cmd->data == NULL) {
297:                     get_string_helper(command_rb, &print);
298:                 } else {
299:                     printf("%s", cmd->data);
300:                 }
301: 
302:                 fflush(stdout);
303:                 break;
304:             }
305:             case TERMINAL_RETURN :
306:             {
307:                 // reinit the GUI, and update the windows in the old structure.
308:                 Gui new_gui = gui_init();
309:                 nodelay(gui.cmd, true);
310: 
311:                 gui.cmd = new_gui.cmd;
312:                 gui.help = new_gui.help;
313:                 gui.status = new_gui.status;
314:                 gui.help_master = new_gui.help_master;
315:                 gui.help_master = new_gui.help_master;
316: 
317:                 help_set_title(&gui, gui.help_title);
318:                 status_set_title(&gui, gui.status_title);
319: 
320:                 term_mode = false;
321: 
322:                 // schedule refresh for everything.
323:                 for (int p = 0; p < 5; p++)
324:                     refresh_scheduled[p] = true;
325: 
326:                 break;
327:             }
328:             case SET_TITLE_HELP :
329:                 help_set_title(&gui, cmd->data);
330:                 break;
331:             case SET_TITLE_STATUS :
332:                 status_set_title(&gui, cmd->data);
333:                 break;
334:             case CLEAR_STATUS :
335:                 wclear(gui.status);
336: 
337:                 gui.status_y = 0;
338:                 gui.status_x = 0;
339:                 break;
340:             case CLEAR_HELP :
341:                 wclear(gui.help);
342: 
343:                 gui.help_y = 0;
344:                 gui.help_x = 0;
345:                 break;
346:             case REFRESH :
347:                 if (cmd->data == NULL || strlen(cmd->data) == 0) {
348:                     // if the user doesn't provide a string specifier, or
349:                     // if they provide an empty string, refresh all windows.
350:                     for (int p = 0; p < 5; p++)
351:                         refresh_scheduled[p] = true;
352:                 }
353: 
354:                 if (strstr(cmd->data, "s") != NULL)
355:                     refresh_scheduled[0] = true;
356: 
357:                 if (strstr(cmd->data, "h") != NULL)
358:                     refresh_scheduled[1] = true;
359: 
360:                 if (strstr(cmd->data, "c") != NULL)
361:                     refresh_scheduled[2] = true;
362: 
363:                 if (strstr(cmd->data, "S") != NULL)
364:                     refresh_scheduled[3] = true;
365: 
366:                 if (strstr(cmd->data, "H") != NULL)
367:                     refresh_scheduled[4] = true;
368: 
369:                 break;
370:             case CMD_ECHO_ON :
371:                 break;
372:             case CMD_ECHO_OFF :
373:                 break;
374:             case GUI_EXIT :
375:                 exit = true;
376:                 break;
377: 
378:             case DATA :
379:                 printf("\\033[31m%c\\033[0m", cmd->c);
380:                 break;
381:             }
382:         }
383: 
384:         // we don't need to refresh or anything if we are in term mode, but we do
385:         // still need to get input.
386:         if ( term_mode ) {
387:             int c = 0;
388: 
389:             int flags = fcntl(0, F_GETFL, 0);
390:             fcntl(0, F_SETFL, flags | O_NONBLOCK);
391: 
392:             if (read(0, &c, 1) < 0)
393:                 continue;
394: 
395:             producer_push(input_rb, &c);
396:             continue;
397:         }
398: 
399:         struct timespec elapsed_time;
400:         clock_gettime(CLOCK_MONOTONIC, &elapsed_time);
401:         long current_time = elapsed_time.tv_sec*1000 + elapsed_time.tv_nsec / 1000000;
402: 
403:         // get input and push it into the input buffer
404:         int c = mvwgetch(gui.cmd, 1, 7);
405:         if (c != ERR) {
406:             producer_push(input_rb, &c);
407:         }
408: 
409:         // dont refresh more often than 4 times per second.
410:         if (current_time - last_refresh >= 250) {
411:             // only refresh the windows which were scheduled
412:             if (refresh_scheduled[0])
413:                 wrefresh(gui.status);
414: 
415:             if (refresh_scheduled[1])
416:                 wrefresh(gui.help);
417: 
418:             if (refresh_scheduled[2])
419:                 wrefresh(gui.cmd);
420: 
421:             if (refresh_scheduled[3])
422:                 wrefresh(gui.status_master);
423: 
424:             if (refresh_scheduled[4])
425:                 wrefresh(gui.help_master);
426: 
427:             last_refresh = current_time;
428: 
429:             // reset trackers
430:             for (int p = 0; p < 5; p++)
431:                 refresh_scheduled[p] = false;
432: 
433:         }
434:     }
435: 
436:     // don't need to delete the GUI if in term mode, since it is already deleted.
437:     if (!term_mode)
438:         gui_del(&gui);
439: 
440:     pthread_exit(NULL);
441: }

Manual Driving Mode

One of the first things we designed the car to do was to drive around manually using keyboard commands. The exact method of achieving this didn’t matter, as long as some form of wireless communication was used. My preferred method was SSH since it allowed me to control the robot and edit/run the program.

I conducted all development over ssh. I would write the code on my desktop (or laptop sometimes), and then copy it over ssh to my robot. After about a week of doing this, I decided to write some scripts to help me upload and run the code:

#!/usr/bin/env sh

echo "tar'ing the directory..."
tar czvf code.tar ${1}/{Makefile,help_files/*,src/*,include/*}
echo "scp'ing the tarball..."
scp code.tar pi@192.168.179.151:~/

I had another script on the raspberry pi that would compile and run the program for me. This workflow worked well since I could compile and run the code with only two commands from my computer. My robot would be across the room, and I could change the code from my desk, drive it to where it needed to be using manual mode, then start the part of the program I was testing.

Line Follow Mode (IR sensor)

The next big thing we worked on was adding a mode that would allow the robot to follow a line using an IR sensor mounted on the front of the robot. I don’t have any videos from this part of the demonstration, unfortunately.

IMU Sensor Balance Board

Our task was to make the robot balance on a clipboard with a pen (or any otherwise cylindrical object) glued to the bottom. We attached an IMU (Inertial Measurement Unit) sensor to the robot, which recorded the rotation and acceleration of the car.

Since the data from the sensor was pretty noisy, we had to use filtering to make it usable. I did a lot of research into this and decided that a complementary filter would be most suitable for this task. Most students used their IMU data as the input to a bang-bang controller or some variation of it. I went another route and used a PID controller, and was the closest to getting the robot to balance on the board.

Even though I think that the task was ultimately impossible, I enjoyed this assignment because I learned so much about PID controllers. Professor Choi told me that my robot came the closest to balancing, which was a huge win in my book. Late one night while I was working on the robot, I did film a video of it trying and failing (yet coming so close!) to balance.

Line Follow Mode (Computer Vision)

This final part of the project required us to use computer vision to

  1. Track a laser pointer
  2. Follow a black line on a whiteboard

We used a USB camera and did all the image processing on the CPU. I tried to optimize my program as much as I could, but this was certainly at the limits of what can be accomplished with the CPU on the Raspberry Pi. My program was able to process video at about 10 frames per second.

For the laser tracker, after applying a gamma function, I used a recursive algorithm to locate the location of the laser pointer. This seemed to work pretty well, though there were some oscillation issues due to the low frame rate. The car had trouble putting the laser in the center of the frame in the forward/backward axis.

35: // recursive function to find the area of an area whose pixels are brighter
36: // than thresh.
37: int find_neighbors(struct image_t* image, uint8_t thresh, int x, int y) {
38:     uint8_t c = image->row[y].column[x].r;
39: 
40:     if (c < thresh)
41:         return 0;
42: 
43:     // keep future iterations from revisiting this pixel
44:     image->row[y].column[x].r = 0;
45:     image->row[y].column[x].g = 0;
46:     image->row[y].column[x].b = 0;
47: 
48:     int num_neighbors = 1;
49:     if (x > 0)
50:         num_neighbors += find_neighbors(image, thresh, x-1, y);
51: 
52:     if (x < IMAGE_WIDTH-1)
53:         num_neighbors += find_neighbors(image, thresh, x+1, y);
54: 
55:     if (y > 0)
56:         num_neighbors += find_neighbors(image, thresh, x, y);
57: 
58:     if (y < IMAGE_HEIGHT-1)
59:         num_neighbors += find_neighbors(image, thresh, x, y+1);
60: 
61:     return num_neighbors;
62: }

Since the line had a much larger search area, the recursive algorithm was too slow. I ended up finding a simple solution. First, average the position of the pixels along the width of the frame. Then, navigate the robot so that this value approaches zero while continuing to move forward.

64: float avg_position(struct image_t* image) {
65:     int accumulator = 0;
66:     int mass = 1;
67:     for (int i = 0; i < IMAGE_WIDTH; i++) {
68:         int x = i % IMAGE_WIDTH;
69:         int y = IMAGE_HEIGHT-1;
70: 
71:         if (image->row[y].column[x].r == 0) {
72:             accumulator += x;
73:             mass += 1;
74:         }
75:     }
76: 
77:     float average = (float)accumulator / mass;
78: 
79:     return (average/IMAGE_WIDTH)*2 - 1.0;
80: }

Overall, the program ended up working fairly well. I recorded a video of it the night before the assignment was graded.

Programming

The projects I have listed below, along with others, can be found on my github.

Capabilities Overview

I currently know the following languages well enough that I would be comfortable working with them in a professional environment immediately:

  • C
  • C++
  • Rust
  • Assembly*
  • Python
  • Lisp**

*My experience is limited embedded systems (microcontrollers)
**I mostly use Emacs Lisp

I am confident I could pick up just about any language relatively quickly, I am currently in the process of learning haskell, which seems to be the hardest language I have touched so far. I have used many more languages in the past, but the ones I have listed I am most confident with.

Code Wars

Every once in a while, I like to spend some time working through programming challenges on the codewars website. I think that it is a great way to hone programming skills, and pick up new languages.

Codewars badge

Hack Chat

I developed this application during the Summer of 2022, while I was at AFIT learning about cyber warfare, network defense, and other topics. The idea is simple: multiple clients can enter a chat room on the server. I had great ideas for this project that never came to fruition, but I did implement the fundamental features. I even had the courtesy to leave it in a working state!

Since hackchat is working and written in Rust, you can try this for yourself by running the following code (assuming your machine has cargo installed):

$ git clone https://github.com/ethanxxxl/hackchat.git
$ cd hackchat
$ cargo run --bin server

You can run clients in a few more terminals. It will prompt you for an ip address, which is printed by the server.

$ cd hackchat
$ cargo run --bin client

An example of the program running and working is shown in the image below.

hackchat-demo.png

Note, that I am running this on Arch Linux. It may work on MacOS, but I’m pretty sure it won’t work on Windows.

Rust-Snake

This was the first real program I wrote using Rust; A rudimentary implementation of the classic game Snake. I learned a lot about the language from this project. Unfortunately, time has not been kind to this project. It is hosted on github, so you can peruse my code if you wish, but unfortunately, when I wrote this program, I didn’t have the forethought to create a binary, or even take pictures of the final product.

I did spend about 30 minutes trying to get it to work, but I was not willing to spend any more time on it.