• Please review our updated Terms and Rules here

PrintChar subroutine works, until I convert AL from bin to hex

otacon14112

Experienced Member
Joined
Apr 19, 2012
Messages
115
Location
Iowa, United States
I slowly keep adding more functionality to my single board computer. The most recent subroutine I wrote converts AL from binary to two ASCII characters representing the two hexadecimal digits of AL. However, when I try to print the newly-converted ASCII characters, my PrintChar subroutine doesn't do its job on the physical system.

The funny thing is, when I step through the program in DOS DEBUG, it works exactly as I intend it to. During debug, it correctly converts the two nibbles into two ASCII hex digits, and stores them in AX. The PrintChar subroutine only cares about AL. Anything can be in AH, because it only sends what's in AL out the IO ports. It correctly steps through everything until it gets to the infinite loop. But not on the physical system.

What would make a subroutine that works earlier in the program not work later? Could any flags cause this? Still, like I said, it works the way I intend it to in debug. After returns, it goes to the next instruction in the program.

Here's a picture of my computer: http://i.imgur.com/9IFvTIi.jpg
You can see that it correctly prints "Hi:", but it won't print text after ToHex8Bit is called.

I can provide a link to the bin so that you can step through the same bin I'm stepping through if you want. Just don't sue me! ;)

Here's my NASM code:
Code:
;              ************************************************
;              *                                              *
;              *     Convert a byte to hex, then print it     *
;              *                                              *
;              ************************************************
; 
; Subroutines in this program:
; ***************************
; ToHex8Bit
; InitDelay
; CharDelay
; LatchDelay
; PrintChar
; LatchCMD
; ClearPort2
; InitPorts
; InitLCD

USE16

section		.data
_1KB		equ	1024
_2KB		equ	2048
_32KB		equ	32*_1KB
_128KB		equ	128*_1KB
_256KB		equ	256*_1KB
PORT1		equ	0x01
PORT2		equ	0x02
ROM_SIZE	equ	_128KB		; Set size of ROM here

org		0x100

section		.text
start:		
		; Set stuff up
		mov	sp,0xFFFF	; Initialize the stack pointer to 64KB.
					;   This is 384KB in memory, 64KB 
					;   above the SS 
		mov	bx, ss		; Initialize BX to 0, since SS hasn't
					;   changed yet
		mov	ax,0x5000 	; Initialize SS to be at 320KB,
		mov	ss,ax		;   64KB below the end of RAM
		call	InitPorts	; Set up the 8255
		call	InitDelay	; Give the LCD time to self-initialize
		call	InitLCD		; Run the initialization sequence

		mov	al,"H"
		call	PrintChar
		mov	al,"i"
		call	PrintChar
		mov	al, ":"
		call	PrintChar 

		mov	al, 11010111b
		call	ToHex8Bit
		mov	bx, ax
		mov	al, ah
		call	PrintChar
		mov	al, bl
		call	PrintChar
		jmp	$

ToHex8Bit:
		push	bx		; Save the registers
		push	ax
		
ParseLoNibble:	shl	al, 4		; Get rid of the high nibble
		shr	al, 4		; Get the low nibble
		cmp	al, 0x09	; Is the nibble numeric or alpha?
		jle	BToHLoNibNum	; If numeric, go here
		jg	BToHLoNibAlpha	; If alpha, go here

ParseHiNibble:	
		mov	bl, al		; Store low hex nibble in BL
		pop	ax		; Get the original byte back
		shr	al, 4 		; Get rid of the low nibble
		cmp	al, 0x09	; Is the nibble numeric or alpha?
		jle	BToHHiNibNum	; If numeric, go here
		jg	BToHHiNibAlpha	; If alpha, go here

ExitToHex8Bit:
		mov	bh, al		; Store high nibble in BH
		mov	ax, bx		; Put both hex digits in AX
		pop	bx		; Get back whatever was in BX
		ret

BToHLoNibNum:	add	al, 48		; Convert the number to ASCII
		jmp	ParseHiNibble

BToHLoNibAlpha:	add	al, 55		; Convert the alpha char to ASCII
		jmp	ParseHiNibble
		
BToHHiNibNum:	add	al, 48		; Convert the number to ASCII
		jmp	ExitToHex8Bit

BToHHiNibAlpha:	add	al, 55		; Convert the alpha char to ASCII
		jmp	ExitToHex8Bit

InitDelay:	mov	word [bx],0x01FF; Set the countdown timer.
StartInitDel:	dec	word [bx]	; Decrement it by 1 each time.  
		cmp	word [bx],00h	; If the timer has counted down
					; all the way, return to the 
					; 'nextloop' label so that it
					; can move on to the next hex
					; value to display on the LEDs.  
		jnz	StartInitDel	; If the counter hasn't counted
					; down to 00h yet, keep going.
		ret 

