
Course Input 02 is based on Course Input 01, explaining how to implement user command input and computer processing and display through a simple command line. This article assumes that you already have the operating system code foundation from Course 11: Input 01[1].
1. Terminal
Almost all operating systems start with a character terminal display. The classic black background with white text allows you to input commands to the computer via the keyboard, which then prompts you with spelling errors or gives you the desired execution result. This method has two main advantages: the keyboard and display provide a simple and robust computer interaction mechanism, which is used by almost all computer systems and is widely applied by system administrators.
Early computing generally took place on a giant computer system in a building, which had many ‘terminals’ for inputting commands. The computer executed commands from different sources in sequence.
Let’s analyze what information we really want:
This type of terminal is defined as a standard input-output device. The screen used for (displaying) input and the screen for printing output are the same (LCTT note: the earliest output was indeed “printed” to printers/teletypes, and the input terminal was just a keyboard; unless echoing was done, the output terminal would not display the input characters). In other words, the terminal is an abstraction of character display. In character display, a single character is the smallest unit, not pixels. The screen is divided into a fixed number of characters of different colors. We can first store characters and their corresponding colors based on the existing screen code, and then use the method DrawCharacter
to push them to the screen. Once we need character display, we just need to draw a line of strings on the screen.
Create a new file named terminal.s
as follows:
.section .data
.align 4
terminalStart:
.int terminalBuffer
terminalStop:
.int terminalBuffer
terminalView:
.int terminalBuffer
terminalColour:
.byte 0xf
.align 8
terminalBuffer:
.rept 128*128
.byte 0x7f
.byte 0x0
.endr
terminalScreen:
.rept 1024/8 core.md Dict.md lctt2014.md lctt2016.md lctt2018.md LICENSE published README.md scripts sources translated 768/16
.byte 0x7f
.byte 0x0
.endr
This is the configuration data file for the terminal. We have two main storage variables: terminalBuffer
and terminalScreen
. terminalBuffer
stores all characters that have been displayed. It stores 128 lines of character text (1 line contains 128 characters). Each character consists of an ASCII character and a color unit, initialized to 0x7f (ASCII delete character) and 0 (foreground and background color are black). terminalScreen
stores the characters currently displayed on the screen. It also stores 128×48 characters, initialized to the same value as terminalBuffer
. You might think that I only need terminalScreen
, why do I need terminalBuffer
? In fact, there are two benefits:
This unique technique is common in low-power systems. Redrawing the screen is a time-consuming operation, so we only perform this operation when absolutely necessary. In this system, we can arbitrarily change terminalBuffer
, and then call a method that only copies the changed bytes on the screen. In other words, we do not need to continuously draw each character, which can save a significant amount of time for cross-line text operations.
You always need to try to design an efficient system; if this system runs faster with little change.
Other meanings of the values in the .data
segment are as follows:
terminalStart
writes the first character to terminalBuffer
.terminalStop
writes the last character to terminalBuffer
.terminalView
indicates the first character of the current screen, allowing us to control the scrolling of the screen.temrinalColour
is the color of the character to be drawn.terminalStart
needs to be saved because termainlBuffer
is a circular buffer. This means that when the buffer is full, the end will wrap around and overwrite the start position, making the last character the first character. Therefore, we need to advance terminalStart
so that we know we have filled it. How to implement buffer detection: if the index goes out of bounds to the end of the buffer, the index will point to the start position of the buffer. Circular buffers are a clever method for storing large amounts of data, often because the most recent parts of this data are more important. It allows unrestricted writing while only guaranteeing that the most recent specific data is valid. This is often used in signal processing and data compression algorithms. In such cases, we can store 128 lines of terminal records, and exceeding 128 lines will not be a problem. If not, when exceeding the 128th line, we would need to copy the 127 lines forward each time, which is very time-consuming.
Illustration of inserting “Hello world” into a circular buffer of size 5.
A circular buffer is an example of a data structure. It is an idea for organizing data, sometimes we implement this idea through software.
As already mentioned, terminalColour
has been mentioned several times. You can implement terminal colors according to your ideas, but this text terminal has 16 foreground colors and 16 background colors (which means there are 162 = 256 combinations). The CGA[2] terminal color definitions are as follows:
Table 1.1 – CGA Color Codes
Index | Color (R, G, B) |
---|---|
0 | Black (0, 0, 0) |
1 | Blue (0, 0, ⅔) |
2 | Green (0, ⅔, 0) |
3 | Cyan (0, ⅔, ⅔) |
4 | Red (⅔, 0, 0) |
5 | Magenta (⅔, 0, ⅔) |
6 | Brown (⅔, ⅓, 0) |
7 | Light Gray (⅔, ⅔, ⅔) |
8 | Gray (⅓, ⅓, ⅓) |
9 | Light Blue (⅓, ⅓, 1) |
10 | Light Green (⅓, 1, ⅓) |
11 | Light Cyan (⅓, 1, 1) |
12 | Light Red (1, ⅓, ⅓) |
13 | Light Magenta (1, ⅓, 1) |
14 | Yellow (1, 1, ⅓) |
15 | White (1, 1, 1) |
We will save the foreground color in the low byte of the color, and the background color in the high byte of the color. Except for brown, the other colors follow a pattern where the high-order bits represent an increase of ⅓ to each component, and the other bits represent an increase of ⅔ to their respective components. This makes RGB color conversion easy.
Brown, as a substitute color (black-yellow), is neither attractive nor useful.
We need a method to read the color code from TerminalColour
using four bits, and then call SetForeColour
with the 16-bit equivalent parameter. Try to implement it yourself. If you find it troublesome or have not completed the screen series course, our implementation is as follows:
.section .text
TerminalColour:
teq r0,#6
ldreq r0,=0x02B5
beq SetForeColour
tst r0,#0b1000
ldrne r1,=0x52AA
moveq r1,#0
tst r0,#0b0100
addne r1,#0x15
tst r0,#0b0010
addne r1,#0x540
tst r0,#0b0001
addne r1,#0xA800
mov r0,r1
b SetForeColour
2. Text Display
Our terminal’s first real method is TerminalDisplay
, which is used to copy the current data from terminalBuffer
to terminalScreen
and the actual screen. As mentioned above, this method must be a minimal overhead operation because we need to call it frequently. It mainly compares the text in terminalBuffer
with terminalDisplay
and only copies the bytes that are different. Remember, terminalBuffer
operates as a circular buffer, in which case, it goes from terminalView
to terminalStop
, or 128*48 characters, depending on which comes faster. If we encounter terminalStop
, we will assume that all characters after this are 7f16 (ASCII delete character), with a color of 0 (black foreground and background color).
Let’s see what needs to be done:
terminalView
, terminalStop
, and terminalDisplay
.terminalView
is not equal to terminalStop
, load the current character and color based on terminalView
.terminalDisplay
.terminalDisplay
.TerminalColour
with r0
as the background color parameter.DrawCharacter
with r0 = 0x7f
(ASCII delete character, a block), r1 = x
, r2 = y
.TerminalColour
with r0
as the foreground color parameter.DrawCharacter
with r0 = character
, r1 = x
, r2 = y
.terminalDisplay
by 2.terminalView
is not equal to terminalStop
, increment the position parameter terminalView
by 2.terminalView
position is at the end of the file buffer, set it to the start position of the buffer.Try to implement it yourself. If you encounter problems, our solution is provided below:
1. Here my variables are a bit messy. For convenience, I use taddr
to store the end position of textBuffer
.
.globl TerminalDisplay
TerminalDisplay:
push {r4,r5,r6,r7,r8,r9,r10,r11,lr}
x .req r4
y .req r5
char .req r6
col .req r7
screen .req r8
taddr .req r9
view .req r10
stop .req r11
ldr taddr,=terminalStart
ldr view,[taddr,#terminalView - terminalStart]
ldr stop,[taddr,#terminalStop - terminalStart]
add taddr,#terminalBuffer - terminalStart
add taddr,#128*128*2
mov screen,taddr
2. Start running from yLoop
.
mov y,#0
yLoop$:
2.1,
mov x,#0
xLoop$:
Start running from xLoop
.
2.1.1, For convenience, I load the character and color into char
variable.
teq view,stop
ldrneh char,[view]
2.1.2, This line supplements the previous line: read the black delete character.
moveq char,#0x7f
2.1.3, For convenience, I load both character and color into col
variable.
ldrh col,[screen]
2.1.4, Now I use the teq
instruction to check if there is a data change.
teq col,char
beq xLoopContinue$
2.1.5, I can easily save the current value.
strh char,[screen]
2.1.6, I use bit shift instructions lsr
and and
instructions to split the char
variable, putting the color into col
variable, and the character into char
variable, then use bit shift instruction lsr
to get the background color and call TerminalColour
.
lsr col,char,#8
and char,#0x7f
lsr r0,col,#4
bl TerminalColour
2.1.7, Write a colored delete character.
mov r0,#0x7f
mov r1,x
mov r2,y
bl DrawCharacter
2.1.8, Use the and
instruction to get the low half-byte of col
variable, and then call TerminalColour
.
and r0,col,#0xf
bl TerminalColour
2.1.9, Write the character we need.
mov r0,char
mov r1,x
mov r2,y
bl DrawCharacter
2.1.10, Increment the screen pointer position.
xLoopContinue$:
add screen,#2
2.1.11, If possible, increment the view
pointer.
teq view,stop
addne view,#2
2.1.12, It’s easy to check if the view
pointer has gone out of bounds to the end of the buffer, because the buffer address is stored in taddr
variable.
teq view,taddr
subeq view,#128*128*2
2.1.13, If there are still characters to display, we need to increment the x
variable and then go to the xLoop
loop.
add x,#8
teq x,#1024
bne xLoop$
2.2, If there are more characters to display we need to increment the y
variable and then go to the yLoop
loop.
add y,#16
teq y,#768
bne yLoop$
3. Don’t forget to clear the variables at the end.
pop {r4,r5,r6,r7,r8,r9,r10,r11,pc}
.unreq x
.unreq y
.unreq char
.unreq col
.unreq screen
.unreq taddr
.unreq view
.unreq stop
This method allows us to print any character to the screen. However, we used color variables but did not actually set them. Generally, terminals use a combination of character attributes to modify colors. For example, ASCII escape (1b16) followed by a hexadecimal number from 0 – f can set the foreground color to CGA color numbers. If you want to try to implement it; there is a detailed example of mine on the download page.
4. Flag Input
Now we have an output terminal that can print and display text. This only says half of the story; we need input. We want to implement a method: ReadLine
, which can save a line of text from a file, the text position is given by r0
, the maximum length is given by r1
, returning the string length in r0
. The tricky part is that when the user outputs characters, an echo function is required, along with a backspace delete function and a command enter execution function. They also need a blinking underscore to indicate that the computer needs input. These completely reasonable requirements make constructing this method more challenging. One way to meet these needs is to store the user input text and file size in some place in memory. Then when calling ReadLine
, move the address of terminalStop
to where it started and then call Print
. This means we only need to ensure that we maintain a string in memory, and construct our own print function.
As a convention, many programming languages allow any program to access stdin and stdout, which can connect to the input and output streams of the terminal. This can also be done in graphical programs, but is rarely used in practice.
Let’s see what ReadLine
does:
terminalStop
and terminalStop
.terminalView
and terminalStop
into memory.Print
to print the current string.TerminalDisplay
.KeyboardUpdate
.KeyboardGetChar
.Print
and TerminalDisplay
to show the final input.To help the reader understand, and then implement it themselves, our implementation provides the following:
.globl ReadLine
ReadLine:
teq r1,#0
moveq r0,#0
moveq pc,lr
2. Considering common scenarios, we did a lot of initialization actions initially. input
represents the value of terminalStop
, view
represents terminalView
. Length
defaults to 0
.
string .req r4
maxLength .req r5
input .req r6
taddr .req r7
length .req r8
view .req r9
push {r4,r5,r6,r7,r8,r9,lr}
mov string,r0
mov maxLength,r1
ldr taddr,=terminalStart
ldr input,[taddr,#terminalStop-terminalStart]
ldr view,[taddr,#terminalView-terminalStart]
mov length,#0
3. We must check for unusually large read operations; we cannot handle input exceeding the size of terminalBuffer
(theoretically possible, but moving terminalStart
beyond the stored terminalStop
will create many problems).
cmp maxLength,#128*64
movhi maxLength,#128*64
4. Since the user needs a blinking cursor, we need a backup character to ideally place an end character after this string.
sub maxLength,#1
5. Write an underscore to let the user know we can input now.
mov r0,#'_'
strb r0,[string,length]
6. Save terminalStop
and terminalView
. This is important for resetting a terminal; it will modify these variables. Strictly speaking, it could also modify terminalStart
, but it’s irreversible.
readLoop$:
str input,[taddr,#terminalStop-terminalStart]
str view,[taddr,#terminalView-terminalStart]
7. Write the current input. Since there’s an underscore, the string length increases by 1.
mov r0,string
mov r1,length
add r1,#1
bl Print
8. Copy the next text to the screen.
bl TerminalDisplay
9. Get the most recent keyboard input.
bl KeyboardUpdate
10. Retrieve the keyboard input key value.
bl KeyboardGetChar
11. If we have an enter key, the loop breaks. If there’s an end character and a backspace, it will also break the loop.
teq r0,#'
'
b eq readLoopBreak$
teq r0,#0
beq cursor$
teq r0,#''
bne standard$
12. Remove a character from length
.
delete$:
cmp length,#0
subgt length,#1
b cursor$
13. Write back a normal character.
standard$:
cmp length,maxLength
bge cursor$
strb r0,[string,length]
add length,#1
14. Load the most recent character; if it’s not an underscore, change it to a new line, if it is, change it to a space.
cursor$:
ldrb r0,[string,length]
teq r0,#'_'
moveq r0,#' '
movne r0,#'_'
strb r0,[string,length]
15. Loop back to user input.
b readLoop$
readLoopBreak$:
16. Write a new line character at the end of the string.
mov r0,#'
'
strb r0,[string,length]
17. Reset terminalView
and terminalStop
, then call Print
and TerminalDisplay
to show the final input.
str input,[taddr,#terminalStop-terminalStart]
str view,[taddr,#terminalView-terminalStart]
mov r0,string
mov r1,length
add r1,#1
bl Print
bl TerminalDisplay
18. Write an end character.
mov r0,#0
strb r0,[string,length]
19. Return the length.
mov r0,length
pop {r4,r5,r6,r7,r8,r9,pc}
.unreq string
.unreq maxLength
.unreq input
.unreq taddr
.unreq length
.unreq view
5. Terminal: Evolution of Machines
Now we theoretically have a terminal that can interact with users. The most obvious thing is to take it for a test! Remove the code after bl UsbInitialise
in main.s
as follows:
reset$:
mov sp,#0x8000
bl TerminalClear
ldr r0,=welcome
mov r1,#welcomeEnd-welcome
bl Print
loop$:
ldr r0,=prompt
mov r1,#promptEnd-prompt
bl Print
ldr r0,=command
mov r1,#commandEnd-command
bl ReadLine
teq r0,#0
beq loopContinue$
mov r4,r0
ldr r5,=command
ldr r6,=commandTable
ldr r7,[r6,#0]
ldr r9,[r6,#4]
commandLoop$:
ldr r8,[r6,#8]
sub r1,r8,r7
cmp r1,r4
bgt commandLoopContinue$
mov r0,#0
commandName$:
ldrb r2,[r5,r0]
ldrb r3,[r7,r0]
teq r2,r3
bne commandLoopContinue$
add r0,#1
teq r0,r1
bne commandName$
ldrb r2,[r5,r0]
teq r2,#0
teqne r2,#' '
bne commandLoopContinue$
mov r0,r5
mov r1,r4
mov lr,pc
mov pc,r9
b loopContinue$
commandLoopContinue$:
add r6,#8
mov r7,r8
ldr r9,[r6,#4]
teq r9,#0
bne commandLoop$
ldr r0,=commandUnknown
mov r1,#commandUnknownEnd-commandUnknown
ldr r2,=formatBuffer
ldr r3,=command
bl FormatString
mov r1,r0
ldr r0,=formatBuffer
bl Print
loopContinue$:
bl TerminalDisplay
b loop$
echo:
cmp r1,#5
movle pc,lr
add r0,#5
sub r1,#5
b Print
ok:
teq r1,#5
beq okOn$
teq r1,#6
beq okOff$
mov pc,lr
okOn$:
ldrb r2,[r0,#3]
teq r2,#'o'
ldreqb r2,[r0,#4]
teqeq r2,#'n'
movne pc,lr
mov r1,#0
b okAct$
okOff$:
ldrb r2,[r0,#3]
teq r2,#'o'
ldreqb r2,[r0,#4]
teqeq r2,#'f'
ldreqb r2,[r0,#5]
teqeq r2,#'f'
movne pc,lr
mov r1,#1
okAct$:
mov r0,#16
b SetGpio
.section .data
.align 2
welcome: .ascii "Welcome to Alex's OS - Everyone's favourite OS"
welcomeEnd:
.align 2
prompt: .ascii "\n> "
promptEnd:
.align 2
command:
.rept 128
.byte 0
.endr
commandEnd:
.byte 0
.align 2
commandUnknown: .ascii "Command `%s' was not recognised.\n"
commandUnknownEnd:
.align 2
formatBuffer:
.rept 256
.byte 0
.endr
formatEnd:
.align 2
commandStringEcho: .ascii "echo"
commandStringReset: .ascii "reset"
commandStringOk: .ascii "ok"
commandStringCls: .ascii "cls"
commandStringEnd:
.align 2
commandTable:
.int commandStringEcho, echo
.int commandStringReset, reset$
.int commandStringOk, ok
.int commandStringCls, TerminalClear
.int commandStringEnd, 0
This code integrates a simple command line operating system. It supports commands: echo
, reset
, ok
, and cls
. The echo
command copies any text to the terminal, the reset
command resets the operating system when there are issues, ok
has two functions: to set the OK light on and off, and finally cls
calls TerminalClear
to clear the terminal.
Try out the Raspberry Pi code. If you encounter problems, please refer to the FAQ page.
If it runs fine, congratulations on completing a basic terminal and input series for an operating system. Unfortunately, this tutorial ends here, but I hope to create more tutorials in the future. For any questions, please feedback to [email protected][3].
You have established a simple terminal operating system. Our code constructs a usable command table in commandTable
. Each entry in the table is an integer that represents the address of the string and an integer that represents the execution entry of the code. The last entry is commandStringEnd
, which is 0. Try to implement your own commands, refer to the existing functions, and create a new one. The function parameters r0
are the address of the user input command, and r1
is its length. You can pass your input values to your command. Maybe you have a calculator program, or perhaps a drawing program or chess. Whatever your idea is, make it run!
via: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/input02.html
Author: Alex Chadwick[5] Topic: lujun9972 Translator: guevaraya Proofreader: wxy
This article is originally compiled by LCTT and honorably presented by Linux China