• Please review our updated Terms and Rules here

Working on Plantronics Viewer

chjmartin2

Experienced Member
Joined
Dec 26, 2012
Messages
424
Hi,

I have decoded the Plantronics mode and can successfully make 320x200x16 color images and 640x200x4 color images using QuickBasic 4.5. As you all know POKE's are far too slow to do anything useful. I have an image converter, I know what registers to tickle to get into Plantronics, and I know the memory layout and what's needed to make an image. But POKE is waaaaay too slow. So, ASM it is. Unfortunately, my only experience is on Z80 ASM. So, have to teach this old dog new tricks, and certainly I should get it, but alas my code is not working and I am not sure why. I just need a nudge in the right direction I hope. First all I want to do is get some graphics on the screen in 640x200.

The code below is supposed to send 26 (00011010) to the CGA mode control register, then load 146 (10010010) to memory locations 47104 through 47113 then go back to DOS. Well, it doesn't do that. Would appreciate any help.

Code:
start:
    mov ax, 26
    mov dx, 984
    out dx, ax

    mov bx, 146
    mov ax, 47104
    mov (ax), bx
    inc ax
    mov (ax), bx
    inc ax
    mov (ax), bx
    inc ax
    mov (ax), bx   
    inc ax
    mov (ax), bx
    inc ax
    mov (ax), bx
    inc ax
    mov (ax), bx
    inc ax
    mov (ax), bx
    inc ax
    mov (ax), bx
    inc ax
    mov (ax), bx

; return to DOS
    mov ax, 4c00h
    int 21h
 
Been a bit since I puttered around with 8086 assembler, but it looks like you're writing entirely to the default data segment, rather than anywhere in the standard video segment.
 
also it looks like you're doing a 16-bit I/O operation. does this end up writing 0 to the register following the mode control register?
 
Why are you sending 26 to the 3d8h port?

To start a Plantronics ColorPlus mode, the usual procedure is:

1) Activate the regular CGA mode 6 (or 4 for 320x200 resolution)

mov ax,0006
int 10h

2) Write to 3DDh port. For 640x200 special mode:

mov dx,3DDh
mov al,00100000b ; Bit 5:= set 640x200 4 color mode
out dx,al

I also don't see any segment specification on your code. I assume you did it elsewhere.

I made these modifications on your code in order to work (they are supposed to work but I do not have time now to test it). It should assemble on TASM (any version) or MASM (5 or higher).

Code:
.model small

.code

start:

    mov        ax,0006
    int        10h                   

    mov        dx,3DDh
    mov        al,00100000b
    out        dx,al
    
    mov        ax,0B800h            ; CGA video RAM buffer
    mov        es,ax
    
    mov        al,146                   ; Value to store into video RAM
    mov        cx,10                    ; Repeat 10 times
    mov        di,47104              ; Store to this offset
    rep stosb                            ; Stores AL into DI, CX times while
                                               ; increases DI by 1

    mov       ah,1
    int         16h                         ; Press any key to continue

    sub        al,al                       ; AL:=0
    out        dx,al                      ; Restore Colorplus to normal CGA op
    mov      ax,0003
    int         10h                        ; Back to text mode

; return to DOS

    mov ax, 4c00h
    int 21h
    
END

Your code does nothing else than assigning values to the AX register. The parenthesis are not needed.

I also don't know why do you want to write to such a high memory address. The Colorplus only has 32 kb of RAM, organized into 2 banks, one at B800H, the other one at BC00h. Hope this helps.
 
A few points:
  • What assembler are you using? Asking because of that "(ax)" construct - from context it looks like you want AX to point to the address being written to, but in that case you'd want to use square brackets.
  • Also, AX can be used as a base index only on 386 and above. For 8086-clean code, you'd want [BX], [SI], or [DI].
  • Technically this code would be storing the same word value (BX is 16 bits) to a sequence of consecutive bytes, which probably isn't what you want - to do this with byte values, use one of the 8-bit high/low registers (say BL or BH)
  • String instructions would greatly simplify what you're trying to achieve.
How to rewrite this properly depends on what segment of memory you're trying to write to. I assume you mean the video segment, but the Plantronics has only 32K of video RAM so these memory locations don't make sense in that context.
 
