Retrochallenge 2016/01:
Maze War for Olivetti M10 and NEC PC-8201A

Episode 4: LCD Talk

Provided with the mockup of the application from the previous episode, it's time to consider how to bring this properly onto the display.

NEC PC-8201A and LCD display interals

Exploring the LCD display …

The LCD — Technical Basics

Let's have a quick look at the technical details of the LCD display of the Kyocera Siblings.
In the PC-8201A Technical Manual (NEC Corporation, 1984), on page 154, we find the following, introductionary discription:

The LCD (LR-201C), full bit map screen which consists of 240 * 64 dots, displays 40 characters per line and 8 lines per screen. A character on the LCD consists of 6 by 8 pixels. The LCD is driven by 10 Segement Drivers (HD44102B) with 200 bytes Display RAM and 2 Common Drivers (HD44023b) [sic]. Segment Drivers are selected by Port A/B of PPI (81C55).

And, since an image says more than a thousand words (at least to those, who know to read it), this is what it looks like in the schematics (same for all original 1983-models; note that the upper row of segment drivers is physically flipped):

TRS-80 Model 100 LCD schematics

LCD schematics from TRS-80 Model 100 Technical Reference Manual; Tandy Corporation, 1983. (Click for a larger image.)

Note: Contrary to the description provided in "PC-8201A Technical Manual", the LCD PCB of the NEC PC-8201A reveals at closer inspection HD44102CH segement drivers and HD44103BLD common drivers, much like the schematics of the TRS-80 Model 100, shown above. This teardown of the TRS-80 Model 100 shows HD44102BH segment drivers and HD44103B common drivers. Another image, at Wikimedia, shows HD44102CH segement drivers and what looks like HD44103BLD common drivers for the TRS-80 Model 100, too. The various revision of the chips actually used were obviously subject to change. However, "HD44023b" for the common drivers in the "PC-8201A Technical Manual" is apparently a typo.
(Please be aware that I don't really know what I'm talking about, when it comes to electronics.)

And here is the real thing:

LCD board of the NEC PC-8201A

LCD board of the NEC PC-8201A.
Note the upper row of horizontal driver chips mounted upside down to facilitate a congruent layout of the PCB.

The Segment Drivers are also called horizontal LCD drivers on the TRS-80 Model 100 and the two Common Drivers vertical LCD drivers respectively. The 10 Segment Drivers are actually mapping the screen, while the Common Drivers are providing synchronisation. A full scan of the screen requires 14.3 milliseconds, resulting in a scan rate of 70 Hz. (Compare: Morgan, Christopher L., Hidden Powers of the TRS-80 Model 100. Plume/Waite 1984; p. 86.)

In praxis we're mainly talking to the 10 Segment Drivers. In order to do so, there are 4 channels of serial communication, namely

In order to make some meaning of this, here's a bit of background on the Segment Drivers: Each Segment IC or block contains 200 bytes of RAM to control a display area of 50 × 32 pixels. These blocks are arranged in horizontal rows across the screen, with each of them dedicated to a rectangular area of 50 pixels width and 32 pixels height, with the exception of the two right-most blocks (B5 and B10), which are controlling just a segment of 40 pixels width each:

 50px 50px 50px 50px 40px
| B1 | B2 | B3 | B4 | B5 | 32px
| B6 | B7 | B8 | B9 | B10| 32px

(Note: We may write to the 10 excess positions of B5 and B10, but this will be without visual effect. — Some secret RAM of 8 × 10 bytes hidden in the LCD drivers?)

Internally, each driver is organized into 4 rows or pages of 50 bytes. Each of theses bytes represents a vertical column of 8 pixels on the screen. So, in order to address a screen location, we'll have first to select a Segement Driver, then the appropriate page (numbered vertically 0 to 3) and the appropriate offset in this page (0 to 49 horizontally, left to right). The offset counter increments (or decrements, depending on the settings) automatically after each read/write operation. The offset counter loops around while incremented or decremented, the page counter isn't auto-incremented at all.

