Shop OBEX P1 Docs P2 Docs Learn Events
[FYI] Using the @ operator in a DAT section — Parallax Forums

[FYI] Using the @ operator in a DAT section

jac_goudsmitjac_goudsmit Posts: 418
edited 2013-06-16 09:59 in Learn with BlocklyProp
I posted this on my website but I figured I would share it here too. Feel free to use and re-publish in any way.

One of the cogs used in my projects needs to exchange data with the Spin cog on a regular basis, and I want to do that in a way that minimizes the risk of future programming errors by me and possibly others. I used the @ operator which (unexpectedly) works in DAT sections to generate constants that represent pointers to variables in the hub, and ran into a problem with the Propeller Tool PASM Assembler. Here's how I worked around it.

Some commands take more data than others. For example, to run the 6502, the control cog only needs to know how many clock cycles to generate and how fast, but when downloading data to the RAM it also needs to know where in the hub to get the data, and where in the RAM chip to store the data. So I created a number of variables in the DAT section to hold all this data.
DAT

  ControlCog              jmp     init

  ' I use the "g_" prefix to indicate that these are global variables.
  g_cmd                   byte    -1                      ' Current command
  g_retval                word    0                       ' Result from previous command
  g_addr                  word    0                       ' Address bus for command
  g_hubaddr               long    0                       ' Hub address for command
  g_hublen                long    0                       ' Length of hub area for command
  g_counter               long    0                       ' Counter
  g_cycletime             long    0                       ' Clock cycle time

  init
  ' Rest of the PASM code goes here

When the Control Cog is started using cognew(@ControlCog, ...), the cog will contain a copy of the hub data, from the time that the cognew instruction was executed. This is, in my opinion, the easiest way to store initialization data before you fire up a cog: the data automatically gets picked up and stored in the cog memory by the cognew instruction. The cog can change the copied data in cog memory once it's running, without affecting the hub data. For example when doing a countdown, you can use DJNZ g_counter, #some_label. The Spin code and the other cogs will never know about it, because that all happens inside the cog that's running the code.

If you change the data from Spin by using e.g. g_cmd := cmd_RUN, it changes the value in the hub, but not in the cog. It's up to the Spin and PASM code to retrieve the updated values and do something with it. In the case of my project, the control cog continuously reads the command from the hub to check if there's something to do, and it keeps looping (doing nothing else) until g_cmd contains a valid command. Similarly, the Spin code waits for the control cog to change g_cmd to indicate that the command has been executed.

So, once the cog is running, there are two versions of each variable: one version of each variable is in the hub (and can be controlled by Spin using simple assignments), the other is in the cog. The cog uses the hub instructions RDLONG, RDWORD and RDBYTE to read the data from the hub, and the WRLONG, WRWORD and WRBYTE instructions to write data back. The cog-local values are stored in the locations that can be accessed from PASM by using the names that start with g_... in the code fragment above. But the cog will also need the hub location of those variables so it can copy the data back and forth when necessary.

In many cases (including the Control Cog code in Propeddle) the code only accesses one field, or perhaps a few fields at a time. So while it makes sense in C or C++ to pass a pointer to a struct and calculate offsets to that pointer, it makes more sense in Spin and PASM to just keep track of pointers to each member of the group of data items.

So I just declared a number of LONGs, to store the hub location for each of the g_... variables. At first I used a bit of Spin code to initialize the pointers:
' NOTE: Bad! See below.