Why are you sending 26 to the 3d8h port?

First - wow thank you for the code help. I was just trying to start with a basic 640x200 CGA screen because I am literally starting from scratch. The 26 in my understanding is the right decimal value for 640x200 mode.

A few points:
  • What assembler are you using? Asking because of that "(ax)" construct - from context it looks like you want AX to point to the address being written to, but in that case you'd want to use square brackets.

I am using NASM - should I be using something else that is better/easier?

  • Also, AX can be used as a base index only on 386 and above. For 8086-clean code, you'd want [BX], [SI], or [DI].

Thank you for this - I didn't know.

  • Technically this code would be storing the same word value (BX is 16 bits) to a sequence of consecutive bytes, which probably isn't what you want - to do this with byte values, use one of the 8-bit high/low registers (say BL or BH)

This context is important to me because I am used to Z80 assembler where the 8 bit register is referred to as a single letter. Now I get it on BL or BH.

  • String instructions would greatly simplify what you're trying to achieve.
How to rewrite this properly depends on what segment of memory you're trying to write to. I assume you mean the video segment, but the Plantronics has only 32K of video RAM so these memory locations don't make sense in that context.
Yeah - clearly my translation on memory addresses is incorrect. The address I was using is what I had to use in DEF SEG in QuickBasic to get to the right memory location for CGA.

To start a Plantronics ColorPlus mode, the usual procedure is:

1) Activate the regular CGA mode 6 (or 4 for 320x200 resolution)

mov ax,0006
int 10h

Ok so in the above you are using BIOS to set the mode by setting the mode into register AX and then calling interrupt 10h? Better to do this than directly to CGA registers?

2) Write to 3DDh port. For 640x200 special mode:

mov dx,3DDh
mov al,00100000b ; Bit 5:= set 640x200 4 color mode
out dx,al

Thank you for that - is it ok to use 989 instead of 3DD? Certainly I need to get more used to hex math.

I also don't see any segment specification on your code. I assume you did it elsewhere.

All I have in addition to what I posted is:

Code:
    cpu 8086
    bits 16
    org 100h

Is this what you mean by segment?

I made these modifications on your code in order to work (they are supposed to work but I do not have time now to test it). It should assemble on TASM (any version) or MASM (5 or higher).

Again thank you so much. I will switch to TASM instead of NASM. I like that I can compile the NASM on the modern computer.

Your code does nothing else than assigning values to the AX register. The parenthesis are not needed.

I also don't know why do you want to write to such a high memory address. The Colorplus only has 32 kb of RAM, organized into 2 banks, one at B800H, the other one at BC00h. Hope this helps.

I will test it and let you know right away!
 
A few points:
  • What assembler are you using? Asking because of that "(ax)" construct - from context it looks like you want AX to point to the address being written to, but in that case you'd want to use square brackets.
I am using NASM. Should I be using something else? Can they compile on a Windows 11 machine?

  • Also, AX can be used as a base index only on 386 and above. For 8086-clean code, you'd want [BX], [SI], or [DI].

I appreciate this understanding. New to x86 ASM.

  • Technically this code would be storing the same word value (BX is 16 bits) to a sequence of consecutive bytes, which probably isn't what you want - to do this with byte values, use one of the 8-bit high/low registers (say BL or BH)

You are right I want the byte registers.

  • String instructions would greatly simplify what you're trying to achieve.
Can you explain more what you mean? Eventually I am going to have raw CGA/Plantronics data copied to the end of the file and will simply want to transfer that data to the two pages of memory. I'll also want to create a test program that cycles through every possible combination of 8 bit values on each screen so 65,536 screens of data to test something else. So code to fill up the screen with a single value is going to be useful to me.

How to rewrite this properly depends on what segment of memory you're trying to write to. I assume you mean the video segment, but the Plantronics has only 32K of video RAM so these memory locations don't make sense in that context.
You are right - I am writing to the wrong memory location.
 
First - wow thank you for the code help. I was just trying to start with a basic 640x200 CGA screen because I am literally starting from scratch. The 26 in my understanding is the right decimal value for 640x200 mode.