(Note: There's also an option to arrange the vertical mapping of the pages to display rows by adding a modal offset, but we won't make use of this here, rather assuming the default order of page 0 being the top-most row and page 3 being the 4th or bottom row.)


In order to address the 60th screen position on the 3rd row, we'll have to select block 2 first (since position 60 is located in the second block of 50 pixels and row 3 in the upper half of the display) and then select page 2 of this driver with an offset count of 9 (we start counting at 0). Then we may actually send a data byte to the driver, and, if the driver is ready again, proceed with the next data byte, in case we would wish to do so. BTW, data bytes are ordered vertically top-down, from bit 0 (lsb) at the top to bit 7 at the bottom, a 1 representing an activated pixel and a zero the respective pixel switched off.

pixel/         4 5                   6
column  0 1 .. 9 0 1 2 3 4 5 6 7 8 9 0 1 ...
row 1  | BLOCK 1 | BLOCK 2, page 0
row 2  |         | page 1
row 3  |         | page 2          X <--- block 2, page 2, offset 9
row 4  |         | page 3
row 5  | BLOCK 6 | BLOCK 7
row 6  |         |
row 7  |         |
row 8  |         |

Communicating with the LCD

As mentioned above, all communications are done over serial ports and we may actually use the BASIC commands IN, OUT, and INP with the appropriate port numbers.

Segement Driver Select

Segement Drivers (or blocks) are selected by ports 0xB9 and 0xBA (185d and 186d) as follows (compare the PC-8201A Technical Manual, p. 156):

  7   6   5   4   3   2   1   0     bit
|PA7|PA6|PA5|PA4|PA3|PA2|PA1|PA0|   OUT 0xB9
| X | X | X | X | X | X |PB1|PB0|   OUT 0xBA

0 = Not selected, 1 = Selected

PA0 - PB7 is associated to BLOCK1 - BLOCK8,
PBB0, PB1 to BLOCK9,10 respectively.

! Note: We may only select a single block at once!

(Update: Not a real problem in BASIC. But we really should read from just a single block at once.)

When writing to port 0xBA (186d) it's important not to overwrite bits 2–7, which contain vital status information of the machine. In fact, writing a zero into bit 4 will power off the machine! (Compare: Hidden Powers of the TRS-80 Model 100, p. 89. — Update: That is, when interrupts are on. It's safe to do so while interrupts are disabled.) So, we'll have to be careful to read this port first, mask its value with 11111100 (AND 252), and then OR it with PB0 + PB1 before writing this value back.

Example, Selecting block 2 (PA1) in BASIC:

10 CALL 30300 : REM disable interrupts (Model 100)
20 OUT 185, 2 : REM select PA1 (block 2)
30 B% = INP(186) AND 252 : REM read port 186 and mask it
40 OUT 186, B% OR 0 : REM 0 = no select for PB0 and PB1 (blocks 9 and 10)

The call at line 10 disables any interrupts in order to not to disturb the communications with the LCD. This will block any cursor blinking, keyboard input, the clock, and any external communications, as well.

LCD Commands (Port 0xFE)

There are 5 commands to control the selected Segment Driver:

Command descriptions are based, again, on the PC-8201A Technical Manual, (NEC Corporation, 1984).

Display On/Off

  7   6   5   4   3   2   1   0      bit
| 0 | 0 | 1 | 1 | 1 | 0 | 0 |DISP|   OUT 0xFE

DISP: 0 = Display Off, 1 = Display On

This is just switching the driver on or off and hasn't any effects on the display RAM.
The general form is "OUT 254, 56 OR D", where D is either 1 (on) or 0 (off).

Set Address Counter

This is the command used to select a position (see above) in the display driver's RAM:

  7   6   5   4   3   2   1   0     bit
|PG1|PG0|OF5|OF4|OF3|OF2|OF1|OF0|   OUT 0xFE

PG0,PG1: Page (0..3)
OF0-OF5: Offset (0..49)

