Saturday, December 27, 2025

Programming VGA: Smooth Scrolling in Text Mode #3: Modifying Base Address


Hi there. This article will be an unexpected follow-up to the previous smooth scroll articles. In the second article of the series, I introduced start address registers and mentioned, that the scroll effect could also be made using these registers and VGA pages, without copying memory blocks. I watched the following video while preparing previous article, and it inspired me to shortly demonstrate this approach as well.



First of all, the owner of this channel does excellent work in retro programming. I'd recommend anyone interested in retro programming, to follow this channel. I had mentioned, that my former approach is CPU intensive due to memory transfer, but in turn, it only uses as much VGA memory as visible screen area. In the video above, an ASCII art text is scrolling up and down at a speed tied to a sine function, and it uses start address registers to scroll the text.

I've implemented this approach in a less visual way. First, I'm not capable of creating such visually appealing work, and second, I had a ready-made code for this task, all I had to do, was modifying it just a bit. Some time ago, I had written a simple reader for a diskmag. I always liked justified text to both sides. This is clearly noticeable in my blog's page layout, I think. Diskmag had been published in text files, limited to 80 characters per line. I had developed a justification algorithm to read these texts on whole screen. The original reader had just some extra features like header and footer lines as well as some escape character codes.

This time, I put my code on github gist and embedded it at the end of this article (let's see. If it doesn't look good, next time I'll add just a link like before). The justify_to_80() function is used to span a row to eighty characters. To do this, the number of characters and spaces in a line are counted. The number of additional spaces needed is then calculated based on these values. If spaces needed exceed the number of existing spaces between words in the line (e.g., the line consists of 3-4 long words), then the each variable holds the number of spaces to be added next to each existing space between words (line 33). On the other hand, if the line consists of many short words (i.e. many already-existing spaces) and just a few spaces are needed to complete it to eighty, in this case extra variable holds the number of spaces that need to be added. Of course, both variables can be non-zero at the same time but statistically speaking, each is usually zero and extra is non-zero most of the time.

Inserting each times space characters is easy,  as they will be inserted to each existing space anyways (line 42). Distributing extra times spaces evenly to a line, is a bit more complicated. The weight variable holds how many spaces need to be added per space in the line. weight is a double type variable, because the number of missing characters cannot be divided by the number of spaces without remainder for most cases. The value of weight increases by extra / spaces for each existing space character (line 46). Depending on this value, a space character is added, whenever it reaches an integer, like when it goes from 1.9 to 2.1. Let's consider following line:

They were hidden from the road by a shallow ridge, but there was only sparse

It has 76 characters, but strlen() returns 77, because it counts CR LF as well. That's also why this number is subtracted from 81 in line thirty two. missing = 4, space count is 14. Therefore each = 0, extra = 4. In the for loop in the thirty sixth line, characters are processed one by one. If it hits a space (line 40), the inner for loop has no effect (it's skipped) because each = 0 in this example. Since sp1 = 0, the weight variable is initially 0. At the second space character (between the words "...were hidden..."), weight = 1 * (4 + 1) / 14 ≈ 0.36. Because of sp1, weight will increase linearly. So, following values are obtained for this example for each space:

0.00   0.36   0.71   1.07   1.43   1.79   2.14   2.50   2.86   3.21   3.57   3.93   4.29   4.64

The integer crossings here occur at fourth (0.71 -> 1.07), seventh (1.79 -> 2.14), tenth (2.86 -> 3.21) and thirteenth (3.93 -> 4.29) spaces.

Actually, looking at both its explanation above, and the number of code lines, this function is more complex than the smooth scroll algorithm itself.

The vga_set_base_addr() function multiplies the lineP by eighty (line 62), writes its high byte to Start Address High and it low byte to Start Address Low registers. lineP is a counter, that determines which line will be displayed at the top of the screen.

I explained waitbl() in detail in the previous article.