Ok so in the above you are using BIOS to set the mode by setting the mode into register AX and then calling interrupt 10h? Better to do this than directly to CGA registers?
Ah, ok. For this kind of things it's usually better calling the BIOS to do the work, just like that code with int 10h. It saves typing work and it also prevent possible incompatibilities between video adapters. This way you also make sure you send all the necessary values to the ports.

I am using NASM - should I be using something else that is better/easier?
NASM is also a great assembler. It's a matter of taste. I use TASM because I got very accustomed to the MASM syntax + the TASM extensions. I also appreciate a lot the possibility of assembling and compiling code in real hardware. NASM AFAIK only works on 386 or higher computers, so this excludes working with my 8086/88 systems.
Thank you for that - is it ok to use 989 instead of 3DD? Certainly I need to get more used to hex math.
Port numbers are usually specified in hexadecimal. Once you get accustomed, hexadecimal is clearer than decimal for this kind of things.
All I have in addition to what I posted is:

Code:
    cpu 8086
    bits 16
    org 100h

Is this what you mean by segment?
No. That code is for the assemblerto warn you that you are using a post-8086 instruction, and for the machine code to start on offset 100h. This is obligatory for building .COM files. So in my code you should change model .small by mode .tiny for generating a .COM. The MASM/TASM abbreviate segment declaration is .code, .data, etc. There's a more complex, but also more flexible, former syntax. But the abbreviate one uses to be good enough for regular cases when a precise segment declaration is not required.
Again thank you so much. I will switch to TASM instead of NASM. I like that I can compile the NASM on the modern computer.
Any option should be fine. Anyway, translating code from TASM->NASM or vice versa is not usually very complicated.
 
I am using NASM. Should I be using something else? Can they compile on a Windows 11 machine?
Nah, NASM is fine. I was just wondering if you were using some other assembler with a syntax I wasn't familiar with.

Yeah - clearly my translation on memory addresses is incorrect. The address I was using is what I had to use in DEF SEG in QuickBasic to get to the right memory location for CGA.
Ah OK, that makes sense now: 47104 is B800 in hex - that's the base segment for CGA video memory, and that's what DEF SEG controls in QuickBasic. If you're coming from the Z80, this stuff is probably worth a brief overview.

In 16-bit x86, the CPU addresses memory with a pair of values: a segment register, and an offset into that segment (the usual notation is "seg:offs"). Both are 16-bit numbers, i.e. hold values of up to 64K. But we know that the 8086 (and later generations in real mode) can address up to 1 MB: to get the real memory location, the CPU treats the segment address as if it's multiplied by 16 - or shifted left by 4 bits. (This is why hexadecimal makes it more intuitive: you can just mentally tack an extra zero on the right.)

The offset is then simply added to that value to get the final address. In other words, the segment/offset pairs "0040:0000", "0000:0400", "0030:0100" (all in hex) all refer to the same real address when you do the math: 400h.

So if CGA memory starts at segment B800, the actual location within the 8086's address space is B8000, which is 736K - some way past the 640K of conventional memory. The usual way to write to video memory is to keep the segment register at B800, and just modify the offset as you go (that means you can technically address the full 64K starting at that location, but remember that the CGA only has 16K RAM).

On the 8086 the segment register can be CS, DS, ES or SS. The offset is more complicated (it can be an immediate value, or a register, or any combination of up to 2 registers and an immediate value)... to get the full picture it's probably a good idea to read up on 8086 segment register usage and addressing modes.

Anyway, in carlos12's code...
Code:
    mov        ax,0B800h            ; CGA video RAM buffer
    mov        es,ax
...this sets the segment register ES to point to the start of CGA VRAM (there's no "mov es, <immediate>" instruction, so you have to go through a non-segment register).
Then, if we want to modify it to start writing at offset 0 (top left of the screen)...

Code:
    mov        al,146                   ; Value to store into video RAM
    mov        cx,10                    ; Repeat 10 times
    xor        di,di                    ; Start writing at offset 0
    rep stosb                           ; Stores AL into DI, CX times while
                                               ; increases DI by 1
...we set DI to 0. rep stosb is an example of the "string instructions" I mentioned: STOSB stores the contents of AL into the location at ES:DI (B800:0000 in our case), while also incrementing DI. The "REP" prefix makes it do that multiple times, with the CX register acting as the counter. In our case that's 10 times, so when it's done you'll have memory locations B8000h through B8009h filled with the value 146 decimal.

Ok, that didn't end up a very "brief" overview, but it's a good demonstration of why people coming from other platforms tend to despise x86 assembly with every fiber of their being. ;)
 