DAT

  ControlCog              jmp     init

  ' I use the "g_" prefix to indicate that these are global variables.
  g_cmd                   byte    -1                      ' Current command
  g_retval                word    0                       ' Result from previous command
  g_addr                  word    0                       ' Address bus for command
  g_hubaddr               long    0                       ' Hub address for command
  g_hublen                long    0                       ' Length of hub area for command
  g_counter               long    0                       ' Counter
  g_cycletime             long    0                       ' Clock cycle time

  ' I use the "p_" prefix here to indicate that these are the hub pointers to the actual data
  ' NOTE: all these need to be LONG, regardless of the length of the data that they point to.
  p_cmd                   long    0                       ' Pointer to g_cmd
  p_retval                long    0                       ' Pointer to g_retval
  p_addr                  long    0                       ' Pointer to g_addr
  p_hubaddr               long    0                       ' Pointer to g_hubaddr
  p_hublen                long    0                       ' Pointer to g_hublen
  p_counter               long    0                       ' Pointer to g_counter
  p_cycletime             long    0                       ' Pointer to g_cycletime

  ...

  init

  ' Initialization goes here

  ...

  ' Let the other cogs know that we're done with a command by setting g_cmd in the hub to 0
  CmdDone
                          wrbyte g_cmd, zero              ' assuming "zero" is a long that contains 0

  ' Now wait for another cog to change the command to something else
  CmdLoop
                          rdbyte g_cmd, p_cmd wz          ' Read command from hub
                  if_z    jmp    CmdLoop

  ' At this point we have a command, execute it

                          ...

  ' When the command is done:
                          jmp    #CmdDone

PUB main

  p_cmd       := @g_cmd
  p_retval    := @g_retval
  p_addr      := @g_addr
  p_hubaddr   := @g_hubaddr
  p_hublen    := @g_hublen
  p_counter   := @g_counter
  p_cycletime := @g_cycletime

  cogid := cognew(@ControlCog, ...)

  repeat until g_cmd == 0 ' Wait until the Control Cog is ready
    ' Nothing to do here
  
  g_cmd := some_command
  repeat until g_cmd == 0 ' Wait until the cog is done processing

  ...


The code fragment above shows that it's easy to update the cog version of the variable from the hub version in PASM code simply by using RDBYTE (or RDWORD or RDLONG of course), or vice versa by using WRBYTE (WRWORD / WRLONG). The Spin code only has access to the hub version.

The nice thing about this code (as compared to e.g. using a struct-based approach with a pointer to the group of data and a bunch of offset constants) is that it's insensitive to the order in which the data items are declared: if someone (including myself) thinks it makes sense to move any item to a different place for any reason, that's okay. The pointers will still contain the correct values and there's no need to read through all the assembly code to make sure that there are no offsets to change.

Nevertheless, I didn't like this way of initializing the pointers at all, because it's too easy to introduce mistakes: if someone would add another field and forgets to edit the initialization code in the Start subroutine in the Spin section, it might result in a bug that's potentially very difficult to trace. But I thought at first that there was no other way. I was wrong.

I found out by looking at Chip Gracey's PS/2 keyboard driver that the Assembler in the Propeller tool does know and understand the @ operator from the Spin language that we all know and arguably love. You can use it e.g. to calculate the difference (offset) of two pointers (as the keyboard driver does), and you can even generate a constant in your PASM code based on an identifier with an @ operand.

Sounds awesome, right? I thought so too. I changed my code to initialize the pointers in the DAT section, and removed the Spin initialization code:
' NOTE: Won't work! See below.

DAT

  ControlCog              jmp     init

  g_cmd                   byte    -1                      ' Current command
  g_retval                word    0                       ' Result from previous command
  g_addr                  word    0                       ' Address bus for command
  g_hubaddr               long    0                       ' Hub address for command
  g_hublen                long    0                       ' Length of hub area for command
  g_counter               long    0                       ' Counter
  g_cycletime             long    0                       ' Clock cycle time

  p_cmd                   long    @g_cmd
  p_retval                long    @g_retval
  p_addr                  long    @g_addr
  p_hubaddr               long    @g_hubaddr
  p_hublen                long    @g_hublen
  p_counter               long    @g_counter
  p_cycletime             long    @g_cycletime

  ...

PUB main

  ' No need to initialize the pointers anymore, right?
  ' Wrong. See below.

  cogid := cognew(@ControlCog, ...)

  ...