vgaprint() copies the given string to the video memory. Since the lines returned by justify_to_80() don't contain '\n', while others coming directly (last paragraph line) do have '\n' at the end, it was necessary to implement a workaround like the line eighty-six. In the for loop (line 89), exactly eighty characters are printed. Even if a line has less than eighty characters (last line of a paragraph), space characters overwrite existing characters in the line, if any. I actually added, making linecount a local variable, to the TODO list (on the line 14 of the code). If the value of this variable was increased in main(), it could easily be made local. However, investigating main() below, it will become clear, that these parts are written bit hastily.

main() first of all, is relatively big. The first while block (line 121) and the second one, handling scrolling (line 137) could have been written as two separate functions. Second, input sanitization should perhaps have been more featureful, but I'm also aware that every diskmag file will have shorter lines than 80 characters. Most important constraint is actually, that an input file cannot be longer than 409 lines. VGA text mode video memory is 32 KB, between B800:0000 and B800:7FFF (color). Max 409 lines of 80 characters can fit here. Input file size cannot be trusted in this case, because we'll be adding spaces to the file.

A line is read from the file (line 119) even before the first while loop, and it's assumed that this line is not the end of a paragraph (EOP). The next lines are read inside the while loop, and checked whether it's a blank line or end of file. The aim here is not to justify any line at EOP, and if there is a blank line after the current line, that current line marks the EOP. If the line is not EOP, it is justified to 80 characters and the execution processes the next line. In short, the next line is also checked at each step.

ESC key leaves the second while block. Keyboard input is checked in the switch/case structure. If the up arrow is pressed, the top line number variable is decreased by 1 (line 142), similarly down arrow key increases it by 1. Of course, while doing this, the number of lines in the file is also checked, so that the text always stays wholly on the screen, it doesn't scroll off the top or bottom. Scrolling effects here are exactly the same as in the previous code, and even simpler as no memory is copied. The only difference is that scroll speed of the arrow keys is dependent on the SCROLLSTEPFINE parameter, and scroll speed of the Pg Up and Pg Dn keys is dependent on the SCROLLSTEPCOARSE parameter. Decreasing these slows down scrolling, increasing these reduces the effect and speeds up the scrolling.

The logic behind the scrolling with Pg Up and Pg Dn keys is exactly the same as scrolling with the arrow keys. However, the effect is intensified by doing 24 small consecutive line scrolls up or down in a for loop.

I've embedded a video of this below, but due to the recording, the scrolling doesn't look right in the clip when scrolling with Pg Up or Pg Dn.

And the source code is given below:

Sunday, December 7, 2025

How to Drive an 8x8 LED Matrix Display with MAX7219 using Arduino


Hi there. In this article, I'll take a look at the MAX7219 integrated circuit and explain, how to drive an 8x8 LED Matrix with this IC. Since there is an Arduino library for this chip, I'll use Arduino for my examples. However, SPI (Serial Peripheral Interface) protocol is independent of microcontroller and my examples can be easily ported to other microcontrollers as well. MAX7219 is a fairly simple IC, it is also really easy to use it without any library. I'll give two versions of an example code, one with library and another without.


MAX7219 and MAX7221 ICs

MAX7219 and MAX7221 ICs provide an interface to 7 segment displays or 8x8 LED matrices via SPI. While they are pin and instruction compatible, they share same datasheet and same functions, MAX7221 supports other serial protocols besides SPI and operates in a more robust way. Therefore, it is also more expensive.

These ICs support up to eight 7 segment displays with decimal points, bar graph and 8x8 LED matrix displays, and have builtin decoding options for them. Up to 64 LEDs can be driven through 8 common cathodes. As I bought an assembled kit, I won't dive deeper into IC pin connections.

These ICs can also be cascaded up to eight times, which means, by purchasing eight of these modules and connecting their DOUT pin to the DIN pin of next module in a chain, I can drive a larger number of LED displays. The second 5-pin connector on the distal side of the kit is for cascading. Different displays can also be cascaded.

Quad 8x8 LED matrix displays are also available as pre-assembled kits. I also bought a quad kit, to understand and experiment cascading. I'll explain it in detail later in this article.