Sadly, I compiled the code here and it doesn't work. It doesn't pause for a keypress, puts some characters on the screen of sorts (I think because it flashes by) and then quits. Any advice? I tried both the original code and @VileR's modification. I didn't just give up and come here - been fighting with it for a bit but am stumped.
 
Woot! So getting it to wait for a keypress helped me see it was working!
 
Code:
    mov        al,146                   ; Value to store into video RAM
    mov        cx,10                    ; Repeat 10 times
    xor        di,di                    ; Start writing at offset 0
    rep stosb                           ; Stores AL into DI, CX times while
                                               ; increases DI by 1

Ok so this works to set the screen to 146. I know that the data is interleaved and now I have image data. Can I load a memory location in a register and reference that instead of mov al, 146 and still use stosb?
 
Code:
; Display Raw CGA Memory Data on 640x200 Screen
; Written by Chris Martin (chjmartin2@gmail.com)
; December 2022
; Target OS: DOS
; Executable extension: *.COM
; use: nasm viewcga.asm -o viewcga.com -f bin

    cpu 8086
    bits 16
    org 100h

    mov        ax,0006
    int        10h                   

    mov        dx,3DDh
    mov        al,00100000b
    out        dx,al
    
    mov        bx, Data
    mov        ax,0B800h                ; CGA video RAM buffer
    mov        es,ax
    

    mov        al,[bx]                      ; Value to store into video RAM
    mov        cx,8000                   
    xor       di,di
    inc       bx
    rep stosb                               ; Stores AL into DI, CX times while
                                ; increases DI by 1

    xor        ah,ah                  ; AH = 0
    int        16h                        ; Press any key to continue

    sub        al,al                      ; AL:=0
    out        dx,al                      ; Restore Colorplus to normal CGA op
    mov        ax,0003
    int        10h                        ; Back to text mode

; return to DOS

    mov ax, 4c00h
    int 21h


Data:

This is where I am. I am trying to set up the BX register to point to the data which I will apend to the end of the file. I don't think rep stosb works the way I think it does (there is a Z80 command called LDIR that does this easily.
 
Boom!

REP MOVSB is my friend...

Code:
    mov        si, Data
    mov        ax,0B800h                ; CGA video RAM buffer
    mov        es,ax
    
    mov        cx,8000                   
    xor           di,di
    rep movsb
 
Well, for starters, INT 16h function 1 doesn't pause waiting for a keypress; it returns ZF clear if one is waiting in the buffer.
Haha, my bad. I wrote it from memory, and my memory failed... That's why it's advisable to test the code before sending it out 😅
 
Hi,

In case anybody is interested here is a small slideshow demo and example images for you to run on your own Plantronics hardware. I don't know if it will work in an emulator as I don't have one for Plantronics. Anyway to use you can either run the slideshow.bat or you can type "view filename.bin" to look at individual files.

Thanks again for all of your help. If anybody is interested in the converter, then let me know and I will package it up and post it.

Regards,

Chris
 

Attachments

  • PL4Show.zip
    224.4 KB · Views: 3
Boom!

REP MOVSB is my friend...
MOVSW is an even bigger friend, since even on 8088 it's faster. Each MOVSB takes 16 clocks, so to move two bytes that's 32 clocks not counting opcode fetching. MOVSW takes 24 clocks so you get two bytes moved for 8 less clocks every loop. Just cut your CX in half.

Code:
    mov   si, Data
    mov   ax,0B800h
    mov   es,ax
    xor   di,di

    mov   cx,4000
    rep   movsw

Provides even bigger benefits on the 8086 and up, since on those machines with 16 bit or wider busses you're talking 16 clocks to move 1 byte vs 16 clocks to move two bytes

See the timings here:

That's my own reference I created for my own projects made off the commonly shared PCGPE.TXT by Mark Feldmen's, but with filtering by processor type. Fun project in and of itself since it's a monolithic web page with HTML and CSS doing all the heavy lifting and little to no scripting involved.

You can do the same with stosw, just be sure AH is the same as AL when clearing the page.
 
Back
Top