Control a Macintosh Classic CRT with a BeagleBone Black – Part 2

This article will cover the software aspect of my Macintosh Classic project. The final solution is running on a BeagleBone Black, using its programmable realtime unit. If you want to learn more about PRU programming, you can do so here.

The software of this project loads an image into the BBB’s DDR memory and then sends the data over to the display with the waveforms discussed in Part 1 of the series:

macintosh_cat
Figure 1: Macintosh Classic displays an image of a cat.

Currently I’ll not upload the complete code, because I’m not completely satisfied with it and it still has an error or two, but I’ll upload it completely as soon as it is perfect. One of the things that bothers is the noise generated at the right hand side of the image. This comes due to slightly imprecise timing of the video signal. Also the current version of the code generates 16 black bars on the screen running vertically over the screen.

I managed to fix the issues and the complete code is now available for download at github!
Here is an image of the updated software displaying the cat image:

IMG_1721
Figure 1.1: Fixed the black-bar-error.

Difficulties during development

I ran into different difficulties while working on this project and I want to tell you about the biggest mistakes I made, so you don’t have to run into all the problems:

The first major problem was to select the wrong platform for this project. I first tried to realize it with a Raspberry Pi which did not work at all. Please don’t misunderstand me: The Pi is a very nice (and cheap) little machine, but unfortunately it’s simply not capable of doing stuff in realtime while having an embedded linux system running. But I learned something about precise signal generation with the Pi, useful for a lot of things, for example controlling servos.

The first version took about two, maybe three hours to develop. The program itself is neither that complicated nor very long. Unfortunately it was not working (I’ll show you the part that caused this problem in the listing below) properly. It gave me something like this:

IMG_1505
Figure 2: The CRT only shows the same image fragment 16 times, not the whole image

I tried to find the cause for this error, but I simply couldn’t. I calculated everything over and over again and the code seemed completely correct but the output wasn’t. After some hours of calculating, changing the code, compiling and testing it over and over again I decided it would be a great idea to delete the whole code (and the github repo it was in) and to start all over. At this point I thought that the problem was in the host program and it had something to do with writing the data to the RAM. I had a very nice solution which was pretty complicated to read (but short and elegant), so I tried to write a much simpler (and longer) solution, which I did. After I came up with a simpler solution (which basically did the same thing as the complicated one) I tested it and it still did not work. So I recalculated each address and pixel again and then it finally came to my mind that it might be the PRU program, that caused that error. And then I found it and I felt so stupid afterwards. It was a simple typo:

In the section that reads one line of pixel data from the DDR memory I had an address I was incrementing after each load operation by 0x04 instead of 0x40. Which made a difference of 60 bytes. So that was the error that almost made me go insane. A simple typo. What I want to say: Sometimes your eyes make you see what you want them to see. Try to read every value carefully and don’t delete your code.

About the code

The program consists of two separate applications and one device tree overlay:

The device tree
This file describes which pins will be used by the PRU program. I’ll not cover this topic here, but if you want to read more about it, you can do so here.

The host application
This part of the program covers the memory management. It is written in C and allocates the necessary memory to hold one frame in, writes new frames into the memory and notifies the PRU program when a frame is ready to be drawn. It is also needed for starting and halting the PRU and for general user IO.

The following snippet shows how the host program writes the image data to the the BBB’s DDR memory. Please note that constant variable values and memory allocation are not included, it is just a snippet. The complete code can be downloaded from github.

void writeFrameToVRAM(void)
{
    // Write each line of the video data to
    // the memory.

    for(int i = 0; i < DISP_HEIGHT; i++)
    {
           // Compose the address to write the current
           // line to.
	   volatile unsigned long *data_addr = vram + 
                                               DDR_OFFSET +
                                               LINEBYTES * i;

           // Optional step to make the code easier
           // to understand: Get one line of pixel data
           // out of the complete screen data
           // One line consists of 64 bytes (64*8 = 512 pixels)
	   volatile char lineData[64];
	   int byteCounter = 0;

           // Pack together one line of video data
           // into a seperate array
	   for(int o = 0; o < 64; o++)
		lineData[o] = test_image[i*64+o];
		
	    // Now split the created array into 16 chunks
            // of 32 bits each (This is the max. address
            // length on the PRU, so it is also the max.
            // size of data you can fit into one register
            // in your PRU program)
            for(int u = 0; u < LINE_REGISTERS; u++)
	    {
	        volatile char registerBytes[4];
		for(int c = 0; c < REGISTER_BYTES; c++)
		{
		    volatile char currByte;
		    if(INVERT_COLORS == 1)
		        currByte = ~lineData[byteCounter];
		    else
			currByte = lineData[byteCounter];
				
		    registerBytes[c] = currByte;
		    byteCounter++;
		}
			
		// Now cast the register data array to a
                // 32-bit data type (i.e. unsigned long)
		volatile unsigned long regData = 0l;
		
                // Watch out for the correct byte order!
		regData += registerBytes[0] << 24;
		regData += registerBytes[1] << 16;
		regData += registerBytes[2] << 8;
		regData += registerBytes[3];
		
		// Now simply write the register data to the
                // DDR memory and increase the data address.
		*data_addr = regData;
		data_addr++;
	    }
      }
}