The display RAM is divided into 4 pages (0 to 3) and each page contains 50 bytes (0 to 49).

The offset counter (in OF0..OF5) is automatically incremeted/decremented after any read/write operation. The page counter (in PG0, PG1) is not changed.

(Note: As already demonstrated in "Display On/Off", offset values of 50 and higher encode some other commands.)

Example, Selecting page 2, offset 9 in BASIC:

10 P = 2 : REM page 2 (row 3)
20 C = 9 : REM offset 9 (10th position)
30 OUT 254, (P * 64) OR C : REM send it to the selected driver

Set Starting Page

  7    6    5   4   3   2   1   0    bit
|SPG1|SPG0| 1 | 1 | 1 | 1 | 1 | 1 |  OUT 0xFE

SPG1 SPG0       Display Order of Pages
 0    0   ....  0  ->  1  -> 2  ->  3
 0    1   ....  1  ->  2  -> 3  ->  0
 1    0   ....  2  ->  3  -> 0  ->  1
 1    1   ....  3  ->  0  -> 1  ->  2

Not much to add here, SPG in bits 6 and 7 adds a modal offset to the display order of the pages.
The general form is "OUT 254, 63 OR (SP * 64)", where 0 >= SP >= 3.

Select Address Counter Mode

  7   6   5   4   3   2   1   0     bit
| 0 | 0 | 1 | 1 | 0 | 0 | 1 |U/D|   OUT 0xFE

U/D: 0 = count up, 1 = count down

Again, not much to add here. This controls the direction of the auto-increment of the address counter offset (see above) after read/write operations.
The general form is "OUT 254, 50 OR UD" where UD is either 0 (up) or 1 (down).

! Update:
And here the documentation is actually wrong. After some painful periods of fruitless debugging and head-scratching, I was eventually able to figure out the form of the command as implemented:

  7   6   5   4   3   2   1   0     bit
| 0 | 0 | 1 | 1 | 1 | 0 | 1 |D/U|   OUT 0xFE

D/U: 0 = count down, 1 = count up (0x3A: down, 0x3B: up)

This controls the direction of the auto-increment of the address counter offset (see above) after read/write operations. The general form is "OUT 254, 58 OR DU" where DU is either 0 (down) or 1 (up).

Read Status

  7      6      5     4    3 - 0    bit

RESET: Status of RST pin
  0 = normal,
  1 = RST is low (BUSY must be 1)

ON/OFF: Display on/off
  0 = on
  1 = off

UP/DOWN: Mode of address counter
  0 = count up
  1 = count down

  0 = normal (ready)
  1 = busy (executing a command or read/write access)

ON/OFF and UP/DOWN reflect the status depending on commands described above. It's really the BUSY signal that is interesting us here. If port 254 reads a value greater than 127, this means we've to wait for the LCD to be ready again for any further operations. Notably, this is the only read operation on port 0xFE.

Example, Waiting for the LCD:

100 IF INP(254) > 127 THEN 100


Finally, there's a last command, but not that unimportant, in order to read or write a byte of data, depending on port 0xFF being used in either read or write mode:

|D7|D6|D5|D4|D3|D2|D1|D0|   IN/OUT 0xFF

D0-D7: data byte, D0 (lsb) top-most pixel

Quote from the PC-8201A Technical Manual, page 164:

If you want to read some portion of the Display RAM, use this command after Setting the PAGE counter and OFFSET counter by 'Set Address Counter' command and 'Set Page Counter' command described before. Note that one dummy read must be done before using this command in order to get a correct data.

Example, Writing a byte with the two top-most pixels set to on:

110 OUT 255, 3

Putting It All Together