CharDelay:	mov	word [bx],0x001F; Set the countdown timer.
StartCharDel:	dec	word [bx]	; Decrement it by 1 each time.  
		cmp	word [bx],00h	; If the timer has counted down
					; all the way, return to the 
					; 'nextloop' label so that it
					; can move on to the next hex
					; value to display on the LEDs.  
		jnz	StartCharDel	; If the counter hasn't counted
					; down to 00h yet, keep going.
		ret 

LatchDelay:	mov	word [bx],0x002F; Set the countdown timer.
StartDel:	dec	word [bx]	; Decrement it by 1 each time.  
		cmp	word [bx],00h	; If the timer has counted down
					; all the way, return to the 
					; 'nextloop' label so that it
					; can move on to the next hex
					; value to display on the LEDs.  
		jnz	StartDel	; If the counter hasn't counted
					; down to 00h yet, keep going.
		ret 

PrintChar:	
		push	ax		; Save the ASCII character
		mov	al,0x02		; Make RS high, E low
		out	PORT2,al
		pop	ax		; Get the ASCII character back
		out	PORT1,al	; Send the character to the display
		mov	al,0x06		; Make E and RS high
		out	PORT2,al
		call	CharDelay
		mov	al,0x02		; Make RS high, E low
		out	PORT2,al
		call	CharDelay
		ret 

LatchCMD:
		mov	al,0x04		; Make E high to latch the data
		out	PORT2,al 
		call	LatchDelay
		call	ClearPort2
		call	LatchDelay
		ret

ClearPort2:
		mov	al,0x00		; Clear the port
		out	PORT2,al
		ret

InitPorts:	
		mov	al,0x90		; This sets the 8255 to operate
                out	0x03,al		;   in Mode 0 (basic I/O)
					;   Input	Output  
					;   *****	******
					;   Port 0	Ports 1, 2 
		ret

InitLCD: 
		; Reset sequence 1
		mov	al,0x30
		out	PORT1,al
		call	LatchCMD

		; Reset sequence 2
		mov	al,0x30
		out	PORT1,al
		call	LatchCMD

		; Reset sequence 3
		mov	al,0x30
		out	PORT1,al
		call	LatchCMD 

		; 8-bit, 2 lines, 5x8 characters
		mov	al,0x38
		out	PORT1,al
		call	LatchCMD
		; End of step 1

		; Increment, and no display shift
		mov	al,0x06
		out	PORT1,al
		call	LatchCMD

		; Turn display on, cursor on, and do not blink the 
		; character at cursor
		mov	al,0x0C
		out	PORT1,al
		call	LatchCMD

		; Clear the display
		mov	al,0x01
		out	PORT1,al
		call	LatchCMD

		; DDRAM address set to home, position top left most character
		mov	al,0x80
		out	PORT1,al
		call	LatchCMD 
		ret 
			
		times	((ROM_SIZE-16) - ($-$$)) db 0
		db	0xEA			; far jump
		dw	start			; Sets the offset IP value
		dw	0x10000-(ROM_SIZE/16)-0x10; Target CS value
		times	ROM_SIZE - ($-$$) db 0

I'm really curious what ideas anyone may have for why it works in debug but not in the physical system. Thanks guys
 
Last edited:
Alas, it's been 35 years since I did MASM....

But as a WAG, is the problem in the timing of the writing to the LCD display? Ie., some NOPs to allow latches to settle?

-- Charles
 
Update: I guess it actually wasn't working the way I expected it to in debug. The carry flag was being set. I am changing my code now to use "and" to mask off the nibbles I don't want.
 
I'm a little puzzled about this:

Code:
shr	al, 4

Are you using an 8088? If so, that instruction doesn't exist on the 8088--only single-bit and (cl) shifts.
 
I'm a little puzzled about this:

Code:
shr	al, 4

Are you using an 8088? If so, that instruction doesn't exist on the 8088--only single-bit and (cl) shifts.

Yes, a 5 MHz 8088. Page 2-66 of intel's 8088/86 manual lists it as an instruction, and it successfully shifts right 4 bits in DOS debug. Also, I have an 8086/88 assembly language programming book that shows examples using SHL and SHR.

But strangely enough, the section in which I have "shr al, 4" is dealing with the high nibble. I was able to get the numeric low nibble to successfully print "7", but when I try to print the high nibble, it has a weird-looking symbol that looks like some kind of kanji or eastern asian symbol, but it is not shown in the table of possible characters in the LCD's datasheet. I'll try what you recommended.

