compudanzas/src/uxn_tutorial_day_3.gmo

383 lines
13 KiB
Plaintext
Raw Normal View History

2021-07-31 02:08:13 +00:00
# uxn tutorial: day 3, the keyboard/controller
this is the third section of the <(uxn tutorial)>!
here we introduce the use of the controller device in the varvara uxn computer: this will allow us to add interactivity to our programs, and to start discussing control flow in uxntal.
we also talk about logic and stack manipulation instructions in uxntal.
# the controller device
the controller device allows us to read inputs from the keyboard in the case of uxnemu, and/or from the controller buttons in the case of uxn ports for handheld consoles.
the definition of its ports would look as follows in a typical uxntal program for the varvara computer:
```
|80 @Controller [ &vector $2 &button $1 &key $1 ]
```
## the button byte
the button byte encodes in each of its eight bits the state of eight different "buttons", based on the NES controller layout.
=> https://wiki.nesdev.com/w/index.php/Standard_controller standard NES controller
numbering the bits from right to left, and from 0 to 7, the corresponding keys (and NES buttons) are:
* 0: Ctrl (button A)
* 1: Alt (button B)
* 2: Shift (Select button)
* 3: Esc (Start button)
* 4: Up
* 5: Down
* 6: Left
* 7: Right
enconding the states in this allows us to press and read many of these keys at the same time.
## the key byte
the key byte stores the ascii code of the keyboard key that is being pressed at the moment.
## the controller vector
a vector refers to an address in main memory where uxn is assigned to jump to when a specific event happens.
in the case of the controller vector, this specific event would be whenever a key is pressed or released.
for example, the following line of code would assign the absolute address of the label on-controller to the controller vector:
```
;on-controller .Controller/vector DEO2
```
uxn will "jump" to that address whenever a key is pressed or released.
let's see next how would that work!
# control flow: vector subroutines
so far our uxntal programs have followed a linear flow: they start at address 0100, and they end at the first BRK instruction that is found.
we can think of these programs as setup routines: they setup the system colors, they might draw or print some things, and then they leave uxn "waiting". what would uxn be waiting for?
yes, an option would be: waiting for keyboard input!
we will start organizing our uxntal programs in terms of subroutines that correspond to different vectors.
each of these subroutines will finish with the BRK instruction, so that they can return uxn to the waiting state.
## controller vector subroutine
for example, the following program takes the sprite drawing we tried in the previous section, but has it ocurring only when a key is pressed:
```
( hello-keyboard.tal )
( devices )
|00 @System [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ]
|20 @Screen [ &vector $2 &width $2 &height $2 &pad $2 &x $2 &y $2 &addr $2 &color $1 ]
|80 @Controller [ &vector $2 &button $1 &key $1 ]
( main program )
|0100
( set system colors )
#2ce9 .System/r DEO2
#01c0 .System/g DEO2
#2ce5 .System/b DEO2
( assign controller vector )
;on-controller .Controller/vector DEO2
BRK
( run this code whenever a key is pressed or released )
@on-controller ( -> )
( set x,y coordinates )
#0008 .Screen/x DEO2
#0008 .Screen/y DEO2
( set sprite address )
;square .Screen/addr DEO2
( draw sprite in the background )
( using color 1 for the outline )
#21 .Screen/color DEO
BRK
( sprite )
@square ff81 8181 8181 81ff
```
how do we take different actions depending on the key that was pressed?
first of all, we have to let our program know which key was pressed, so let's take a look at some uxntal instructions that will help us with that!
# comparison and logic instructions
## comparison instructions
uxntal has four instructions for comparing two bytes:
* EQU: push 01 down into the stack if the top two elements of the stack are equal, 00 otherwise ( a b -- a==b )
* NEQ: push 01 down into the stack if the top two elements of the stack are not equal, 00 otherwise ( a b -- a!=b )
* GTH: push 01 down into the stack if the first element is greater than the second, 00 otherwise ( a b -- a>b )
* LTH: push 01 down into the stack if the first element is less than the second, 00 otherwise ( a b -- a>b )
for example, the following code will read the value of the controller key, and push into the stack a flag corresponding to it being equal to character 'a':
```
.Controller/key DEI ( read key )
LIT 'a ( push ascii code of character 'a' )
EQU ( compare both bytes and push 01 if they are the same, 00 if not )
```
EQU2, NEQ2, GTH2 and LTH2 will work in the same way, but comparing shorts instead of bytes.
## logic instructions
uxntal has three bitwise logic instructions, that can work as logic operators with the results given by the comparison instructions we discussed above:
* AND: perform a bitwise AND with the top two elements of the stack, and push down the result ( a b -- a&b )
* ORA: perform a bitwise OR with the top two elements of the stack, and push down the result ( a b -- a|b )
* EOR: perform a bitwise exclusive-OR with the top two elements of the stack, and push down the result ( a b -- a^b )
AND2, ORA2, EOR2 will work in the same way, but with shorts instead of bytes.
### AND
the following will push down into the stack a flag that indicates if the key byte is between 30 and 39 inclusive, using 01 to answer "true", and 00 to answer "false":
```
.Controller/key DEI ( read key )
#2f GTH ( is it greater than 2f? push flag into the stack )
.Controller/key DEI ( ready key )
#3a LTH ( is it less than 3a? push flag into the stack )
AND ( apply an AND to the flags in the stack, and push the result in the stack )
```
that the instruction is bitwise means that it applies the AND operation to each of the bits of the operands.
if both flags were "true":
```
0000 0001 ( true )
AND 0000 0001 ( true )
----------
0000 0001 ( true )
```
if any (or both) of the flags were "false":
```
0000 0001 ( true )
AND 0000 0000 ( false )
----------
0000 0000 ( false )
```
### OR
the following code will push a flag down into the stack if the key byte is either '1' or 'a':
```
.Controller/key DEI ( read key )
LIT '1 EQU ( is it '1'? push flag into the stack )
.Controller/key DEI ( ready key )
LIT 'a EQU ( is it 'a'? push flag into the stack )
ORA ( apply an OR to the flags in the stack, and push the result in the stack )
```
when any of the flags are true, the flag will be true:
```
0000 0001 ( true )
OR 0000 0000 ( false )
----------
0000 0001 ( true )
```
### EOR
an exclusive OR can be used to invert the value of a flag, by using a "mask" where the bit(s) that we'd like to invert are set as 1.
for example, the folowing code will push a flag corresponding to the key being greater than or equal to 20, by calculating first if it's less than 20, and then inverting the result:
```
.Controller/key DEI ( read key )
#20 LTH ( is it less than 20? push flag into the stack )
#01 EOR ( invert flag and push the result into the stack )
```
when the original flag is true, which means that the value is less than 20, the EOR will invert it and make it false: the value is not greater than or equal to 20:
```
0000 0001 ( true )
EOR 0000 0001 ( mask )
----------
0000 0000 ( false )
```
when the original flag is false, which means that the value is not less than 20, the EOR will invert it and make it true: the value is greater than or equal to 20:
```
0000 0000 ( false )
EOR 0000 0001 ( mask )
----------
0000 0001 ( true )
```
# control flow: conditional jumps
ok, so how can we use these flags in order to have conditional expressions in our programs?
let's introduce another set of new instructions to have uxn break its linear flow
## instructions for jumps
* JMP: unconditionally jump to the address in the stack ( addr -- )
* JCN: take an address and a value from the stack, and jump to the address if the value is not 00; otherwise continue with the next instruction ( value addr -- )
in the byte mode, the addresses that these instructions take are one byte long.
these addresses are relative and signed: they indicate how many bytes have to be skipped, either forward (positive) or backwards (negative). the range for these relative addresses is from -128 to 127 inclusive.
in short mode, the addresses that these instructions take are absolute (i.e. two-bytes long), but the value that JCN takes in order to decide is still a byte.
## runes for addresses
there are several runes that refer to addresses and labels. uxnasm reads them and converts them to the corresponding binary values.
in the previous days we talked already about some of them; this is a recap of those, and an introduction of the new ones:
* literal address in zero page: .label (one byte)
* literal address in main memory: ;label (one short)
* literal relative address in main memory: ,label (one byte)
* raw address in main memory: :label (one short)
in order to define labels, we use:
* label definition: @label
* sublabel definition: &sublabel
and finally, to refer to labels within our uxntal code, we have the following cases:
* for a main label: use the label name
* for a sublabel: use label/sublabel
* for a local sublabel: use &sublabel
## conditional jump
let's join all of this together!
the following on-controller subroutine, illustrates the use of jumps, drawing our sprite only when the key that was pressed was '1':
```
@on-controller
.Controller/key DEI ( read key )
LIT '1 EQU ( is it '1'? )
,&draw-sprite JCN ( jump to draw-sprite if that's the case )
,&end JMP ( otherwise, jump to the end )
&draw-sprite
( set x,y coordinates )
#0008 .Screen/x DEO2
#0008 .Screen/y DEO2
( set sprite address )
;square .Screen/addr DEO2
( draw sprite in the background )
( using color 1 for the outline )
#21 .Screen/color DEO
&end
BRK
```
note the use of sublabels "inside" on-controller.
also note how the expression ,&sublabel corresponds to the relative address that is needed in order to jump to that location in the code, either using JCN or JMP.
## conditional jumps
the following code illustrates the use of many conditions: the color of the sprite changes accordingly if the keys 1, 2 or 3 are pressed.
```
@on-controller
( set x,y coordinates )
#0008 .Screen/x DEO2
#0008 .Screen/y DEO2
( set sprite address )
;square .Screen/addr DEO2
.Controller/key DEI LIT '1 EQU ( is the key '1'? )
,&color-1 JCN ( jump to color-1 if that's the case )
.Controller/key DEI LIT '2 EQU ( is the key '2'? )
,&color-2 JCN ( jump to color-2 if that's the case )
.Controller/key DEI LIT '3 EQU ( is the key '3'? )
,&color-3 JCN ( jump to color-3 if that's the case )
( in any other case, finish )
BRK
&color-1
( draw sprite in the background )
( using color 1 for the outline )
#21 .Screen/color DEO
BRK
&color-2
( draw sprite in the background )
( using color 2 for the outline )
#22 .Screen/color DEO
BRK
&color-3
( draw sprite in the background )
( using color 3 for the outline )
#23 .Screen/color DEO
BRK
BRK
```
note how the conditions are concatenated: whenever a flag is false, JCN allows uxn to continue with the next instruction in memory.
also note that this code is not optimized for size or speed, but for readability.
it would be up to you to, for example, perform arithmetic with the value of the key that was pressed in order to calculate the color to assign to the sprite :)
# stack manipulation
# controller button
* controller vector
* flow control: conditionals, relative and absolute jumps
* runes for addresses
* button and key
* bitwise masks
* practice: move/change sprite with keyboard
# instructions of day 3
## comparison instructions
* EQU: push 01 down into the stack if the top two elements of the stack are equal, 00 otherwise ( a b -- a==b )
* NEQ: push 01 down into the stack if the top two elements of the stack are not equal, 00 otherwise ( a b -- a!=b )
* GTH: push 01 down into the stack if the first element is greater than the second, 00 otherwise ( a b -- a>b )
* LTH: push 01 down into the stack if the first element is less than the second, 00 otherwise ( a b -- a>b )
## bitwise logic
* AND: perform a bitwise AND with the top two elements of the stack, and push down the result ( a b -- a&b )
* ORA: perform a bitwise OR with the top two elements of the stack, and push down the result ( a b -- a|b )
* EOR: perform a bitwise exclusive-OR with the top two elements of the stack, and push down the result ( a b -- a^b )
## jumps
* JMP: unconditionally jump to the address in the stack ( addr -- )
* JCN: take an address and a value from the stack, and jump to the address if the value is not 00; otherwise continue with the next instruction ( value addr -- )