To give a complete example of low level display operations, we'll draw a short dash of 4 pixels starting at pixel position 60 in the third row:

 10 CALL 30300 :REM disable interrupts (Model 100)
 20 OUT 185,2 :REM select PA1 (block 2)
 30 B% = INP(186) AND 252 :REM read port 186 and mask it
 40 OUT 186,B% :REM no select for PB0 and PB1 (blocks 9 and 10)
 50 IF INP(254)>127 THEN 50 :REM wait for the display
 60 OUT 254,137 :REM select page 2, offset 9:  (2 * 64) OR 9 = 137
 70 IF INP(254)>127 THEN 70 :REM wait for the display
 80 OUT 254,50 :REM set offset counter to auto-increment
 90 REM write 4 times 16 (5th pixel from top set to on) to the driver
100 FOR I=0 TO 3
110 IF INP(254)>127 THEN 110 :REM wait
120 OUT 255,16 :REM write data
130 NEXT
140 CALL 29756 :REM enable interrupts again (Model 100)

Please note that the addresses of the two ROM calls are here given for the TRS-80 Model 100.
Here is a cross-reference:

Model PEEK(1) 75ON – Enable Interrupts OFF75 – Disable Interrupts
TRS-80 Model 100 51 0x743C (29756) 0x765C (30300)
Olivetti M10 (Europe) 35 0x7146 (28998) 0x7376 (29558)
NEC PC-8201A 148 0x74C0 (29888) 0x76E5 (30437)
Kyotronic 85 225 0x70EA (28906) 0x730A (29450)

If you have further informations regarding the Kyotronic 85, the US-version of the Olivetti M10, the NEC PC-8201 (Japan), the NEC PC-8300, or the TRS-80 Model 102, please contact me. (Update: Added data for the Kyotronic 85, compare Espisode 10.)

Based on this information we may provide a mechanism for auto-configuration as follows:

10 REM Set up a value M% to identify the model (note: true is -1)
11 REM M%: 1 = NEC PC-8201A, 2 = Olivetti M10, 3 = Model 100
20 ID=PEEK(1) : M% = (ID=148)*-1 + (ID=35)*-2 + (ID=51)*-3
30 ON M% GOTO 100, 110, 120
40 PRINT "Model not identified / not supported.":END
100 EXEC 30437:GOTO 200:REM M%=1, NEC PC-8201A
110 CALL 29558:GOTO 200:REM M%=2, Olivetti M10 (no modem)
120 CALL 30300:GOTO 200:REM M%=3, TRS-80 Model 100
130 REM add other models here
200 REM program continues ...

Note: In MS BASIC true is represented as -1 whereas false is 0. Therefor we may assign a positive index K based on a comparison to X by the operation "M%=(ID=X)*-K". Since false is represented as zero, we may safely chain multiple conditions one to another by simple additions in order to optain a single value, as seen in the example above.

Considering the Implementation, Again

A Display of Character(s)

As may be seen easily, three of the four segment borders (at pixel positions 50, 100, and 200) are not multiples of 6, thus there are not less than 24 character positions occupying adjecent segments (or blocks). There are plenty of LCD-related ROM routines provided in all of the Kyocera Siblings, but most of them deal with addressing problems related to writing characters of 6 pixels width to segement drivers of 50 pixels width. Notably, there is a low-level routine "Write LCD Bytes" (at 0x74F6 on the Model 100) for writing a sequence of bytes in memory to the LCD that may be of any length. Again, this is very useful for writing characters of 6 bytes length, but comes, with a nod to adjecent segments, again with some overhead. Since we're perfectly aware of the current byte postion and the block to be selected while drawing the player's view into the maze, we wont need most of this.

Going Serial

Ironically, we may achieve our cross-platform project best by opting for the most low-level approach of directly talking to the drivers via serial ports, thus avoiding differences in BASIC dialects and subtle differences in ROMs. Moreover, since BASIC is reportedly quicker than the LCD response, we may do this entirely in BASIC, with the only exception of the ROM calls for enabling/disabling the system interrupts. Further, to our utter enjoyment, this drastically reduces the need for separate code execution for the various models. (At least in comparison to an approach by system calls at varying cross-referenced ROM addresses with arguments to be provided by the differing mechanisms of the CALL and EXEC commands. Here, we have just two of them, at the very beginning and ending of a display sequence, and these calls do not use any arguments at all.)