By looking at the PCB from back, you can see how simply and easily the cascading is done:

The module has five pins. Vcc and GND pins require no explanation. DIN (Data In) pin is where the data is written to. CS (Chip Select) pin is active low. When this pis is set to active, the latches are enabled and the data coming thru DIN is received by the internal shift registers. In the meantime, CLK carries the clock signal synchronously with the data coming from DIN. The data is processed with the rising edge of CS.


Registers of MAX7219

At the beginning of this article, I mentioned that the MAX7219 is a fairly simple IC. All the functionality is handled by a total of 14 registers and eight of them are simple data registers that control LEDs. For convenience, I've copied the table of registers from the datasheet and pasted it to the right side.

All of these addresses (or commands from another perspective) are 8-bit, and each of them is followed by another 8-bit data (or operand). In other words, each data packet arriving at DIN must be 16-bit. To send data, CS' (nChipSelect) signal must be set to logic zero first, and the data must be sent to DIN synchronously with CLK. Setting CS' back to logic 1 terminates data transmission. Communication is described in detail in the "Serial Addressing Modes" section of the datasheet.

I'll come to the first register No-Op later, because No-Op doesn't actually perform a No-Op.

The registers Digit 0 to Digit 7 are holding the LED states, i.e. a row of a 8x8 LED Matrix display or a segment of a 7-segment display. For example, by sending the value 0x0F to the Digit 0 register, i.e. by pushing the data 0x01 0x0F onto data bus, I turn the rightmost four LEDs of the first row on, and turn the leftmost four off. For Digit 1, this controls the second LED row, for Digit 2, the third row, and so on.

Decode Mode (0x09) controls the internal 7-segment decoder unit of the IC. If it has 0xFF, the ICs only looks at the lower four bits of Digit registers and decodes them for a 7-segment display. If it has 0x00, no decoding is performed. This is the appropriate mode for 8x8 LED Matrix displays.

Intensity (0x0A) register is used to adjust the LED intensity with PWM.

Scan Limit (0x0B) register is used to optimize the scan rate of LEDs, if not all LEDs will be used (e.g. a 7-segment display without decimal point), by deactivating unused LED pins. As all LEDs are used in a LED Matrix display this should be usually zero.

If the shutdown (0x0C) register is zero, the IC shuts itself down. The scan oscillator of IC is shut down and all LEDs turn off. The supply current drops to 150 µA. It is in mA range during normal operation, and is approx. 300 mA when all LEDs are on. The IC boots up in shutdown mode. Therefore, first step of initialization is to write 0x1 to this register.

When 0x1 is written to the display test (0x0F) register, all LEDs turns on. This allows you to check if any of them are faulty. During the test, Digit registers are not touched, the output is overridden. When 0x0 is written here, IC returns to its normal operating mode.

No-Op operation (0x0) is used for cascading ICs. This has no effect on the display receiving the command. The IC receiving a No-Op simply sends the subsequent command via DOUT pin. For example, in a quad 8x8 LED matrix display, if you need to do something just with the fourth display, the microcontroller issues three No-Op commands followed by the actual operation. In this case, first display receives these data, stripes first No-Op and sends two No-Ops followed by the actual operation to the second module via its DOUT pin. Second display likewise sends just one No-Op and the actual operation to the third display. The third display receives the remaining No-Op and the operation destined to fourth display and forwards only the actual operation one last time to the fourth. Below is a diagram illustrating this process.



The block diagram in the datasheet doesn't show a register named "No-Op", but since it is covered under the "No-Op Register" section on page ten, I can't tell if this is an operation or a register. By the way, as it can be seen above, even though it's completely meaningless, even No-Op command is 16-bit, so it has be to packed with a 8-bit data.


Arduino LedControl Library

After explaining the registers in such detail, using a library may actually seem pointless, but the LedControl library simplifies some tasks quite a lot. For example, while Digit registers provide row-by-row access to LEDs, the library has some other functions like setColumn(), setLed() and setChar() in addition to setRow().

