
Course OK01 explains how to get started with the Raspberry Pi and how to enable the OK or ACT LED indicators near the RCA and USB ports. This indicator was initially designed to indicate the OK status but was renamed ACT in the second version of the Raspberry Pi.
1. Getting Started
We assume you have accessed the download[1] page and obtained the required GNU toolchain. Also, download a file called the operating system template. Please download this file and extract it in a new directory.
2. Start
Now that you have extracted this template file, create a file named main.s
in the source
directory. This file contains the code for this operating system. Specifically, the structure of this folder should look like this:
build/
(empty)
source/
main.s
kernel.ld
LICENSE
Makefile
Open the main.s
file with a text editor so we can input the assembly code. The Raspberry Pi uses a variant of assembly code called ARMv6, which is the type of assembly code we will write.
Files with the
.s
extension are generally assembly code, and it is important to remember that here it is ARMv6 assembly code.
First, we copy the following commands.
.section .init
.globl _start
_start:
In fact, the above instructions do not do anything on the Raspberry Pi; they are instructions provided to the assembler. The assembler is a conversion program that translates assembly code we can understand into machine code that the Raspberry Pi can understand. In assembly code, each line is a new command. The first line above tells the assembler 1 where to place our code. The reason we place it in a section called .init
in the provided template is that it is the starting point of the output. This is important because we want to ensure we can control which code runs first. If we do not do this, the first code to run will be the one that comes first alphabetically! The .section
command simply tells the assembler which section to place the code in, from this point until the next .section
or the end of the file.
In assembly code, you can skip lines and place spaces before or after commands to enhance readability.
The next two lines stop a warning message, and they are not important. 2
3. The First Line of Code
Now, we officially start writing code. When a computer executes assembly code, it simply executes each instruction in order unless explicitly told not to. Each instruction begins on a new line.
Copy the following instruction.
ldr r0,=0x20200000
ldr reg,=val
loads the numberval
into the register namedreg
.
That is our first command. It tells the processor to save the number 0x20200000
in register r0
. Here I need to answer two questions: what is a register? What kind of number is 0x20200000
?
A register is a tiny block of memory in the processor where the processor saves the numbers it is processing. There are many registers in the processor, many of which have specific purposes, and we will encounter them one by one later. The most important ones are thirteen (named r0
, r1
, r2
, …, r9
, r10
, r11
, r12
), which are called general-purpose registers, and you can use them for any calculations. Since we are writing our first line of code, we used r0
in the example, but you can use any of them. As long as you remain consistent, there is no problem.
A single register on the Raspberry Pi can store any integer between
0
and4,294,967,295
(inclusive); it may seem like a large memory, but it only has 32 binary bits.
0x20200000
is indeed a number. It is just represented in hexadecimal. Below is a detailed explanation of hexadecimal:
Further Reading: Explanation of Hexadecimal
Hexadecimal is another way of representing numbers. You may only know the decimal representation of numbers, which has ten digits:
0
,1
,2
,3
,4
,5
,6
,7
,8
, and9
. Hexadecimal has sixteen digits:0
,1
,2
,3
,4
,5
,6
,7
,8
,9
,a
,b
,c
,d
,e
, andf
.You may also remember how decimal is represented in positional notation. That is, the rightmost digit is the unit place, the next left is the tens place, and the next left is the hundreds place, and so on. This means its value is 100 × the digit in the hundreds place, plus 10 × the digit in the tens place, plus 1 × the digit in the unit place.
567 is 5 hundreds, 6 tens, and 7 units.
From a mathematical perspective, we can discover a pattern; the rightmost digit is 100 = 1s, the next left is 101 = 10s, the next left is 102 = 100s, and so on. We set in the system that 0 is the least significant digit, followed by 1, and so on. But what if we use a different base than 10? The hexadecimal we use in the system is such a number.
567 is 5×10^2+6×10^1+7×10^0
567 = 5×10^2+6×10^1+7×10^0 = 2×16^2+3×16^1+7×16^0
The mathematical equation above shows that the decimal number 567 equals the hexadecimal number 237. Usually, we need to clarify them in the system, we use the subscript 10 to indicate it is a decimal number and the subscript 16 to indicate it is a hexadecimal number. Since writing subscripts in assembly code is difficult, we use 0x to indicate it is a hexadecimal number, so 0x237 means 23716.
So, what are the
a
,b
,c
,d
,e
, andf
? Good question! In hexadecimal, to write each digit, we need additional symbols. For example, 916 = 9×160 = 910, but 1016 = 1×161 + 1×160 = 1610. Therefore, if we only use 0, 1, 2, 3, 4, 5, 6, 7, 8, and 9, we cannot write 1010, 1110, 1210, 1310, 1410, or 1510. So we introduced 6 new symbols, so a16 = 1010, b16 = 1110, c16 = 1210, d16 = 1310, e16 = 1410, f16 = 1510.Thus, we have another way to write numbers. But why go through all this trouble? Good question! Since computers always work in binary, it turns out that hexadecimal is very useful because each hexadecimal digit is exactly four binary digits long. This method also has the added benefit that many computer numbers are multiples of hexadecimal integers rather than decimal integers. For instance, the number 2020000016 I used in the assembly code above. If we write it in decimal, it is a not-so-memorable number 53896806410.
We can convert decimal to hexadecimal using the simple method below:
Conversion example
1. We take the decimal number 567 as an example. 2. Divide the decimal number 567 by 16 and calculate the remainder. For example, 567 ÷ 16 = 35 remainder 7. 3. In hexadecimal, the remainder is the last digit of the answer, in our case, it is 7. 4. Repeat steps 2 and 3 until the integer part of the division result is 0. For example, 35 ÷ 16 = 2 remainder 3, so 3 is the next digit in the answer. 2 ÷ 16 = 0 remainder 2, so 2 is the next digit in the answer. 5. Once the integer part of the division result is 0, stop. The answer is the reverse of the remainders, so 56710 = 23716. Converting hexadecimal numbers to decimal is also easy; just expand the number, so 23716 = 2×162 + 3×161 + 7 ×160 = 2×256 + 3×16 + 7×1 = 512 + 48 + 7 = 567.
Therefore, the first assembly command we wrote loads the number 2020000016 into register r0
. That command may seem useless, but it is not. In a computer, there are many blocks of memory and devices. To access them, we assign each block of memory and device an address. Just like a postal address or a website address, it is used to identify the location of the memory block or device we want to access. The number 2020000016 is the address of the GPIO controller. This address is determined by the manufacturer’s design, and they can use other addresses as long as they do not conflict with others. I know this address is the address of the GPIO controller because I looked at its manual, 3 there are no specific specifications for address usage (except that they are all large numbers represented in hexadecimal).
4. Enable Output
A diagram showing key parts of the GPIO controller.
Reading the manual reveals that we need to send two messages to the GPIO controller. We must tell it in its language that if we do this, it will be very happy to implement our intention to turn on the OK LED indicator. Fortunately, it is a very simple chip, and to make it understand what we want to do, we just need to set a few numbers.
mov r1,#1
lsl r1,#18
str r1,[r0,#4]
mov reg,#val
puts the numberval
into the register namedreg
.
lsl reg,#val
left shifts the binary operand in registerreg
byval
bits.
str reg,[dest,#val]
saves the number in registerreg
to the addressdest + val
.
These commands enable output on GPIO pin 16. First, we get a necessary value in register r1
, then we send this value to the GPIO controller. Therefore, the first two commands attempt to get the value into register r1
, we could use another ldr
command as before, but the lsl
command is more useful for setting any given GPIO pin, so deriving the value from a formula is better than directly writing it. The LED representing OK is directly connected to GPIO pin 16, so we need to send a command to enable output on pin 16.
The value in register r1
is what is needed to enable the LED pin. The first line command puts the number 110 into r1
. In this operation, the mov
command is much faster than the ldr
command because it does not need to interact with memory, while the ldr
command loads the required value from memory into the register. Nevertheless, the mov
command can only be used to load certain values. 4 In ARM assembly code, each instruction is represented by a three-letter code. They are called mnemonics, which indicate the purpose of the operation. mov
is short for “move,” while ldr
is short for “load register.” mov
moves the second parameter #1
into the preceding r1
register. Generally, #
indicates a number, but we have seen an exception to this.
The second instruction is lsl
(logical left shift). It means to shift the binary operand of the first parameter to the left by the number of bits indicated by the second parameter. In this case, shifting 110 (that is 12) left by 18 bits (turning it into 10000000000000000002 = 26214410).
If you are not familiar with binary representation, you can look below:
Further Reading: Explanation of Binary
Like hexadecimal, binary is another way of writing numbers. In binary, there are only two digits:
0
and1
. It is very useful in computers because we can implement it with circuits, where current flowing through the circuit represents1
, and current not flowing through the circuit represents0
. This is how computers can perform real work and do mathematics. Although binary has only two digits, it can represent any number, it just takes a bit longer to write.
567 in decimal = 1000110111 in binary
This image shows that the binary representation of 56710 is 10001101112. We use subscript 2 to indicate that this number is written in binary.
One of the coincidences of using binary extensively in assembly code is that numbers can easily be multiplied or divided by powers of
2
(that is1
,2
,4
,8
,16
). Generally, multiplication and division are very difficult, but in certain special cases, they become very easy, so binary is very important.
13*4 = 52, 1101*100=110100
Shifting a binary number left by
n
bits is equivalent to multiplying that number by 2n. Therefore, if we want to multiply a number by 4, we just need to shift that number left by 2 bits. If we want to multiply it by 256, we just need to shift it left by 8 bits. If we want to multiply a number by a number like 12, we can have an alternative approach, which is to multiply that number by 8 first, then multiply that number by 4, and finally add the results of the two multiplications to get the final result (N × 12 = N × (8 + 4) = N × 8 + N × 4).
53/16 = 3, 110100/10000=11
Shifting a binary number
n
bits to the right is equivalent to dividing that number by 2n. In right shift operations, the remainder bit of the division will be discarded. Unfortunately, it is very difficult to perform division on a binary number that cannot be evenly divided by a power of 2, which will be discussed in Course 9 Screen04[2] later.
Binary Terminology
This image shows common terms used in binary. A bit is a single binary digit. A “nibble” is 4 binary digits. A byte is 2 nibbles, which is 8 bits. A half is half of a word length, which here is 2 bytes. A word refers to the size of the register on the processor, so the word length of the Raspberry Pi is 4 bytes. By convention, the highest significant bit of a word is denoted as 31, while the lowest significant bit is denoted as 0. The top or highest bit represents the most significant bit, while the bottom or lowest bit represents the least significant bit. A kilobyte (KB) is 1000 bytes, a megabyte is 1000 KB. This representation can lead to some confusion about whether it should be 1000 or 1024 (the integers in binary). Given this situation, the new international standard states that a KB equals 1000 bytes, while a Kibibyte (KiB) is 1024 bytes. A Kb is 1000 bits, while a Kib is 1024 bits.
The Raspberry Pi defaults to little-endian, meaning when loading a byte from the address you just wrote, it starts loading from the low byte of a word.
To emphasize again, we must read the manual to know the values we need. The manual states that the GPIO controller has a set of 24 bytes that determine the settings for the GPIO pins. The first 4 bytes relate to the first 10 GPIO pins, the next 4 bytes relate to the next 10 pins, and so on. There are a total of 54 GPIO pins, so we need a set of 6 4-byte groups, totaling 24 bytes. In each 4-byte group, every 3 bits relate to a specific GPIO pin. We want to enable GPIO pin 16, so we need to set the second group of 4 bytes, as the second group of 4 bytes is used for GPIO pins 10-19, and we need the 6th group of 3 bits, which is numbered 18 in the code above (6×3).
The final str
(“store register”) command saves the value in the first parameter, writing the value in register r1
to the address calculated from the expression. That expression can be a register, in this case, r0
, which we know contains the address of the GPIO controller, and another value added to it, in this case, #4
. This means to write the value in register r1
to the address calculated as the GPIO controller address plus 4
. That address is the location of the second group of 4 bytes we mentioned earlier, so we send our first message to the GPIO controller to tell it to prepare to enable output on GPIO pin 16.
5. The Signal of Life
Now that the LED is ready to turn on, we still need to actually turn it on. This means we need to send a message to the GPIO controller to turn off pin 16. Yes, you read that right; we need to send a message to turn it off. The chip manufacturers think it makes more sense to turn on the LED when the GPIO pin is off. 5 Hardware engineers often make this counterintuitive decision, seemingly to keep operating system developers on their toes. It can be seen as a warning to themselves.
mov r1,#1
lsl r1,#16
str r1,[r0,#40]
I hope you can recognize all the commands above; do not worry about their values for now. The first command is the same as before, putting the value 1
into register r1
. The second command left shifts the binary 1
by 16 bits. Since we want to turn off GPIO pin 16, we need to set the 16th bit to 1 in the next message (to set other pins, just change the corresponding bit). Finally, we write this value to the address of the GPIO controller plus 4010, which will turn off that pin (adding 28 will turn it on).
6. Happily Ever After
It seems we can end now, but unfortunately, the processor does not know what we did. In fact, the processor runs continuously as long as it is powered. Therefore, we need to give it a task to keep running, otherwise, the Raspberry Pi will go to sleep (in this example, it will not, and the LED will stay on).
loop$:
b loop$
name:
names the next line.
b label
branches to run the command at the labellabel
.
The first line is not a command but a label. It names the next line loop$
, meaning we can refer to that line by name. This is called a label. When the code is converted to binary, the label will be discarded, but it is useful for us to find the line by name rather than by number (address). By convention, we use a $
to indicate that this label only applies to the code block, letting others know it does not apply to the entire program. The b
(“branch”) command will run the command at the specified label instead of running the next command after it. Therefore, the next line will run that b
command again, causing it to enter an infinite loop. Thus, the processor will be in an infinite loop until it is safely powered down.
The blank line at the end of the code block is intentional. The GNU toolchain requires all assembly code files to end with a blank line, so you can be sure you are done and the file has not been truncated. If you do not handle it this way, you will receive annoying warnings when the assembler runs.
7. The Raspberry Pi Takes the Stage
Now that we have finished writing the code, we can upload it to the Raspberry Pi. Open a terminal on your computer, change the current working directory to the parent directory of the source
folder. Type make
and press enter. If there are errors, refer to the troubleshooting section. If there are no errors, you will generate three files. kernel.img
is your compiled operating system image. kernel.list
is a listing of the assembly code you wrote, which is actually generated. This is very useful for checking if the program is correct in the future. The kernel.map
file contains a mapping of all label end positions, which is useful for tracking values.
To install your operating system, you need an SD card that already has the Raspberry Pi operating system installed. If you browse the files on the SD card, you should see a file named kernel.img
. Rename this file to something else, like kernel_linux.img
. Then, copy your compiled kernel.img
file to the original location on the SD card, which will replace the current Raspberry Pi operating system image with your operating system image file. When you want to switch back, just delete your kernel.img
file and rename the previously renamed file back to kernel.img
. I find it very useful to keep a backup of the original Raspberry Pi operating system in case you need it.
Insert this SD card into the Raspberry Pi and turn on its power. The OK LED light will turn on. If not, check the troubleshooting page. If everything goes as planned, congratulations, you have written your first operating system. Course 2 OK02[3] will guide you to make the LED blink and turn off.
_start
. Since we are developing an operating system, it always starts from _start
, and we can use the .section .init
command to set it. Therefore, if we do not tell it where the entry point is, it will confuse the toolchain and generate warning messages. So we first define a symbol named _start
, which is visible to all (global), and then generate the address of the symbol _start
on the next line. We will get to this address soon.↩mov
can only load values whose binary representation has the first 8 bits as 1
. In other words, it is a 0 followed by 8 1
or 0
.↩The reason is that modern chips are made using a technology called CMOS, which stands for complementary metal-oxide-semiconductor. Complementary means that each signal is connected to two transistors, one made using N-type semiconductor material, which pulls the voltage low, and the other made using P-type semiconductor material, which pulls the voltage high. At any time, only one semiconductor is on; otherwise, it will short circuit. The conductivity of P-type material is not as good as that of N-type material. This means that three times the P-type semiconductor material is needed to provide the same current as N-type semiconductor material. That is why the LED is always turned on by lowering to a low voltage because the N-type semiconductor pulls the voltage down more efficiently than the P-type semiconductor pulls it up.
There is another reason. Back in the 1970s, chips were entirely made of N-type material (NMOS), and P-type material was partially replaced by a resistor. This means that when the signal is low voltage, even if it does nothing, the chip still consumes energy (and generates heat). Your phone, when sitting in your pocket doing nothing, still heats up and drains your battery; this is not good design. Therefore, signals are designed to be “active low” and high voltage when inactive, so it does not consume energy. Although we do not use NMOS anymore, the fact that low voltage signals from N-type material are faster than high voltage signals from P-type material is still used in this design. Typically, there will be a bar mark above a signal name indicating “active low,” or it will be written as SIGNAL_n
or /SIGNAL
. However, even so, it is still very confusing, even hardware engineers cannot avoid this confusion!
via: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/ok01.html
Author: Robert Mullins[6] Selection: lujun9972 Translator: qhwdw Proofreader: wxy
This article is originally compiled by LCTT and honorably released by Linux China