(Note: The notion of BASIC being even quicker than the not-that-fast response of the LCD is observed by a few sources and underlined by example programs in BASIC with wait loops and/or FOR–NEXT delays.)

! Caution: Since we have to disable interrupts while talking to the LCD, we'll end up without a keyboard and without a BREAK key, if anything goes rogue (esp. with loops). — So, yeah, proofreading may be a thing …

A Simple Test

To put our approach to a test, we'll paint a simple pattern onto the screen: We're going to fill the two first Segment Drivers by some horizontal lines (just a value of 17 for each byte, meaning the first and the 5th pixel from the top being active). This will give us also an idea of the speed of the basic display implementation. We're not going for any kind of optimization regarding segements/blocks, rather we're drawing 4 rows of bytes horizontally, each 100 pixels long, starting at the top left origin of the screen. Therefor, we'll have to switch the block select at each 50th pixel position (block 2) and at each beginning of a row (block 1).

To have this executed in the same way and at best speed on all machines, we're using integer variables only (DEFINT).
Let's go for it:

10 REM A simple LCD test, 100*32 px of stripes
29 REM Get the model. M: 1 = PC-8201A, 2 = M10 (no modem), 3 = Model 100
30 P=PEEK(1):M = (P=148)*-1 + (P=35)*-2 + (P=51)*-3
40 IF M=0 THEN PRINT "Model not identified / not supported.":END
50 CLS:PRINT:PRINT:PRINT:PRINT:REM provide 4 lines of space
59 REM Some constants ...
60 PA=185:REM port: block select B1-B8  (PA0-PA7)
70 PB=186:REM port: block select B9-B10 (PB0,PB1)
80 PC=254:REM control port
90 PD=255:REM data port
100 SR=127:REM max display ready status
110 B=17:REM the data byte / pattern
120 CP=64:CM=252:UP=50:REM values for port operations
130 C0=0:C1=1:C2=2:C3=3:CL=49:REM some numeric constants
140 X=0:Y=0:REM vars for pixel positions
200 REM === Main ===
210 ON M GOSUB 910,920,930:REM disable interrupts
220 FOR Y=C0 TO C3:REM loop over first 4 rows
230 P=C1:GOSUB 710:REM select block 1, page Y
240 GOSUB 810:REM write pixels 0..49
250 P=C2:GOSUB 710:REM select block 2, page Y
260 GOSUB 810:REM write pixels 50..99
270 NEXT
280 ON M GOSUB 960,970,980:REM enable interrupts
290 END
699 REM Subroutines
700 REM === Select a block (P) and row/page (Y) ===
710 OUT PA,P:REM select block in P (1 or 2)
720 P=INP(PB) AND CM:OUT PB,P:REM no select for blocks 9 and 10
730 IF Y>C0 THEN 760:REM init only on 1st row
740 IF INP(PC)>SR THEN 740:REM wait for display ready
750 OUT PC,UP:REM set counter to auto-increment
760 IF INP(PC)>SR THEN 760:REM wait ...
770 OUT PC,Y*CP:REM select page Y, offset 0
800 REM: === Write 50 pixel columns of B ===
810 FOR X=C0 TO CL:REM pixels 0..49
820 IF INP(PC)>SR THEN 820:REM wait for display ready
830 OUT PD,B:REM write data byte
840 NEXT
900 REM === Disable interrupts ===
910 EXEC 30437:RETURN:REM PC-8201A
920 CALL 29558:RETURN:REM M10
930 CALL 30300:RETURN:REM Model 100
950 REM === Enable interrupts ===
960 EXEC 29888:RETURN:REM PC-8201A
970 CALL 28998:RETURN:REM M10
980 CALL 29756:RETURN:REM Model 100

And here is the program running on the Olivetti M10 — success on first try:

Olivetti M10 running the display test program

Olivetti M10 running the display test program