I think when I was shifting, it set a carry flag. Instead, I masked off the nibbles I didn't want using AND, and then my little computer printed more reasonable stuff, such as Hi:<weird symbol>7 (when the SHL and SHR were in there, the computer was going bonkers).
 
Last edited:
It turns out I was using shr wrong. I tried it the way you recommended Chuck, by putting 4 into CL, then doing shr al, cl, and it worked. But I also discovered the second part to what was going wrong. For the heck of it, I inserted CLC right afterward, and that did the trick. It works as intended now. In the morning when I wake up, I'll share the code. I also want to thank you Chuck.
 
Last edited:
As I think got mentioned in another thread, set NASM to only alllow 8088/8086 instructions, because there is no such thing as shr al, 4 -- that should NOT run properly, and shouldn't even make it past the assembler!

Code:
BITS 16
CPU 8086

Should be the FIRST thing you do in your source!

I'd also suggest creating macros instead of slopping jumps all over the place for those +48, +55, etc, additions. It's going to be damned near the same amount of code and just doing it would be WAY faster.

there's also some logic failings akin to how beginner C programmers don't quite grasp "eise" after a >=.

Code:
		jle	BToHHiNibNum	; If numeric, go here
		jg	BToHHiNibAlpha	; If alpha, go here
jg is the opposite of JLE, so just jmp... Though really given what you are doing, lose those jumps altogether and use a lookup table! For something like this, XLAT is your friend!

Code:
hexLookup:
	db '0123456789ABCDEF'

byteToHex:
; ACCEPTS
;   AL = byte
; ASSUMES
;   DS is where hexLookup is.
; CORRUPTS
;   BX, CL
; RETURNS
;   AH = low character
;   AL = high character
;     This reverse order is usually more useful during output
	mov  bx, hexLookup
	mov  ah, al
	and  al, 0x0F
	xlat
	xchg ah, al
	mov  cl, 4
	shr  al, cl
	xlat
	ret

If your output routine leaves AH alone, or if you just push/pop you can xchg between output for each byte.

clearport2 is only called once, so lose that as a pointless call.

your DEC [bx] sets zf, so why the compare inside the delay routine? Of course that's not exactly the cleanest way to create a delay either. CX with a port read perhaps?

Since EVERY call to LATCHCMD is preceded by an OUT to PORT1, move the out into the call. I'd probably also create a macro for that so you could just go "SET_AND_LATCH 0x30" -- that and/or use a lookup table for the reset values. Remember, tables are your friend.

I'm assuming you're on a tiny memory model? Ditch LATCHCMD as a separate entity altogether for the reset.

Code:
initSequence:
	db 0x30, 0x30, 0x30, 0x38, 0x06, 0x0C, 0x01, 0x80
		
initLCD:
; ACCEPTS
;   nothing
; CORRUPTS
;   SI, CX, AX
; RETURNS
;   nothing
	mov  si, initSequence ; assuming DS == CS / tiny
	mov  cx, 8
.initLCDLoop:
	lodsb
	out  PORT1, al
	mov  al, 4
	out  PORT2, al
	call latchDelay
	xor  al, al
	out  PORT2, al
	call latchDelay
	loop .initLCDLoop
	ret

If you're not in TINY, move the sequence table to your data segment.

Just some ideas for you.
 
Thanks Deathshadow. I tried looking for the way to set the target cpu, but didn't see anything in nasm's man page (it only referred to output format), nor in search results. Maybe I was using the wrong search words. I finally left it because under NASM's manual section for USE16 it said it defaulted to to 8086/88, so I thought it was fine. I want to learn the right way to write programs. Can you recommend a specific book which you have found is enlightening on assembly?
 
Last edited:
If you need space instead of speed, this is my favorite value-to-hex routine (credit to Norbert Juffa)

Code:
; value in AL
  cmp al,10       {if x<10, set CF=1}
  sbb al,69h      {0-9: 96h..9Fh, A-F: A1h..A6h}
  das             {0-9: subtr. 66h -> 30h-39h;
                   A-F: subtr. 60h -> 41h-46h}

Good candidate for a macro.
 
If you need space instead of speed, this is my favorite value-to-hex routine (credit to Norbert Juffa)

Code:
; value in AL
  cmp al,10       {if x<10, set CF=1}
  sbb al,69h      {0-9: 96h..9Fh, A-F: A1h..A6h}
  das             {0-9: subtr. 66h -> 30h-39h;
                   A-F: subtr. 60h -> 41h-46h}

Good candidate for a macro.

Nice, thank you!
 
Sort of a variation on the code that originated with the 8008:

Code:
   ADD  AL, 90h
   DAA
   ADC  AL, 40h
   DAA

