Shop OBEX P1 Docs P2 Docs Learn Events
How to clear a PASM cog memory address from pub main? (title change) — Parallax Forums

How to clear a PASM cog memory address from pub main? (title change)

turbosupraturbosupra Posts: 1,088
edited 2012-06-16 16:51 in Propeller 1
Hi,

I'm using waitpeq and waitpne to measure a frequency, but there are times that the frequency will stop and I'd like to turn off the cog and clear all values, or at least clear the previous values out that affect the current frequency calculation. Otherwise when the frequency starts back up again all the calcs are hosed and it doesn't measure correctly.

I think if I wasn't using wait commands I could incorporate a timeout. But if I have a waitpeq and the peq=1 doesn't come, I believe it pauses the cog until it does come and I'm not sure how I would handle that with a timeout period. I'd want something like waitpeq unless waitcycles > clkfreq. If >clkfreq, clear values out in cog.

Is there a way to handle this that I haven't thought of or do I need to rewrite and not use waitpeq/waitpne?
«1

Comments

  • Tracy AllenTracy Allen Posts: 6,664
    edited 2012-06-13 09:00
    One way to deal with that is to add a second pin to the escape condition. It has to be waitpne, not waitpeq, to get the OR logic. That second pin can be connected to something as simple as an RCtimer circuit. Or, for more precision, to a cog counter output.
  • turbosupraturbosupra Posts: 1,088
    edited 2012-06-13 09:14
    Hey Tracy, thanks for the reply.

    Ok, so I'm trying to wrap my head around this. With waitpne, it AND's the mask with the state and makes sure they don't match.

    So if I had a pin added to the mask, that representation bit would be a 1. Then I would have whatever was monitoring the timeout go high after a timeout period and the state of the pin would then be 1, ANDed with the mask would also be 1 and it would just hang at waitpeq paused?

    What about the memory addresses that have some previous values used for averaging. How would I clear those?
  • turbosupraturbosupra Posts: 1,088
    edited 2012-06-13 09:35
    Maybe I can have a looptime variable that pub main can monitor and also send an array of variable addresses back to the spin part of the object and have a public clear array routine that pub main can call if the looptime value is higher than (clkfreq/2)? This way the code is self monitoring?

    What do you think? How do you get PASM addresses to spin and clear them to 0?
  • turbosupraturbosupra Posts: 1,088
    edited 2012-06-13 12:24
    Ok, a little progress I think
                            wrlong  gapTimingCntT, pstptr10
                            mov     gapTimingCntTAddr, #gapTimingCntT
                            wrlong  gapTimingCntTAddr, pstPtr11
    

    pstPtr10 shows the timing value it should
    pstPtr11 shows a constant value of 361, which I believe is a cog ram address. I enabled a long before gapTimingCntTAddr and then recompiled and as expected, the numerical value jumped up by 1 to 362. So, now that I have the cogram address, how is that translated into a memory address location accessible by pub main?

    If I can translate it to an address, then I can set that address to a value of 0 when needed
  • Mike GreenMike Green Posts: 23,101
    edited 2012-06-13 12:55
    One cog cannot access the memory of another cog, so it's impossible for another cog (like the one running a Spin interpreter) to access another cog's memory. The two cogs have to communicate via hub memory. The way this is normally done is via the PAR register. When you start up a cog with assembly code, the 2nd parameter to COGNEW is normally a long-aligned hub address which is passed in the PAR register. This address commonly points to a work area which the Spin code and assembly code use to communicate. This work area may contain data or addresses of other areas. Look at FullDuplexSerial or other OBEX objects for examples of this.
  • turbosupraturbosupra Posts: 1,088
    edited 2012-06-13 14:20
    Hi Mike,

    I understand how a cog can update hub memory, I'm looking to do it the other way around but you've said it isn't possible. Since my waitpne/waitpeq's actually pause the operation of the cog, I cannot communicate with it to tell it to clear some values.

    Is there a way to read the value of a numeric memory address?
  • Duane DegnDuane Degn Posts: 10,588
    edited 2012-06-13 14:56
    turbosupra wrote: »
    Is there a way to read the value of a numeric memory address?

    Read from where?

    The only way to read memory from a cog (let's call it cog X) is to have cog X write the value to hub RAM (which of course it couldn't do if it were stuck in a waitpxx statement).

    You your case, you'll either need to add an additional pin to your waitpne statement or have a different cog (cog Y) monitor when there's no activity in the cog X and issue a cogstop(X) command.

    If you had cog X write a value (let's say 1) to a variable in hub RAM, then cog Y could check the variable and if it equals 1 then it know cog X is still active. Cog Y would then write a zero to the variable and recheck it after a set amount of time to see if it had changed back to one. If the veriable had stayed zero, cog Y would know cog X was stuck and cog Y could stop cog x and restart it.
  • turbosupraturbosupra Posts: 1,088
    edited 2012-06-13 15:02
    Hi Duane,

    The way that you've stated I have done before after you all taught me that :) . Your communication between cogs makes sense too except I need to have the child cog checking the parent cogs monitoring output value. The problem with that is if I'm using wait commands which don't allow it to do that. I'd have to remove those commands and rewrite it so that I wasn't using commands that paused the cogs execution, allowing to read the shared hub ram variable that would be changed/written to by pub main.

    Let's try this, say I wanted to read memory address (byte) 12460 or hex 3654.

    Can I write pst.dec(12460) or something like that?
  • Duane DegnDuane Degn Posts: 10,588
    edited 2012-06-13 15:11
    turbosupra wrote: »
    Hi Duane,

    The way that you've stated I have done before after you all taught me that :) . Your communication between cogs makes sense too except I need to have the child cog checking the parent cogs monitoring output value. The problem with that is if I'm using wait commands which don't allow it to do that. I'd have to remove those commands and rewrite it so that I wasn't using commands that paused the cogs execution, allowing to read the shared hub ram variable that would be changed/written to by pub main.

    Let's try this, say I wanted to read memory address (byte) 12460 or hex 3654.

    Can I write pst.dec(12460) or something like that?

    I'm still confused about what you mean by "read".

    "x := byte[12460]" will move (read?) the value of the byte at location 12460 to the variable x.

    Is this what you're asking about?

    Of if you want to display the value of the byte in memory location 12460 you could use:

    "pst.dec(byte[12460])"

    Am I getting warm?
  • Mike GreenMike Green Posts: 23,101
    edited 2012-06-13 15:16
    In any kind of multi-processor communications, you have to define very carefully just how the two (or more) processors are going to communicate, particularly who changes the shared data and under what circumstances. You can always use semaphores (locks) to ensure that only one processor is going to access the shared data at any one time. There are examples in the Propeller Manual in the sections on the LOCKxxx statements and instructions. There are some special cases where you don't have to use a semaphore, but you'll have to define the circumstances where things are read and/or written and by whom.

    Sure you can read or write the contents of a hub location. You'd use BYTE[12460] or WORD[12460] or LONG[12460] depending on what you've got stored there. You can also change that data by using an ordinary assignment with one of those items on the left side of the assignment, but you'll run into problems when another cog is trying to read that value at the same time.
  • turbosupraturbosupra Posts: 1,088
    edited 2012-06-13 16:46
    Sorry I wasn't very clear, you were very warm. I would like to read the long starting at 12460, but "pst.dec(long[12460])" was not working and since hub ram is bytes I figured byte 12460 was the best way to start.

    If I understand cog/hub ram correctly. When you start cognew(@func, @par) @func is a memory address and from there to @func + 511 is a block of memory that is allocated to that cog. If this is correct, I don't understand why I can't read from a cog ram variable directly if I can obtain its numeric memory address. I'm sure all of you who are far better at this have tried this, I just want to satisfy this for my own knowledge.

    If I know a cog ram long is sitting at memory address 12460, why can't I read it directly from anywhere?



    Duane Degn wrote: »
    I'm still confused about what you mean by "read".

    "x := byte[12460]" will move (read?) the value of the byte at location 12460 to the variable x.

    Is this what you're asking about?

    Of if you want to display the value of the byte in memory location 12460 you could use:

    "pst.dec(byte[12460])"

    Am I getting warm?
  • turbosupraturbosupra Posts: 1,088
    edited 2012-06-13 17:11
    Thanks Mike ... I wonder why this isn't working then? Maybe I don't understand how cog ram is allocated.

    Mike Green wrote: »
    In any kind of multi-processor communications, you have to define very carefully just how the two (or more) processors are going to communicate, particularly who changes the shared data and under what circumstances. You can always use semaphores (locks) to ensure that only one processor is going to access the shared data at any one time. There are examples in the Propeller Manual in the sections on the LOCKxxx statements and instructions. There are some special cases where you don't have to use a semaphore, but you'll have to define the circumstances where things are read and/or written and by whom.

    Sure you can read or write the contents of a hub location. You'd use BYTE[12460] or WORD[12460] or LONG[12460] depending on what you've got stored there. You can also change that data by using an ordinary assignment with one of those items on the left side of the assignment, but you'll run into problems when another cog is trying to read that value at the same time.
  • kwinnkwinn Posts: 8,697
    edited 2012-06-13 17:14
    turbosupra wrote: »
    Sorry I wasn't very clear, you were very warm. I would like to read the long starting at 12460, but "pst.dec(long[12460])" was not working and since hub ram is bytes I figured byte 12460 was the best way to start.

    If I understand cog/hub ram correctly. When you start cognew(@func, @par) @func is a memory address and from there to @func + 511 is a block of memory that is allocated to that cog. If this is correct, I don't understand why I can't read from a cog ram variable directly if I can obtain its numeric memory address. I'm sure all of you who are far better at this have tried this, I just want to satisfy this for my own knowledge.

    If I know a cog ram long is sitting at memory address 12460, why can't I read it directly from anywhere?

    That hub memory is not allocated to the cog. The data in those memory locations are copied to the cog ram. Both hub ram and cog ram then have copies data.
  • turbosupraturbosupra Posts: 1,088
    edited 2012-06-13 17:50
    Thanks for the replies everyone.

    I understand how pointers work and how to use wrlong to write a copy of some data from cog memory to hub memory.

    My question is ... you have 32kb of ram. When cognew is used, 2048 bytes of that 32kb of ram are allocated to that cog in the form of 512 longs correct?

    If this is correct, why couldn't I read the memory address directly? If it is all part of the same 32kb of ram. I'm sorry if from the other side of things, it seems like I am asking the same dumb question in different ways, I'm not trying to do that.


    The whole thing is 0000 to 8000 right?

    Let's say cognew(@func, @par) starts at 7D0, it would end at 9CF right?

    So why can't I have x := 8A2 or pst.hex(8A2,32) and read the value using memory addressing?
  • Duane DegnDuane Degn Posts: 10,588
    edited 2012-06-13 17:56
    turbosupra wrote: »
    If I understand cog/hub ram correctly. When you start cognew(@func, @par) @func is a memory address and from there to @func + 511 is a block of memory that is allocated to that cog. If this is correct, I don't understand why I can't read from a cog ram variable directly if I can obtain its numeric memory address. I'm sure all of you who are far better at this have tried this, I just want to satisfy this for my own knowledge.

    I think the others have gotten you straightened out.

    The 2K of each cog's RAM is separate RAM from the hubs.

    If you have PASM cog that only needs to be launched once, you can reuse that area of hub ram once the PASM has been copied into a cog.

    I was able to greatly increase the rx buffers of Tim Moore's four port serial driver by using reusing the hub RAM that originally held the PASM code. So even though I increased all four rx buffers from 64 bytes to 512 bytes, the size of the program was only increased by a few longs.

    Some object can be very confusing since the variable names in the PASM section get used both in Spin and in PASM.

    Edit: I missed your last post. The Prop really has 48kB of RAM. The 32K in hub and 2K in each cog. As others have said, the 2K of PASM code gets copied to a cog. The cog doesn't not use the hub's RAM directly once it has been launched. Cogs launched using Spin cog instead of PASM have a Spin interpreter copied into the cog and the interpreter reads the Spin code from the hub and executes it. This is one reason Spin is so much slower, since the code isn't copied all at once to the cog. This is also the reason cogs using Spin code don't have the 2K limit imposed on PASM code.

    I think it took me at least a year of reading the forum before I started to make sense of all this hub RAM vs cog RAM stuff. The Prop really is eight individulal little microcontrollers with each processor having its own 2K of RAM with a large chunk (32K) of hub RAM it can use to share data (by writting to and reading from hub RAM).

    This is all pretty darn cool, if you ask me.
  • turbosupraturbosupra Posts: 1,088
    edited 2012-06-13 19:11
    Ok, I'm telling you there is a way to read the cog ram variables directly :)

    In one of my cogs I have this
    oneHunFiftyThou         long    150000
    zero                    long    0
    one                     long    1
    quarterClk              long    20000000
    eighthClk               long    10000000        
    tenthClk                long    8000000
    twentyFifthClk          long    3200000
    fiftiethClk             long    1600000
    hundrethClk             long    800000
    twoNintiethClk          long    275000
    thousandthClk           long    80000
    sixFifty                long    650
    threeHunHzCycles        long    266666       
    
    

    and if I use the following code and increment l_localPtr by 1 (starting around 1690), it will display every one of those numbers in the proper succession. I haven't figured out how this is working yet, but it does every time. Any theories?
          lockId := locknew
          lockset(lockId)
          'localPtr := ((calcCrank.getCogStartAddr))
         [b] l_localPtr := l_localPtr + 1
          l_localPtr2 := long[calcCrank.getCogStartAddr][/b]'l_localPtr2+4
          charAt( 12, 26, " " )  ' ("Pst3:") ) 
          'pst.dec(@long[localPtr])'@long[(calcCrank.getCogStartAddr+1448)])
          pst.dec(long[@localPtr])
          pst.str(@spaces)
    
          charAt( 12, 27, " " )  ' ("Pst4:") )
          pst.dec(long[l_localPtr2])
          'pst.dec(long[13920])
          pst.str(@spaces)
          lockret(lockId)
    
          charAt( 12, 28, " " )  ' ("Pst5:") )
          [b]pst.dec(long[l_localPtr2][l_localPtr])'calcCrank.getPst5)[/b] ' works!!!! 9848 = simCam
          pst.str(@spaces)
          
          charAt( 12, 29, " " )  ' ("Pst6:") )
          pst.dec(simCam.getCogStartAddr)'calcCrank.getPst6)
          pst.str(@spaces)
    
          charAt( 12, 30, " " )  ' ("Pst7:") )
          pst.dec(l_localPtr)
          pst.str(@spaces)
    
          
          
          charAt( 12, 31, " " )  ' ("Pst8:") )
          pst.dec(simCam.getCogStartAddr + l_localPtr)'calcCrank.getPst8)
          pst.str(@spaces)
    
          waitcnt((clkfreq)+cnt)
       
    
  • Duane DegnDuane Degn Posts: 10,588
    edited 2012-06-13 19:35
    turbosupra wrote: »
    Ok, I'm telling you there is a way to read the cog ram variables directly :)

    Brad, Your code is reading from hub RAM.

    Lets say you start some PASM code and you know that the PASM code starts at location 1692 (it has to be a multiple of four (long aligned)).

    After you start a cog running the PASM code, you have some Spin code do the following.

    "bytefill(1692, 0, 2000)".

    So the above line of code should wipe out your PASM code right? Since the PASM has been copied to a cog it still runs just fine in the cog.

    However, if you had executed the bytefill command before launching your PASM cog, there wouldn't be any code to execute and your PASM cog wouldn't do anything.

    Did you see that part about the Prop having 48KB of RAM?
  • Mike GreenMike Green Posts: 23,101
    edited 2012-06-13 19:48
    If you try Duane's suggestion of trying a bytefill after starting a cog, remember that the cog takes about 100us to copy the 2K bytes to the cog ram and start executing the code. If the bytefill gets executed too quickly, some of the 2K copied to the cog will be cleared first.
  • turbosupraturbosupra Posts: 1,088
    edited 2012-06-13 19:50
    Hi Duane,

    I did see that part and I did not know that but it makes sense and I understand. So that I understand the rest, every single line in PASM ram is copied to hub ram when the PASM cog is started?

    Here is why I asked, I added the bottom line and I am not writing that line to hub ram anywhere in my code, all that I did was add it and then recompile and it did display right after threeHunHzCycles displayed.

    So you are saying that it is copied to hub ram when the pasm cog is launched right?

    oneHunFiftyThou         long    150000
    zero                    long    0
    one                     long    1
    quarterClk              long    20000000
    eighthClk               long    10000000        
    tenthClk                long    8000000
    twentyFifthClk          long    3200000
    fiftiethClk             long    1600000
    hundrethClk             long    800000
    twoNintiethClk          long    275000
    thousandthClk           long    80000
    sixFifty                long    650
    threeHunHzCycles        long    266666       
    [b]tester                  long    987654321[/b]
    
  • turbosupraturbosupra Posts: 1,088
    edited 2012-06-13 20:05
    So this would be filling hub ram address 1692 to 3692?

    Ok I added
    add tester, #1
    to my pasm object and it didn't do anything, so I believe what I understood in the previous post is correct or close and I take it there is no way to address the upper 16k of memory directly?

    Thanks for everyone's patience, I almost caught my it : )

    stock-illustration-10836751-chasing-tail.jpg

    Mike Green wrote: »
    If you try Duane's suggestion of trying a bytefill after starting a cog, remember that the cog takes about 100us to copy the 2K bytes to the cog ram and start executing the code. If the bytefill gets executed too quickly, some of the 2K copied to the cog will be cleared first.
  • Mike GreenMike Green Posts: 23,101
    edited 2012-06-13 20:28
    Although the total amount of memory in the Propeller is 48K, it's physically 10 separate blocks, one 32K byte block of hub RAM followed by one 32K byte masked ROM, treated as a single 64K byte address space plus 8 completely separate 512 x 32-bit RAM blocks. The hub RAM/ROM is actually organized into 32-bit long words, but can be accessed one byte at a time or one 16-bit word at a time as well as one 32-bit long word at a time.

    When a cog is started, the specified block of hub RAM is copied one long word at a time during each hub time slot (every 16 clock cycles) until a total of 512 longs are copied. I believe that the 16 final longs of the cog RAM, called "shadow RAM" are set to copied values from hub RAM while the corresponding special registers are effectively zeroed (some are read-only). PAR is set to the value passed from the COGNEW or COGINIT.
  • turbosupraturbosupra Posts: 1,088
    edited 2012-06-13 20:32
    Ah, ok. I am humbled by you and Duane once again! But all was not lost, I learned a fair amount today, other than that a pretty big waste of time, but I did learn. It's a shame you cannot address the other memory blocks.

    Thanks for the kindness and patience everyone.

    Mike Green wrote: »
    Although the total amount of memory in the Propeller is 48K, it's physically 10 separate blocks, one 32K byte block of hub RAM followed by one 32K byte masked ROM, treated as a single 64K byte address space plus 8 completely separate 512 x 32-bit RAM blocks. The hub RAM/ROM is actually organized into 32-bit long words, but can be accessed one byte at a time or one 16-bit word at a time as well as one 32-bit long word at a time.
  • StefanL38StefanL38 Posts: 2,292
    edited 2012-06-13 22:35
    Hi Brad,

    I want to throw in a break:
    if I remember right you want to measure a frequency. There are ways to do this with the counters.
    Counters can count completely independend from the cogs code-execution.
    Of course the code has to read/write counter-registers in certain timeframes.

    Now it depends on the highest frequency and the precision you want to measure the frequency
    which way of measuring the frequency will work.

    Can you post a brief specification of the frequency you want to measure including highest frequency that can occur
    needed precision, character of the signal considering beeing an always continous signal just changing frequency
    or beeing pulsetrains of different or constant length with constant or different times with no signal in between?

    best regards
    Stefan
  • turbosupraturbosupra Posts: 1,088
    edited 2012-06-13 22:58
    Hi Stefan!

    It's good to hear from you! I'm using a modified JonnyMac object right now that does use counters, but with wait commands. I'm not very familiar with counter use and will be up front about that.

    The minimum frequency would be 240 highs and 240 lows per second, the maximum would be 6600 highs and 6600 lows per second, it is an ever changing frequency. The frequency is 34 highs/34lows and then a -2 which is the equivalent in time domain of 2 highs and 2 lows that is 100% low, and then it starts over. From rising edge to rising edge, the high portion is 43% and the low portion is 57% of the total time. It is a hall effect sensor with a 36-2 trigger wheel.

    What brought this about is that when the engine is turned off, the frequency stops and I need a way to clear my values out if a timeout is reached, so that when the signal starts up again (when the engine is started) the old values (averages and a software hysteresis) do not wreck the new signal values being collected and/or exclude them. With the wait commands I cannot do that because they pause the cog and it cannot do anything else while waiting.

    Did I answer all of your questions, including the needed precision one, completely?

    Thanks Stefan!
  • SapiehaSapieha Posts: 2,964
    edited 2012-06-14 00:20
    Hi turbosupra.

    If You calculate that Yours engine can run 6600 revolutions per second --- be sure You build Yours device that can measure at least 20% more else You will have BIG problems in real world.

    turbosupra wrote: »
    Hi Stefan!

    It's good to hear from you! I'm using a modified JonnyMac object right now that does use counters, but with wait commands. I'm not very familiar with counter use and will be up front about that.

    The minimum frequency would be 240 highs and 240 lows per second, the maximum would be 6600 highs and 6600 lows per second, it is an ever changing frequency. The frequency is 34 highs/34lows and then a -2 which is the equivalent in time domain of 2 highs and 2 lows that is 100% low, and then it starts over. From rising edge to rising edge, the high portion is 43% and the low portion is 57% of the total time. It is a hall effect sensor with a 36-2 trigger wheel.

    What brought this about is that when the engine is turned off, the frequency stops and I need a way to clear my values out if a timeout is reached, so that when the signal starts up again (when the engine is started) the old values (averages and a software hysteresis) do not wreck the new signal values being collected and/or exclude them. With the wait commands I cannot do that because they pause the cog and it cannot do anything else while waiting.

    Did I answer all of your questions, including the needed precision one, completely?

    Thanks Stefan!
  • turbosupraturbosupra Posts: 1,088
    edited 2012-06-14 16:51
    Hi Sapieha,

    Here is what I calculated. I gave myself an approximate 20% ceiling I believe at 11k, figuring that 9k would be my limit. The 6600hz number is actually ((11000rpm/60seconds per minute)*36teeth per rotation) as I need to measure the teeth and there are 36 highs and 36 lows in a single crank rotation.

    Does that sound about right to you?


    Sapieha wrote: »
    Hi turbosupra.

    If You calculate that Yours engine can run 6600 revolutions per second --- be sure You build Yours device that can measure at least 20% more else You will have BIG problems in real world.
  • SapiehaSapieha Posts: 2,964
    edited 2012-06-14 17:58
    Hi turbosupra.

    Yes and no - As I don't know what Yours engine have as highest nominal Revolutions per second.
    It is from that You need calculate at least 20% more.

    As You need think as if you run CAR downhill it is possible for engine to to much more that it is build for.and Electronic need function even in that circumstances

    turbosupra wrote: »
    Hi Sapieha,

    Here is what I calculated. I gave myself an approximate 20% ceiling I believe at 11k, figuring that 9k would be my limit. The 6600hz number is actually ((11000rpm/60seconds per minute)*36teeth per rotation) as I need to measure the teeth and there are 36 highs and 36 lows in a single crank rotation.

    Does that sound about right to you?
  • turbosupraturbosupra Posts: 1,088
    edited 2012-06-14 21:49
    Hello Sapieha

    The cars maximum rpm is a little shy of 9000 (highest nominal Revolutions per second), so I rounded it up to 9000rpm as a round number, even though it is actually a little less. I chose 11,000rpm to give myself the buffer that you are recommending and it turns out that is a 22% buffer, so pretty close inline with your recommended percentage. So based on this, I believe we both have the same idea here as far as a buffer.
  • SapiehaSapieha Posts: 2,964
    edited 2012-06-14 22:15
    Yes.
    That is correct.

    My question is now if You understand why? ---
    turbosupra wrote: »
    Hello Sapieha

    The cars maximum rpm is a little shy of 9000 (highest nominal Revolutions per second), so I rounded it up to 9000rpm as a round number, even though it is actually a little less. I chose 11,000rpm to give myself the buffer that you are recommending and it turns out that is a 22% buffer, so pretty close inline with your recommended percentage. So based on this, I believe we both have the same idea here as far as a buffer.
  • turbosupraturbosupra Posts: 1,088
    edited 2012-06-15 07:41
    The reason I did that was because in my experience you always over build something to give yourself that buffer in case something unexpected happens. It's the same with a cars fuel system or anything else. If you think you are going to make xxx hp and you need xxx cc/hour of fuel from the injectors, you always go a little bigger to make sure you have that buffer. Same with clutch lbs/tq, turbocharger impeller air flow/size, intercooler sizing, etc.

    I guess I just applied that experience for mechanical items to what I'm doing here because the philosophy has always been sound when I was building other things.
Sign In or Register to comment.