It shows, we actually don't need to wait for the LCD using constructs like, "820 IF INP(PC)>SR THEN 820". Especially in line 820 we're losing some time. — Seems, either the LCD isn't as slow or BASIC not as fast as advertised …

[Edit] Notably, the LCD Operation Test program (also written in BASIC) provided at page 5-6 of the Personal Computer Service Manual PC-8201 (NEC Corporation, 1983) doesn't wait for the ready state as well. A sure sign that we won't have to do this either.

Thus, we arrive at the following revised version which executes a bit (read: noticeably) faster:

10 REM A simple LCD test, 100*32 px of stripes (no wait)
29 REM M: 1 = PC-8201A, 2 = M10 (no modem), 3 = Model 100
30 P=PEEK(1):M = (P=148)*-1 + (P=35)*-2 + (P=51)*-3
40 IF M=0 THEN PRINT "Model not identified / not supported.":END
59 REM define some constants ...
60 PA=185
70 PB=186
80 PC=254
90 PD=255
100 B=17
110 CP=64:CM=252:UP=50
120 C0=0:C1=1:C2=2:C3=3:CL=50
130 X=0:Y=0
200 REM === Main ===
210 ON M GOSUB 910,920,930
220 FOR Y=C0 TO C3
230 P=C1:GOSUB 710
240 GOSUB 810
250 P=C2:GOSUB 710
260 GOSUB 810
270 NEXT
280 ON M GOSUB 960,970,980
290 END
699 REM Subroutines
700 REM === Select a block (P) and row/page (Y) ===
710 OUT PA,P
800 REM: === Write 50 pixel columns of B ===
900 REM === Disable interrupts ===
910 EXEC 30437:RETURN
920 CALL 29558:RETURN
930 CALL 30300:RETURN
950 REM === Enable interrupts ===
960 EXEC 29888:RETURN
970 CALL 28998:RETURN
980 CALL 29756:RETURN

Edit/Note: Comparing other sample programs, we may assume it's save to just write a value directly to PA (compare line 720) as long as we make sure it's in the range of 0..3 — even, if the docs tell us not to do so.


Now, that we've finally run an actual program and have put something on the display for real, it's time to close this episode, with still more to come.


Update: The Peculiar Case of the NEC PC-8201 (Japan)

Apparently, the original NEC PC-8201 (no "A", sold in Japan only) differs a little in the LCD implementation. The Personal Computer Service Manual PC-8201 (NEC Corporation, 1983) provides the following specifications for the LCD at pages 3-3 to 3-6:


Segment Drivers:   10 × HD44102B
Common Drivers:     2 × HD44103B

Communication Ports

                    NEC PC-8201       NEC PC-8201A (and others)
Block Select A:     0xB9 (185d)       0xB9 (185d)
Block Select B:     0xBA (186d)       0xBA (186d)
Control Port:       0xF0 (240d)       0xFE (254d)
Data Port:          0XF1 (241d)       0xFF (255d)

! Note that the channel numbers of the control port and the data port differ, while everything else, like the values to be sent to the various ports, is the same as with the other Kyocera Siblings. (This is also confirmed by the LCD Operations Test program at page 5-6, which uses port 240 as the control port.) In order to make a low-level program compatible to the Japanese NEC PC-8201, we would have to alter the port numbers in constants PC and PD in the program listed above, depending on the model identification.

Nuts, if you thought the PC-8201 and the PC-8201A were basically the same — they are even wired up differently!

On the other hand, this is a weak hint on the PC-8201, as sold in Japan, actually being an early sibling of the family. We may also want to check the port numbers for the Kyotronic 85, too, as it is allegedly the original example, but I lack the documentation for this.

Update: As confirmed by ROM listings, the Kyotronic 85 uses ports 0xFE and 0xFF just like the other models.



Next:   Episode 5: From Mockup to Data

Previous:   Episode 3: Drafting Time

Back to the index.

— This series is part of Retrochallenge 2016/01. —