Now, whenever someone adds a g_... item that needs to be updated to/from the hub by the PASM code, it's unavoidable that a p_... pointer item also needs to be added. And it's pretty obvious that such a new pointer item would have to be initialized to a value that's represented by the g_... item that belongs to it, prefixed by the @ operator, so even inexperienced programmers will easily figure out what's going on here, and if they don't, the compiler will tell them what's missing instead of just causing a silent fail like the version with the Spin-based initialization. So this code is much less sensitive to maintenance errors because everything is now (pretty much) in one place.

Unfortunately, it doesn't work. When you store a pointer in a DAT section using the @ operator, it actually stores an offset from the start of the module instead of an actual usable hub address. Whenever you use the @ operator in a DAT section, you will get the offset of the symbol from the start of the module, NOT the actual runtime hub address. This is documented somewhat vaguely in the Propeller manual (in version 1.2 it's on page 173).

EDIT: The offset is always $10 for the first module, NOT for any other modules in your program, so you can't depend on that value.

So how can we initialize a list of hub pointers such as the one above?

(EDIT: Thanks to Mike Green for pointing out how @@ works) The only solution as far as I can see is to run a short Spin routine that replaces the module-relative pointers by absolute pointers by using the @@ operator. That operator takes a value that's assumed to be a module-relative pointer such as a value in a DAT value generated with the @ operator, and converts it to an absolute pointer, i.e. a hub memory location that's valid at runtime.
' NOTE: This works!

DAT

  ControlCog              jmp     init

  g_cmd                   byte    -1                      ' Current command
  g_retval                word    0                       ' Result from previous command
  g_addr                  word    0                       ' Address bus for command
  g_hubaddr               long    0                       ' Hub address for command
  g_hublen                long    0                       ' Length of hub area for command
  g_counter               long    0                       ' Counter
  g_cycletime             long    0                       ' Clock cycle time

  pointertable            long    @pointertable           ' Table must start with a pointer-to-self
  p_cmd                   long    @g_cmd
  p_retval                long    @g_retval
  p_addr                  long    @g_addr
  p_hubaddr               long    @g_hubaddr
  p_hublen                long    @g_hublen
  p_counter               long    @g_counter
  p_cycletime             long    @g_cycletime
                          ' Insert more pointer constants here as needed
                          long    0                       ' Mark the end of the table

  ...

PUB main

  AdjustPointers(pointertable)

  cogid := cognew(@ControlCog, ...)

  ...

PUB AdjustPointers(p)

  ' Make sure the subroutine doesn't get called for the same table twice
  ' The first pointer in the table points to itself, let's see if it's already correct 
  if long[p] <> p
    repeat until long[p] == 0 ' A pointer to 0 indicates the end of the table
      long[p] += @@long[p] ' Replace relative pointer by absolute pointer
      p += 4 ' Go to the next pointer in the table
     

The first pointer in the table is a pointer pointing to itself. The subroutine uses this to make sure that it's not called twice (which would generate invalid results: Spin has no way to recognize whether pointers are relative to the module or relative to the hub, an doing the adjustment twice will result in incorrect results. The AdjustPointers subroutine compares the value of the first pointer to its location, and only does the corrections if they are unequal. Obviously no matter how many times you call the subroutine, it needs to run before you use cognew to start a cog to run the PASM code.

I decided that this is so useful that I would move the code into a separate module so you have to add an OBJ line (e.g. rel: "relocate") and you have to use rel.AdjustPointers instead of simply AdjustPointers before you fire up your cog, but you can use the same subroutine for many PASM cog modules. You can download it here. Feel free to use it in your own projects in any way you wish.

When you deal with PASM code that needs to transfer a significant amount of data to and/or from the hub on a regular basis, it makes sense to declare those variables in a DAT section inside the area that gets loaded into a cog using cognew, for easy initialization. And it makes sense to declare a pointer for each variable that may be transferred from cog to hub or vice versa during runtime, by the PASM code. But make sure you work around the problem of the (arguably incorrectly working) @ operator in DAT sections by using a subroutine similar to AdjustPointers above, before you load the cog for the first time.

===Jac

