💾 Archived View for compudanzas.net › uxn_tutorial_day_3.gmi captured on 2023-03-20 at 17:40:47. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-01-29)
-=-=-=-=-=-=-
en español:
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 in the varvara computer allows us to read inputs from the keyboard and/or from controller buttons.
the definition of its ports would look as follows in a typical program:
|80 @Controller [ &vector $2 &button $1 &key $1 ]
the button byte encodes in each of its eight bits the state of eight different "buttons", based on the NES controller layout.
numbering the bits from right to left, and from 0 to 7, the corresponding keys (and NES buttons) are:
enconding the states of the buttons in this way allows us to press and read many of these keys at the same time.
the key byte stores the ascii code of the keyboard key that is being pressed at the moment.
the difference between the 'key' byte and the 'button' byte can be confusing, especially when running varvara from uxnemu where the buttons are in the same place as the keys.
a possible way to remember might be to think of the 'button' byte as referring to a gamepad controller.
in the context of uxn programming, 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 consists in whenever a key is pressed or released.
in other words: uxn will jump to the address assigned as the controller vector, whenever a key is pressed or released.
the following line of code would assign that vector, using the absolute address of the label on-controller:
;on-controller .Controller/vector DEO2
let's see next how that would work!
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 end with the BRK instruction, so that they can make uxn return to the waiting state.
to illustrate that behavior, let's read the following program.
it uses the sprite drawing procedure we tried in the previous day, but has it happening only when a key is pressed. in the beginning, the screen is empty, and when we press a key a square is drawn:
( 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 &pixel $1 &sprite $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 ) #01 .Screen/sprite DEO BRK ( sprite ) @square ff81 8181 8181 81ff
nice, isn't it?
now, how can 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 that it can act accordingly.
in order to achieve that, let's take a look at some new uxntal instructions!
uxntal has four instructions for comparing the top two elements from the stack:
we can think of the results pushed by these instructions as boolean flags: they are 01 if the comparison was true, and they are 00 if it was false.
here's a small 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 and push it down into the stack ) 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.
uxntal has three bitwise logic instructions.
they can work as logic operators that use as operands the flags given by the comparison instructions we discussed above:
AND2, ORA2, EOR2 will work in the same way, but with shorts instead of bytes.
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 represent 'true', and 00 to represent 'false':
.Controller/key DEI ( read key and push into the stack ) #2f GTH ( is it greater than 2f? push flag into the stack ) .Controller/key DEI ( read key and push into the stack ) #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 into 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 )
as these flags only use the least significant bit (the rightmost bit) to encode their value, a bitwise AND is equivalent to a conventional logic AND.
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 and push into the stack ) LIT "1 EQU ( is it '1'? push flag into the stack ) .Controller/key DEI ( read key and push into the stack ) 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 or both of the flags are true, the flag will be true:
0000 0001 ( true ) OR 0000 0000 ( false ) ---------- 0000 0001 ( true )
only when both flags are false, the resulting flag will be false.
an exclusive-OR is a logical operation that has a result of true only when one or the other inputs are true. if both inputs are true, or if both inputs are false, the result is false.
based on that behavior, this instruction can be used to invert the value of a flag by using a special value where the bit(s) that we'd like to invert are set as 1. these kind of values are called masks.
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 and push into the stack ) #20 LTH ( is it less than 20? push flag into the stack ) #01 EOR ( invert rightmost bit of the flag and push the result into the stack )
when the original flag is true, which means that the key 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 )
observe how because the two input bits are 1, the output bit is 0.
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 )
note that the mask is the same, and the result is the opposite value of the flag.
ok, so now our programs can identify and store in flags if a value (like the read keyboard key) is a specific value, or within some range.
how can we use these flags in order to have conditional behaviors in our programs, where different actions are taken depending on the results?
let's introduce another set of new instructions to have uxn break its linear flow!
in byte mode, the addresses that these instructions use are one byte long.
these byte-long addresses are relative and signed: they indicate how many bytes have to be skipped in main memory from the current position of the program counter, 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 of the value that JCN takes in order to decide is still a byte.
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:
a more advanced set of runes let us use "raw" addresses, i.e. without an implicit LIT or LIT2:
in order to define labels, we use:
and finally, to refer to labels within our uxntal code, we have the following cases:
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'? ) ( jump to draw-sprite if that's the case ) ,&draw-sprite JCN ,&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 ) #01 .Screen/sprite DEO &end BRK
note the use of sublabels "inside" (after) 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 named with a local sublabel (&).
these relative, one-byte sized, addresses are used by JCN or JMP.
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 ) #01 .Screen/sprite DEO BRK &color-2 ( draw sprite in the background ) ( using color 2 for the outline ) #02 .Screen/sprite DEO BRK &color-3 ( draw sprite in the background ) ( using color 3 for the outline ) #03 .Screen/sprite DEO BRK BRK
notice how the conditions are written one after another: 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 - you could get inspiration from your PRINT-DIGIT macro from day 1!
so far we have been using the stack as a place to store operands of instructions and their results, but we haven't used yet the full potential of this stack-based environment!
uxntal has six instructions that act upon elements in the stack closer to the top:
in short mode, POP2, DUP2, SWP2, NIP2, OVR2 and ROT2 perform the same actions but using shorts instead of bytes.
we'll be using these instructions in many different ways during the following days.
the following are some examples based on snippets of code that we discussed already.
keep in mind that using these instructions may contribute to a code that is hard to follow or read, so it will be always a good idea to use them within macros or to have comments in the code explaining what's happening :)
we discussed above this piece of code, that pushes a flag that answers if the key that is pressed has an ascii code between 30 and 39, inclusive (i.e., it calculates if a byte has an ascii code corresponding to a decimal digit)
.Controller/key DEI ( read key and push into the stack ) #2f GTH ( is it greater than 2f? push flag into the stack ) .Controller/key DEI ( read key and push into the stack ) #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 )
instead of reading the key twice, we could do it once, and then use the DUP instruction to copy the value:
.Controller/key DEI DUP ( read and duplicate key )
the stack after these instructions would have two copies of the key value:
key key <- top
then in our code we can continue adding the first comparison:
#2f GTH ( is it greater than 2f? push flag into the stack )
after this, the stack would look like:
key flag1 <- top
in order to perform the second comparison, we need to have the key at the top, not the flag.
how do we achieve that? that's right, using a SWP:
SWP ( put key at the top )
now the stack looks like this:
flag1 key <- top
finally we can proceed with the comparison and the AND:
#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 )
ending with a stack that has the result only:
result <- top
the complete code would read as:
.Controller/key DEI DUP ( read and duplicate key ) #2f GTH ( is it greater than 2f? push flag into the stack ) SWP ( put key at the top ) #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 )
the first code is assembled as 13 bytes, and this one is assembled as 12 bytes. maybe not too much of a difference on that front.
however, a more meaningful advantage is that this new routine now needs its input pushed down into the stack only at the beginning.
in the case we just discussed the input is the key that is pressed, but we could easily have as an input any other value from the stack.
this implies that we could write the routine as a macro:
%?ASCII-DIGIT { DUP #2f GTH SWP #3a LTH AND } ( byte -- flag )
and use it with whatever byte we like:
#30 ?ASCII-DIGIT ( pushes 01 down into the stack ) #20 ?ASCII-DIGIT ( pushes 00 down into the stack ) .Controller/key DEI ?ASCII-DIGIT ( pushes a corresponding flag down into the stack )
another instance above where we repeated many reads of the keyboard key was when using the multiple conditionals.
we could rewrite it using several DUPs and POPs:
@on-controller ( set x,y coordinates ) #0008 .Screen/x DEO2 #0008 .Screen/y DEO2 ( set sprite address ) ;square .Screen/addr DEO2 .Controller/key DEI ( read key ) DUP LIT "1 EQU ( is the key '1'? ) ,&color-1 JCN ( jump to color-1 if that's the case ) DUP LIT "2 EQU ( is the key '2'? ) ,&color-2 JCN ( jump to color-2 if that's the case ) DUP LIT "3 EQU ( is the key '3'? ) ,&color-3 JCN ( jump to color-3 if that's the case ) ( in any other case, finish ) POP BRK &color-1 ( draw sprite in the background ) ( using color 1 for the outline ) #01 .Screen/sprite DEO POP BRK &color-2 ( draw sprite in the background ) ( using color 2 for the outline ) #02 .Screen/sprite DEO POP BRK &color-3 ( draw sprite in the background ) ( using color 3 for the outline ) #03 .Screen/sprite DEO POP BRK BRK
can you tell why we need all those POPs?
hint: compare the final state of the stack with and without the POP instructions.
in the following days we'll see more uses and examples of stack manipulation!
the last thing we'll discuss today is the use of the controller button byte in the varvara computer.
as we mentioned already, the main difference here is that this byte holds the state of 8 buttons in each of its bits.
depending on our application, we might need to be able to allow for some of these buttons to be pressed at the same time.
in that case, how would we isolate each of the bits to check their state individually?
meet the bitwise AND masks!
an AND mask is a special value that we will use to keep or lose specific bits from another given value, like the controller button byte.
in our AND mask, we will set as 1 the bits in the positions where we want to keep the value of the input bits. the positions where the bits of the mask are 0 will be converted to 0 in the output.
for example, let's say we want to see if bit number 4, corresponding to the Up button, is on or off, regardless of the state of the other buttons.
our AND mask will have a 1 in bit number 4 (from right to left, and starting at 0), and 0 elsewhere:
0001 0000: 10
what would happen if button A (Ctrl key), with its state encoded in bit 0, is pressed, and nothing else?
0000 0001 ( button ) AND 0001 0000 ( mask ) ---------- 0000 0000 ( result )
what happens if the Up button is pressed?
0001 0000 ( button ) AND 0001 0000 ( mask ) ---------- 0001 0000 ( result )
and if both Up and Ctrl are pressed?
0001 0001 ( button ) AND 0001 0000 ( mask ) ---------- 0001 0000 ( result )
see how the mask allows us to effectively isolate the bit that matters to us, regardless of the state of the other bits.
applying this mask would be as simple as writing:
#10 AND ( apply 0001 0000 mask )
the following uxntal program allows you to draw using the arrows keys and the Ctrl key (button A).
the arrows move the position of a sprite, and pressing Ctrl while moving it will draw it with the inverse colors in fill and stroke.
note the use of AND masks, conditional jumps, and some stack operations!
( draw-with-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 &pixel $1 &sprite ] |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 ( set initial x,y coordinates ) #0008 .Screen/x DEO2 #0008 .Screen/y DEO2 ( set sprite address ) ;square .Screen/addr DEO2 BRK @on-controller ( -> ) .Controller/button DEI DUP ( read and duplicate button byte ) #01 AND ( isolate bit 0, corresponding to Ctrl ) ,&fill JCN ( if the bit is not 0, jump to fill, otherwise continue ) &outline #01 .Screen/sprite DEO ( draw outline ) ,&check-arrows JMP ( continue to check-arrows ) &fill #04 .Screen/sprite DEO ( draw filled ) &check-arrows ( use button byte from the stack ) DUP #10 AND ( isolate bit 4, corresponding to Up ) ,&up JCN ( jump if not 0 ) DUP #20 AND ( isolate bit 5, corresponding to Down ) ,&down JCN ( jump if not 0 ) DUP #40 AND ( isolate bit 6, corresponding to Left ) ,&left JCN ( jump if not 0 ) DUP #80 AND ( isolate bit 7, corresponding to Right ) ,&right JCN ( jump if not 0 ) POP BRK &up .Screen/y DEI2 #0008 SUB2 .Screen/y DEO2 ( decrement y ) POP BRK &down .Screen/y DEI2 #0008 ADD2 .Screen/y DEO2 ( increment y ) POP BRK &left .Screen/x DEI2 #0008 SUB2 .Screen/x DEO2 ( decrement x ) POP BRK &right .Screen/x DEI2 #0008 ADD2 .Screen/x DEO2 ( increment x ) POP BRK BRK ( sprite ) @square ff81 8181 8181 81ff
some possibilities for you to practice:
.Screen/x INCREMENT .Screen/y INCREMENT
remember that .Screen/x is a literal address in the zero page, i.e. it pushes a byte corresponding to the address of the Screen/x sublabel :)
here are some other ideas for you to practice with what we covered today!
note that for smooth interactive movement it might be better to use the screen vector that is called 60 times per second!
we'll cover that in depth in the next day of the tutorial!
these are all the uxntal instructions that we discussed today!
in uxn tutorial day 4 we cover the use of the screen vector in order to create animation, either interactive or not!
we also explore possibilities for using "variables" in uxntal than can help us creating more elaborate programs!
before jumping in, i invite you to keep exploring and to also take a break!
stay tuned!
if you enjoyed this tutorial and found it helpful, consider sharing it and giving it your support :)
text, images, and code are shared with the peer production license