Displaytech 64128A 64x128-pixel graphic LCD using SPI


This page has my working Forth code, including primitives assembled with my very simple Forth assembler, for using SPI to operate the above LCD which has the Samsung S6B1713 controller.  Later models, and some from other manufacturers, have different controllers, but the instruction sets are nearly identical.  I connected the LCD to my workbench computer by way of 65SIB (the very flexible 6502.org Serial Interface Bus which accommodates SPI and similar interfaces), and built the LCD onto one of the popular Radio Shack 276-168B prototyping boards, 7.1x9.5cm, along with a tiny (SO-8) 4Mx8 flash memory, meaning this module takes two 65SIB addresses.  (This file only shows the general-purpose 65SIB code and then the LCD code though, not code specific to the flash or of any of the other things I've operated on SPI.)

I might later write the code for forming text, as I did for my raster graphics on an analog oscilloscope which you can see here, and for displaying icons, reverse-videoing a block (like for highlighting a menu item), and who knows what else.  For now, I have the routines below to plot points or segments, with the option to turn the dots on, off, or XOR them with what was there before.  For arcs, doing them with a series of segments will be plenty good for a low-res display like this, so I don't think there's any need to go further with arcs.

Done for experiment purposes, here's a demo of using a random number generator to produce sets of 25 random segments then displaying the new screen memory contents:

As I was considering how to make it draw the segments between specified endpoints, I was delighted to notice that it could be done like direct digital synthesis (DDS) in a numerically controlled oscillator (NCO), so there's only one multiplication and one division to set up each segment, and no need for any more multiplications or divisions for the dots connecting the end points.  Wikipedia has an article on NCOs here.  I had done it several times in software to generate sine waves for work projects.  In this case, we don't need to cycle like to do a repetitive waveform, and we don't need to use the phase accumulator's higher bits (or any bits at all) as an index into a wave table, just use the phase accumulator itself as the output.

I don't really have any use for color, or even for grayscale.  I definitely won't be doing CAD or photo editing with this LCD, but it will be able to act as a low-res oscilloscope display or spectrum analyzer or other things where a graph is needed.  I might go for another size up for my next workbench computer, 240x128 dots.



To test Displaytech 64128A graphic LCD, Aug 2016

                      [B]               \ Numbers in this part are binary, not hex.
CREATE  INIT_65SIB  PRIMITIVE   ( -- )  \ Leaves 65SIB interrupts disabled.
        LDA#     11000000  C,           \ Make VIA3PA6 an output for disabling 65SIB IRQ,
        TSB_ABS  VIA3DDRA   ,           \ and  VIA3PA7 an output for 65SIB CONFIG\.

        LDA_ABS  VIA3PA     ,
        ORA#     10000000  C,           \ Output a 1 on CONFIG\ (bit 7)
        AND#     10111111  C,           \ and a 0 on bit 6 to disable 65SIB IRQs.
        STA_ABS  VIA3PA     ,

        LDA_ABS  VIA3DDRB   ,           \ Make VIA3PB6 an  input for MISO,
        ORA#     00011111  C,           \      VIA3PB4, 3, and 2 outputs to the '138 for CS\'s,
        AND#     10111111  C,           \      VIA3PB1 an output for MOSI, and
        STA_ABS  VIA3DDRB   ,           \      VIA3PB0 an output for CLK.

        LDA#     00011111  C,           \ Write 0's to the '138 to de-select all 65SIB devices,
        TRB_ABS  VIA3PB     ,           \ and to the clock and MOSI lines.
        JMP      NEXT       ,


CREATE  EN_65SIB_IRQ  PRIMITIVE    ( -- )     \ enable 65SIB interrups
        LDA#     01000000  C,
        TSB_ABS  VIA3PA     ,
        JMP      NEXT       ,


CREATE  NO_65SIB_IRQ  PRIMITIVE    ( -- )     \ disable 65SIB interrupts
        LDA#     01000000  C,
        TRB_ABS  VIA3PA     ,
        JMP      NEXT       ,
                      [H]


NO_65SIB_IRQ   INIT_65SIB


0 CONSTANT NONE                                 \ These assume the LCD/flash breadboard is the first 65SIB device.
1 CONSTANT LCD
2 CONSTANT FLASH

                      [B]
CREATE  SELECT  PRIMITIVE       ( n -- )        \ n must be in the range of 0-7.  No error-checking.
        LDA#     00011100  C,                   \ Syn:  FLASH SELECT  |  LCD SELECT  |  NONE SELECT
        TRB_ABS  VIA3PB     ,                   \ First, de-select anything else that might have been selected.
        LDA_ZP,X TOS_LO    C,
        ASL_A    ASL_A                          \ Shift left two bit places, to get the 0-7 into bits 2, 3, and 4.
        TSB_ABS  VIA3PB     ,
        JMP      POP        ,

CREATE  NONE_SELECT  PRIMITIVE  ( -- )          \ De-select anything that might have been selected.
        LDA#     00011100  C,
        TRB_ABS  VIA3PB     ,
        JMP      NEXT       ,
                      [H]                       \ Go back to hex.


\ There's no need to make the following IMMEDIATE, because they run when you're assembling, not when the compiler is on.

: clk-up        ( -- )          \ assembly macro        NOTE!  Accum is not preserved!
   LDA#          1 C,
   TSB_ABS  VIA3PB  ,   ;


: clk-dn        ( -- )          \ assembly macro        NOTE!  Accum is not preserved!
   LDA#          1 C,
   TRB_ABS  VIA3PB  ,   ;


: clk-lo-pulse  ( -- )          \ assembly macro        NOTE!  Clock must already be known to be high!
   DEC_ABS  VIA3PB  ,
   INC_ABS  VIA3PB  ,   ;


: clk-hi-pulse  ( -- )          \ assembly macro        NOTE!  Clock must already be known to be low!
   INC_ABS  VIA3PB  ,
   DEC_ABS  VIA3PB  ,   ;


: mosi-up       ( -- )          \ assembly macro        NOTE!  Accum is not preserved!
   LDA#          2 C,
   TSB_ABS  VIA3PB  ,   ;


: mosi-dn       ( -- )          \ assembly macro        NOTE!  Accum is not preserved!
   LDA#          2 C,
   TRB_ABS  VIA3PB  ,   ;


: config-up     ( -- )          \ assembly macro        NOTE!  Accum is not preserved!
   LDA#         80 C,
   TSB_ABS  VIA3PA  ,   ;


: config-dn     ( -- )          \ assembly macro        NOTE!  Accum is not preserved!
   LDA#         80 C,
   TRB_ABS  VIA3PA  ,   ;


\ =======================================================================================================

\ Now the stuff particular to the Displaytech 64128A 128x64 graphic LCD.  The first five words are copied
\ from C:\PIC-DEV\MPLAB\ACR\FLASH.FTH and adapted (for a negative clock pulse instead of a positive one):


                                            \ My original SEND_BYT.  There's a slightly improved version below.
\ CREATE  SEND_BYT  PRIMITIVE   ( b -- )    \ Device must already be selected.
\     clk-up                                \ (macro from above)  Leaves A=1.
\     INA                                   \ Turn the 1 into a 2, the value of the MOSI bit for TSB & TRB below.
\     LDY#  8  C,                           \ 8 is the number of bits we will shift out and test in the loop below.
\  HERE                                     \ Mark the top of the loop for branching back up to.
\     ASL_ZP,X  TOS_LO  C,    BCS 1$        \ If the bit was a 1, branch.
\     TRB_ABS   VIA3PB   ,    BRA 2$        \ If the bit was a 0, clear the MOSI bit in VIA3PB and skip the next instr.
\ 1$: TSB_ABS   VIA3PB   ,                  \ If the bit was a 1,  set  the MOSI bit in VIA3PB.
\ 2$: clk-lo-pulse                          \ Pulse the clock line.  The DEC/INC does not affect A.
\     DEY                     BNE GO_BACK   \ Decr the counter and see if we need to repeat the loop.
\     JMP POP ,                             \ When finished, DROP the input from the stack.
\                                           \ (The BNE GO_BACK branches back to the HERE a few lines above.)


\ Here below is an alternate way to send the byte.  It uses a trick suggested by Jeff Laughton to make it just a tad
\ faster.  Rather than using Y as a loop counter, it sets C then does a pre-rotate.  The 1 from the carry flag and the
\ later shifts mean that after each shift you can branch on whether there's 0 in the number or not.  When (and only when)
\ it's 0, you've completed 8 shifts.  Most dots will be white, the this gets through the loop 2 cycles faster on white.

CREATE  SEND_BYT  PRIMITIVE   ( b -- )    \ Device must already be selected.
    clk-up                                \ (macro from above)  Leaves A=1.
    INA                                   \ Turn the 1 into a 2, the value of the MOSI bit for TSB & TRB below.
    SEC                                   \ We rotate C in at the next line as a finish flag.  Later as we shift left,
    ROL_ZP,X  TOS_LO  C,                  \ this becomes the last "1" bit; so when it's out and TOS_LO=0, we're done.
 HERE                       BCC 1$        \ Mark the top of the loop for branching back up to.
    TSB_ABS   VIA3PB   ,    BRA 2$        \ If the bit was a 1, set the MOSI bit in VIA3PB and skip the next instruction.
1$: TRB_ABS   VIA3PB   ,                  \ If the bit was a 0, clear the MOSI bit in VIA3PB.
2$: clk-lo-pulse                          \ Pulse the clock line.  The DEC/INC does not affect A or C.
    ASL_ZP,X  TOS_LO  C,    BNE GO_BACK   \ Shift next bit into C and also see if we need to repeat the loop.
    JMP POP ,                             \ When finished, DROP the input from the stack.
                                          \ (The BNE GO_BACK branches back to the HERE a few lines above.)



\ RCV_BYT is omitted, as the LCD doesn't send any data of any kind in serial mode.


CREATE  CLK_DN   PRIMITIVE  ( -- )  clk-dn     JMP NEXT ,   \ The words in lower-case in these four lines are macros.
CREATE  CLK_UP   PRIMITIVE  ( -- )  clk-up     JMP NEXT ,
CREATE  CONF_DN  PRIMITIVE  ( -- )  config-dn  JMP NEXT ,
CREATE  CONF_UP  PRIMITIVE  ( -- )  config-up  JMP NEXT ,


: INST_FACTOR           ( b -- )                        \ This is a factor for single-byte instructions below.
   CONF_DN
   CLK_UP        LCD SELECT
   SEND_BYT     NONE_SELECT        ;

                                                        \        Start with device not selected for all of these.
: BIAS1/9          ( -- )   A2 INST_FACTOR      ;       \ This 1/9 bias initially seems to be the right one for this LCD.
: BIAS1/7          ( -- )   A3 INST_FACTOR      ;       \ Try both biases out tho, and see what you think.


: FWD_DIR          ( -- )   A0 INST_FACTOR      ;       \ I think FWD_DIR is normal, and
: REV_DIR          ( -- )   A1 INST_FACTOR      ;       \ REV_DIR makes it readable in a mirror, but check it out.


: FWD_DIR2         ( -- )   C0 INST_FACTOR      ;       \ FWD_DIR2 and REV_DIR2 are like FWD_DIR & REV_DIR above but
: REV_DIR2         ( -- )   C8 INST_FACTOR      ;       \ for dot rows instead of columns.


: PWR_UP_SEQ       ( -- )                               \ Power-up sequence, used in the reset routine.
                WAIT1ms     2C INST_FACTOR              \ Turn on internal voltage converter circuit.
                WAIT1ms     2E INST_FACTOR              \ Turn on internal voltage regulator.
                WAIT1ms     2F INST_FACTOR      ;       \ Turn on internal voltage follower.


: REG_RES_SEL               25 INST_FACTOR      ;       \ Regulator resistor select.  ??  For reset routine.


: VIEW_ANGLE     ( n -- )                               \ In this version, select stays true between bytes.
   CONF_DN   CLK_UP   LCD SELECT
   81   SEND_BYT                                        \ This is a two-byte instruction.  First you set it in "reference
        SEND_BYT     NONE_SELECT                ;       \ voltage" mode, then tell it the viewing angle, 0 to 3F.


: 1st_DISP_LN    ( n -- ) 40 + INST_FACTOR      ;       \ Input range 0-3F.  For vertical scrolling.


: SET_COL_ADR    ( n -- )                               \ Column addr must be in the range of 0-131 decimal.
   CONF_DN   CLK_UP                LCD SELECT           \ This version, w/o de-selecting between bytes, might work
   DUP -4 SHIFT 10 OR   SEND_BYT                        \ better than the first try.  Most-significant nybble,
                0F AND  SEND_BYT  NONE_SELECT   ;       \ then least-significant.


: SET_PAGE_ADR   ( n -- )                               \ "Page" is the line of 8 dot rows.
              07 AND   B0 OR   INST_FACTOR      ;


: POS_DISP         ( -- )   A6 INST_FACTOR      ;
: NEG_DISP         ( -- )   A7 INST_FACTOR      ;       \ For swapping white and black.


: DISP_ON          ( -- )   AF INST_FACTOR      ;       \ Turn the display on!
: ENT_DISP_ON      ( -- )   A5 INST_FACTOR      ;       \ This turns entire display, all dots, black.
: ENT_DISP_NORM    ( -- )   A4 INST_FACTOR      ;       \ This undoes ENT_DISP_ON.


: VID_NOP          ( -- )   E3 INST_FACTOR      ;       \ No-operation.  (Why did they include this in the set?)


[D] 128 64 8 / * [H]  VAR  VID_MEM      \ "Video" memory.  Make array for 128 columns times 64 rows, 1KB total.
                                        \ Each byte does a vertical set of 8 pixels, not horizontal (hence the "pages").
                                        \ Most-significant bit is on the bottom.

: DISP_SCR  ( -- )                      \ Display screen.  Shoot the entire video memory array out to the LCD.
   8 0                                  \ Later I'll probably write ones for displaying only the changed pages.
   DO I SET_PAGE_ADR                    \ "Page" is a strip of 8 dot rows.  There are 8 "pages."
      0 SET_COL_ADR
 \    0 1st_DISP_LN
      CONF_UP    LCD  SELECT            \ Get ready to send data.  (CONF_UP sets RS high.  Yel 65SIB LED will be off.)
      I   7 SHIFT   VID_MEM +
      DUP  [D] 128 [H] +  SWAP          \ The 128 in this line is in decimal, not hex.
      DO   I C@ SEND_BYT
      LOOP       NONE_SELECT
   LOOP     ;


: INIT-SIB
   NO_65SIB_IRQ  INIT_65SIB  NONE_SELECT  ;

' INIT-SIB  INIT-AP  !        \ If I crash the computer and choose not to re-load anything, INIT-AP has the CFA of
                              \ the setup words to run immediately after reset, so I can recover in a couple of seconds.


INIT-SIB          \ Set up the LCD.  Let these lines execute during compilation.
BIAS1/9
FWD_DIR
FWD_DIR2
PWR_UP_SEQ
REG_RES_SEL
20 VIEW_ANGLE     \ 3F is max.  0 in min, and is invisible.
0  1st_DISP_LN
0  SET_PAGE_ADR
0  SET_COL_ADR
DISP_ON


: CLS   ( -- )  VID_MEM  400  ERASE     ;       \ Clear 1K screen memory.  To update the display, follow it with DISP_SCR.


\ : PIX_FACTOR    ( X Y -- BytAdr BitMask )     \ Pixel factor, commented out because it's re-written below as a primitive.
\    3F AND SWAP 7F AND SWAP      \ Start with limits, to prevent crashes.
\    3F SWAP -                    \ Since Y min is bottom of screen, reverse the input.   ^ LCDcol#  LCDRow#
\    DUP -3 SHIFT (  8 / )        \ Get LCD page# (range 0-7).                            ^ LCDcol#  LCDrow#  page#
\         7 SHIFT ( 80 * )        \ Get how far to go into VID_MEM for right page begin.  ^ LCDcol#  LCDrow#  pageBaseAdr
\    ROT      +                   \ Get byte number in VID_MEM.                           ^ LCDrow#  Byt#inArray
\    VID_MEM  +      SWAP         \ Get the addr of the actual byte to affect in VID_MEM. ^ BytAdr   LCDrow#
\    7 AND                        \ We already have the page#, ie, which set of 8 dot rows, so get row in page.
\    1 SWAP SHIFT         ;       \ Get the bit mask.                                     ^ BytAdr   BitMask



                                \ Used in forming segments, using a method like direct digital synthesis to calc dots:
WSIZE  VAR  SEG_ACCUM           \ Instead of slowing things down w/ a lot of mult and div, we'll do the slope like
WSIZE  VAR  SEG_SLOPE           \ direct digital synthesis of a sawtooth wave!  It's straight, so we don't need a table!


HERE   1 C,  2 C,  4 C,  8 C,  10 C,  20 C,  40 C,  80 C,       CONSTANT  PIX_TBL


 \ PIX_FACTOR as a primitive below has been streamlined quite a bit for assembly, so it is no longer a 1:1 translation of
 \ the secondary.  N was used to replace the adding and deleting of another stack cell as well as some of the data-moving.
 \ A DEX DEX was removed from just after the AND# F8 in the rough translation of line 3 of the secondary, and the INX INX
 \ was removed from just before the CLC of line 6's translation.


CREATE  PIX_FACTOR  PRIMITIVE
   LDA_ZP,X  TOS_LO  C,   AND#  3F  C,   STA_ZP,X  TOS_LO  C,   STZ_ZP,X  TOS_HI  C,
   LDA_ZP,X  NOS_LO  C,   AND#  7F  C,   STA_ZP,X  NOS_LO  C,   STZ_ZP,X  NOS_HI  C,


   LDA#  3F C,  SEC   SBC_ZP,X  TOS_LO  C,   STA_ZP,X  TOS_LO  C,       \ TOS_HI remains at 0.          ^ LCDcol# LCDrow#
                                                                        \ LCDrow# is 3F max (6 bits, bits 5:0.)

 \ instead of -3 SHIFT (line 3 of secondary):
        AND#    F8        C,    \ Significant bits now:  00XX X000
        STZ_ZP  N  1+     C,    \ N, N+1 are used in place of another stack cell on top.
                                \ ^ LCDcol#  LCDrow#            Page number * 8 hi byte is in N+1, lo byte in A.


                                \ Get how far to go into VID_MEM for start of right page.  (A page is a set of 8 dot rows.)
 \ and 7 SHIFT (line 4 of secondary):
        ASL_A   ASL_A    ASL_A  ROL_ZP  N 1+  C,    ASL_A  ROL_ZP  N 1+  C,    ( STA_ZP  N  C, not necessary )
                                \ Now the result of line 4 is in A and N+1; and:   ^ LCDcol#  LCDrow#


 \ ROT + (w/o complete ROT)  (line 5 of secondary):
        CLC                     \ A already has the low byte of how far to go into VID_MEM for the right page beginning.
        ADC_ZP,X  NOS_LO  C,    \ Add a single-byte column number to the offset into VID_MEM for the start of the page.
                                \ Leave it in A for further down, start of line 6.

        BCC  1$                 \ If that made the high byte carry (because VID_MEM did not start on a «-page boundary),
        INC_ZP    N  1+   C,    \ then increment the high byte.
  1$:
        LDY_ZP,X  TOS_LO  C,    \ This is part of the ROT.  We don't need to do the whole thing tho.
        STY_ZP    N 2+    C,    \ We will use N+2 to hold the LCD row #.  Quicker than NOS_LO
                                \ NOS_HI was already 0'ed above.  NOS contains LCDrow#, 0-7F.
 \ line 6 of secondary:
        CLC                     \ A & N+1 contain the offset into VID_MEM.
        ADC#      VID_MEM C,    \ low byte of secondary's line 6
        STA_ZP,X  NOS_LO  C,

        LDA_ZP    N 1+    C,
        ADC#  VID_MEM ><  C,    \ followed by high byte.  Here we get the completed actual byte of the addr in VID_MEM.
        STA_ZP,X  NOS_HI  C,    \ (We'll just put it in NOS_HI now since we don't need previous contents.  Then we
                                \ won't need to transfer it at the end.
 \ line 7 of secondary:
        LDA_ZP    N 2+    C,    \ Get LCD row #.
        AND#      7       C,    \ Look at only the row # in the particular page, 0-7.
        TAY
        LDA,Y     PIX_TBL  ,    \ We don't need to OR it with the previous value.  The calling routine will do that.
        STA_ZP,X  TOS_LO  C,    \ TOS_HI already zeroed above.

        CLC
        LDA_ABS  SEG_ACCUM    , \ Getting these two done here removes  SEG_SLOPE @  SEG_ACCUM +!  from the segment-
        ADC_ABS  SEG_SLOPE    , \ forming word below, and speeds it up.  It updates the phase accum for the next point.
        STA_ABS  SEG_ACCUM    ,

        LDA_ABS  SEG_ACCUM 1+ ,
        ADC_ABS  SEG_SLOPE 1+ ,
        STA_ABS  SEG_ACCUM 1+ ,

        JMP      NEXT         ,
  ( END-CODE )



: TRN_ON_PIX    ( X Y -- )      \ X range is 0-63, Y range is 0-127, 6 bits and 7 bits. ^ LCDcol#  viewRow#
   PIX_FACTOR                   \ Remember you have to do DISP_SCR to see the results.
   C_OR_BITS            ;


: TRN_OFF_PIX   ( X Y -- )
   PIX_FACTOR  FF XOR
   C_ANDBITS            ;


: XOR_PIX       ( X Y -- )
   PIX_FACTOR  OVER  C@         \ ^ BytAdr  BitMask  Byte
   XOR         SWAP  C! ;


WSIZE   VAR   PIX_INSTR         \ Pixel instruction.  Holds the CFA of either TRN_ON_PIX, TRN_OFF_PIX, or XOR_PIX above.
' TRN_ON_PIX  PIX_INSTR  !      \ Initialize a default.


: PIXEL         ( X Y -- )      \ Variable PIX must already hold the desired effect.  The reason not to
   PIX_INSTR  PERFORM   ;       \ integrate PIX_FACTOR here is that you might want to use the above w/o PIXEL.



: SEGMENT  ( X1 Y1 X2 Y2 -- )   \ X's and Y's must be within the display area, 0<=Y<=3F, and 0<=X<=7F.
                                \ PIX_INSTR should already be set for on, off, of XOR.  "On" is the default.
   2OVER 2OVER D=               \ Is the end point the same as the beginning point?
   IF  2DROP
       PIXEL                    \ If so, just do the pixel directly so you don't have /0 situation below.
       EXIT
   THEN
                                \ ^ X1 Y1 X2 Y2  (the same as you started with)
   OVER  4 PICK <               \ Is X2<X1?  If so, swap the ends, to
   IF 2SWAP THEN                \ make sure we plot left to right.  (I hope that will make it easier.)         { 4 }

   2OVER  2OVER                 \ Make a copy of the coordinates, to find the slope next.                      { 8 }
   ROT  -     -ROT              \ Get deltaY.  Neg result means it slopes downward.  Put it under the X's.     { 7 }
   SWAP -     SWAP              \ Get deltaX.  We made sure above that it will never be neg.  Now we have a rational, deltaX,deltaY.

   2DUP   ABS  <                \ Take abs val of Y.  Is the slope more vert than +/-45deg?
   IF                           \ If so, step 1 Y dot each time.  Else, later we'll step 1 X dot at a time.    { 6 }
                                \ ^ X1 Y1 X2 Y2 deltaX deltaY  (and deltaY is signed.)  0<=Y<=3F.  0<=X<=7F.
      100 SWAP */    ABS        \ Get deltaX/deltaY scaled by $100 (you'll get 256 for +/-45deg, less for steeper angles),
      SEG_SLOPE !               \ and store for the value to add to the phase accumulator.                ^ X1 Y1 X2 Y2
      NIP                       \ Now we won't need X2 anymore, since we'll arrive there automatically.   ^ X1 Y1 Y2
      ROT  ><   80 OR           \ Take the initial X val, scale it by $100, add $80 for rounding below,
      SEG_ACCUM !               \ and store it in the phase accumulator to start.                         ^ Y1 Y2

      2DUP  <                   \ Is Y1 < Y2, meaning pos slope?
      IF    1+ SWAP             \ If so, add 1 to the limit (for how LOOP works) and put them in normal DO order.
         DO SEG_ACCUM @         \ Loop once for each dot, going up.  Get the scaled accum contents,
            ><  FF AND          \ and de-scale and truncate to round to get the X value.
            I  PIXEL            \ Now we have X,Y, & we plot the point.  phase accum is updated in PIX_FACTOR primitive above.
         LOOP                   \ Go for next point moving up, until                                      ^ empty
         EXIT                   \ we're done.  (EXIT is better than IF-ELSE-THEN.)                        ^ empty
      THEN

      SWAP                      \ If it got here, Y1 >= Y2, meaning a 0 or neg slope.  Draw from top-left to bottom-right.
      DO SEG_ACCUM @            \ The index is the Y val.  Loop once for each dot, going down.  Get scaled accum contenst,
         >< FF AND              \ and de-scale and truncate to round to get the X value.
         I  PIXEL               \ Now we have X,Y, and we plot the point.                                 ^ empty
         -1                     \ (Remember we're going down this time, not up, hence the -1.
      +LOOP
      EXIT
   THEN
                                \ If it got here, the slope is NOT more vertical than 45deg, so we'll step 1 X dot each time.
                                \ ^ X1 Y1 X2 Y2 deltaX deltaY  (and deltaY is signed.)  0<=Y<=3F.  0<=X<=7F.
   100 ROT */                   \ Get deltaY/deltaX scaled by $100, signed,
   SEG_SLOPE !                  \ and store as the value to add to the phase accumulator.                 ^ X1 Y1 X2 Y2
   DROP                         \ Now we won't need Y2 anymore, since we'll arrive there automatically.   ^ X1 Y1 X2
   SWAP  ><   80 OR             \ Take the initial Y val, scale it by $100, add $80 for rounding below,
   SEG_ACCUM !                  \ and store it in the phase accumulator to start.                         ^ X1 X2
   1+  SWAP                     \                                                                         ^ X2+1  X1
   DO I                         \                                                                         ^ I
      SEG_ACCUM @               \ Loop once for each dot, going toward the right.  Get the scaled accum contents,      { 2 }
      ><  FF AND                \ and de-scale and truncate to round to get the Y value.                  ^ X Y
      PIXEL                     \ Now you have X (from the I above) and Y, and plot the point.            ^ empty
   LOOP                 ;



last updated Sep 7, 2016