The cool thing is that this works on everything from an 8008 to a x64 x86. The 8008, 8080, 8085, Z80 do not have a "DAS". instruction. There aren't many assembly tricks that have a 45-year lifetime. :)

If you add an "or al,20h" after the second DAA, the uppercase A-F become lowercase a-f. Some prefer this representation--it's easier to read.

You can also do this with an XLAT instruction:

Code:
    MOV   BX,offset HEXTABLE
    XLAT   BYTE PTR CS:[BX]
...
HEXTABLE   DB '0123456789abcdef'.

The advantage of the XLAT form is that it works with any representation, including some ancient hex symbolisms.
 
Last edited:
As I think got mentioned in another thread, set NASM to only alllow 8088/8086 instructions, because there is no such thing as shr al, 4 -- that should NOT run properly, and shouldn't even make it past the assembler!

There are some assemblers that have a feature to automatically translate shr al, 4 to a sequence of 4x shr al, 1 on 8088/8086 though.
Not that I would recommend using it, since this is a potential performance hazard, and you'd rather have your assembler warn you about trying to do something the CPU can't do, than having it generate a bunch of nonsense code, without you even being aware of it.
 
There are a lot of interesting pieces of wisdom here, so thanks - I appreciate everyone's willingness to help. I've learned a lot from folks here, more than I have in "tutorials" on the 'net, since I've found out from other people in the past that tutorials I've read contain bad practices that are taught as "proper". Even some books I have apparently teach bad programming, according to people who've read code that I've used from examples in them. Which is why I really want a good recommendation on a book aimed at the 8088/86 cpus.

Now I have a question for when you guys program ROMs. When you declare constants and variables, what can you do to make it fit in your ROM? When I put DBs, DWs in the data section, this makes the raw binary file too big for the ROM. Correct me if I'm wrong, but is this because the space padding makes the code segment a particular size and ignores other segments like the data segment? This isn't a problem when I write assembly programs for Linux, because programs can be any size, but that's different.

What's the proper way to adjust for this extra file size?
 
Where code, data and other segments end up, is up to the linker, is it not?
If you create a ROM, there are probably strict rules for where your code and data should be. There is probably also some special kind of header or other info you need to include (What kind of ROM are we talking about here?).
You'll need a linker and linker settings that will be compatible with the ROM you want to generate.

The simplest way is probably to put everything in a single segment, and you can use the 'org' statement to align certain parts of data or code to specific offsets in the segment.
Then you can generate a .COM file and just pretend it's a ROM image. After all, a .COM file puts everything in a single segment, and is headerless.
If you get all headers and offsets correct 'by hand', it will work. You're basically using a 'null' linker.
(Disclaimer: I'm not familiar with NASM or the linkers you may use with that, or what its options are. I mainly have experience with MASM and TASM under DOS).
 
What I've been doing that worked so far is:

Code:
otacon@gamingpc $ nasm filename.asm
which generates a raw binary file to fill my ROM. I'm using a 128KB EEPROM (1Mb, 128K x 8 ).

Code:
otacon@gamingpc $ l
132K -rw-r--r-- 1 otacon otacon 129K Jun 27 18:43 filename
4.0K -rw-r--r-- 1 otacon otacon 3.8K Jun 27 18:43 filename.asm

When I don't db stuff in the data section, the assembled file is precisely 128KB. Nasm's default output format is bin (DOS COM) format.
 
Where code, data and other segments end up, is up to the linker, is it not?
If you create a ROM, there are probably strict rules for where your code and data should be. There is probably also some special kind of header or other info you need to include (What kind of ROM are we talking about here?).
You'll need a linker and linker settings that will be compatible with the ROM you want to generate.

The simplest way is probably to put everything in a single segment, and you can use the 'org' statement to align certain parts of data or code to specific offsets in the segment.
Then you can generate a .COM file and just pretend it's a ROM image. After all, a .COM file puts everything in a single segment, and is headerless.
If you get all headers and offsets correct 'by hand', it will work. You're basically using a 'null' linker.
(Disclaimer: I'm not familiar with NASM or the linkers you may use with that, or what its options are. I mainly have experience with MASM and TASM under DOS).

I've been going by this page linked to below. Since no OS is present, I'm essentially making the BIOS. I use raw binary files, but not .COM files, since the binary is the ROM. NASM doesn't require a linker to produce raw binary files.
http://www.nasm.us/doc/nasmdoc8.html
 
These are really slow. It is quite likely that you need more delay
someplace. It may not be an issue with your code.
Dwight
 
I'm currently capturing data with my logic analyzer for various versions of programs that I'm trying on my single board 8088 computer. I've discovered some interesting things, possibly shining some light on bugs.
 
Back
Top