Translated by qhwdw

This commit is contained in:
qhwdw 2019-02-07 12:55:22 +08:00
parent db499ef228
commit 851cded669
No known key found for this signature in database
GPG Key ID: 59DADCB9A1C4B3C0
2 changed files with 504 additions and 503 deletions

View File

@ -1,503 +0,0 @@
[#]: collector: (lujun9972)
[#]: translator: (qhwdw)
[#]: reviewer: ( )
[#]: publisher: ( )
[#]: url: ( )
[#]: subject: (Computer Laboratory Raspberry Pi: Lesson 6 Screen01)
[#]: via: (https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/screen01.html)
[#]: author: (Alex Chadwick https://www.cl.cam.ac.uk)
Computer Laboratory Raspberry Pi: Lesson 6 Screen01
======
Welcome to the Screen lesson series. In this series, you will learn how to control the screen using the Raspberry Pi in assembly code, starting at just displaying random data, then moving up to displaying a fixed image, displaying text and then formatting numbers into text. It is assumed that you have already completed the OK series, and so things covered in this series will not be repeated here.
This first screen lesson teaches some basic theory about graphics, and then applies it to display a gradient pattern to the screen or TV.
### 1 Getting Started
It is expected that you have completed the OK series, and so functions in the 'gpio.s' file and 'systemTimer.s' file from that series will be called. If you do not have these files, or prefer to use a correct implementation, download the solution to OK05.s. The 'main.s' file from here will also be useful, up to and including mov sp,#0x8000. Please delete anything after that line.
### 2 Computer Graphics
There are a few systems for representing colours as numbers. Here we focus on RGB systems, but HSL is another common system used.
As you're hopefully beginning to appreciate, at a fundamental level, computers are very stupid. They have a limited number of instructions, almost exclusively to do with maths, and yet somehow they are capable of doing many things. The thing we currently wish to understand is how a computer could possibly put an image on the screen. How would we translate this problem into binary? The answer is relatively straightforward; we devise some system of numbering each colour, and then we store one number for every pixel on the screen. A pixel is a small dot on your screen. If you move very close, you will probably be able to make out individual pixels on your screen, and be able to see that everything image is just made out of these pixels in combination.
As the computer age advanced, people wanted more and more complicated graphics, and so the concept of a graphics card was invented. The graphics card is a secondary processor on your computer which only exists to draw images to the screen. It has the job of turning the pixel value information into light intensity levels to be transmitted to the screen. On modern computers, graphics cards can also do a lot more than that, such as drawing 3D graphics. In this tutorial however, we will just concentrate on the first use of graphics cards; getting pixel colours from memory out to the screen.
One issue that is raised immediately by all this is the system we use for numbering colours. There are several choices, each producing outputs of different quality. I will outline a few here for completeness.
Although some images here have few colours they use a technique called spatial dithering. This allows them to still show a good representation of the image, with very few colours. Many early Operating Systems used this technique.
| Name | Unique Colours | Description | Examples |
| ----------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------- |
| Monochrome | 2 | Use 1 bit to store each pixel, with a 1 being white, and a 0 being black. | ![Monochrome image of a bird][1] |
| Greyscale | 256 | Use 1 byte to store each pixel, with 255 representing white, 0 representing black, and all values in between representing a linear combination of the two. | ![Geryscale image of a bird][2] |
| 8 Colour | 8 | Use 3 bits to store each pixel, the first bit representing the presence of a red channel, the second representing a green channel and the third a blue channel. | ![8 colour image of a bird][3] |
| Low Colour | 256 | Use 8 bits to store each pixel, the first 3 bit representing the intensity of the red channel, the next 3 bits representing the intensity of the green channel and the final 2 bits representing the intensity of the blue channel. | ![Low colour image of a bird][4] |
| High Colour | 65,536 | Use 16 bits to store each pixel, the first 5 bit representing the intensity of the red channel, the next 6 bits representing the intensity of the green channel and the final 5 bits representing the intensity of the blue channel. | ![High colour image of a bird][5] |
| True Colour | 16,777,216 | Use 24 bits to store each pixel, the first 8 bits representing the intensity of the red channel, the second 8 representing the green channel and the final 8 bits the blue channel. | ![True colour image of a bird][6] |
| RGBA32 | 16,777,216 with 256 transparency levels | Use 32 bits to store each pixel, the first 8 bits representing the intensity of the red channel, the second 8 representing the green channel, the third 8 bits the blue channel, and the final 8 bits a transparency channel. The transparency channel is only considered when drawing one image on top of another and is stored such that a value of 0 indicates the image behind's colour, a value of 255 represents this image's colour, and all values between represent a mix. | |
In this tutorial we shall use High Colour initially. As you can see form the image, it is produces clear, good quality images, but it doesn't take up as much space as True Colour. That said, for quite a small display of 800x600 pixels, it would still take just under 1 megabyte of space. It also has the advantage that the size is a multiple of a power of 2, which greatly reduces the complexity of getting information compared with True Colour.
```
Storing the frame buffer places a heavy memory burden on a computer. For this reason, early computers often cheated, by, for example, storing a screens worth of text, and just drawing each letter to the screen every time it is refreshed separately.
```
The Raspberry Pi has a very special and rather odd relationship with it's graphics processor. On the Raspberry Pi, the graphics processor actually runs first, and is responsible for starting up the main processor. This is very unusual. Ultimately it doesn't make too much difference, but in many interactions, it often feels like the processor is secondary, and the graphics processor is the most important. The two communicate on the Raspberry Pi by what is called the 'mailbox'. Each can deposit mail for the other, which will be collected at some future point and then dealt with. We shall use the mailbox to ask the graphics processor for an address. The address will be a location to which we can write the pixel colour information for the screen, called a frame buffer, and the graphics card will regularly check this location, and update the pixels on the screen appropriately.
### 3 Programming the Postman
```
Message passing is quite a common way for components to communicate. Some Operating Systems use virtual message passing to allow programs to communicate.
```
The first thing we are going to need to program is a 'postman'. This is just two methods: MailboxRead, reading one message from the mailbox channel in r0. and MailboxWrite, writing the value in the top 28 bits of r0 to the mailbox channel in r1. The Raspberry Pi has 7 mailbox channels for communication with the graphics processor, only the first of which is useful to us, as it is for negotiating the frame buffer.
The following table and diagrams describe the operation of the mailbox.
Table 3.1 Mailbox Addresses
| Address | Size / Bytes | Name | Description | Read / Write |
| 2000B880 | 4 | Read | Receiving mail. | R |
| 2000B890 | 4 | Poll | Receive without retrieving. | R |
| 2000B894 | 4 | Sender | Sender information. | R |
| 2000B898 | 4 | Status | Information. | R |
| 2000B89C | 4 | Configuration | Settings. | RW |
| 2000B8A0 | 4 | Write | Sending mail. | W |
In order to send a message to a particular mailbox:
1. The sender waits until the Status field has a 0 in the top bit.
2. The sender writes to Write such that the lowest 4 bits are the mailbox to write to, and the upper 28 bits are the message to write.
In order to read a message:
1. The receiver waits until the Status field has a 0 in the 30th bit.
2. The receiver reads from Read.
3. The receiver confirms the message is for the correct mailbox, and tries again if not.
If you're feeling particularly confident, you now have enough information to write the two methods we need. If not, read on.
As always the first method I recommend you implement is one to get the address of the mailbox region.
```
.globl GetMailboxBase
GetMailboxBase:
ldr r0,=0x2000B880
mov pc,lr
```
The sending procedure is least complicated, so we shall implement this first. As your methods become more and more complicated, you will need to start planning them in advance. A good way to do this might be to write out a simple list of the steps that need to be done, in a fair amount of detail, like below.
1. Our input will be what to write (r0), and what mailbox to write it to (r1). We must validate this is by checking it is a real mailbox, and that the low 4 bits of the value are 0. Never forget to validate inputs.
2. Use GetMailboxBase to retrieve the address.
3. Read from the Status field.
4. Check the top bit is 0. If not, go back to 3.
5. Combine the value to write and the channel.
6. Write to the Write.
Let's handle each of these in order.
1.
```
.globl MailboxWrite
MailboxWrite:
tst r0,#0b1111
movne pc,lr
cmp r1,#15
movhi pc,lr
```
```
tst reg,#val computes and reg,#val and compares the result with 0.
```
This achieves our validation on r0 and r1. tst is a function that compares two numbers by computing the logical and operation of the numbers, and then comparing the result with 0. In this case it checks that the lowest 4 bits of the input in r0 are all 0.
2.
```
channel .req r1
value .req r2
mov value,r0
push {lr}
bl GetMailboxBase
mailbox .req r0
```
This code ensures we will not overwrite our value, or link register and calls GetMailboxBase.
3.
```
wait1$:
status .req r3
ldr status,[mailbox,#0x18]
```
This code loads in the current status.
4.
```
tst status,#0x80000000
.unreq status
bne wait1$
```
This code checks that the top bit of the status field is 0, and loops back to 3. if it is not.
5.
```
add value,channel
.unreq channel
```
This code combines the channel and value together.
6.
```
str value,[mailbox,#0x20]
.unreq value
.unreq mailbox
pop {pc}
```
This code stores the result to the write field.
The code for MailboxRead is quite similar.
1. Our input will be what mailbox to read from (r0). We must validate this is by checking it is a real mailbox. Never forget to validate inputs.
2. Use GetMailboxBase to retrieve the address.
3. Read from the Status field.
4. Check the 30th bit is 0. If not, go back to 3.
5. Read from the Read field.
6. Check the mailbox is the one we want, if not go back to 3.
7. Return the result.
Let's handle each of these in order.
1.
```
.globl MailboxRead
MailboxRead:
cmp r0,#15
movhi pc,lr
```
This achieves our validation on r0.
2.
```
channel .req r1
mov channel,r0
push {lr}
bl GetMailboxBase
mailbox .req r0
```
This code ensures we will not overwrite our value, or link register and calls GetMailboxBase.
3.
```
rightmail$:
wait2$:
status .req r2
ldr status,[mailbox,#0x18]
```
This code loads in the current status.
4.
```
tst status,#0x40000000
.unreq status
bne wait2$
```
This code checks that the 30th bit of the status field is 0, and loops back to 3. if it is not.
5.
```
mail .req r2
ldr mail,[mailbox,#0]
```
This code reads the next item from the mailbox.
6.
```
inchan .req r3
and inchan,mail,#0b1111
teq inchan,channel
.unreq inchan
bne rightmail$
.unreq mailbox
.unreq channel
```
This code checks that the channel of the mail we just read is the one we were supplied. If not it loops back to 3.
7.
```
and r0,mail,#0xfffffff0
.unreq mail
pop {pc}
```
This code moves the answer (the top 28 bits of mail) to r0.
### 4 My Dearest Graphics Processor
Through our new postman, we now have the ability to send a message to the graphics card. What should we send though? This was certainly a difficult question for me to find the answer to, as it isn't in any online manual that I have found. Nevertheless, by looking at the GNU/Linux for the Raspberry Pi, we are able to work out what we needed to send.
```
Since the RAM is shared between the graphics processor and the processor on the Pi, we can just send where to find our message. This is called DMA, many complicated devices use this to speed up access times.
```
The message is very simple. We describe the framebuffer we would like, and the graphics card either agrees to our request, in which case it sends us back a 0, and fills in a small questionnaire we make, or it sends back a non-zero number, in which case we know it is unhappy. Unfortunately, I have no idea what any of the other numbers it can send back are, nor what they mean, but only when it sends a zero it is happy. Fortunately it always seems to send a zero for sensible inputs, so we don't need to worry too much.
For simplicity we shall design our request in advance, and store it in the .data section. In a file called 'framebuffer.s' place the following code:
```
.section .data
.align 4
.globl FrameBufferInfo
FrameBufferInfo:
.int 1024 /bin /boot /dev /etc /home /lib /lib64 /lost+found /media /mnt /opt /proc /root /run /sbin /srv /sys /tmp /usr /var #0 Physical Width */
.int 768 /bin /boot /dev /etc /home /lib /lib64 /lost+found /media /mnt /opt /proc /root /run /sbin /srv /sys /tmp /usr /var #4 Physical Height */
.int 1024 /bin /boot /dev /etc /home /lib /lib64 /lost+found /media /mnt /opt /proc /root /run /sbin /srv /sys /tmp /usr /var #8 Virtual Width */
.int 768 /bin /boot /dev /etc /home /lib /lib64 /lost+found /media /mnt /opt /proc /root /run /sbin /srv /sys /tmp /usr /var #12 Virtual Height */
.int 0 /bin /boot /dev /etc /home /lib /lib64 /lost+found /media /mnt /opt /proc /root /run /sbin /srv /sys /tmp /usr /var #16 GPU - Pitch */
.int 16 /bin /boot /dev /etc /home /lib /lib64 /lost+found /media /mnt /opt /proc /root /run /sbin /srv /sys /tmp /usr /var #20 Bit Depth */
.int 0 /bin /boot /dev /etc /home /lib /lib64 /lost+found /media /mnt /opt /proc /root /run /sbin /srv /sys /tmp /usr /var #24 X */
.int 0 /bin /boot /dev /etc /home /lib /lib64 /lost+found /media /mnt /opt /proc /root /run /sbin /srv /sys /tmp /usr /var #28 Y */
.int 0 /bin /boot /dev /etc /home /lib /lib64 /lost+found /media /mnt /opt /proc /root /run /sbin /srv /sys /tmp /usr /var #32 GPU - Pointer */
.int 0 /bin /boot /dev /etc /home /lib /lib64 /lost+found /media /mnt /opt /proc /root /run /sbin /srv /sys /tmp /usr /var #36 GPU - Size */
```
This is the format of our messages to the graphics processor. The first two words describe the physical width and height. The second pair is the virtual width and height. The framebuffer's width and height are the virtual width and height, and the GPU scales the framebuffer as need to fit the physical screen. The next word is one of the ones the GPU will fill in if it grants our request. It will be the number of bytes on each row of the frame buffer, in this case 2 × 1024 = 2048. The next word is how many bits to allocate to each pixel. Using a value of 16 means that the graphics processor uses High Colour mode described above. A value of 24 would use True Colour, and 32 would use RGBA32. The next two words are x and y offsets, which mean the number of pixels to skip in the top left corner of the screen when copying the framebuffer to the screen. Finally, the last two words are filled in by the graphics processor, the first of which is the actual pointer to the frame buffer, and the second is the size of the frame buffer in bytes.
```
When working with devices using DMA, alignment constraints become very important. The GPU expects the message to be 16 byte aligned.
```
I was very careful to include a .align 4 here. As discussed before, this ensures the lowest 4 bits of the address of the next line are 0. Thus, we know for sure that FrameBufferInfo will be placed at an address we can send to the graphics processor, as our mailbox only sends values with the low 4 bits all 0.
So, now that we have our message, we can write code to send it. The communication will go as follows:
1. Write the address of FrameBufferInfo + 0x40000000 to mailbox 1.
2. Read the result from mailbox 1. If it is not zero, we didn't ask for a proper frame buffer.
3. Copy our images to the pointer, and they will appear on screen!
I've said something that I've not mentioned before in step 1. We have to add 0x40000000 to the address of FrameBufferInfo before sending it. This is actually a special signal to the GPU of how it should write to the structure. If we just send the address, the GPU will write its response, but will not make sure we can see it by flushing its cache. The cache is a piece of memory where a processor stores values its working on before sending them to the RAM. By adding 0x40000000, we tell the GPU not to use its cache for these writes, which ensures we will be able to see the change.
Since there is quite a lot going on there, it would be best to implement this as a function, rather than just putting the code into main.s. We shall write a function InitialiseFrameBuffer which does all this negotiation and returns the pointer to the frame buffer info data above, once it has a pointer in it. For ease, we should also make it so that the width, height and bit depth of the frame buffer are inputs to this method, so that it is easy to change in main.s without having to get into the details of the negotiation.
Once again, let's write down in detail the steps we will have to take. If you're feeling confident, try writing the function straight away.
1. Validate our inputs.
2. Write the inputs into the frame buffer.
3. Send the address of the frame buffer + 0x40000000 to the mailbox.
4. Receive the reply from the mailbox.
5. If the reply is not 0, the method has failed. We should return 0 to indicate failure.
6. Return a pointer to the frame buffer info.
Now we're getting into much bigger methods than before. Below is one implementation of the above.
1.
```
.section .text
.globl InitialiseFrameBuffer
InitialiseFrameBuffer:
width .req r0
height .req r1
bitDepth .req r2
cmp width,#4096
cmpls height,#4096
cmpls bitDepth,#32
result .req r0
movhi result,#0
movhi pc,lr
```
This code checks that the width and height are less than or equal to 4096, and that the bit depth is less than or equal to 32. This is once again using a trick with conditional execution. Convince yourself that this works.
2.
```
fbInfoAddr .req r3
push {lr}
ldr fbInfoAddr,=FrameBufferInfo
str width,[fbInfoAddr,#0]
str height,[fbInfoAddr,#4]
str width,[fbInfoAddr,#8]
str height,[fbInfoAddr,#12]
str bitDepth,[fbInfoAddr,#20]
.unreq width
.unreq height
.unreq bitDepth
```
This code simply writes into our frame buffer structure defined above. I also take the opportunity to push the link register onto the stack.
3.
```
mov r0,fbInfoAddr
add r0,#0x40000000
mov r1,#1
bl MailboxWrite
```
The inputs to the MailboxWrite method are the value to write in r0, and the channel to write to in r1.
4.
```
mov r0,#1
bl MailboxRead
```
The inputs to the MailboxRead method is the channel to write to in r0, and the output is the value read.
5.
```
teq result,#0
movne result,#0
popne {pc}
```
This code checks if the result of the MailboxRead method is 0, and returns 0 if not.
6.
```
mov result,fbInfoAddr
pop {pc}
.unreq result
.unreq fbInfoAddr
```
This code finishes off and returns the frame buffer info address.
### 5 A Pixel Within a Row Within a Frame
So, we've now created our methods to communicate with the graphics processor. It should now be capable of giving us the pointer to a frame buffer we can draw graphics to. Let's draw something now.
In this first example, we'll just draw consecutive colours to the screen. It won't look pretty, but at least it will be working. How we will do this is by setting each pixel in the framebuffer to a consecutive number, and continually doing so.
Copy the following code to 'main.s' after mov sp,#0x8000
```
mov r0,#1024
mov r1,#768
mov r2,#16
bl InitialiseFrameBuffer
```
This code simply uses our InitialiseFrameBuffer method to create a frame buffer with width 1024, height 768, and bit depth 16. You can try different values in here if you wish, as long as you are consistent throughout the code. Since it's possible that this method can return 0 if the graphics processor did not give us a frame buffer, we had better check for this, and turn the OK LED on if it happens.
```
teq r0,#0
bne noError$
mov r0,#16
mov r1,#1
bl SetGpioFunction
mov r0,#16
mov r1,#0
bl SetGpio
error$:
b error$
noError$:
fbInfoAddr .req r4
mov fbInfoAddr,r0
```
Now that we have the frame buffer info address, we need to get the frame buffer pointer from it, and start drawing to the screen. We will do this using two loops, one going down the rows, and one going along the columns. On the Raspberry Pi, indeed in most applications, pictures are stored left to right then top to bottom, so we have to do the loops in the order I have said.
```
render$:
fbAddr .req r3
ldr fbAddr,[fbInfoAddr,#32]
colour .req r0
y .req r1
mov y,#768
drawRow$:
x .req r2
mov x,#1024
drawPixel$:
strh colour,[fbAddr]
add fbAddr,#2
sub x,#1
teq x,#0
bne drawPixel$
sub y,#1
add colour,#1
teq y,#0
bne drawRow$
b render$
.unreq fbAddr
.unreq fbInfoAddr
```
```
strh reg,[dest] stores the low half word number in reg at the address given by dest.
```
This is quite a large chunk of code, and has a loop within a loop within a loop. To help get your head around the looping, I've indented the code which is looped, depending on which loop it is in. This is quite common in most high level programming languages, and the assembler simply ignores the tabs. We see here that I load in the frame buffer address from the frame buffer information structure, and then loop over every row, then every pixel on the row. At each pixel, I use an strh (store half word) command to store the current colour, then increment the address we're writing to. After drawing each row, we increment the colour that we are drawing. After drawing the full screen, we branch back to the beginning.
### 6 Seeing the Light
Now you're ready to test this code on the Raspberry Pi. You should see a changing gradient pattern. Be careful: until the first message is sent to the mailbox, the Raspberry Pi displays a still gradient pattern between the four corners. If it doesn't work, please see our troubleshooting page.
If it does work, congratulations! You can now control the screen! Feel free to alter this code to draw whatever pattern you like. You can do some very nice gradient patterns, and can compute the value of each pixel directly, since y contains a y-coordinate for the pixel, and x contains an x-coordinate. In the next lesson, [Lesson 7: Screen 02][7], we will look at one of the most common drawing tasks, lines.
--------------------------------------------------------------------------------
via: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/screen01.html
作者:[Alex Chadwick][a]
选题:[lujun9972][b]
译者:[译者ID](https://github.com/译者ID)
校对:[校对者ID](https://github.com/校对者ID)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
[a]: https://www.cl.cam.ac.uk
[b]: https://github.com/lujun9972
[1]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/images/colour1bImage.png
[2]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/images/colour8gImage.png
[3]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/images/colour3bImage.png
[4]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/images/colour8bImage.png
[5]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/images/colour16bImage.png
[6]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/images/colour24bImage.png
[7]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/screen02.html

View File

@ -0,0 +1,504 @@
[#]: collector: (lujun9972)
[#]: translator: (qhwdw)
[#]: reviewer: ( )
[#]: publisher: ( )
[#]: url: ( )
[#]: subject: (Computer Laboratory Raspberry Pi: Lesson 6 Screen01)
[#]: via: (https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/screen01.html)
[#]: author: (Alex Chadwick https://www.cl.cam.ac.uk)
计算机实验室 树莓派:课程 6 屏幕01
======
欢迎来到屏幕系列课程。在本系列中,你将学习在树莓派中如何使用汇编代码控制屏幕,从显示随机数据开始,接着学习显示一个固定的图像和显示文本,然后格式化文本中的数字。假设你已经完成了 `OK` 系列课程的学习,所以在本系列中出现的有些知识将不再重复。
第一节的屏幕课程教你一些关于图形的基础理论,然后用这些理论在屏幕或电视上显示一个图案。
### 1、入门
预期你已经完成了 `OK` 系列的课程,以及那个系列课程中在 `gpio.s``systemTimer.s` 文件中调用的函数。如果你没有完成这些,或你喜欢完美的实现,可以去下载 `OK05.s` 解决方案。在这里也要使用 `main.s` 文件中从开始到包含 `mov sp,#0x8000` 的这一行之前的代码。请删除这一行以后的部分。
### 2、计算机图形
将颜色表示为数字有几种方法。在这里我们专注于 RGB 方法,但 HSL 也是很常用的另一种方法。
正如你所认识到的,从根本上来说,计算机是非常愚蠢的。它们只能执行有限数量的指令,仅仅能做一些数学,但是它们也能以某种方式来做很多很多的事情。而在这些事情中,我们目前想知道的是,计算机是如何将一个图像显示到屏幕上的。我们如何将这个问题转换成二进制?答案相当简单;我们为每个颜色设计一些编码方法,然后我们为生个像素在屏幕上保存一个编码。一个像素在你的屏幕上就是一个非常小的点。如果你离屏幕足够近,你或许能够在你的屏幕上辨别出单个的像素,能够看到每个图像都是由这些像素组成的。
随着计算机时代的到来,人们希望显示更多更复杂的图形,于是发明了图形卡的概念。图形卡是你的计算机上用来在屏幕上专门绘制图像的第二个处理器。它的任务就是将像素值信息转换成显示在屏幕上的亮度级别。在现代计算机中,图形卡已经能够做更多更复杂的事情了,比如绘制三维图形。但是在本系列教程中,我们只专注于图形卡的基本使用;从内存中取得像素然后把它显示到屏幕上。
不念经使用哪种方法,现在马上出现的一个问题就是我们使用的颜色编码。这里有几种选择,每个产生不同的输出质量。为了完整起见,我在这里只是简单概述它们。
不过这里的一些图像几乎没有颜色,因为它们使用了一个叫空间抖动的技术。这允许它们以很少的颜色仍然能表示出非常好的图像。许多早期的操作系统就使用了这种技术。
| 名字 | 唯一颜色数量 | 描述 | 示例 |
| ----------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------- |
| 单色 | 2 | 每个像素使用 1 位去保存,其中 1 表示白色0 表示黑色。 | ![Monochrome image of a bird][1] |
| 灰度 | 256 | 每个像素使用 1 个字节去保存,使用 255 表示白色0 表示黑色,介于这两个值之间的所有值表示这两个颜色的一个线性组合。 | ![Geryscale image of a bird][2] |
| 8 色 | 8 | 每个像素使用 3 位去保存,第一位表示红色通道,第二位表示绿色通道,第三位表示蓝色通道。 | ![8 colour image of a bird][3] |
| 低色值 | 256 | 每个像素使用 8 位去保存,前三位表示红色通道的强度,接下来的三位表示绿色通道的强度,最后两位表示蓝色通道的强度。 | ![Low colour image of a bird][4] |
| 高色值 | 65,536 | 每个像素使用 16 位去保存,前五位表示红色通道的强度,接下来的六位表示绿色通道的强度,最后的五位表示蓝色通道的强度。 | ![High colour image of a bird][5] |
| 真彩色 | 16,777,216 | 每个像素使用 24 位去保存,前八位表示红色通道,第二个八位表示绿色通道,最后八位表示蓝色通道。 | ![True colour image of a bird][6] |
| RGBA32 | 16,777,216 带 256 级透明度 | 每个像素使用 32 位去保存,前八位表示红色通道,第二个八位表示绿色通道,第三个八位表示蓝色通道。只有一个图像绘制在另一个图像的上方时才考虑使用透明通道,值为 0 时表示下面图像的颜色,值为 255 时表示上面这个图像的颜色,介于这两个值之间的所有值表示这两个图像颜色的混合。 ||
在本教程中,我们将从使用高色值开始。这样你就可以看到图像的构成,它的形成过程清楚,图像质量好,又不像真彩色那样占用太多的空间。也就是说,显示一个比较小的 800x600 像素的图像,它只需要小于 1 MiB 的空间。它另外的好处是它的大小是 2 次幂的倍数,相比真彩色这将极大地降低了获取信息的复杂度。
```
保存帧缓冲给一台计算机带来了很大的内存负担。基于这种原因,早期计算机经常作弊,比如,保存一屏幕文本,在每次单独刷新时,它只绘制刷新了的字母。
```
树莓派和它的图形处理器有一种特殊而奇怪的关系。在树莓派上,首先运行的事实上是图形处理器,它负责启动主处理器。这是很不常见的。最终它不会有太大的差别,但在许多交互中,它经常给人感觉主处理器是次要的,而图形处理器才是主要的。在树莓派上这两者之间依靠一个叫 “邮箱” 的东西来通讯。它们中的每一个都可以为对方投放邮件,这个邮件将在未来的某个时刻被对方收集并处理。我们将使用这个邮箱去向图形处理器请求一个地址。这个地址将是一个我们在屏幕上写入像素颜色信息的位置,我们称为帧缓冲,图形卡将定期检查这个位置,然后更新屏幕上相应的像素。
### 3、编写邮差程序
```
消息传递是组件间通讯时使用的常见方法。一些操作系统在程序之间使用虚拟消息进行通讯。
```
接下来我们做的第一件事情就是编写一个“邮差”程序。它有两个方法MailboxRead从寄存器 `r0` 中的邮箱通道读取一个消息。而 MailboxWrite将寄存器 `r0` 中的头 28 位的值写到寄存器 `r1` 中的邮箱通道。树莓派有 7 个与图形处理器进行通讯的邮箱通道。但仅第一个对我们有用,因为它用于协调帧缓冲。
下列的表和示意图描述了邮箱的操作。
表 3.1 邮箱地址
| 地址 | 大小 / 字节 | 名字 | 描述 | 读 / 写 |
| 2000B880 | 4 | Read | 接收邮件 | R |
| 2000B890 | 4 | Poll | 不检索接收 | R |
| 2000B894 | 4 | Sender |发送者信息 | R |
| 2000B898 | 4 | Status | 信息 | R |
| 2000B89C | 4 | Configuration | 设置 | RW |
| 2000B8A0 | 4 | Write | 发送邮件 | W |
为了给指定的邮箱发送一个消息:
1. 发送者等待,直到 `Status`字段的头一位为 0。
2. 发送者写入到 `Write`,低 4 位是要发送到的邮箱,高 28 位是要写入的消息。
为了读取一个消息:
1. 接收者等待,直到 `Status` 字段的第 30 位为 0。
2. 接收者读取消息。
3. 接收者确认消息来自正确的邮箱,否则再次重试。
如果你觉得有信心,你现在有足够的信息去写出我们所需的两个方法。如果没有信心,请继续往下看。
与以前一样,我建议你实现的第一个方法是获取邮箱区域的地址。
```assembly
.globl GetMailboxBase
GetMailboxBase:
ldr r0,=0x2000B880
mov pc,lr
```
发送程序相对简单一些,因此我们将首先去实现它。随着你的方法越来越复杂,你需要提前去规划它们。规划它们的一个好的方式是写出一个简单步骤列表,详细地列出你需要做的事情,像下面一样。
1. 我们的输入将要写什么(`r0`),以及写到什么邮箱(`r1`)。我们必须验证邮箱的真实性,以及它的低 4 位的值是否为 0。不要忘了验证输入。
2. 使用 `GetMailboxBase` 去检索地址。
3. 读取 `Status` 字段。
4. 检查头一位是否为 0。如果不是回到第 3 步。
5. 将写入的值和邮箱通道组合到一起。
6. 写入到 `Write`
我们来按顺序写出它们中的每一步。
1.
```assembly
.globl MailboxWrite
MailboxWrite:
tst r0,#0b1111
movne pc,lr
cmp r1,#15
movhi pc,lr
```
```assembly
tst reg,#val 计算寄存器 reg 和 #val 的逻辑与,然后将计算结果与 0 进行比较。
```
这将实现我们验证 `r0``r1` 的目的。`tst` 是通过计算两个操作数的逻辑与来比较两个操作数的函数,然后将结果与 0 进行比较。在本案例中,它将检查在寄存器 `r0` 中的输入的低 4 位是否为全 0。
2.
```assembly
channel .req r1
value .req r2
mov value,r0
push {lr}
bl GetMailboxBase
mailbox .req r0
```
这段代码确保我们不会覆盖我们的值,或链接寄存器,然后调用 `GetMailboxBase`
3.
```assembly
wait1$:
status .req r3
ldr status,[mailbox,#0x18]
```
这段代码加载当前状态。
4.
```assembly
tst status,#0x80000000
.unreq status
bne wait1$
```
这段代码检查状态字段的头一位是否为 0如果不为 0循环回到第 3 步。
5.
```assembly
add value,channel
.unreq channel
```
这段代码将通道和值组合到一起。
6.
```assembly
str value,[mailbox,#0x20]
.unreq value
.unreq mailbox
pop {pc}
```
这段代码保存结果到写入字段。
MailboxRead 的代码和它非常类似。
1. 我们的输入将从哪个邮箱读取(`r0`)。我们必须要验证邮箱的真实性。不要忘了验证输入。
2. 使用 `GetMailboxBase` 去检索地址。
3. 读取 `Status` 字段。
4. 检查第 30 位是否为 0。如果不为 0返回到第 3 步。
5. 读取 `Read` 字段。
6. 检查邮箱是否是我们所要的,如果不是返回到第 3 步。
7. 返回结果。
我们来按顺序写出它们中的每一步。
1.
```assembly
.globl MailboxRead
MailboxRead:
cmp r0,#15
movhi pc,lr
```
这一段代码来验证 `r0` 中的值。
2.
```assembly
channel .req r1
mov channel,r0
push {lr}
bl GetMailboxBase
mailbox .req r0
```
这段代码确保我们不会覆盖掉我们的值,或链接寄存器,然后调用 `GetMailboxBase`
3.
```assembly
rightmail$:
wait2$:
status .req r2
ldr status,[mailbox,#0x18]
```
这段代码加载当前状态。
4.
```assembly
tst status,#0x40000000
.unreq status
bne wait2$
```
这段代码检查状态字段第 30 位是否为 0如果不为 0返回到第 3 步。
5.
```assembly
mail .req r2
ldr mail,[mailbox,#0]
```
这段代码从邮箱中读取下一条消息。
6.
```assembly
inchan .req r3
and inchan,mail,#0b1111
teq inchan,channel
.unreq inchan
bne rightmail$
.unreq mailbox
.unreq channel
```
这段代码检查我们正在读取的邮箱通道是否为提供给我们的通道。如果不是,返回到第 3 步。
7.
```assembly
and r0,mail,#0xfffffff0
.unreq mail
pop {pc}
```
这段代码将答案(邮件的前 28 位)移动到寄存器 `r0` 中。
### 4、我心爱的图形处理器
通过我们新的邮差程序,我们现在已经能够向图形卡上发送消息了。我们应该发送些什么呢?这对我来说可能是个很难找到答案的问题,因为它不是任何线上手册能够找到答案的问题。尽管如此,通过查找有关树莓派的 GNU/Linux我们能够找出我们需要发送的内容。
```
由于在树莓派的内存是在图形处理器和主处理器之间共享的,我们能够只发送可以找到我们信息的位置即可。这就是 DMA许多复杂的设备使用这种技术去加速访问时间。
```
消息很简单。我们描述我们想要的帧缓冲区,而图形卡要么接受我们的请求,给我们返回一个 0然后用我们写的一个小的调查问卷来填充屏幕要么发送一个非 0 值,我们知道那表示很遗憾(出错了)。不幸的是,我并不知道它返回的其它数字是什么,也不知道它意味着什么,但我们知道仅当它返回一个 0才表示一切顺利。幸运的是对于合理的输入它总是返回一个 0因此我们不用过于担心。
为简单起见,我们将提前设计好我们的请求,并将它保存到 `framebuffer.s` 文件的 `.data` 节中,它的代码如下:
```assembly
.section .data
.align 4
.globl FrameBufferInfo
FrameBufferInfo:
.int 1024 /* #0 物理宽度 */
.int 768 /* #4 物理高度 */
.int 1024 /* #8 虚拟宽度 */
.int 768 /* #12 虚拟高度 */
.int 0 /* #16 GPU - 间距 */
.int 16 /* #20 位深 */
.int 0 /* #24 X */
.int 0 /* #28 Y */
.int 0 /* #32 GPU - 指针 */
.int 0 /* #36 GPU - 大小 */
```
这就是我们发送到图形处理器的消息格式。第一对两个关键字描述了物理宽度和高度。第二对关键字描述了虚拟宽度和高度。帧缓冲的宽度和高度就是虚拟的宽度和高度,而 GPU 按需要伸缩帧缓冲去填充物理屏幕。如果 GPU 接受我们的请求,接下来的关键字将是 GPU 去填充的参数。它们是帧缓冲每行的字节数,在本案例中它是 `2 × 1024 = 2048`。下一个关键字是每个像素分配的位数。使用了一个 16 作为值意味着图形处理器使用了我们上面所描述的高色值模式。值为 24 是真彩色,而值为 32 则是 RGBA32。接下来的两个关键字是 x 和 y 偏移量,它表示当将帧缓冲复制到屏幕时,从屏幕左上角跳过的像素数目。最后两个关键字是由图形处理器填写的,第一个表示指向帧缓冲的实际指针,第二个是用字节数表示的帧缓冲大小。
```
当设备使用 DMA 时对齐约束变得非常重要。GPU 预期消息都是 16 字节对齐的。
```
在这里我非常谨慎地使用了一个 `.align 4` 指令。正如前面所讨论的,这样确保了下一行地址的低 4 位是 0。所以我们可以确保将被放到那个地址上的帧缓冲是可以发送到图形处理器上的因为我们的邮箱仅发送低 4 位全为 0 的值。
到目前为止,我们已经有了待发送的消息,我们可以写代码去发送它了。通讯将按如下的步骤进行:
1. 写入 `FrameBufferInfo + 0x40000000` 的地址到邮箱 1。
2. 从邮箱 1 上读取结果。如果它是非 0 值,意味着我们没有请求一个正确的帧缓冲。
3. 复制我们的图像到指针,这时图像将出现在屏幕上!
我在步骤 1 中说了一些以前没有提到的事情。我们在发送之前,在帧缓冲地址上加了 `0x40000000`。这其实是一个给 GPU 的特殊信号,它告诉 GPU 应该如何写到结构上。如果我们只是发送地址GPU 将写到它的回复上,这样不能保证我们可以通过刷新缓存看到它。缓存是处理器使用的值在它们被发送到存储之前保存在内存中的片段。通过加上 `0x40000000`,我们告诉 GPU 不要将写入到它的缓存中,这样将确保我们能够看到变化。
因为在那里发生很多事情,因此最好将它实现为一个函数,而不是将它以代码的方式写入到 `main.s` 中。我们将要写一个函数 `InitialiseFrameBuffer`,由它来完成所有协调和返回指向到上面提到的帧缓冲数据的指针。为方便起见,我们还将帧缓冲的宽度、高度、位深作为这个方法的输入,这样就很容易地修改 `main.s` 而不必知道协调的细节了。
再一次,来写下我们要做的详细步骤。如果你有信心,可以略过这一步直接尝试去写函数。
1. 验证我们的输入。
2. 写输入到帧缓冲。
3. 发送 `frame buffer + 0x40000000` 的地址到邮箱。
4. 从邮箱中接收回复。
5. 如果回复是非 0 值,方法失败。我们应该返回 0 去表示失败。
6. 返回指向帧缓冲信息的指针。
现在,我们开始写更多的方法。以下是上面其中一个实现。
1.
```assembly
.section .text
.globl InitialiseFrameBuffer
InitialiseFrameBuffer:
width .req r0
height .req r1
bitDepth .req r2
cmp width,#4096
cmpls height,#4096
cmpls bitDepth,#32
result .req r0
movhi result,#0
movhi pc,lr
```
这段代码检查宽度和高度是小于或等于 4096位深小于或等于 32。这里再次使用了条件运行的技巧。相信自己这是可行的。
2.
```assembly
fbInfoAddr .req r3
push {lr}
ldr fbInfoAddr,=FrameBufferInfo
str width,[fbInfoAddr,#0]
str height,[fbInfoAddr,#4]
str width,[fbInfoAddr,#8]
str height,[fbInfoAddr,#12]
str bitDepth,[fbInfoAddr,#20]
.unreq width
.unreq height
.unreq bitDepth
```
这段代码写入到我们上面定义的帧缓冲结构中。我也趁机将链接寄存器推入到栈上。
3.
```assembly
mov r0,fbInfoAddr
add r0,#0x40000000
mov r1,#1
bl MailboxWrite
```
`MailboxWrite` 方法的输入是写入到寄存器 `r0` 中的值,并将通道写入到寄存器 `r1` 中。
4.
```assembly
mov r0,#1
bl MailboxRead
```
`MailboxRead` 方法的输入是写入到寄存器 `r0` 中的通道,而输出是值读数。
5.
```assembly
teq result,#0
movne result,#0
popne {pc}
```
这段代码检查 `MailboxRead` 方法的结果是否为 0如果不为 0则返回 0。
6.
```assembly
mov result,fbInfoAddr
pop {pc}
.unreq result
.unreq fbInfoAddr
```
这是代码结束,并返回帧缓冲信息地址。
### 5、在一帧中一行之内的一个像素
到目前为止,我们已经创建了与图形处理器通讯的方法。现在它已经能够给我们返回一个指向到帧缓冲的指针去绘制图形了。我们现在来绘制一个图形。
第一示例,我们将在屏幕上绘制连续的颜色。它看起来并不漂亮,但至少能说明它在工作。我们如何才能在帧缓冲中设置每个像素为一个连续的数字,并且要持续不断地这样做。
将下列代码复制到 `main.s` 文件中,并放置在 `mov sp,#0x8000` 行之后。
```assembly
mov r0,#1024
mov r1,#768
mov r2,#16
bl InitialiseFrameBuffer
```
这段代码使用了我们的 `InitialiseFrameBuffer` 方法,简单地创建了一个宽 1024、高 768、位深为 16 的帧缓冲区。在这里,如果你愿意可以尝试使用不同的值,只要整个代码中都一样就可以。如果图形处理器没有给我们创建好一个帧缓冲区,这个方法将返回 0我们最好检查一下返回值如果出现返回值为 0 的情况,我们打开 `OK` LED 灯。
```assembly
teq r0,#0
bne noError$
mov r0,#16
mov r1,#1
bl SetGpioFunction
mov r0,#16
mov r1,#0
bl SetGpio
error$:
b error$
noError$:
fbInfoAddr .req r4
mov fbInfoAddr,r0
```
现在,我们已经有了帧缓冲信息的地址,我们需要取得帧缓冲信息的指针,并开始绘制屏幕。我们使用两个循环来做实现,一个走行,一个走列。事实上,树莓派中的大多数应用程序中,图片都是以从左右然后从上下到的顺序来保存的,因此我们也按这个顺序来写循环。
```assembly
render$:
fbAddr .req r3
ldr fbAddr,[fbInfoAddr,#32]
colour .req r0
y .req r1
mov y,#768
drawRow$:
x .req r2
mov x,#1024
drawPixel$:
strh colour,[fbAddr]
add fbAddr,#2
sub x,#1
teq x,#0
bne drawPixel$
sub y,#1
add colour,#1
teq y,#0
bne drawRow$
b render$
.unreq fbAddr
.unreq fbInfoAddr
```
```assembly
strh reg,[dest] 将寄存器中的低位半个字保存到给定的 dest 地址上。
```
这是一个很长的代码块,它嵌套了三层循环。为了帮你理清头绪,我们将循环进行缩进处理,这就有点类似于高级编程语言,而汇编器会忽略掉这些用于缩进的 `tab` 字符。我们看到,在这里它从帧缓冲信息结构中加载了帧缓冲的地址,然后基于每行来循环,接着是每行上的每个像素。在每个像素上,我们使用一个 `strh`(保存半个字)命令去保存当前颜色,然后增加地址继续写入。每行绘制完成后,我们增加绘制的颜色号。在整个屏幕绘制完成后,我们跳转到开始位置。
### 6、看到曙光
现在,你已经准备好在树莓派上测试这些代码了。你应该会看到一个渐变图案。注意:在第一个消息被发送到邮箱之前,树莓派在它的四个角上一直显示一个渐变图案。如果它不能正常工作,请查看我们的排错页面。
如果一切正常,恭喜你!你现在可以控制屏幕了!你可以随意修改这些代码去绘制你想到的任意图案。你还可以做更精彩的渐变图案,可以直接计算每个像素值,因为每个像素包含了一个 Y 坐标和 X 坐标。在下一个 [课程 7Screen 02][7] 中,我们将学习一个更常用的绘制任务:行。
--------------------------------------------------------------------------------
via: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/screen01.html
作者:[Alex Chadwick][a]
选题:[lujun9972][b]
译者:[qhwdw](https://github.com/qhwdw)
校对:[校对者ID](https://github.com/校对者ID)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
[a]: https://www.cl.cam.ac.uk
[b]: https://github.com/lujun9972
[1]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/images/colour1bImage.png
[2]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/images/colour8gImage.png
[3]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/images/colour3bImage.png
[4]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/images/colour8bImage.png
[5]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/images/colour16bImage.png
[6]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/images/colour24bImage.png
[7]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/screen02.html