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