💾 Archived View for compudanzas.net › uxn_tutorial_day_3.gmi captured on 2021-12-03 at 14:04:38. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2021-11-30)
-=-=-=-=-=-=-
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 in this 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.
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.
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!
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.
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 &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
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. in order to achieve that, let's take a look at some uxntal instructions that will help us!
uxntal has four instructions for comparing the top two elements from the stack:
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.
uxntal has three bitwise logic instructions.
they can work as logic operators that have as operands the results 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 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 ( read 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 )
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 ( read 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 )
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 leftmost bit of the 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 )
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
in the byte mode, the addresses that these instructions take 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 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:
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'? ) ,&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 ) #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, either using 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
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 :)
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 data structure in a forth-like environment like uxn.
uxntal has six instructions that act upon the elements in the stack:
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 in 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 talked above about 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., if a byte has an ascii code corresponding to a decimal digit)
.Controller/key DEI ( read key ) #2f GTH ( is it greater than 2f? push flag into the stack ) .Controller/key DEI ( read 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 )
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 this would have:
key key <- top
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:
flag1 key <- top
and 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, an advantage is that this new routine now needs its input pushed down into the stack only at its beginning.
in this case, 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 where we repeated many reads of the keyboard key was when using the multiple conditionals above.
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?
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 will have bit(s) set as 1 in the position(s) where we want to keep the value of the original bit(s). in all the other positions, the original bits will be converted to 0.
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 000: 10
what would happen if button A (Ctrl key), with its state 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 sate of the other bits.
applying this mask would be as simple as writing:
#10 AND ( apply 0001 000 mask )
the following uxntal program allows you to draw using the arrows keys and the Ctrl key.
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 it in depth in day 4 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 :)
most recent update on: 12021-10-12
text, images, and code are shared with the peer production license