The PRU application
This is the delicate bit. It reads the video data and then bit bangs it over to the CRT’s display hardware. It also generates the horizontal and vertical synchronization signals that are needed to build an image on the screen. This section needs to be perfectly timed, which can easily be missed. I calculated the exact timings, but this part is really easy to mess up and I have to tweak the timings a little bit to get a perfect result.

The first snippet shows how the data is read from DDR memory:

.macro READVRAMDATA
	LBBO    r4, r27, 0, 64
	ADD	r27, r27, 0x00000040
.endm

Even though this is just a two line macro this was the bit that caused me a lot of problems during development. the ADD instruction’s third parameter was misspelled, so the address was incremented by a wrong value and thus a wrong section of the memory was read. There is nothing special to say about this macro, just that it reads 64 bytes from the DDR memory (at the address stored in r27) and stores the data to the registers r4 to r20. The address is incremented afterwards. The following macro shows the bit-banging part of the application:

.macro WRITEREG
.mparam reg
	MOV    r21, 31
	QBA    WRITEPIXEL
	WRITEPIXEL:
		SUB        r21, r21, 1
		DELAY_NS   85;
		QBBS	   WRITEPIXEL_HIGH, reg.t31
		QBBC	   WRITEPIXEL_LOW, reg.t31
	WRITEPIXEL_HIGH:
		SET	   r30.t0
		LSL	   reg, reg, #1
		QBNE	   WRITEPIXEL, r21, 0
                QBA        WRITEPIXEL_END
	WRITEPIXEL_LOW:
		CLR	   r30.t0
		LSL	   reg, reg, #1
		QBNE	   WRITEPIXEL, r21, 0
        WRITEPIXEL_END:
                NOP
.endm

Update (See bold lines in the snippet above): I fixed the bug where the display showed vertical black lines across the image. This was due to the fact that I forgot to tell the processor to jump to the end of the WRITEREG macro after it had written all the bits of one one register. So if the last pixel was white, the display put out an additional black pixel afterwards. The new code jumps to the end (thus leaving the WRITEPIXEL_LOW untouched) when the last bit was output.

This macro bursts out one register at a time. It simply sets the output register’s bit zero to the value of the bit that is currently being sent out and then waits a specified time until it repeats the process. This is done 32 times in total, so each pixel of the register is sent once. There might be a better solution for this (like using logical functions) and I suspect the cause of the timing issues here, so I have to improve this section before I publish the final code. Anyway, the output generated by the PRU looks like shown in fig. 3:

macintosh_crt_pixeldata_somedata
Figure 3: The video data (Ch1, yellow) and the HSYNC signal (Ch2, cyan)

Basically this is all the PRU program does. It reads the data for one line and then sends each register, that holds pixel data, to the CRT. It also generates the HSYNC and VSYNC signal according to the timing table discussed in Part 1 of the series. It generates the two signals the same way I discussed it in this article.

The complete code

I’ll update this section as soon as I fixed the issues described above.
The complete code is available for download at github!

Table of contents

Part 0 – The story behind the project
Part 1 – The CRT
Part 2 – The Software (You are here)
Part 3 – Additional Thoughts

An alternative version of this project using a Raspberry Pi 4.

comment-banner

27 thoughts on “Control a Macintosh Classic CRT with a BeagleBone Black – Part 2

  1. I couldn’t resist commenting. Perfecfly written!|
    I’ll right away grab your rss as I can not too find your e-mail subscription hyperlink
    or e-newsletter service. Do you’ve any? Kindly allow me recognise
    in order that I could subscribe. Thanks.

    Like

    1. You can subscribe to our email newsletter in the sidebar on the right side of this page. If you’re viewing this page on a mobile device, you can find the subscription form in the footer.
      Cheers!

      Like

    1. You are right, my program doesn’t do the dithering. I’ve done it in Photoshop. Then I’ve written a second application, that’s not running on the Macintosh, which converts JPG images to the binary format used by my Macintosh PRU application.
      Cheers!

      Like

  2. do you have any pictures of the display working correctly? what does the displayed image look like, because the image on git is also shown with the bars

    Liked by 1 person

    1. I shot one quick picture for you, but there will be more later on. I’m also planning to make a video when I finished the project completely, where I show the macdisplay to you guys in it’s total glory.
      Link to the image

      EDIT: I also inserted the image into the article. Enjoy!

      Like

  3. This looks like an interesting project. Have you somehow managed to display the output of the linux computer on the screen or is it just displaying the cat images? I’m looking forward to read the last part btw, hope you publish it soon!

    Liked by 1 person

    1. Hi!
      Thanks for your interest. A short answer: No I have not managed to display the OS’s output on the CRT, but I’m working on it. Currently I have some other projects to work on, so this might take a while to finish and as soon as I finished it, I’ll upload the last article of the series.

      Like

Leave your two cents, comment here!

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.