Comments

  • Duane DegnDuane Degn Posts: 10,588
    edited 2013-06-15 20:29
    I'm pretty sure the offset between the DAT @ value and the actual location of the variable has a consistent value of hexadecimal $10. This is the amount of memory used in the setup section of memory.

    So I think if you changed:
    pointertable            long    @pointertable           ' Table must start with a pointer-to-self
      p_cmd                   long    @g_cmd
      p_retval                long    @g_retval
      p_addr                  long    @g_addr
    

    To:
    ' no longer needed pointertable            long    @pointertable           ' Table must start with a pointer-to-self
      p_cmd                   long    @g_cmd + $10
      p_retval                long    @g_retval + $10
      p_addr                  long    @g_addr + $10
    

    You should be able to get rid of the code to adjust the pointers from within Spin.

    (I think the above code should work but I haven't tested it. I just add $10 to all my pointers stored in the DAT section as I use them. I haven't used your technique of modifying pointers for use with PASM code.)

    Besides being useful in your technique of presetting pointers in PASM code, I occasionally make lists of pointers in the DAT section. This makes generating menus in my touchscreen projects much easier. Each menu has a list of buttons. For example, here's the list of buttons for my touchscreen TV remote.
    DAT
    
    menuTv                  word @channelUp0, @channelUp1, @channelDown0, @channelDown1, @volumeUp0, @volumeUp1, {                        }    @volumeDown0, @volumeDown1, @vcrPower0, @vcrPower1, @tvPower0, @tvPower1, @boxPower0, {
                            }    @boxPower1, @topButton0, @topButton1, 0                                           
    

    I have simialar lists for other menus to be displayed.

    In this example, when I want the TV menu displayed. The location of "menuTv" is passed to a method which generates the touchscreen buttons. The locations of the various buttons are adjusted for the offset amount and then each button is created using the data stored in these locations.

    Here's the button data for the TV menu:
    channelUp0              byte 0, 5, Header#_White, Header#_Red, 0, _ChannelUp,      "   &#61600;   ", 0channelUp1              byte 0, 7, Header#_White, Header#_Red, 0, _ChannelUp,      "  Up   ", 0
    channelDown0            byte 0, 11, Header#_White, Header#_Green, 0, _ChannelDown, " Down  ", 0
    channelDown1            byte 0, 13, Header#_White, Header#_Green, 0, _ChannelDown, "   &#61602;   ", 0
    volumeUp0               byte 14, 5, Header#_White, Header#_Yellow, 0, _VolumeUp,   "   &#61600;   ", 0
    volumeUp1               byte 14, 7, Header#_White, Header#_Yellow, 0, _VolumeUp,   "  Up   ", 0
    volumeDown0             byte 14, 11, Header#_White, Header#_Green, 0, _VolumeDown, " Down  ", 0  
    volumeDown1             byte 14, 13, Header#_White, Header#_Green, 0, _VolumeDown, "   &#61602;   ", 0 
    vcrPower0               byte 7, 0, Header#_White, Header#_Red, 0, _VcrPower,       "  VCR  ", 0
    vcrPower1               byte 7, 2, Header#_White, Header#_Red, 0, _VcrPower,       " Power ", 0
    tvPower0                byte 0, 0, Header#_White, Header#_Yellow, 0, _TvPower,     "  TV   ", 0
    tvPower1                byte 0, 2, Header#_White, Header#_Yellow, 0, _TvPower,     " Power ", 0 
    boxPower0               byte 14, 0, Header#_White, Header#_Red, 0, _BoxPower,      "  Box  ", 0
    boxPower1               byte 14, 2, Header#_White, Header#_Red, 0, _BoxPower,      " Power ", 0 
    
    

    These are just the buttons used to make the TV menu. I have many lists like this in DAT section for the other possible menus used by the program.

    These lists of button data hold all the information needed for the program to create the desired menu.

    The button data consists of two bytes to indicate where on the screen to draw the button followed by the colors for text and background. The next two bytes either point to another menu or can hold a code to indicate what action should be taken when the button is pressed. The final bytes of the button data contain the text to display on the button.

    This makes creating and modifying menus much easier than trying to have a separate method for each menu.
  • Dave HeinDave Hein Posts: 6,347
    edited 2013-06-15 20:39
    Adding an offset of $10 works in the top object, but not in any other objects.
  • Duane DegnDuane Degn Posts: 10,588
    edited 2013-06-15 20:46
    Dave Hein wrote: »
    Adding an offset of $10 works in the top object, but not in any other objects.

    I was wondering if I was making it too easy. Thanks.
  • Mike GreenMike Green Posts: 23,101
    edited 2013-06-15 21:09
    The @@ operator in Spin was put in specifically to allow for this sort of correction. When the Spin compiler is compiling your code for an object, it doesn't know where the DAT section will be placed in memory, so the @ operator, as you've noticed, stores the offset of that address from the start of the data area for the object. If you use the @@ operator, the compile adds the base address for the object to the address (offset) value before using it. This base address is determined as the Spin compiler puts the objects together to create the final binary code for the program.
    PUB AdjustPointers(p)
    
      repeat until long[p] == 0 ' A pointer to 0 indicates the end of the table
        long[p] := @@long[p]  ' Apply the base address for the object's data area
        p += 4 ' Go to the next pointer in the table
    
  • Heater.Heater. Posts: 21,230
    edited 2013-06-16 01:15
    Boy does this topic come up a lot.

    Basically @ is totally broken. It will give you different numbers depending on if you use it in Spin or as an initializer in a DAT section. Neither of them seem to be ever be what you want.

    As far as I remember @@ does not compile in in a DAT section...no it does not see below:
    PUB start
        repeat
    
    VAR
        long a
    
    DAT
        b long 0
    
        w long @a        'Compile error
        x long @@a       'Compile error
    
        y long @b        'Useless result
        z long @@b       'Compile error
    

    Better to use BST or HomeSpun which have an @@@ operator that finally gives you what you want the actual real physical address of a thing in HUB.

    Other wise you have to hack some run time fix ups.
  • jac_goudsmitjac_goudsmit Posts: 418
    edited 2013-06-16 01:20
    Mike Green wrote: »
    The @@ operator in Spin was put in specifically to allow for this sort of correction. When the Spin compiler is compiling your code for an object, it doesn't know where the DAT section will be placed in memory, so the @ operator, as you've noticed, stores the offset of that address from the start of the data area for the object. If you use the @@ operator, the compile adds the base address for the object to the address (offset) value before using it. This base address is determined as the Spin compiler puts the objects together to create the final binary code for the program.
    PUB AdjustPointers(p)
    
      repeat until long[p] == 0 ' A pointer to 0 indicates the end of the table
        long[p] := @@long[p]  ' Apply the base address for the object's data area
        p += 4 ' Go to the next pointer in the table
    

    Thanks Mike! I didn't know @@ could be used that way, I thought it could only be used on identifiers. I'll change the first post.

    ===Jac
  • Phil Pilgrim (PhiPi)Phil Pilgrim (PhiPi) Posts: 23,514
    edited 2013-06-16 06:59
    heater wrote:
    PUB start
        repeat
    
    VAR
        long a
    
    DAT
        b long 0
    
        [b]w long @a        'Compile error[/b]
        x long @@a       'Compile error
    
        y long @b        'Useless result
        z long @@b       'Compile error
    

    Regarding the example in boldface, it would be impossible to assign a value to this at compile time, since a is an instance variable, and there could be more than one instance.

    Also, IIRC, you can add @@0 at runtime to the @ values in the DAT section to get their actual locations. By passing @@0 via par, the fixup could be done in PASM, too.

    -Phil
  • Dave HeinDave Hein Posts: 6,347
    edited 2013-06-16 09:59
    BST supports an @@@ operator that generates the absolute address in DAT sections. Unfortunately, it's not supported in the Prop tool.
Sign In or Register to comment.