To install the library, go to Tools -> Manage Libraries in Arduino IDE and search for "LedControl" and click Add. Then, include LedControl.h header file in your code and create a LedControl object. While creating this, you need to pass which pin is connected to which Arduino pin and how many devices are cascaded, to constructor function. E.g.

#include "LedControl.h"
LedControl lc = LedControl(data = 12, clk = 11, chipSel = 10, 4);

In all my examples, DIN is connected to Arduino's 12th pin, CLK to the 11th and CS to the 10th pin, like this:

The Matrix display image in Fritzing has six pins. The top pin is not connected, which is the second Vcc, so it doesn't really matter.


Code Examples

My first example is a simple character scrolling. Here, the characters are scrolled module by module. There won't be any bit operations. I uploaded the code to my github account. As I mentioned before, MAX7219 starts up in shutdown mode. In the for loop, on line 61 inside the setup() routine, each display is first taken out of shutdown mode one by one, and LED intensity is set to lowest. The "dizi" array holds the character sequence to be displayed. It is actually a string variable in broader sense. At the end of the array, the first three characters repeat for endless scrolling effect on display.

The "table" array contains the bitmaps of characters. I downloaded a font package from here, and used BIOS.F08 font. This file contains the classic 8x8 BIOS font. I opened it in GIMP and exported it as C source code or C header. Since the entire character table 2 KB (256 * 8), it is not possible to to load whole table to Arduino UNO's 2 KB RAM, yet it's not necessary anyway. I only imported the characters, that is going to be displayed. In the main for loop, the values of bitmaps are sent to MAX7219 row by row using the setRow() function. Although the character sequence is 20 characters long, when the pointer value is 16, 16th, 17th, 18th and 19th characters will appear on the display, so the pointer must not exceed 20 - 4.


Second example (counter) is a counter as its name suggests, which is even simpler than the first example. Arduino increments the values in the "a" array in full speed, starting from the zero-indexed element. In the for loop on line 25, the array elements are checked for overflow at byte boundary. If an overflow occurs, this array element is reset, and the carry is transferred to the next element (line 29), and this binary counter is visualized with LEDs on line 32.

Counting from zero to 256 takes less than a second. The LED corresponding to the tenth bit flashes approximately at one second intervals. If we ignore less significant nine bits, it would take roughly 2(64-10)=254 seconds for the remaining 54 LEDs to light up completely, which is about 5.709 * 108 (571 million) years.


My third example is the same of the second one, but I wrote it without library. In this code, the pins are first set to OUTPUT. Then, the IC is taken out of shutdown mode (line 32), the scan limit is set to 7 (line 37), decoding is disabled on line 43, as we have an 8x8 LED display. That's the initialization sequence. On line 51, the LED intensity is set to the lowest level and all LEDs are cleared (line 60). The logic in the loop() procedure is same as the above example, with one difference. Each array element is written to the corresponding digit register directly row by row (line 77).

As you can see, the IC can also be easily programmed without library. My goal here was not perform a speed test with or without library. I'd probably get similar results anyway. But if my focus were speed, I would be using Assembly.


In the fourth and final example, I created a smooth scrolling text using bit operations. To do this, I took two long German words. German is really perfect for this task. I converted these words into char arrays (lines 16 or 19). I then created a bitmap table like I did in the first example, but with more characters this time. As usual, the IC is initialized in the setup() routine, and the bitmap images of characters are copied to the "kayanyazi" array.

In the loop() function, the first four characters of "kayanyazi" array are sent to display. Since the text will be scrolled to the left, I assign the most significant bits (MSB) of each LED line to the "carry_old" variable, which means, carry_old contains the first column of character array (or first LED column). Then all characters are shifted by one bit (line 108), but the first column of each character is copied to carry_new (line 106) before any shift operation, so that any carry bits of byte order is kept before it gets lost and this is inserted to the least significant bit (LSB) of the trailing character.