Mastering Raspberry Pi: Course 7 – Screen 02

Mastering Raspberry Pi: Course 7 - Screen 02

Screen 02 course builds on the foundation of Screen 01, teaching you how to draw lines and a small feature that generates pseudo-random numbers.

— Alex Chadwick

Screen 02 course builds on the foundation of Screen 01, teaching you how to draw lines and a small feature that generates pseudo-random numbers. Assuming you already have the operating system code from Course 6: Screen 01[1], we will build on it.

1. Points

Now that our screen is working properly, it’s time to create a more practical image, which is a natural progression. It would be even better if we could draw a line between two points on the screen, allowing us to combine these lines to create more complex shapes.

We will try to implement it using assembly code, but initially, we do need to use some other functions to assist. We need a function, which I will call SetPixel, to modify the color of a specified pixel, with inputs provided in registers r0 and r1. If the code we write can draw shapes in any memory, not just on the screen, this will be very useful later, so we first need some control methods for the actual drawing position. I think the best way to achieve this goal is to have a memory segment to store the shape to be drawn. What I should ultimately get is a storage address that typically points to the frame buffer structure from the last frame. We will use this drawing method throughout our code. This way, if we want to draw a different image in another part of our operating system, we can generate a different structured address value while using exactly the same code. For simplicity, we will use another data segment to control the color we are drawing.

To draw more complex shapes, some methods use a shading function instead of a color to draw. Each point can call the shading function to determine what color to draw there.

Copy the following code into a new file named drawing.s.

.section .data
.align 1
foreColour:
.hword 0xFFFF

.align 2
graphicsAddress:
.int 0

.section .text
.globl SetForeColour
SetForeColour:
cmp r0,#0x10000
movhs pc,lr
ldr r1,=foreColour
strh r0,[r1]
mov pc,lr

.globl SetGraphicsAddress
SetGraphicsAddress:
ldr r1,=graphicsAddress
str r0,[r1]
mov pc,lr

This code is what I mentioned above, a pair of functions and their data. We will use them in main.s to control where to draw what content before rendering images.

Our next task is to implement a SetPixel method. It needs to take two parameters, the pixel’s x and y axes, and it should use graphicsAddress and foreColour to precisely control where to draw what image. If you think you can implement this immediately, go ahead; if not, follow the steps we provide and implement it according to the example.

Building a generic method like SetPixel that we can build another method on top of is a good idea. But we must ensure this method is fast because we will use it frequently.

1. Load graphicsAddress.
2. Check if the pixel’s x and y axes are less than the width and height.
3. Calculate the pixel address to write to (hint: frameBufferAddress + (x + y * width) * pixelSize)
4. Load foreColour.
5. Save to address.

The above steps are implemented as follows:

1. Load graphicsAddress.

.globl DrawPixel
DrawPixel:
px .req r0
py .req r1
addr .req r2
ldr addr,=graphicsAddress
ldr addr,[addr]

2. Remember, the width and height are stored at offsets 0 and 4 in the frame buffer. You can refer to frameBuffer.s if necessary.

