Raspberry Pi Laboratory: Course 11 Input 02 | Linux China

Raspberry Pi Laboratory: Course 11 Input 02 | Linux China

Course Input 02 is based on Course Input 01, explaining through a simple command line how to implement user command input and computer processing and display.

— Alex Chadwick

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:

1. The computer displays a welcome message after turning on.
2. The computer can accept input after starting.
3. The user inputs parameterized commands from the keyboard.
4. The user presses the enter key or submit button.
5. The computer parses the command and executes the available commands.
6. The computer displays the execution results and process information of the commands.
7. Loops back to step 2.

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:

1. We can easily see the changes in the string by just drawing the changed characters.
2. We can roll back the historical characters displayed on the terminal, which are the buffered characters (with limitations).

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.

Raspberry Pi Laboratory: Course 11 Input 02 | Linux China

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

< Slide left and right if not fully displayed >
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:

1. Load the addresses of terminalView, terminalStop, and terminalDisplay.
2. For each line:

1. For each column:

1. If terminalView is not equal to terminalStop, load the current character and color based on terminalView.
2. Otherwise, load 0x7f and color 0.
3. Load the current character from terminalDisplay.
4. If the character and color are the same, jump directly to step 10.
5. Store the character and color into terminalDisplay.
6. Call TerminalColour with r0 as the background color parameter.
7. Call DrawCharacter with r0 = 0x7f (ASCII delete character, a block), r1 = x, r2 = y.
8. Call TerminalColour with r0 as the foreground color parameter.
9. Call DrawCharacter with r0 = character, r1 = x, r2 = y.
10. Increment the position parameter terminalDisplay by 2.
11. If terminalView is not equal to terminalStop, increment the position parameter terminalView by 2.
12. If terminalView position is at the end of the file buffer, set it to the start position of the buffer.
13. Increase the x coordinate by 8.
2. Increase the y coordinate by 16.

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:

1. If the maximum length of the string that can be saved is 0, return directly.
2. Retrieve the current values of terminalStop and terminalStop.
3. If the maximum length of the string is about half of the buffer, set the size to half of the buffer.
4. Subtract 1 from the maximum length to ensure the input blinking character or end character.
5. Write an underscore into the string to indicate that the user can input.
6. Write the addresses of terminalView and terminalStop into memory.
7. Call Print to print the current string.
8. Call TerminalDisplay.
9. Call KeyboardUpdate.
10. Call KeyboardGetChar.
11. If it’s a new line, jump directly to step 16.
12. If it’s a backspace, reduce the string length by 1 (if greater than 0).
13. If it’s a normal character, write it into the string (ensuring the string size is less than the maximum value).
14. If the string ends with an underscore, write a space; otherwise, write an underscore.
15. Jump back to step 6.
16. Write a new line character at the end of the string.
17. Call Print and TerminalDisplay to show the final input.
18. Replace the new line with an end character.
19. Return the length of the string.

To help the reader understand, and then implement it themselves, our implementation provides the following:

1. Quick handling of the case where the length is 0.
.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&gt; "
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

Raspberry Pi Laboratory: Course 11 Input 02 | Linux China

Leave a Comment

×