Seyed Masoud Hosseini · Overview · Study log · Ideas · Transcript · RSS feed
Embedded Systems, 6502 breadboard computer · Lecture 9 of 29 · 31:38
How assembly language loops work
Study guide
What this lecture covers
Picking up from the previous video's discovery that a real 1MHz clock outruns the LCD, this lecture builds a proper fix: reading the LCD's busy flag and branching in a loop until it clears, instead of guessing at a fixed delay. Getting there means introducing the 6502's condition flags, the CMP and AND instructions, and several of its branch instructions, and carefully working out which branch condition corresponds to "still busy."
The second half of the lecture uses the same branching tools to replace the repetitive, one-subroutine-call-per-character printing code with a real loop that walks through a message stored in memory using the X register as an index, stopping automatically at a null terminator. The result is a much shorter, more general "Hello world" program that can print an arbitrary message across both lines of the display.
Key ideas
- Busy-flag polling: reading the LCD's status byte (with read/write high and register select low) returns a busy flag in the top bit, which the code must check before sending the next instruction or character.
- Processor status flags: the 6502 keeps a status register with carry, zero, overflow, and negative flags that many instructions update, and that branch instructions test.
CMP(compare): subtracts a value from the accumulator without storing the result, but sets the status flags as if it had, enabling equality and magnitude checks via branch instructions.ANDfor bit masking: bitwise-ANDing a value with a mask (like%10000000) isolates specific bits (the busy flag) while zeroing out the rest, and updates the zero flag based on the result.- Branch instructions: conditional jumps such as
BEQ(branch if zero flag set) andBNE(branch if zero flag clear) redirect execution based on the status flags rather than an unconditional jump. - Preserving registers across subroutines: pushing the accumulator onto the stack at the start of a subroutine and pulling it back before returning prevents a called subroutine from silently clobbering the caller's data.
- Indexed addressing (
message,X): using the X register as an offset into a labeled block of memory lets a loop step through consecutive bytes of a string instead of hard-coding each character. - Null-terminated strings (
.asciz): the assembler directive stores a string followed by a zero byte, which the loop uses as a natural stop condition by checking whether a loaded byte set the zero flag.
Walkthrough
Diagnosing why fixed delays don't reliably work (0:00)
The lecture recaps that the LCD's datasheet specifies each instruction takes about 37 microseconds, and tries inserting no-op instructions to add a matching delay. Even after tuning the delay repeatedly, it eventually takes roughly 750 no-ops (about 1.5 milliseconds) to reliably work, far more than the documented instruction time, showing that a hardcoded delay is fragile and wasteful.
Reading the busy flag (6:05)
The lecture removes the no-op delay entirely and builds a new lcd_wait subroutine: it sets port B to input, issues the busy-flag read instruction with register select low and read/write high, toggles enable, and reads the result from port B into the accumulator.
Branching on the busy flag (10:09)
After introducing the 6502's status flags, the CMP instruction, and the AND-based bit-masking technique, the lecture works out that AND #%10000000 isolates the busy flag and that BNE (branch if not equal, i.e. the masked result is nonzero) is the correct instruction to keep looping while the LCD is still busy.
Preserving the accumulator and wiring the wait into existing subroutines (17:19)
Because lcd_wait uses the accumulator internally, it pushes the accumulator's value onto the stack at entry and pulls it back before returning, so calling it from lcd_instruction or the character-printing subroutine doesn't destroy the value being sent. Both subroutines are updated to call lcd_wait before doing their work.
Comparing timing on the oscilloscope (20:20)
The lecture wires an oscilloscope to the LCD's enable pin and captures single-shot traces for both the 750-no-op version and the busy-flag-polling version, showing the new version completes noticeably faster overall, with the clear-display instruction taking the longest individual wait.
Rewriting the message as a loop with indexed addressing (23:23)
The message is moved into memory using a .asciz directive, and the character-by-character printing code is replaced with a loop that uses the X register as an offset (LDA message,X), increments X each pass, and calls the print-character subroutine, checking the loaded byte's zero flag to detect the string's null terminator and exit the loop.
Verifying the final program and extending the message (29:24)
After reassembling and reprogramming the EEPROM, "Hello world" prints correctly with the new loop-based code. The lecture then edits the message text and adds characters past the 40-character mark to show they wrap onto the display's second line, confirming the layout of the LCD's internal character memory.
Before you watch
- Watch the previous video (the clock-speed timing bug) first, since this lecture directly builds the fix it sets up.
- Familiarity with the 6502 opcode table, subroutines, and the stack from earlier videos in the series is assumed.
- Basic understanding of binary bit patterns is useful for following the AND-masking explanation.
Check your understanding
- Why was a fixed delay of no-op instructions an unreliable way to wait for the LCD, even after tuning it to work once?
- What does the
ANDinstruction do to the busy-flag byte, and why is that necessary before checking the zero flag? - Why does the
lcd_waitsubroutine need to push and pull the accumulator, and what would go wrong if it didn't? - How does indexed addressing with the X register let the printing loop step through the message one character at a time?
- How does the loop know when it has reached the end of the message, and what assembler feature makes that possible?
Chapters
- 0:00 Intro
- 2:33 Adding delay
- 7:03 Reading the busy flag
- 10:54 Conditional jump instructions
- 11:20 CPU flags
- 15:34 Branching
- 19:54 Testing
- 23:36 Improvements
- 26:01 Loading a message
From the YouTube description
More 6502: https://eater.net/6502
Support these videos on Patreon: https://www.patreon.com/beneater or https://eater.net/support for other ways to support.
------------------
Social media:
Website: https://www.eater.net
Twitter: https://x.com/beneater
Patreon: https://patreon.com/beneater
Reddit: https://www.reddit.com/r/beneater
Special thanks to these supporters for making this video possible:
Adam Lininger, Adrien Friggeri, Alexander Wendland, Anson VanDoren, Anthony Cuccia, Armin Brauns, Ben Dyson, Ben Kamens, Ben Williams, Bill Cooksey, Bouke Groenescheij, Bradley Pirtle, Bryan Brickman, Carlos Ambrozak, Charles Haseltine, Christopher Blackmon, Clayton Parker Coleman, Cole Johnson, Daniel Jeppsson, Daniel Tang, Dave Walter, David Boardman, David Brown, David Clark, David H. Friedman, David House, David Sastre Medina, David Turnbull, David Turner, Dean Winger, Dmitry Guyvoronsky, Dušan Dželebdžić, Dzevad Trumic, Emilio Mendoza Palafox, Eric Brummer, Eric Busalacchi, Eric Dynowski, Eric Twilegar, Erik Broeders, Eugene Bulkin, fxshlein, George Miroshnykov, Harry McDow, HaykH, Hidde de Jong, Ian Tait, Ingo Eble, Ivan Sorokin, Jason DeStefano, Jason Specland, JavaXP, Jay Binks, Jayne Gabriele, Jeremy A., Jeremy Wise, Joe Pregracke, Joel Jakobsson, Joel Messerli, Joel Miller, Johannes Lundberg, John Fenwick, John Meade, Jon Dugan, Jordan Scales, Joshua King, Kefen, Kenneth Christensen, Kent Collins, Koreo, Lambda GPU Workstations, London Dobbs, Lucas Nestor, Lukasz Pacholik, Maksym Zavershynskyi, Marcel Wiget, Marcus Classon, Martin Roth, Mats Fredriksson, Matt Alexander, Matteo Mohr, Matthäus Pawelczyk, Michael, Michael Burke, Michael Garland, Michael Tedder, Michael Timbrook, Miguel Ríos, Nicholas Counts, Nicholas Moresco, Örn Arnarson, Örper Forilan, Paul Pluzhnikov, Paul Randal, Pete Dietl, Philip Hofstetter, Randy True, Ric King, Richard Wells, Rob Bruno, Robert Butler, Robert Diaz, Sachin Chitale, sam raza, Sam Rose, Scott, Sergey Ten, SonOfSofaman , Stefan Nesinger, Stefanus Du Toit, Stephen Riley, Stephen Smithstone, Steve Jones, Steve Gorman, Steven Pequeno, TheWebMachine, Vladimir Kanazir, Warren Miller, xisente, Yusuke Saito
← Why build an entire computer on breadboards? · Binary to Decimal: Why It's Not That Simple →