height .req r3
ldr height,[addr,#4]
sub height,#1
cmp py,height
movhi pc,lr
.unreq height

width .req r3
ldr width,[addr,#0]
sub width,#1
cmp px,width
movhi pc,lr

3. Indeed, this code is specifically for high-color frame buffers because I use a logical left shift operation to calculate the address. You might want to write a function version that doesn’t require a dedicated high-color frame buffer, and remember to update the code for SetForeColour. It may be more complicated to implement.

ldr addr,[addr,#32]
add width,#1
mla px,py,width,px
.unreq width
.unreq py
add addr, px,lsl #1
.unreq px

mla dst,reg1,reg2,reg3 multiplies the values in registers reg1 and reg2, then adds the result to the value in register reg3, storing the low 32 bits of the result in dst.

4. This is specific to high color.

fore .req r3
ldr fore,=foreColour
ldrh fore,[fore]

5. This is specific to high color.

strh fore,[addr]
.unreq fore
.unreq addr
mov pc,lr

2. Lines

The problem is that drawing lines is not as simple as you might imagine. So far, you must realize that when writing an operating system, almost everything must be done ourselves, and drawing lines is no exception. I suggest you take some time to think about how to draw a line between any two points.

I estimate that most strategies might be to calculate the gradient of the line and draw along it. This seems perfect, but it’s actually a terrible idea. The main problem is that it involves division, which we know is not easy to do in assembly, and keeping track of decimals is also challenging. In fact, there is an algorithm called Bresenham’s algorithm that is excellent for assembly code because it only uses addition, subtraction, and bit-shifting operations.

In our daily programming, we often neglect to optimize operations like division. But operating systems are different; they must be efficient, so we must always focus on how to do things as well as possible.

We start by defining a simple line-drawing algorithm, as shown below:

/* We want to draw a line from (x0,y0) to (x1,y1) using only a function setPixel(x,y), which draws a point at the given (x,y). */

if x1 > x0 then

set deltax to x1 - x0
set stepx to +1

otherwise

set deltax to x0 - x1
set stepx to -1

end if

if y1 > y0 then

set deltay to y1 - y0
set stepy to +1

otherwise

set deltay to y0 - y1
set stepy to -1

end if

if deltax > deltay then

set error to 0
until x0 = x1 + stepx

setPixel(x0, y0)
set error to error + deltax ÷ deltay
if error ≥ 0.5 then

set y0 to y0 + stepy
set error to error - 1

end if
set x0 to x0 + stepx

repeat

otherwise

end if

This algorithm represents what you might imagine. The variable error is used to track your distance from the actual line. Each step along the x-axis increases this error value, while each step along the y-axis decreases it by one unit. error is used to measure the distance from the y-axis.

While this algorithm is effective, it has a significant problem; clearly, we used decimals to store error, and we also used division. So, an immediate optimization would be to change the unit of error. There is no need to store it in specific units, as long as we scale it by the same amount every time we use it. Therefore, we can rewrite this algorithm by simply multiplying error by deltay in all equations involving error, simplifying it. Below only shows the main loop:

set error to 0 × deltay
until x0 = x1 + stepx

setPixel(x0, y0)
set error to error + deltax ÷ deltay × deltay
if error ≥ 0.5 × deltay then

set y0 to y0 + stepy
set error to error - 1 × deltay

end if
set x0 to x0 + stepx

repeat

It simplifies to:

set error to 0
until x0 = x1 + stepx

setPixel(x0, y0)
set error to error + deltax
if error × 2 ≥ deltay then

set y0 to y0 + stepy
set error to error - deltay

end if
set x0 to x0 + stepx

repeat

Suddenly, we have a better algorithm. Now, let’s see how to completely eliminate the division operations. It is best to keep the only multiplication by 2, which we know can be achieved by shifting left by one bit! Now, this is very close to Bresenham’s algorithm, but it can be further optimized. Now we have an if statement that leads to two code blocks, one for lines with a larger x difference and the other for lines with a larger y difference. For both types of lines, if examining the code can convert them into a single statement, it is still worth doing.

The difficulty lies in the fact that in the first case, error varies with y, while in the second case, error varies with x. The solution is to record them simultaneously in one variable, using negative error to represent an error in x, and positive error to represent it in y.

set error to deltax - deltay
until x0 = x1 + stepx or y0 = y1 + stepy

setPixel(x0, y0)
if error × 2 > -deltay then

set x0 to x0 + stepx
set error to error - deltay

end if
if error × 2 < deltax then

set y0 to y0 + stepy
set error to error + deltax

end if

repeat

You may need some time to figure it out. At each step, we correctly move in x and y. We do this by checking if moving in the x or y direction decreases the error value, then we continue to move in that direction.

Bresenham’s algorithm was developed in 1962 by Jack Elton Bresenham when he was 24 years old and pursuing his PhD.

The Bresenham algorithm for drawing lines can be described by the following pseudocode. The following pseudocode is text that just looks a bit like computer instructions, but it allows programmers to genuinely understand the algorithm rather than being machine-readable.

/* We want to draw a line from (x0,y0) to (x1,y1) using only a function setPixel(x,y), which draws a point at the given (x,y). */

if x1 > x0 then
    set deltax to x1 - x0
    set stepx to +1
otherwise
    set deltax to x0 - x1
    set stepx to -1
end if

set error to deltax - deltay
until x0 = x1 + stepx or y0 = y1 + stepy
    setPixel(x0, y0)
    if error × 2 ≥ -deltay then
        set x0 to x0 + stepx
        set error to error - deltay
    end if
    if error × 2 ≤ deltax then
        set y0 to y0 + stepy
        set error to error + deltax
    end if
repeat

Unlike the numbered list we are currently using, this algorithm’s representation is more common. See if you can implement it yourself. Below I provide my implementation as a reference.

.globl DrawLine
DrawLine:
push {r4,r5,r6,r7,r8,r9,r10,r11,r12,lr}
x0 .req r9
x1 .req r10
y0 .req r11
y1 .req r12

mov x0,r0
mov x1,r2
mov y0,r1
mov y1,r3

dx .req r4
dyn .req r5  /* Note, we only use -deltay, hence I save its negative value. (thus named dyn)*/
sx .req r6
sy .req r7
err .req r8

cmp x0,x1
subgt dx,x0,x1
movgt sx,#-1
suble dx,x1,x0
movle sx,#1

cmp y0,y1
subgt dyn,y1,y0
movgt sy,#-1
suble dyn,y0,y1
movle sy,#1

add err,dx,dyn
add x1,sx
add y1,sy

pixelLoop$:

    teq x0,x1
    teqne y0,y1
    popeq {r4,r5,r6,r7,r8,r9,r10,r11,r12,pc}
    
    mov r0,x0
    mov r1,y0
    bl DrawPixel
    
    cmp dyn, err,lsl #1
    addle err,dyn
    addle x0,sx
    
    cmp dx, err,lsl #1
    addge err,dx
    addge y0,sy
    
    b pixelLoop$

.unreq x0
.unreq x1
.unreq y0
.unreq y1
.unreq dx
.unreq dyn
.unreq sx
.unreq sy
.unreq err

3. Randomness

So far, we can draw lines. While we can use it to draw images and such (feel free to do so!), I think we should take this opportunity to introduce the concept of randomness in computers. I will do this by choosing a pair of random coordinates and drawing a line to that point from the previous pair using a gradient color. I do this purely because it looks nice.

So, to summarize, how can we generate random numbers? Unfortunately, we do not have some device to generate random numbers (such devices are rare). Therefore, we can only utilize what we have learned so far to somehow invent “random numbers.” You will quickly realize that this is impossible. Various operations always yield defined results; running the same sequence of instructions on the same register will always give the same answer. What we need to deduce is a pseudo-random sequence. This means that the numbers appear random to outsiders, but in reality, they are completely determined. Therefore, we need a formula to generate random numbers. Some might think of some garbage mathematical operations like: 4x2! / 64, which actually produces a low-quality random number. In this example, if x is 0, then the answer will be 0. It seems foolish, and we need to be very careful in choosing a formula that can produce high-quality random numbers.

Hardware random number generators are rarely used in security, as predictable random number sequences may affect the security of certain encryptions.

The method I am going to teach you is called the “Quadratic Congruential Generator.” This is a very good choice because it can be implemented in 5 instructions and can produce a seemingly random sequence of numbers between 0 and 232-1.

Unfortunately, the study of why using so few instructions can produce such a long sequence has far exceeded the scope of this course. But I still encourage those interested to research it. Its entire core lies in the following quadratic equation, where xn is the nth generated random number.

This kind of discussion often seeks a question: what do we call random numbers, anyway? Usually, from a statistical perspective, randomness is: a sequence of numbers that has no obvious patterns or properties that can summarize it.

Mastering Raspberry Pi: Course 7 - Screen 02

This equation is subject to the following restrictions:

1. a is even
2. b = a + 1 mod 4
3. c is odd

If you have not seen the mod operation before, let me explain; it means the remainder after dividing by the number that follows it. For example, b = a + 1 mod 4 means b is the remainder of a + 1 divided by 4. So, if a is 12, then b will be 1, because a + 1 is 13, and 13 divided by 4 gives a remainder of 1.

Copy the following code into a file named random.s.

.globl Random
Random:
xnm .req r0
a .req r1

mov a,#0xef00
mul a,xnm
mul a,xnm
add a,xnm
.unreq xnm
add r0,a,#73

.unreq a
mov pc,lr

This is an implementation of the random function, using a value finally generated in register r0 as input, while the next number is the output. In my case, I use a = EF0016, b = 1, c = 73. This choice is arbitrary but needs to meet the above restrictions. You can use any numbers to replace them, as long as they comply with the aforementioned rules.

4. Pi-casso

OK, now we have all the functions we need, let’s try them out. After obtaining the address of the frame buffer information, modify main as follows:

1. Use the register r0 containing the frame buffer information address to call SetGraphicsAddress.
2. Set four registers to 0. One will be the last random number, one will be the color, one will be the last x coordinate, and the last will be the last y coordinate.
3. Call random to generate the next x coordinate, using the last random number as input.
4. Call random again to generate the next y coordinate, using the generated x coordinate as input.
5. Update the last random number to the y coordinate.
6. Use the colour value to call SetForeColour, then increment the colour value. If it exceeds FFFF~16~, ensure it returns to 0.
7. The generated x and y coordinates will be between 0 and FFFFFFFF16. By logically right shifting them 22 bits, convert them to numbers between 0 and 102310.
8. Check if the y coordinate is on the screen. Verify that the y coordinate is between 0 and 76710. If it is not in this range, return to step 3.
9. Draw a line from the last x and y coordinates to the current x and y coordinates.
10. Update the last x and y coordinates to the current coordinates.
11. Return to step 3.

As always, you can find this solution on the download page.

After you finish, test it on the Raspberry Pi. You should see a series of color-incrementing random lines appearing on the screen at a very fast rate. It continues indefinitely. If your code doesn’t work correctly, please check our troubleshooting page.

If everything goes well, congratulations! We have now learned meaningful graphics and random numbers. I encourage you to use it to draw lines because it can be used to render anything you want, and you can explore more complex patterns. Most of them can be generated by lines, but that requires better strategies? If you are willing to write a line drawing program, try using the SetPixel function. What happens if instead of setting the pixel value, you increment it a bit? What kind of patterns can you generate with it? In the next lesson Course 8: Screen 03[2], we will learn the valuable skill of drawing text.

via: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/screen02.html

Author: Alex Chadwick[4] Topic: lujun9972 Translator: qhwdw Proofreader: wxy

This article is originally compiled by LCTT and proudly presented by Linux China

Mastering Raspberry Pi: Course 7 - Screen 02

Leave a Comment