Tachyon Encyclopedia

Here I started to collect some of

Peter's Tachyon and comment snippets from the Tachyon thread.  MJB

Those can finally move into the Tachyon documentation.

1. Kernel & EXTEND

2. Interactive execution

3. Tachyon vs other Forths

4. : pub pri  Defining Words

5. A CONSTANT can be changed ;-)

6. Working with Arrays:



9. S modified print Stack routine

10. SD Card Logging






11.3.2. ALARM - caution

11.3.3. Timer Timeout

11.4. Simple Multi COG Example

11.5. Additional COG with own STACK / serial IO / Register-Set.

11.6. Inter TASK Communication

12. Filesystem EASYFILE

12.1. Hierarchical Folders


14. RS485 Network

15. Runtime Interpreter

16. TACHYON  Hardware Explorer

16.1. lshw

17. FRUN (Batch / Script Files)

18. Scripts

19. Peripheral / IO Modules

19.1. PWM32

19.2. Tachyon's 32 channel PWM as an arbitrary or analog waveform generator

19.3. Bit-Banging 4-digit 7-segment display  + PIN-SPY

19.4. MAX7219-based 8 x 7-segment LEDs

19.4.1. Peter's Driver

19.4.2. MJB's Driver

20. SPI Interfacing to the SPI bus

21. I2C Devices

21.1. AD7993 I2C ADC

22. 32-Channel Full Duplex Serial

23. Compiler Directives IFDEF / IFNDEF

24. Applications

24.1. Flash programmer (Parallel Flash/EPROM/EEPROM)

25. Tachyon V3

26. SD-File System


27.1. - Serial in Parallel out shift register / IO extender  595

1. Kernel & EXTEND

You may notice that the kernel is compiled with the "Spin" tool IDE which can only compile Spin and PASM yet there are only a handful of lines of Spin to boot the Prop. Of the remainder of the Tachyon source code there are a few k of PASM for the kernel, serial receive, VGA, and some small RUNMODs. The bulk of all the source though is "Forth" but painstakingly written in DAT sections as "byte" definitions with branch calculations and table setups and hand-crafted dictionary entries. WAP!

But it is the necessary step to booting something that resembles Forth, and because it is such a pain we do just the basics and then let that Forth do the rest at run-time. Now the bulk and remainder of Tachyon source code can be written in pure Forth which is the whole point of having a Forth. Sensible don't you think?

Another PITA is trying to learn Tachyon by reading this long long thread instead of just loading Tachyon in one of its forms and following the intro or just plain interacting and playing with it even. The thread is a discussion and announcements etc and a lot of this is history in that while it may have applied back then it mightn't apply the same right now. Therefore start "in the moment" and learn what is and when you want to know why it is then perhaps a search of the thread may prove to be elucidating.

Interestingly because the kernel uses bytecode instructions which represent the absolute address of the kernel PASM code in the cog it keeps the source mostly readable and makes it possible keep the compilation process a simple one-step "F11" compile. Furthermore by using a vector table for high level calls to other bytecode functions we can even code the call address as a byte by name, even though the vector table is half wasted because we compile 16-bit "XCALL" vectors as a long, the other half is reused during runtime compilation. Plus because the dictionary is separate from code we can skip have the traditional link field addresses and having to specify and calculate them. So too even the count for the variable length names can be dummied as the Forth kernel calculates and sets them at boot time. All to make the source easier to read and maintain and life less a PITA.

So always consider EXTEND.fth a necessary part of Tachyon but one which the hand-crafted precompiled kernel can take off our hands as pure Forth source code to compile and even use at compile time to then know how to backup to EEPROM. Whew, what a relief!

2. Interactive execution

Peter Jakacki Posts: 5,447

3:57AM Flag0

@proplem - Tachyon Forth is different from most Forths and one of the differences is that instead of assembling a whole line of text in a "terminal input buffer" then interpreting and executing word by word that instead it simply compiles each word as it is encountered from the "terminal" and normally only executes the lot on a CR. So this saves room on a terminal buffer, speeds up text handling, allows you use "compile time only" words, and runs the code the same way and speed as it would if it were compiled into a definition.

That is why we can use BEGIN UNTIL DO LOOP etc interactively and we don't need special words either just to print literal strings etc.

Now, I did use the word "normally" and there are certain words that are IMMEDIATE and unless overridden will execute immediately when they are found such as IF which needs to setup a branch but there may be other words too and they may need parameters at "compile time" but these parameters are not available because they have been compiled by default. This is where the GRAB word comes in as it basically executes all the stuff that has been temporarily compiled up to that point such as parameters and in effect grabs those parameters for the use of an upcoming IMMEDIATE word. This may sound confusing but when there are times you need to change the way words are compiled and this method starts to make sense.

Now "normally" the line of compiled text executes on a CR but you can also tell it to continue and I happen to use the | symbol to indicate line continuation without executing. Thinking about this symbol I know I'd like to use the nix \ instead but that is a Forth comment word, however I guess if it is the last character on a line then it could become the line continuation instead of |. Now putting that aside we are looking at your multi-line interactive compilation. You may have noticed at first that every time you entered the line that it would execute then say ok after which you realized you needed to put that in a definition. You don't have to create a definition to do this though rarely would we need an interactive input continued beyond one line but that doesn't mean we can't which is why we could use | until we are ready.

Things to help you understand Tachyon Forth.

1) Get to know Forth (Starting Forth etc)

2) Forget that you "know" Forth, ANSI or otherwise

3) Remember the KISS principle, that we need it to be practical and to perform

4) Remember that this is the quirky and powerful Propeller chip but that we only have 32k of memory.

Now the 4th point brings us to Tachyon, the version of Forth that was created to run on the Propeller in a limited amount of memory etc. Unlike many Forths or other languages that run on PCs with gigs of resources we are using the Propeller to talk to peripherals and signals in real-time and interactively to boot, hence the choice of Tachyon. BTW, we could have more standard interactive languages but unless they are practical and perform then what is the point of using the Propeller chip itself? So Tachyon is a tool to get things done efficiently, and the reason for the tool is to create working and commercial applications with the Propeller. That statement in itself highlights a big difference with other more "standard" approaches which may look nice but what is the point if you can't get the job done. It's no good being able to run a standard benchmark, or only have enough room to compile part of the whole system or have it run too slowly or need expanded memory kludges that still run slowly, which from a practical viewpoint you may as well not use the Propeller then.

If you get your job done and have fun at the same time then these are all pluses in my book. But Tachyon is not an exercise in writing a "language", it is a means to an end when no other solution worked or works. When you can run a compiler, a debugger, a run-time library, a file-system, network servers, VGA, communications ports, and many many other uses all together in an ordinary otherwise unexpanded 32k RAM Propeller chip then you know that this solution must be good and practical and useful and worthwhile considering if you are serious about deploying the Propeller chip in real 24/7 working projects in the field.

3. Tachyon vs other Forths

Looking at that link about Forths I'd say it didn't anticipate the Propeller! The closest they seem to get to Tachyon bytecode is Token Threaded Code but they seem to think that it would be limited to 256 words and that it would be much slower. Boy, are they are wrong! The interesting thing about TF bytecode is that there is no "extra indirection" as we use the bytecode as a direct address into the first 256 longs of cog code. As for only 256 words I can't imagine why they would even say that but it only takes one opcode (unique bytecode) to say "use another byte to index into the second page", and a few more opcodes to say "call via table x using the next byte as an index". Then there is the opcode that says "just read the next two bytes as a 16-bit call address" or "call the address that is on the data stack". Wow, what's the limit!? (btw, since a bytecode is an address, we can only use some of the 256 combinations possible)

Another thing about P1 TF is that I didn't want a horrible looking source code that was buried in all the operators necessary to make each word compile or hide all that by going through special "compile to Spin compatible" tools. I just wanted a single page of source that was clearly readable as Forth source could be in the Spin tool environment. This is where Brad C's BST helped a lot too as I could view the code listing and rearrange source to make sure everything was in the optimum place. Practically all compilers these days fail to produce absolute code listings which I think is rather sad but also leaves you in the dark as to what has actually been compiled. Anyway because having to work in with the Spin tool and keep the source code as uncluttered as possible I developed the XCALL method using half of a long in a vector table so that a higher level word could be called easily as in "XCALL,xEMIT" for instance. (the other otherwise unusable half gets used later by the Forth compiler itself for more vectors)

Having a separate name space has meant I could save on a link field which is also clutter at "assembler" level but the separate name space has also permitted an easy way to reclaim unnecessary headers (pri) and also allows them to be reorganized as in EEWORDS which indexes them into upper EEPROM to save memory.

There are heaps of different under-the-hood features too in TF that is very different from standard Forths, even just the use of loop and branch stacks for instance. The list goes on but it's all about creating a powerful synergy between the Prop and the software to make what might not be possible, possible.

4. : pub pri  Defining Words

Since Tachyon Forth headers are stored in name space rather than code space this allows headers to be selectively removed without disrupting code. In fact the headers don't even need link fields but they all have attribute bits to indicate IMMEDIATE, SMUDGE, code type, etc. Code space grows up in memory while name space grows down until they meet in which case there is no more memory left although if this happens there are already solutions to deal with this.

So sometimes for reasons such as reclaiming memory or for security concerns we want to remove headers but we can only do this after compilation as the headers are used in a similar way to a symbol table. To remove them we need a way of identifying them and since it was desirable to make Tachyon more readable to Spin users I decided to use the pub and pri in place of a colon with pri setting the "private" bit in the header's attributes.

Everything still works the same but when we run RECLAIM it will strip out all the headers with the "private" attribute and repack them as it goes thus freeing up and reclaiming memory. Colon definitions are the same as pub definitions.

Perhaps this post could go into the "Tachyon encyclopedia"

BTW, the structure of the header and attributes bits are:

1      Count byte - This speeds up searching the dictionary both in comparing and in jumping to the next string
2      Name string
3      Attribute byte (8th bit set also terminates name string as strings can be terminated by a null or >$7F )
4      (1st bytecode, 2nd bytecode) or 16-bit absolute address if CALL16

7      indicates this is an attribute (delimits the start of a null terminated name)
6      smudge bit - set to deactivate word during definition
5      lexicon immediate bit
3      private (can be removed from the dictionary)
2..0   code type (single opcode, double opcode, XCALL, CALL16)

5. A CONSTANT can be changed ;-)

proplem wrote: »

Hi there, thanks to all who gave me input but I'm still struggling with my statemachine code. Peter gave an advice which seemed to me "most appropriate to my mindset" so I tried:

0        == 'sOne
0        == 'sTwo

First off.

The purpose of == is the same as CONSTANT is in Forth, to define a constant although I prefer a symbol instead which does not detract from seeing the value and the name stand out more than a distracting CONSTANT. So:

4     == four

Where I prefer the latter as I focus on 4 and four rather than the overpowering CONSTANT


Now the whole reason I introduced constants for code addresses into your code was simply to handle any cases of what otherwise would be forward referencing which is not handled normally in the single-pass back referencing to what it "already knows" manner that Forth works. So I could keep those definitions of yours happy as they could refer to a defined (by then) word (a constant) at compile time but that this constant could be tinkered with later on in the compile once the actual definitions had "become known" by Forth that we could tell it "set this constant with the address of this code". So all the constant does is hold a vector which btw could have been done with a variable but we NEVER want to change this vector once we set it as it represents its namesake and was only created to handle this case of forward referencing.


Okay, now when we define a constant it is meant to be read-only as we are used to however remember that a typical Forth constant is not a sealed box and we can always open up the same box and rewire it so that everything that has been connected to it now gets a different value from that box, the "constant". But you say "that's what variables are for, you can't change a constant!" which is true for a compiled language's binary blob but in Forth that constant has its own name, code, and data space that can be tinkered with and so we do as we have the advantages of a cleaner looking constant which can be referenced without a C@/W@/@ that a variable would require each and every time.

To tinker with a constant though requires a bit more knowledge about how that constant is stored which is unlike a variable which simply returns with the address of the data space. In Tachyon Forth a constant is an independent definition which when called is simply:


The opcode itself is a bytecode that is positioned just before a LONG boundary so that the long value can be read by the Prop with a single RDLONG. Here is the kernel code:

' Long aligned constant - created with CONSTANT and already aligned
                       rdlong  X,IP            ' get constant
                       jmp     #PUSHX_EXIT

This constant does not reveal where it is in memory though, it only returns with the value of the constant. To change it would require that we know the address where the value is held. We can do this in Tachyon using the tick symbol which finds the code address of <name>" like this:

1234 == mycon  ok
mycon . 1234 ok
' mycon .WORD 41BF ok
' mycon 10 DUMP
0000_41BF:   7F D2 04 00  00 72 41 BF   73 0A C0 BD  0C 0C 0C 00   .....rA.s....... ok
' mycon 1+ @ . 1234 ok
5678 ' mycon 1+ !  ok
' mycon 10 DUMP
0000_41BF:   7F 2E 16 00  00 72 41 BF   73 0A C0 BD  0C 0C 0C 00   .....rA.s....... ok
mycon . 5678 ok

The word TO that is also used is just a shortcut on the ' <constant> 1+ !

Now this next bit is a fundamental mistake.

' sOne == 'sOne
' sTwo == 'sTwo

You have created two new constants with the same name as the old one which still exists and to which your previous definitions are still "wired" to. So the new constants by the same name are unconnected to anything and do not change the outcome.

Now the "pièce de résistance"

Then I did interactively:

AST  ok
nextstate W@  ok
. 4264 ok
nextstate W@ CALL
One ok
nextstate W@ . 0 ok

The last line drives me crazy. Why isn't there the execution token of `sTwo´ in nextstate? It seems that the 0 from the earlier `0 == sTwo´ is still in effect if `PNS´ is called.

@Peter: BTW I didn't get managed with the `TO´ word you gave me for homework :-) I made some attempts with `==´ and `W!´ but had no success.

sTwo did exactly what you told it to do:

0        == 'sTwo

State sOne
        CR ." One"
        'sTwo PNS

So 'sTwo that is referenced is 0 which is what you read. The newer 'sTwo that you later defined is not referenced or connected to anything. Perhaps the problem here is that you may be used to an interpretive Lisp-like language in a PC environment where it doesn't need to run in real-time but can afford to interpret source code and thus allow processing redefinitions dynamically. However everything in Tachyon is compiled as you type and statically linked and any new definitions by the same name are independent of the previous but have the effect that any new references to this name always refer to the latest. There is however a mechanism in Tachyon where old definitions can be "redefined" but normally only for debug purposes and the explanation of which is best left out here.

More tricks

ALIAS simply creates a new name in the dictionary but copies the attributes and the two code compilation bytes into this new name so that the new name behaves exactly like the old one: ALIAS <old> <new>

Notice the identical attributes and compilation codes 82 BF AB. btw, there is no overhead in using an alias other than the extra name space.

0000_60A6:   04 44 55 4D  50 82 BF AB   05 44 55 4D  50 4C 82 C0   .DUMP....DUMPL.. ok
NFA' D 10 D
0000_4D75:   01 44 82 BF  AB 02 51 56   A2 BD B9 09  3F 41 55 54   .D....QV....?AUT ok


Most immediate words are used to modify the way Forth compiles so that an IF is immediate and compiles an equivalent JZ type of instruction with a dummy branch while leaving the address to the dummy branch on the stack along with a check flag, in this case $1F Likewise THEN is an immediate word which simply checks the top of the stack for the $1F flag and if true uses the address of the IF to calculate and update the IF branch offset.

Look at this incomplete compilation so that we can see the $1F flag (merged with the address).

HERE .WORD 4C24 ok
: DEMO IF ;  ok
HERE .WORD 4C27 ok
.S  Data Stack (1)
$001F.4C26 - 2051110 ok
0000_4C24:   CC 00 0C 72  4C 24 73 0A   BF AB 0C 0C  0C 0C 00 00   ...rL$s......... ok

Resetting and completing that exact same definition:

0000_4C24:   CC 0D C0 64  51 55 49 54   45 20 54 52  55 45 00 0C   ...dQUITE TRUE..

Notice now that instead of CC 00 we have CC 0D which says JZ +0D which ends up jumping to the last byte in this line 0C which is the EXIT opcode. So immediate words are useful in extending the compiler itself. Look to the end of this post for a sneaky trick based on this knowledge.

GRAB is an immediate word which if we need it compiled we then need to force the compilation with [COMPILE] which itself is an immediate word. Where it is useful is when we want to grab parameters that have already been compiled but are needed for these immediate words to wreak their havoc. Here is an example taken from my terse command mode:

--- table building word
: AS ( ch -- ) IMMEDIATE         [COMPILE] ' [COMPILE] GRAB SWAP  BL - 2* tcodes + W! ;

In action I may assign a code vector to a single terse command character in this manner:


When AS executed immediately it needed the value 'D' which unfortunately had already been compiled, but no problem, we run GRAB which executes anything that has been compiled in this definition while reseting the compilation address which all results in 'D' being made available on the stack. Now along with the tick address of DUMP also on the stack we simply store that address into the tcodes table using 'D' as an index ('D'-$20)*2.

I will get back to you in another post on how to implement State to automatically create a constant.

sneaky trick: How to get a definition to jump to a definition that hasn't been created yet?


running it:


6. Working with Arrays:

Use 60 BYTES SERBUF rather than the manual method as besides being easier to read and know that it's a byte array, it also initializes the array, as well as aligning it.

This is how your method looks:

' SERBUF .WORD 474A ok
0000_474A:   80 72 3C BF  79 0C 0C 0C   0C 00 00 00  00 00 00 00   .r<.y...........
0000_475A:   00 00 00 00  00 00 00 00   00 00 00 00  00 00 00 00   ................ ok

But this works the same but it also cleaner: (Note: current versions long align all BYTES and WORDS arrays)

0000_474B:   80 00 00 00  00 00 00 00   00 00 00 00  00 00 00 00   ................
0000_475B:   00 00 00 00  00 00 00 00   00 00 00 00  00 00 00 00   ................ ok

The other way to create arrays that are not inline with code is to use the ORG and DS words. For instance you could have done this:



The ORG sets a DS pointer to BUFFERS ($7300) and 60 DS SERBUF creates a constant with the current DS pointer then advances the DS pointer by 60 for the next DS word.

60 DS SERBUF  ok
0000_474B:   7E 00 73 00  00 71 47 4B   BE BF 0C 0C  00 00 00 00   ~.s..qGK........
0000_475B:   00 00 00 00  00 00 00 00   00 00 00 00  00 00 00 00   ................ ok
HERE .WORD 4750 ok
0000_7300:   F0 49 BF A0  A4 EB BF 00   00 EB FF 68  11 01 7C 5C   .I.........h..|\
0000_7310:   AE 4F BF 00  01 4E FF 84   7E 4E 7F 85  01 5C F3 80   .O...N..~N...\.. ok

Creating arrays and variables this way allows for the data to be lumped together in one area that may be useful for reading into say from a partition table for instance as all the data fields are contiguous it is possible to deal with individual elements or a range of differing elements.




\ === evaluate the FORTH sentence in a null-terminated string =======

\ define dummy word
: $ULTW  ;  

pub (EVALSTR) ( strAddr -- )
        C@++ DUP 0= IF DROP CONIO  $0D  THEN   ;  
         \ just deliver one char after the other, on last one add $0D to get it executed and set reader back to CONsole

pub EVAL ( strAddr -- \ evaluate the FORTH sentence in the null-teminated string starting at strAddr )
        \  NULLOUT  \ suppress output
   ' (EVALSTR) ukey W! ;

\         " 1 1 + . " EVAL

SERBUF EVAL  \ puts the numbers on the stack for further processing

Funny that, after I posted the code I thought I should add a $ to enforce hex number processing in case the number started with an alpha (not 0..9) as a number such as FEED will be rejected but not 0FEED or $FEED .

(@forum: good to see the Parallax font is supported here as I wanted to say <zero>FEED and not OFEED )

BTW, I wrote an EVAL the other day for this purpose and turns out unbeknown that I did something similar to you,

pri (EVAL)    evalp W@ C@ evalp W++ DUP 0= IF DROP CONIO $0D THEN ;
pub EVAL$ ( str -- )    evalp W! ' (EVAL) ukey W! NULLOUT  ;

So it ends up that after preprocessing that I just do a simple SERBUF EVAL$ and $ULTW sets a deferred action (via the accept vector) so that it takes the numbers off the stack and in this case just stores them in variables. Even ---- or in this updated case it would be $---- just leaves a dummy zero.

--- Let $ULTW set a defered action for execution at the end of the line after parameters are stacked                                                          
pub $ULTW    ' (ULTW) prompt 2+ W! ;

I hardly look at my own code In this case I figured this would be the easiest way to make EVAL work and I have used the unlabelled accept vector in the past for setting an end of line action, even to simulate infix notation. If the accept vector is non-zero then it will be executed at the end of the line, that is, all the other words have already been compiled, either in a definition or in the scratch-pad area at HERE. So the referenced (ULTW) just zeroes it back with a "prompt 2+ W~" and of course I could label the vector although it is seldom used:

prompt 2+ == accept

There is an application I wrote for this "Ultimeter" device which displays the weather information on a VGA screen. I found that the Ultimeter only outputs serial information no more than a few times a second at the very most so there was no need to dedicate a cog to serial receive and instead the same cog collects and preprocesses the string then hands it over to EVAL$ which then updates the variables and screen etc before returning back to waiting for another string. Not having an Ultimeter doesn't stop me from pasting the sample strings in for testing.


This implementation follows the C method to some degree.
Each CASE statement simply compares the supplied value with the SWITCH and executes an IF
To prevent the IF statement from falling through a BREAK is used (also acts as a THEN at CT)
The SWITCH can be tested directly and a manual CASE can be constructed with IF <code> BREAK

SWITCH ( switch -- )             \ Save switch value used in CASE structure
CASE ( val -- )                  \ compare val with switch and perform an IF. Equivalent to SWITCH= IF
BREAK                            \ EXIT (return) but also completes IF structure at CT. Equivalent to EXIT THEN
\ extra functions to allow the switch to be manipulated etc
SWITCH@ ( -- switch )            \ Fetch last saved switch value
SWITCH= ( val -- flg )           \ Compare val with switch. Equivalent to SWITCH@ =
SWITCH>< ( from to -- flg )    \ Compare switch within range. Equivalent to SWITCH@ ROT ROT WITHIN


pub CASEDEMO  ( val -- )
  SWITCH                \ use the key value as a switch in the case statement
  "A" CASE CR ." A is for APPLE " BREAK
  "H" CASE CR ." H is for HAPPY " BREAK
  "Z" CASE CR ." Z is for ZEBRA " BREAK
  $08 CASE 4 REG ~ BREAK
  \ Now accept 0 to 9 and build a number calculator style
  "0" "9" SWITCH>< IF SWITCH@ $30 - 4 REG @ #10 * + 4 REG ! ." *" BREAK
  \ On enter just display the accumulated number
  $0D CASE CR ." and our lucky number is " 4 REG @ .DEC BREAK
  \ show how we can test more than one value
  "Q" SWITCH= "X" SWITCH= OR IF CR ." So you're a quitter hey?" CR CONSOLE BREAK
  CR ." I don't know what " SWITCH@ EMIT ."  is"
pub DEMO

CAVEAT: there is no END SWITCH, so best put the SWITCH into it's own definition/word.

There is one thing to watch with this switch case break in that since the break is compiled as a "exit then" then it won't try to execute anything else as if you could tell it to jump to an end case and continue. This keeps it simple and efficient and so you should factor the case statements together in their own routine rather than one big long routine that continues on and on long after.

9. S modified print Stack routine

artkennedy wrote: »

Is there a Q&E way to see a number on the stack expressed in binary form when it exceeds $1FF.FFFF ?

That's a strange request, to check to see if a number exceeds a certain limit and if so to then express it in binary? I'm sure that isn't too hard to do but you may as well express it in binary all the time as well as decimal and hex of course with this code:

BL NFA' .S ?DUP IF 1+ C! THEN  --- disable kernel's version of .S as otherwise this would take precedence
pub .S
        PRINT" Data Stack (" DEPTH PRINT PRINT" )"
        DEPTH IF
          16 COGREG@ 8 + DEPTH 4* OVER +
            CR "$" EMIT DUP @ DUP .LONG
\            PRINT"  - %" DUP $1F02 PRINTNUM
( NOTE: bug at present if string is too large )
            PRINT"  - " $0A PRINTNUM
            4 - 2DUP 4 - =

The trouble is that the binary print part of this code doesn't seem to work at the moment as I seem to have a bug in my number formatting for long numbers such as 32-bit binary numbers although 16-bit are fine. I will have to sort this out first as I have a feeling that when I butted up the end of the ascending word buffer with the end of the descending number that something has gone awry.


10. SD Card Logging

Peter Jakacki wrote: »

The folder navigation is fairly simple but I plan to add full path names as well if necessary.

A sector write seems to take around 4ms so I guess you could interleave buffers if you need higher speeds so this would give a sustained 128kBytes/sec. Sustained read speeds of 250kBytes/sec are possible too as average sector read is around 2ms.

I was thinking of a SD-card logging 'logic analyzer'

just writing all 32 pins at max speed to SD

so with above numbers this should run @ 32kHz

(or 8 bits @128kHz)

and a 4GB SD would last > 8 hours !!

Now, if I dedicate a cog just for SD SPI imagine the speeds I'd get then.

a resident RUNMOD should give most of the speedup - isn't it.

running in a small Tachyon code in a separate COG

For your SD card setup there are two different ways to handle this. One is that you create a header file like P8.H or else pass the pin parameters to SDPINS at runtime with the pins encoded as a long. To help encode bytes represented in decimal form as in IP notation there is the & prefix available so this makes it easy to enter and see which pins you are using even though it's packed as a long.

To setup pins at runtime use the SDPINS word:

SDPINS ( ce-miso-mosi-clk -- )

So if your CE were P26 and CLK happened to be the same as I2C at P28 and MOSI (data to DI of card) were P24 and MISO (data from card) were P25 you would run this:


After which you can type MOUNT and proceed from there.


There are several option in Tachyon to run code tasks quasi or physically parallel.

  1. AUTORUN is not really about multitasking, but allows to specify a WORD to run automatically at powerup.
  2. the very light wheight KEYPOLL provides a hook inside the keyboard poll routine to run
    code snippets whenever the loop comes around. No restriction on execution time, but long code will interfere with the responsiveness of the interactive input.
  3. the background TIMERTASK which runs in it's own COG provides a list of WORDS to
    execute every timer increment. All timer tasks together may not exceed the timer increment of 1 ms
    or the timer will be getting out of step.
  4. TASKS can be set up to run in their own dedicated COGs.
  1. Either simple without local stack or registers
  2. or with additional local STACK or local REGSITER space.
  3. InterTASK communication is provided with ATN! ATN? ATN@ ENQ! ENQ? ENQ@


You can specify ANY word to run at cold / warm boot time with:



There is nothing magical about AUTORUN and instead of typing in a word to get it to run you just tell the system to do it for you. However the system finds and runs EXTEND.boot at startup which starts up the background timer cog and then checks the autorun vector and verifies that the code still exists before running it. There is no need to worry about anything else except your application and no need to run it in another cog either unless that is what you want to do. However for debugging purposes it is good to be able to talk to the main console cog but there are a variety of ways of doing this. Also if you need to abort the autorun you can hold down ^A after a reset (manual or serial break) until the system responds.

If my data requires initialization on a cold start (first time) then I have a variable like "firstrun" for instance that I check for a pattern such as $A55A or something but if it isn't then I execute code to load up the variables I need to set after which I set the firstrun variable to the match pattern and run BACKUP. It's much better though to make the system tolerant of uninitialized data though and the LONG/WORD/BYTE words will initially zero that data.

If you turn off autorun ( AUTORUN NONE ) and on reset you can type MyWord before doing anything to get it to run then there should not be a problem with AUTORUN. I'm guessing you haven't initialized some setting such as baudrate etc. Avoid initializing anything during source code load, even though that's handy, it doesn't help when you reboot. You did do a BACKUP command, didn't you?


NOTES: The KEYPOLL was updated a while ago to include up to 8 different routines that can be polled. So the direct write to the keypoll vector is not recommended. Instead you ' <routine> +POLL and these are wiped at boot time so they should be part of your initialization routine.

Maybe you don't need a regular timeout, you just want to check things in the background in which case you can just tie in with keypoll so that while the Tachyon cog is waiting for KEY input it will run a few errands for you.

keypoll ( -- waddr )

Address of low-priority task that is polled while waiting for a key in the same cog.

Not quite multitasking but if all you want is a lazy background poll that occurs when the console isn't busy then you can use keypoll so named because while it's waiting for a KEY or character input it will happily execute a background function.




 CNT@ 8 REG @ CLKFREQ 10d / + >                '' 100 ms or more ?

  IF CNT@ 8 REG !                                  '' reset for next match

  RDYLED IN 0= RDYLED OUT                         '' toggle the LED



' BLINKER keypoll W!                                        '' run the blinking LED in the background

The Tachyon console starts up in cog 0 while the serial receive driver takes over cog 1. This leaves cogs 2 to 7 available so they are loaded with the VM kernel and told to wait for an assignment by running a loop called IDLE which monitors it's hub variable for something to do..All we need to do is tell it what we want it to run so we don't have to do any coginits at all. Once it's running another task it's not possible to get it to change unless your code has instructions for it to check it's task variable. This normally isn't a problem in a completed application, only during interactive development but then we always have the reset button, don't we? :)

RUN ( pfa cogid -- )

Runs a task in a cog from 2 to 6.





TASK? ( -- task )

Find the next available cog that's free to run a task (ready and in IDLE).

( To listen for a command )

ATN@ ( -- cmd )

Read in a command from the tasks variable and return with it and also clear the tasks variable                

( To send a response )

ENQ! ( data -- )

( To talk to tasks in other cogs )

ATN! ( cmd cog -- )

( To listen to tasks in other cogs )

ENQ@ ( cog -- data )

To help you see what task are running just use the .TASKS word:


0000: CONSOLE                             0000 00 00 00 00 00 00

0002: IDLE                                0000 01 00 00 00 00 00

0003: IDLE                                0000 01 00 00 00 00 00

0004: IDLE                                0000 01 00 00 00 00 00

0005: IDLE                                0000 01 00 00 00 00 00

0006: IDLE                                0000 01 00 00 00 00 00

0007: TIMERTASK                           306E 01 00 00 00 00 00  ok

If you are wondering what TIMERTASK is then it's part of EXTEND.fth which maintains soft timers which are useful for application timeouts etc and it's used by SDCARD.fth that way too.


Now that we have mentioned TIMERTASK, let's have a look at how we can use it. First off, the timertask maintains a chain of user specified 32-bit countdown timers in software which are decremented down to zero every millisecond. If the timer is zero then nothing happens. A countdown is useful because it times out and a check can be made to see if it has reached zero, that is, it has timed out. If your application starts a motor which normally only stops once it has engaged a home switch for instance then it might not stop if that sensor is faulty, this is where you need a timeout to check for faults.

Consider this code which asks the DOOR motor to proceed in the CLOSE direction where both CLOSE and DOOR are simply constants and MOTOR is the action word. This is the prefered way of factoring in Forth rather than having "special" words such as Close_Door_Motor and Open_Door_Motor etc. It's like the Latin alphabet where we make up words from the letters rather than create a new character for the alphabet each time. It also means that English language programmers have to start thinking differently.

TIMER mtrtimer                                               \ define storage space for a new timer variable

\ then this code section

CLOSE DOOR MOTOR                             \ MOTOR ( dir n -- ) Close door motor (self-commenting)

#3000 mtrtimer TIMEOUT                       \ Load timer to timeout in 3 seconds

BEGIN CLOSED? mtrtimer TIMEOUT? OR UNTIL     \ Test for door closed or a timeout

OFF DOOR MOTOR                               \ Stop the motor

mtrtimer TIMEOUT? IF FAULT THEN              \ Now we can check for a fault and decide what to do about it.

In this example we load mtrtimer with a value of 3000 which corresponds to 3 seconds. In the BEGIN UNTIL loop we test the door sensor to see if it's closed yet but in case of a fault we also accept a timeout from mtrtimer as a termination condition to stop the motor.

TIMEOUT ( milliseconds timeraddr -- )

As well as loading the specified timer with the timeout value this routine also checks to see if it needs to be linked into the chain of timers that are decremented every millisecond. The timeraddr should be a double long so that one long holds the timer value, a word holds the link to the next timer, and the other word is the alarm or flags.

TIMEOUT? ( timeraddr -- flg )

Sometimes rather than checking for a timeout you would like to do something automatically on a timeout. This is called an ALARM and the TIMERTASK can execute user specified code automatically upon timeout which could also include reloading the timer which is not automatic. \

ALARM ( cfa timeraddr -- )

Here's some sample code which I modified from D.P.'s sample to demonstrate using 8 timers with alarms to blink LEDs at different rates. This method uses a common ALARM method that accesses timer reload values via a table. If you happen to have a demoboard or any board with LEDs connected to P16..P23 then try it otherwise you can use another pin group by changing the ledpins constant.


\ Updated Feb 11, 2014

TABLE mytimers 8 8 * ALLOT         \ allot space for 8 doubles

pub @mytimers        2* 2* 2* mytimers + ;

#16        == ledpins

TABLE blinkrates

        #50 || #500 ||

        #50 || #300 ||

        #50 || #150 ||

        #50 || #285 ||

        #50 || #175 ||

        #50 || #335 ||

        #50 || #100 ||

        #50 || #195 ||

pub BLINKER ( timeraddr -- timeraddr )

         DUP mytimers - 2/ 2/ 2/ >L                        \ convert timeraddr to an IX index onto loop stack

         IX 2* 2* blinkrates +                                \ point to a blinkrate for this timer

         IX ledpins +                                         \ point to the ledpin to use

         DUP PIN@ IF PINCLR 2+ ELSE PINSET THEN        \ toggle the pin and (re)point to blinkrate

         W@ L> @mytimers TIMEOUT                                \ Fetch and set that blinkrate (either the on or off one)


pub clrpin               I ledpins + PINCLR ;

pub BLINKERS ( flg -- )

        IF ' BLINKER ELSE ' clrpin THEN

         8 0 DO DUP I @mytimers ALARM 1 I @mytimers TIMEOUT LOOP DROP


{ Usage:

ON BLINKERS   \ will activate the timers and blink functions

OFF BLINKERS  \ will cause the timers to timeout in the next millisecond and turnoff the LEDs  


11.3.2. ALARM - caution

A word of caution regarding the use of the ALARM as this is called from the TIMERTASK cog. First off, the word that is called should not stall the TIMERTASK process for more than one millisecond at the absolute maximum but preferably this word should take less than 100us per timer which in Tachyon means there is plenty of time to get things done. Also, since each cog has it's own direction registers this should be taken into account as to which cog has control of certain I/O pins. It may be necessary with shared I/O to leave lines low or as inputs so that any cog can still drive it high etc.

11.3.3. Timer Timeout

Originally timer variables were constructed with DOUBLE but that was quite some time ago. Use TIMER instead as that will create the correct stucture in this case two longs and a word which is used to link to other timers. The old version just dedicated room for 8 timers in one block whereas the newer version links these dynamically and transparently at runtime, as many as can be linked.

@proplem: The background timer task maintains a linked list of timers on a 1ms basis and you can setup a new timer to countdown to zero or count up instead and also optionally have an alarm condition that is executed when it counts down to zero. Otherwise you can read "runtime" which counts milliseconds from reset. Then again simply create an "Elapsed?" word that does a CNT@ then divides that by 80,000 to give you milliseconds.

BTW, this is how you would normally add a timer to your app which automatically links itself to the list of timers maintained by the timer task.

TIMER mytimer (create a timer structure)

100 mytimer TIMEOUT (will timeout in 100ms)

BTW in regards to keypoll it is best not to access this variable directly as it already is set to point to a general-purpose polling routine that handles many keypoll events. Just use +POLL to set a new keypoll event like this: ' <mypoll> +POLL

You can have up to 8 different keypoll events which are very handy for all those lazy background checks but especially useful since it runs from your main cog which is important if any of those keypolls access I/O that is "owned" by the main cog.

TIMER totest
: SCRLEDS 50,000 totest TIMEOUT BEGIN 23 16 DO I HIGH 50 ms I LOW LOOP totest TIMEOUT? UNTIL ;

No need to check the timeout at the end as that would be redundant, it will always be true since that is the only way it can exit via the UNTIL

Yeah, the lockup will be because that cog also tries to emit on the same serial TX pin as the main cog and after it's done it leaves the pin as a high output, unable to be overridden by the console cog thereafter nor is the new cog able to transmit either as the transmit is already high etc. Nonetheless the second cog is unable to print the number successfully as its default stack size is only 4 deep which is okay for simple loops but insufficient for conversion operations inside of number printing.

However this will work:

TIMER totest
' SCANLEDS 5 RUN 30 FLOAT 7 seconds

While I'm updating the intro page here are a few examples.

Create a timer structure

TIMER mytimer

To set the timer to count down 100ms

100 mytimer TIMEOUT

This will link mytimer in with any other timers that get serviced by the timer cog. Every millisecond the list of timers are counted down if non-zero and once they transition to zero an alarm vector in the timer structure is checked and executed if set. If I wanted an LED on P0 to blink for 100 ms but didn't want to sit around waiting for it I could do this with mytimer.

0 LOW (turn LED on)
100 mytimer TIMEOUT

and if I setup an alarm, which could be set up once at startup...

' LEDOFF mytimer ALARM

Then when mytimer transitions to zero it will execute LEDOFF.

If though you want something polled every 100ms you would then have the alarm function setup as a polling function if it also reloads the timer. Have a look at this simple example which toggles an LED from a button.

TIMER mytimer
: RunStop
        ( Reload timer to check again in 100ms )
        100 mytimer TIMEOUT
        ( is button pressed? )
        1 PIN@ 0=
        ( then toggle LED on P0 )
         IF 0 PIN@
           IF 0 LOW ELSE 0 HIGH THEN

( During setup set alarm code )
' RunStop mytimer ALARM
( then trip with any timeout )
1 mytimer TIMEOUT

I even have one single timer checking 12 LEDs for timeouts as they are used to indicate activity on multiple communications ports. Each channel has two red/green LEDs and one stays on for a second with any valid activity on that channel while another one flashes for 50ms when data is received. This way I can see that the channel is active and when data comes in. The important thing to observe about timers is since they are called every 1ms is that the alarm function should be quick about its business, take much less than 1ms (easy enough), and that it has control of any outputs (don't try to exercise those outputs from another cog).

11.4. Simple Multi COG Example

To help me get a better handle on handling multiple cogs, would someone be so kind as to show me the code for the following:

using 3 cogs; each to control an LED; blink each LED at a different rate.

Peter JakackiPeter Jakacki 

Oh the pain, the pain.

There are so so many ways to do this and keeping in mind that you are asking for this to run in individual cogs (what a waste) here are but a few.

1. The simple dedicated cog and code soft blinky

: BLINKY1   BEGIN 0 HIGH 200 ms 0 LOW 200 ms AGAIN ;
: BLINKY2   BEGIN 1 HIGH 400 ms 1 LOW 400 ms AGAIN ;
: BLINKY3   BEGIN 2 HIGH 50 ms 2 LOW 50 ms AGAIN ;


Here's a SPLAT shot of that working:


This image has been resized to fit in the page. Click to enlarge.

2. Use a counter in each cog and return back to IDLE (so it is ready to run some real code)

: BLINKY1   0 BLINK 2 HZ ;
: BLINKY2   1 BLINK 5 HZ ;
: BLINKY3   2 BLINK 10 HZ ;


3. This one uses global variables so that you can change speed and leds on the fly from the console

8 BYTES leds
8 WORDS speed

: BLINKY        BEGIN COGID leds + C@ DUP HIGH COGID 2* speed + W@ DUP ms SWAP LOW ms AGAIN ;

--- setup some leds and speeds
0 leds 6 + C! 200 speed 6 2* + W!
1 leds 5 + C! 400 speed 5 2* + W!
2 leds 4 + C! 50 speed 4 2* + W!
--- now start up just 3 cogs - usually 6,5,4

--- you can of course create words to handle the settings thus
: BLINKY! ( speed led cog -- )
        SWAP OVER leds + C!
        2* speed + W!
--- to use BLINKY! --- 50 2 4 BLINKY!

The SPLAT shot for method 3 which shows they are all synchronized as we started them at the same time in the FOR NEXT loop


This image has been resized to fit in the page. Click to enlarge.

As mentioned there are much better ways of doing this but because the task is simple there is no need for more stack space or indeed for a copy of task registers.

11.5. Additional COG with own STACK / serial IO / Register-Set.

EDIT: back on the pc but the other thing to watch out for is the data stack as each cog only maintains a 4 level deep stack in cog memory and if you need more then you should create an array

8 LONGS mystk   \  and set the stack pointer at the beginning of the task

mystk SP!             \

The other thing to allow register memory for is if your task needs it's own Forth input and output buffers such as when reading a word from the input stream or printing a number etc. So if you need to do this then create an array of say 128 BYTES myregs and also set these up with myregs 7 COGREG! but in this case you probably don't need to do either.

Note: Whenever you use the BYTES or WORDS or LONGS to create an array it is always long aligned.

@FORUM: Where's the source/preview toggle button? I only have source view now. Testing code tags now:

' Registers used by PASM modules to hold parameters such as I/O masks and bit counts etc
REG0                long 0
REG1                long 0
REG2                long 0
REG3                long 0
REG4                long 0
txticks                long (sysfreq / baud )        ' set transmit baud rate
txmask                long |<txd                        ' change mask to reassign transmit
regptr                long
 @registers+s                ' used by REG

As regards to stack space you only need to set this up for anything that goes beyond the 4 level cog stack space. If the pointer isn't set then it won't try to access hub memory. However you have seen examples already and it is simply a matter of allocating some memory, perhaps using LONGS and seting the stack pointer during the startup of a task with SP! thus:

8 LONGS mystk

: MYTASK mystk SP! <and the rest> ;

Any task or cog that talks to streaming I/O such as the serial console sometimes needs to have its own I/O vectors since it may revector the EMIT to talk to the VGA screen for instance. The solution would be to have a copy of these vectors for all cogs but the trouble is that whenever you input or output you need to print numbers perhaps which converts numbers to strings before printing the string. So these cogs need to have their own copy that the Forth primitives such as KEY EMIT PRINT GETWORD and their variants use that doesn't involve anything time consuming and complex to implement.

A simple internal opcode called REG accepts an 8-bit offset from a base register as a quick and simple way to access local hub variables such as mentioned. This cuts down the need to specify a 16-bit address and while it makes the code more compact it also allows each cog to change the address stored in that base address register to use a different set of hub variables.

Now the order of the hub variables are arranged in such a way that rather than having to allocate 256 bytes of memory for another cog you only need 128 bytes since memory is limited. So therefore this is what you do to change it.

128 BYTES myregs --- allocate space for task registers (local variables)


mystk SP!

myregs 7 COGREG! --- set REG base address of this cog to point to new set

<and the rest>


But none of those "new" registers in myregs will be initialized, they will all be zero so it is prudent to preset them by making a copy of the console cog's (cog 0) registers like this:

$24 myregs 128 CMOVE

This operation BTW can be performed during compile time without any side effects or included in your task's startup either way. The $24 is the default address of cog0's task registers and although they could be changed there isn't any reason to do so.

11.6. Inter TASK Communication

artkennedy wrote: »

Can't figure out how ATN! and ATN@ work.

If a cog is running a loop and listens with ATN@ can you send UNLOOP with ATN! to exit the loop?

Generally the question is what is the form of 'cmd'.

( To talk to tasks in other cogs )

ATN! ( cmd cog -- )

Well ATN@ and ATN! is just a way of passing a byte between tasks much as KEY or EMIT would do so it's up to the software to interpret it anyway it wants to. So you can't send an UNLOOP command but that gets me to thinking that maybe that's not a bad way to talk to it either. There would have to be a SETCMD and a RUNCMD though.

The truth is I've never used any of these words as I put them there mainly at user request. If you let me know a bit more about what you want to do I may come up with a better scheme.

artkennedy wrote: »

» show previous quotes

OK, I just confirmed that. I found the term "command" confusing. I guess you could send the address of a word such as UNLOOP to a cog if you needed to.

Just trying to find the handle on these tools.

ATN@ ( -- cmdword ) reads in this cog's command which is sent by another cog and also clears it.

So you could pass the address of an actual definition to be executed if you wanted and in your code you might have:


ENQ! ( enqword -- ) --- may be sent as a response to a command or sent as status

--- there is no handshake here except if the receiving cog clears it in which case you could use:

" READY" ENQ! --- send the address of the ready message (perhaps)

BEGIN COGID ENQ? 0= UNTIL --- wait until the response has been "acknowledged" (perhaps, although this is potentially blocking)

ATN! ( cmdword cog -- ) --- use this if you want to send a message or command or data to another task which of course depends upon that task using ATN@

ATN? ( cog -- cmdword ) --- check to see if the command that was sent (by any cog) has been processed yet (zero).

ENQ@ ( cog -- enqword ) --- read back and clear the response from a cog, perhaps the one we sent a command to

' UNLOOP 5 ATN! --- command cog 5 to UNLOOP

( maybe wait or poll )

5 ENQ@ ?DUP IF CR ." Response from cog 5 is " PRINT$ THEN --- if cog 5 has a response then interpret that as a string (points to string in hub memory)

Since I have never really used this mechanism I will try to incorporate the words into a demo.

BTW, the terms cmdword, datword, enqword or whatever are just used as an indication that it is a word but how the application uses it is up to the application

12. Filesystem EASYFILE

The FS needn't be "complicated" and there certainly isn't any need for the application to worry about the details. Most of the time we either need to open a file for reading or in the case of datalogging for appending. So it's a case of open a file and usually read it from start to finish or open a file for append and write new data to it. What's complicated about that?

For instance in my EASYNET server module if a webpage is not found then this bit of code runs:

FOPEN$ NOT IF " HTTP404.HTM" FOPEN$ DROP THEN                                 --- on file not found - use default 404 file

and then when it's time to send it it is simply:

FILE@ FSIZE@ BLKSEND                                                                \ or just send the whole file

where FILE@ returns with the 32-bit starting address of the file in virtual memory, FSIZE@ returns with the size of the file as read from the directory and BLKSEND armed with the source and count sends that off in chunks down the current socket.

MOUNT is designed to mount the FAT32 volume which also involves initializing the SD card itself. Some routines such as DIR will check to see if it needs mounting however once you start opening files it is assumed all this has been done already in your boot routine. There is ?SDCARD which if added as a background poll will automount cards.

So either MOUNT at startup or add ?SDCARD as a poll with: ' ?SDCARD +POLL which off course still involves a user startup. However just for interacting you would just type MOUNT or else DIR after startup.

Remember too that you can have 4 files 0..3 open at a time although file 1 is also used for the directory.

In fact we can interact with all the various components of the FS for instance:


Mounted 31B0.8C07-7720.7D43 mkfs.fat TACHYON     FAT32   3,965MB (4,096/cluster) ok


HOME    .HTM   P8CPU   .JPG   IOTPINS .JPG   LOVE    .WAV   P8      .H    
IOT5500H.JPG   DRAGON  .JPG   IOT5500 .JPG   128K    .BIN   256K    .BIN  
W5200   .FTH   POPCORN .WAV   P8X32A  .PDF   SYSLOG  .TXT   IMAGE3        
FRED    .PNG   FSRSCH  .PNG   FSRPCB  .PNG   IMAGE          HTTP404 .HTM  
PARALLAX.PNG   HOME1   .HTM   HCB4208 .JPG   CE1372  .JPG   CE1372  .PDF  

FOPEN HTTP404.HTM...opened at 0001.D7DA ok



Data Stack (2)
$0000.0234 - 564
$03AF.B400 - 61846528 ok


03AF_B400:   48 54 54 50  2F 31 2E 31   20 34 30 34  20 4E 6F 74   HTTP/1.1 404 Not
03AF_B410:   20 46 6F 75  6E 64 0D 0A   44 61 74 65  3A 20 4D 6F    Found..Date: Mo
03AF_B420:   6E 2C 20 30  32 20 44 65   63 20 32 30  31 33 20 32   n, 02 Dec 2013 2
03AF_B430:   33 3A 35 35  3A 30 34 20   47 4D 54 0D  0A 53 65 72   3:55:04 GMT..Ser
03AF_B440:   76 65 72 3A  20 41 70 61   63 68 65 2F  32 2E 32 2E   ver: Apache/2.2.
03AF_B450:   32 32 20 28  55 6E 69 78   29 20 6D 6F  64 5F 73 73   22 (Unix) mod_ss
03AF_B460:   6C 2F 32 2E  32 2E 32 32   20 4F 70 65  6E 53 53 4C   l/2.2.22 OpenSSL
03AF_B470:   2F 30 2E 39  2E 38 65 2D   66 69 70 73  2D 72 68 65   /0.9.8e-fips-rhe
03AF_B480:   6C 35 20 6D  6F 64 5F 62   77 6C 69 6D  69 74 65 64   l5 mod_bwlimited
03AF_B490:   2F 31 2E 34  0D 0A 43 6F   6E 74 65 6E  74 2D 4C 65   /1.4..Content-Le
03AF_B4A0:   6E 67 74 68  3A 20 33 32   31 0D 0A 43  6F 6E 6E 65   ngth: 321..Conne
03AF_B4B0:   63 74 69 6F  6E 3A 20 63   6C 6F 73 65  0D 0A 43 6F   ction: close..Co
03AF_B4C0:   6E 74 65 6E  74 2D 54 79   70 65 3A 20  74 65 78 74   ntent-Type: text
03AF_B4D0:   2F 68 74 6D  6C 3B 20 63   68 61 72 73  65 74 3D 69   /html; charset=i
03AF_B4E0:   73 6F 2D 38  38 35 39 2D   31 0D 0A 0D  0A 3C 21 44   so-8859-1....<!D
03AF_B4F0:   4F 43 54 59  50 45 20 48   54 4D 4C 20  50 55 42 4C   OCTYPE HTML PUBL
03AF_B500:   49 43 20 22  2D 2F 2F 49   45 54 46 2F  2F 44 54 44   IC "-//IETF//DTD
03AF_B510:   20 48 54 4D  4C 20 32 2E   30 2F 2F 45  4E 22 3E 0D    HTML 2.0//EN">.
03AF_B520:   0A 3C 68 74  6D 6C 3E 3C   68 65 61 64  3E 0D 0A 3C   .<html><head>..<
03AF_B530:   74 69 74 6C  65 3E 34 30   34 20 4E 6F  74 20 46 6F   title>404 Not Fo
03AF_B540:   75 6E 64 3C  2F 74 69 74   6C 65 3E 0D  0A 3C 2F 68   und</title>..</h
03AF_B550:   65 61 64 3E  3C 62 6F 64   79 3E 0D 0A  3C 68 31 3E   ead><body>..<h1>
03AF_B560:   4E 6F 74 20  46 6F 75 6E   64 3C 2F 68  31 3E 0D 0A   Not Found</h1>..
03AF_B570:   3C 70 3E 54  68 65 20 72   65 71 75 65  73 74 65 64   <p>The requested
03AF_B580:   20 70 61 67  65 20 77 61   73 20 6E 6F  74 20 66 6F    page was not fo
03AF_B590:   75 6E 64 20  6F 6E 20 74   68 69 73 20  73 65 72 76   und on this serv
03AF_B5A0:   65 72 2E 3C  2F 70 3E 0D   0A 3C 70 3E  41 64 64 69   er.</p>..<p>Addi
03AF_B5B0:   74 69 6F 6E  61 6C 6C 79   2C 20 61 20  34 30 34 20   tionally, a 404
03AF_B5C0:   4E 6F 74 20  46 6F 75 6E   64 0D 0A 65  72 72 6F 72   Not Found..error
03AF_B5D0:   20 77 61 73  20 65 6E 63   6F 75 6E 74  65 72 65 64    was encountered
03AF_B5E0:   20 77 68 69  6C 65 20 74   72 79 69 6E  67 20 74 6F    while trying to
03AF_B5F0:   20 75 73 65  20 61 6E 20   45 72 72 6F  72 44 6F 63    use an ErrorDoc
03AF_B600:   75 6D 65 6E  74 20 74 6F   20 68 61 6E  64 6C 65 20   ument to handle
03AF_B610:   74 68 65 20  72 65 71 75   65 73 74 2E  3C 2F 70 3E   the request.</p>
03AF_B620:   0D 0A 3C 2F  62 6F 64 79   3E 3C 2F 68  74 6D 6C 3E   ..</body></html>
03AF_B630:   0D 0A 0D 0A  00 00 00 00   00 00 00 00  00 00 00 00   ................ ok

cat HTTP404.HTM...opened at 0001.D7DA

HTTP/1.1 404 Not Found
Date: Mon, 02 Dec 2013 23:55:04 GMT
Server: Apache/2.2.22 (Unix) mod_ssl/2.2.22 OpenSSL/0.9.8e-fips-rhel5 mod_bwlimited/1.4
Content-Length: 321
Connection: close
Content-Type: text/html; charset=iso-8859-1

<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested page was not found on this server.</p>
<p>Additionally, a 404 Not Found
error was encountered while trying to use an ErrorDocument to handle the request.</p>


The interactive FOPEN <name> confirmed that the file was opened (reporting sector address) and promptly set file pointers etc for us. Next we ask for the virtual memory address of the file with FILE@ and the reported size with FSIZE@ and have a look at the stack with .S. After that I just decide to do a hex dump just as I would any memory but use the SD modifier so that it uses the SD virtual memory space instead and viola! Then list the file using the "cat" command. Simple, maybe that's why I call it "EASYFILE".

Oh, and one more little thing, how about we index in and read a byte or long from that file, say from offset 64:

FILE@ $40 + XC@ .BYTE 76 ok

FILE@ $40 + X@ .LONG 3A72.6576 ok

12.1. Hierarchical Folders

August 26 edited August 26 Flag0

Just been tweaking the virtual memory layers of the filesystem so that random byte reads execute a little faster when the sector is already cached. Without anything too special the random cached byte/word/long read takes 18.2us or 2ms if the sector is not cached. Since there are buffers for up to four open files this also helps to keep a sector cached.

So an indexed read of 32K takes 804ms and that doesn't use any dedicated cog for this, just a couple of short and simple RUNMOD modules that get loaded as new instructions when needed. There is no sequential RDBYTE definition but since it's only a one-liner I will add it anyway.

32K indexed read:

$8000 0 LAP DO I XC@ DROP LOOP LAP .LAP 804.342ms ok

EDIT: If you read longs instead of bytes it's much faster: $8000 0 LAP DO I X@ DROP 4 +LOOP LAP .LAP 311.184ms ok

Also the time it takes to find and open a file is:


Remember too that you can traverse folders and also select multiple drives if the pins for these have been defined, as normally we only need a chip select extra.

Mounted 31B0.8C07-7720.7D43 mkfs.fat TACHYON     FAT32   3,964MB (4,096/cluster) ok
HOME    .HTM   P8CPU   .JPG   IOTPINS .JPG   LOVE    .WAV   P8      .H    
IOT5500H.JPG   DRAGON  .JPG   IOT5500 .JPG   128K    .BIN   256K    .BIN  
W5200   .FTH   POPCORN .WAV   P8X32A  .PDF   SYSLOG  .TXT   IMAGE3        
FRED    .PNG   FSRSCH  .PNG   FSRPCB  .PNG   IMAGE          HTTP404 .HTM  
PARALLAX.PNG   HOME1   .HTM   HCB4208 .JPG   CE1372  .JPG   CE1372  .PDF  
cd SOURCE  ok
[.       ]     [..      ]     SDCARD2 .FTH   EXTEND  .FTH   LED4    .FTH  
TFT     .FTH   TF-2V7  .SPN     ok

In the meantime I'm looking at some other enhancements and will release these shortly.

The folder navigation is fairly simple but I plan to add full path names as well if necessary.

A sector write seems to take around 4ms so I guess you could interleave buffers if you need higher speeds so this would give a sustained 128kBytes/sec. Sustained read speeds of 250kBytes/sec are possible too as average sector read is around 2ms. Now, if I dedicate a cog just for SD SPI imagine the speeds I'd get then.


FDS has a lot of jitter which doesn't cause too much of a problem if it's not operated too fast or talks to a proper UART which is normally the FT232 style serial interface to USB. If you want to talk to another FDS especially at higher speeds (even though 115,200 isn't particulary fast) and/or over longer links then the errors may make it unreliable.

The decision I made in Tachyon is to dedicate a cog just to receive and also to make sure that it didn't leave all its buffering until the stop bit as some transmissions can be back-to-back with only a single stop bit which doesn't leave a lot of time at 3M baud, so therefore the checks and buffering are interleaved with receiving data bits.

The other decision was that transmit buffering (and unbuffering) at higher speeds can take a lot of time when compared to just sending it and also that since it's possible for the application cog to "just send it" then that's what it should do. In a way the application cog is a dedicated transmitter and so the timing is very precise although the period between characters is up to the application. Note that Spin is unable to transmit from the Spin cog at higher speeds although it is possible at lower speeds but that doesn't stop us from having this capability "built-in". The bit-bash opcode (emit) in Tachyon is only 10 instructions long and I prefer higher speeds because at 2M baud for instance it only spends 5us sending a character and the whole opcode fetch/execute takes 6.6us. Sadly I mostly set my binaries at 115,200 for users due to the mix of SLOW terminal emulators that are used. Even TeraTerm goes up to 921,600 at least although I use minicom on Linux and that has no problem with high speeds.

I also have serial objects which are "auto-duplex" which once they detect a start bit they are locked into receiving a character and when they are transmitting a character they are locked into that. The reason for this is simply because most serial communications is effectively "half-duplex" in that we receive a command for instance and we send back a response. So once it is receiving a character it normally sits there undisturbed by the application to receive the whole packet which is then processed by the application and then a response is sent. That's being pragmatic.

BTW, Clark's third law really means that advanced technology, just like magic, disappears in a puff of smoke!

Buffered Serial Input in own COG

D.P Posts: 424

March 2014 edited March 2014 Flag0

Peter Jakacki wrote: »

Let's just say that we leave that other cog to just buffer the data and then we have the main cog switch it's input to the buffered serial stream as if we were typing it in from the console. This isn't any different then what FLOAD does for instance, in that it opens a file, then redirects console input to FGET reading in a byte at a time from the file "stream" vs the RS232 "stream".

So give this snippet:

WORD rxrd,rxwr
#128 == rxsize
rxsize BYTES buf232
12 LONGS stack232
#11 == prx

\ Buffer data from the RS232 port - nothing else
pub RS232.TASK
   stack232 SP! !SP                     \ init data stack to 0, assign stack to this task
   #9600 SERBAUD                        \ how fast are we going
   rxrd W~ rxwr W~ buf232 rxsize ERASE  \ clear rxrd, rxwr and erase buf232
      prx SERIN rxwr W@        \ get serial byte, fetch rxwr buf232 index( -- 0 datadr )    
      rxsize 1- AND buf232 + C! \ compute buf232 addr and write data to buffer
      rxwr W++                  \ increment buffer pointer

\ You can read the buffered data with this

pub GET232 ( -- ch|0 )
    rxrd W@ rxsize 1- AND rxwr W@ rxsize 1- AND <>   \ compute if the buffer is empty
      rxrd W@ rxsize 1- AND buf232 + C@
      rxrd W++
    ELSE 0

\ We can do this

pub start232 '  RS232.TASK 4 RUN ;

pub getstream ' GET232 ukey ! ;

100 ms

It can't be that easy? Really? If so no wonder I'm confused, constantly over engineering the solution. Seem though GET232 needs to return nothing if the buffer is empty?

14. RS485 Network

Peter Jakacki Posts: 5,395

June 13 edited June 13 Flag0

PaulRowntree wrote: »

This 485 topic is interesting, but outside my experience. Peter tried to get me to consider 485 instead of tcp/ip on a project some time ago, and I am starting to come around.

but first ...

1) If I understand the ping/pong it is a send/confirm approach, byte by byte. What would the estimated byte/sec throughput be at say 115200 baud? 1M?

2) do you generally hard code the device address in code or use on-board jumpers to id a propeller ?

3) with the glitchy chips in mind, what would be a recommended 485 interface chip in a DIP format?

4) Is the master set by who starts the conversation?

5) Are the 4-wire TELCO runs limited to 1 A/contact? Is there any advantage to using cat5?


RS485 itself is very basic, it is simply using balanced lines to achieve noise resistant signaling speed and distance over a shared half-duplex bus. It's the protocols that are used that can become rather complex but PP is not so.

1. It is not a send/confirm except in the case of an individual address select whereas all selected slave traffic is essentially receive something then send something back to implement full-duplex over half-duplex lines. What is received can often be a null simply to elicit more data which in itself might be a null, none of which are passed as data. Imagine a full-duplex line, even when the receive line is idle we can still send something back or nothing. The nulls are the equivalent of an idle "ball", you can't bounce the ball back until you've got it and vice-versa. Each character received is like a clocking signal to send a character back. On a fully utilized line you can expect the full-duplex speed to be around 43% of the bus speed but a future enhancement to the protocol may allow bulk transfers too increasing that figure close to the bus speed.

2. ID jumpers are too bulky and a hardware hog for most of my designs but the PP network is disabled on a fresh kernel load and is only activated after a PPCFG command where you can specify the address which is locked into EEPROM. Of course your design could use jumpers somehow if you want and your software could read these then set the address via PPCFG.

3. Apparently it is only these 20Mbit 65HVD75 chips that have the glitch but the software works with these anyway so no problem. But I do recommend that you always use full fail-safe chips as RS485 lines can be troublesome otherwise. Just please don't use the ancient 75176 bipolar chips that some places still try to sell you along with the free layer of dust.

4. Slaves don't speak until they are spoken to and the master initiates all communication and is responsible for handling "lost balls". It is possible later on that I will introduce multi-master but most applications only use a single master which also keeps everything nicely regulated.

5. Telco connectors are just plain bulky too plus you need two on every board but this also creates a discontinuity in the transmission path increasing ringing and limiting speeds. The ideal is to have every node connect onto a line without breaking it or adding a lengthy stub, the shorter the stub the better. But of course you can use anything you like etc. I use switching regs on my nodes and supply 24V so you are only talking tens of milliamps per node.

Cat5 is handy but it's also a relatively bulky cable to wire directly to my nodes although I have used it. I mostly prefer the proper telco grade phone cable, the copper is single-core but soft and corrosion resistant, very easy to work with. Both of these cables are twisted pair which is what you want, but make sure you actually use a pair that is twisted. You can though get away with non-twisted pair cable depending upon the application and my current project uses continuous 10-way ribbon with RS485 as the inner pair surrounded by ground pairs and a 24V pair on one outer. Because it is limited to several meters it has lots of advantages such as very short stub lengths due to the IDC crimped inline without disturbing the cable and allowing nodes to intimately tap into the bus.

However don't overthink RS485, it's not complicated and if you look back at my glitch thread you will see a very simple RS485 layout.

15. Runtime Interpreter

The runtime interpreter looks like this:

doNEXT                  rdbyte  token,IP                'read byte code instruction
                       add     IP,#1                   'advance IP to next byte token
                       shl     token,#1                'expand to 9-bits - all byte codes point to code on double-long boundary
                       cmp    token,#$180 wc          'tokens $C0..$FF are calls to kernel byte code via kbctbl
              if_c     jmp     token                   'directly execute PASM byte codes without further ado

16. TACHYON  Hardware Explorer

The "CREATE EXPLORER" allows the compilation to detect that this word exists and add in some extras for exploring hardware etc. Those sections that are only included in the Explorer configuration are bounded by "IFDEF EXPLORER" and a closing brace but this configuration should be the default normally.

Remember that there are the binaries which include all this and are a simple one-click and done operation.

Peter Jakacki Posts: 4,580

January 15 edited January 15 Flag0

I don't know how many have been playing with this software but I'm playing with automatically detecting hardware just by the I/O pin state and I2C bus scan, and perhaps some deeper scans on the I/O to reveal capacitance too. From this information I should be able to encode a signature for each unique piece of hardware so that the Explorer can automatically detect the platform it is connected to and also connect automatically to it's peripherals on the basis of that signature. So if for instance it detects that this looks like a BOE board then it can scan deeper and confirm and connect to the microSD and ADC etc automatically. Once it has detected the hardware via user prompting it can lock it in so that it boots up correctly each time.

Now if anyone is interested could you please run lshw for your board and submit results so I can build up a database to build into Tachyon itself so it can perform this auto hardware detection.

I am also looking at building in little applets into Explorer such as capacitance and resistance meters, LCD drivers with hardware configuration menu etc. Just looking at making it so easy to connect and configure without having to search through the obex and then trying to figure out how to interface it as you normally would in Spin.

16.1. lshw  

So this is an example of running lshw from one of my boards: (don't forget to put the listing into [ code ] tags to preserve the content)

P00 inp H up    ||       H inp P31
P01 inp L down  ||       H out P30
P02 inp H up    || up    H inp P29
P03 inp L down  || float L inp P28
P04 inp L float || float L inp P27
P05 inp L float || up    H inp P26
P06 inp L float || up    H inp P25
P07 inp H up    || down  L inp P24
P08 inp L down  || float L inp P23
P09 inp H float || float L inp P22
P10 inp L down  || float L inp P21
P11 inp L down  || float L inp P20
P12 inp L float || float L inp P19
P13 inp L float || float L inp P18
P14 inp L float || float L inp P17
P15 inp L float || float L inp P16
COG #0 registers
$01F0: PAR  = $0000_17E8   %0000_0000_0000_0000_0001_0111_1110_1000
$01F1: CNT  = $02A3_0295   %0000_0010_1010_0011_0000_0010_1001_0101
$01F2: INA  = $E600_0085   %1110_0110_0000_0000_0000_0000_1000_0101
$01F3: INB  = $0000_0000   %0000_0000_0000_0000_0000_0000_0000_0000
$01F4: OUTA = $7000_0000   %0111_0000_0000_0000_0000_0000_0000_0000
$01F5: OUTB = $0000_0146   %0000_0000_0000_0000_0000_0001_0100_0110
$01F6: DIRA = $4000_0000   %0100_0000_0000_0000_0000_0000_0000_0000
$01F7: DIRB = $0000_0000   %0000_0000_0000_0000_0000_0000_0000_0000
$01F8: CTRA = $0580_0000   %0000_0101_1000_0000_0000_0000_0000_0000
$01F9: CTRB = $0000_0000   %0000_0000_0000_0000_0000_0000_0000_0000
$01FA: FRQA = $4000_0000   %0100_0000_0000_0000_0000_0000_0000_0000
$01FB: FRQB = $0000_0000   %0000_0000_0000_0000_0000_0000_0000_0000
$01FC: PHSA = $8000_0000   %1000_0000_0000_0000_0000_0000_0000_0000
$01FD: PHSB = $0000_0000   %0000_0000_0000_0000_0000_0000_0000_0000
$01FE: VCFG = $2000_0000   %0010_0000_0000_0000_0000_0000_0000_0000
$01FF: VSCL = $0000_18F6   %0000_0000_0000_0000_0001_1000_1111_0110
Scanning I2C bus
Fast I/O EXPANDER    #0 at $40: 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E
Fast 64K BOOT EEPROM    at $A0: 45 80 81 0F 04 4C 4F 4E 47 A2 C3 21 04 48 49 47
Fast RTC/ADC?        #7 at $DE: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF  ok

17. FRUN (Batch / Script Files)

Peter JakackiPeter Jakacki Posts: 4,590

April 2014 edited April 2014 Flag0

EDIT: Dropbox files are up-to-date and I have created a FRUNS folder where these files can be copied to the SD card and are useful when you have a fully loaded system. It feels a bit like running batch files from floppy

There's been quite a few improvements on many fronts and some not so obvious. The network servers have been fixed up as there was a problem with the disconnection procedure but I believe that is sorted out now, it seems to run smoothly from remote sites. I have even been using ftp from the browser just as you enter [URL]ftp://tachyonforth.com[/URL] it will come up with a file listing etc. Mind you I still intend to leave a Prop server running permanently although at present this system is under tests and constantly being bombarded by bing bots.

There are so many things to list but one of the things that I have been looking at doing is extending TF code into the SD file system. Well it turns out that there is a very very simple mechanism where if I type in a word that's unknown that it fails the dictionary search, the number conversion and finally bombs to an exception vector, which if set can find and open it as a file name and input it into TF. This may mean it is a simple script or it compiles a definition and runs it. For instance when I create a small build of EXTEND.fth there are some words that have been left out that I would like to use sometimes but I can't afford to have them hogging the little hub memory that remains.

So using a simple definition FRUN and tying to the exception vector then typing the unknown TASKS word automatically does what I need it to do:

[FONT=courier new]pub FRUN word FOPEN$ IF FINPUT THEN ;  ok
' FRUN unum W!  ok
[B]TASKS[/B]  ok
[~       ok
0001: CONSOLE                         0000 00 00 00 00 00 00
0002: IDLE                            0000 01 00 00 00 00 00
0003: IDLE                            0000 01 00 00 00 00 00
0004: IDLE                            0000 01 00 00 00 00 00
0005: IDLE                            0000 01 00 00 00 00 00
0006: RS232.TASK                      51F4 01 00 00 00 00 00
0007: TIMERTASK                       24F4 01 00 00 00 00 00  ok

and so with MAP which is only ever used from the console rarely:

[B]MAP[/B]  ok
[~       ok
0024: 01 04 .. .. 01 .. .. .. .. .. 01 01 02 04 01 ..
0124: 0F 11 14 10 12 18 11 12 14 13 0D 14 10 14 18 16
0524: 14 17 16 18 15 18 0F 18 18 1B 1C 1F 01 .. .. ..
0928: 64 5F 87 89 60 58 61 6F 70 69 6F 60 65 71 63 40
CODE x256
1881: 68 57 4A 5E 78 66 65 5F 64 6B 74 77 58 63 6D 37
2881: 86 2E 51 82 7C 7E 29 55 40 63 44 67 5B 5D 69 65
3881: 5A 66 86 66 5E 52 60 5D 6F 61 69 67 5B 5F 5F 6D
4881: 65 5D 75 78 66 77 6B 7C 77 50 87 66 5A 2F 62 64
 @5567: for 154
5601: 62 61 61 5D 5A 59 55 53 5C 55 6B 62 62 64 62 5E
6601: 5F 5C 57 54 48 43 44 4B 52 59 52 55 70 59 14 13
 @7423: for 221
7500: 0D 06 .. .. .. .. .. .. .. .. .. .. .. .. .. ..
7900: 14 11 12 12 10 0F 14 0E 14 07 .. .. .. .. .. ..
7D00: .. 15 23 1D 1E 04 1E ..
7F60: .. 02 01 .. .. .. .. .. .. ..

I will no doubt make this more general and may even allow precompiled code to run from the file system. There are a lot of diagnostics and "shell" commands that are well suited to this approach which will allow me to cram even more functionality into TF that goes beyond 32K.

EDIT: If I type .MAP which ends up searching the dictionary, number processing, file searching, loading, compiling, running (including scanning 32K hub RAM), then forgetting the functions just compiled, the whole process takes less than a second.

EDIT: What this automatic file search mechanism allows is that I can load code simply by typing the modules name, I no longer need to specifically type FLOAD <name>, now I just type <name>.

18. Scripts

Peter Jakacki Posts: 5,646

August 8 Flag0

MJB wrote: »

And with the script execution non time critical code can be loaded, executed and forgotten, not consuming any RAM permanently.

I timed loading a normal "script" to scan and display the background timers and to compile and run to completion took 441ms. Timing the code itself it takes 154ms to execute and sector read about 1.5ms so almost 300ms was needed to compile about 20 lines of code and forget it afterwards. Obviously a couple of lines of code will be much faster, a single line to print out all printable ASCII characters takes 44ms total.

Create a file and open it:

$8000 mk RUNME


Read/write access (from beginning) and insert by printing a string to the file:


Manually open the file (after closing) so I can time the whole launch and execution:

44.713ms ok

10ms of 44.7ms was actually printing out so it took around 34ms for a load and compile on a single line.

19. Peripheral / IO Modules

19.1. PWM32

19.2. Tachyon's 32 channel PWM as an arbitrary or analog waveform generator

Peter JakackiPeter Jakacki Posts: 4,580

March 2014 edited July 12 in Propeller 1 Flag0

I was just thinking that since the 32 channel PWM uses a 256 long table to store its PWM samples and that since the table is read at an almost 2MHz rate that it would be an easy thing to add resistors to the port pins as ladder DACs (one or more) of variable width. Doing this means it's easy then to generate all kinds of waveforms simply by accessing the table.

Using a lower frequency will yield very smooth audio waveforms etc. At present there are a few words that calculate and write the values to the table whenever you set the duty cycle you want on those channels.

30 2 SETPWM \ Set P2 to 30/256 duty cycle

75 % 3 SETPWM \ Set P3 to 75 % duty cycle

50 % 8 16 SETPWMS \ Set P8..P16 to 50 % duty cycle

This has also meant that I can downgrade the resolution of individual channels and increase the update rate accordingly so that turning that pin into a 4-bit PWM results in >120kHz update rate.

4 16 1 SETPWMW \ Set P1 with a duty of 4/16 so that it repeats every 16 PWM cycles (>120KHz update)

Now I can add modes to specify which pins form a ladder DAC and generate sine waves etc, without affecting the other channels, some can be 8-bit PWMs, some faster 6-bit, and some analog waveforms. The waveforms can even be set from a table loaded into from SD if needed.

Some possible ways of specifying an analog waveform

256 16 23 SINWAVE \ Generates a sine wave over the full 256 samples on P16..P23

16 4 8 COSWAVE \ generates a cosine wave over 16 samples onto P4..P8

noise 64 16 23 WAVE \ copy 64 values (at a time) from "noise" table (just an address in memory) to P16..P23

When I have some time I will set it up and generate some sine/cosine waves alongside 8-bit/4-bit PWMs etc

BTW, this is all interactive so I can type and change it on the fly.

I don't think it's possible to do 10MHz at all even if the table were loaded into cog ram as a mov+incptr+outa+jmp gives a maximum 5MHz update rate. The PWM routine however reads from hub ram which can be modified on the fly by another cog so hub access slows things down a lot, however kuroneko rewrote the whole PWM and increased the read rate from 1.28MHz to almost 2MHz.

The main point about using the PWM this way is that it is a very versatile module because you can use your PWMs in the usual way on any and as many pins as you choose and also you can increase the update rate by decreasing the resolution on any pin without affecting the resolution on other pins. Being able to throw a few resistors on it so to speak means you can have analog waveform generators for peanuts. In this regards I find it very easy to use smd resnets as they normally four individual resistors and it's very easy to build cheap and compact R2Rs with these.

19.3. Bit-Banging 4-digit 7-segment display  + PIN-SPY

Notes: It is easier and better to driver the cathodes directly with a ULN2803 or discrete transistors although the obsolete 74LS47 will sae 4 I/O, but then again there are other easier ways too.

K6MLE wrote: »

Still working on the 'bit banging' for manual mux of a 3-digit display. Right now there appears to be some 'ghosting' between the digits. I guess it's a matter of figuring out timing ... ! The circuit is pretty straight forward ... 3 transistors and a 74LS47 decoder for the segments.

In case you are struggling with the concept or coding here is my take on this and the result, try it for yourself.

--- P10 -P13 as digit value to a 74LS47 and P14 - P17 as digit select via PNPs ---
: LED4.fth ;

#P10        == segpin
#P14        == selpin

TIMER scantmr                                                --- use a timer to call LED scan routine
BYTE ledn                                                --- led digit select count
4 BYTES leds                                                --- led digit buffer uses a byte/4-bit character

        $0F selpin << OUTSET                                --- disable all PNP digit drives which blanks display
        ledn C++                                        --- next digit (only lsbs of counter are used)
        ledn C@ 3 AND leds + C@ >N                        --- read next character (4 lsbs)
        $0F segpin << OUTCLR                                --- prep segments for I/O set mask
        segpin << OUTSET                                --- write the segments to the pin group
        ledn C@ 3 AND MASK                                --- proceed to next digit
        selpin << OUTCLR                                ---  write active low digit select lines (PNNs?)
        10 scantmr TIMEOUT                                --- reload LED scan timer for 10ms

--- LED EMIT routine shifts valid characters into a 4-digit buffer
: (LED)        
        DUP "0" "9" WITHIN IF >N ELSE DROP $0F THEN        --- blank all alphas and symbols etc
        leds @ 8 << + leds !                                --- insert new digit into right of display
        scantmr @ 0=                                        --- start up the scanner if it isn't running
          IF 5 scantmr TIMEOUT ' SCANLED scantmr ALARM THEN
--- Switch stream output to the LED display
--- Usage: LED 123 . CON
: LED        ' (LED) uemit W! ;

{ Testing
LED 5678 . CON  ok
leds @ .LONG 0506.0708 ok

{ SPY on the outputs to test the software without hardware
LED 1234 . CON  ok
1111 02
0111 01
1111 01
1110 04
1111 04
1101 03
1111 03
1011 02
1111 02
0111 01
1111 01
1110 04

        segpin 8 PINS@                                        --- setup for change of state
          BEGIN segpin 8 PINS@ SWAP OVER <> UNTIL        --- wait for a change of state and return with latest
          DUP 4 >> $402 PRINTNUM                        --- display binary digit select pattern
          DUP >N SPACE .BYTE

~] END

Obviously I have a little bug in there according to the SPY outputs so I will leave that for you to find perhaps?

Found my bug as I committed the big no-no of P1 in setting outputs from the console cog before handing it over to the background cog. So I changed the "SCANLED ' SCANLED scantmr ALARM" initialization to "5 scantmr TIMEOUT ' SCANLED scantmr ALARM" as the first version ran SCANLED which set the pins directly from the console cog.

EDIT: I also just realized that the 74LS47 is active low outputs so the digit select must be via PNPs so I've changed it to suit.

19.4. MAX7219-based 8 x 7-segment LEDs

19.4.1. Peter's Driver

K6MLE wrote: »

I am presently working with a MAX7219-based board I got from ebay, containing 8 7-segment LEDs on it. I think the mode to communicate with it is perhaps SPI, but I need to dig a bit to confirm. It has 5 pins on the board: Vcc, Gnd, DIN, CS, & CLK. That doesn't look like I2C, to me!

Has anyone fired one of these up with Tachyon yet?

I know MJB has and he also has code for it but I like to keep it simple and treat it just like any other output device so I've taken a few minutes to peruse the datasheet and create a simple driver. I'm assuming for the moment that you are only interested in digits, not alphas and that we are printing numbers right justified as we do for digits.

Have a look at this code then and I will see about adding the extra functions needed to initialize etc. You should be able to define your pins and then when you use it it will be like this:


So MAX7219 switches the output to the MAX7219 driver and when Tachyon prints the number it will appear on the display and if we didn't have CON in there then Tachyon would continue to try to talk to you via this display. Of course we could put alphas on there but I wouldn't bother. The driver is updating the display for every character it receives but I may change that so that it waits for a CR perhaps? So in that case after updating the display it can clear its buffer ready for a new number which will update the display on the next CR. Does that sound reasonable?

19.4.2. MJB's Driver

here is my code

similar, but a bit more verbose than Peter's version

20. SPI Interfacing to the SPI bus  

SPI is perhaps the most basic serial interface there is as essentially you interface to a shift register which at a minimum requires a clock and a data signal. The idea is that to send data to a shift register we output the data a bit at a time and pulse the clock line, nice and simple. SPI simply allows for both directions with an output to the device (MOSI:Master Out Slave In) and an input to read data from the device (MISO:Master In Slave Out). Finally a chip select signal is used to indicate the start and end of a valid transfer.

Tachyon has SPI support at the bytecode level, it has fast single byte instructions for SPIRD and SPIWR as well as SPIWRB to write a byte or SPIWR16 to write 16-bits. So sending a single byte over SPI takes only 2.4us which includes making sure the chip enable is selected.

Before these instructions can be used we need to setup the masks which indicate which pins to use. In Tachyon these pins are combined and encoded as a single long but to make it easier to enter we can use the decimal byte method that is used for IP notation. For instance if we use P20 for the clock (SCK), P21 for MOSI, P22 for MISO, and P23 for chip enable (CE) we can specify the long as & Internally the long in hex will be $17161514 so the IP notation is certainly friendlier. Now use this pin id like this & SPIPINS

This is all that is required to setup a device and if you switch between devices a bit you could give them a name instead or if it's only the CE line that's different you could use a faster method of just setting the CE mask directly.

The SPI instructions are optimized for fast bit-bashed operation so they also do not push or pop the stack. If you want to read from the SPI bus you must supply a stack parameter to start off with. If you want to write then you must lastly also drop the result. This is not a waste as it first seems because if you want to read 32 bits together you simply 0 SPIRD SPIRD SPIRD SPIRD and it will accumulate 32-bits of data and conversely $12345678 SPIWR SPIWR SPIWR SPIWR DROP will transmit 32-bits starting from the most significant byte. In fact the result we dropped after the SPIWR was the same $12345678 that had been rotated left 32 times so we could send it many times without having to reload.

Now here is SPI being used to talk to the WIZnet W5500 Ethernet chip highlighting just two basic methods of reading and writing a byte at an address.

pub LC! ( byte wizadr -- ) ( 34.6us )
   SPIWR16 DROP                --- write address
   WR_WCTRL                     --- control byte
   SPIWRB DROP                  --- write a single byte
   &WNCS OUTSET              --- release chip select
pub LC@ ( wizadr -- data )
   SPIWR16 DROP               --- send addr
   RD WCTRL                     --- control byte
   0 SPIRD                         --- read back data

Similarly to write to a much simpler device like the MAX7219 8-digit 7-segment LED display driver we need to send one byte as the address and another byte as the data and all it is is this:

pub  LED! ( data adr -- )        SPIWRB DROP SPIWRB DROP ledce HIGH ;

Here ledce is simply a constant with the pin number of the chip enable whereas previously we had &WNCS OUTSET which was actually a mask constant needing OUTSET to take the pin high. This latter form is used if we want to trim and save every microsecond which is important for high volume high speed traffic talking to the WIZnet chip but not so important for the lazy LED display where we can use the perhaps friendlier form of <pin> HIGH.

Summary of definitions:

SPIRD ( xxxxxxxx -- xxxxxxnn ) Read a byte from the SPI bus and rotate in left into the current long (2.4us)

SPIWR ( nnxxxxxx -- xxxxxxnn ) Write the msb byte of the long to the SPI bus and rotate for the next most msb byte (2.4us)

SPIWRB ( xxxxxxnn -- xxxxxxnn ) Write the byte to the SPI bus and return with the same (2.6us)

SPIWR16 ( xxxxnnnn -- xxxxnnnn ) Write the word of the long to the SPI bus and return with the same (4.6us)

More about the finer points of using SPI in another blog.

21. I2C Devices

As soon as one cog calls I2CSTART as part of any I2C transfer the routine will check to see if the bus is busy and if not it will set a busy flag and proceed. This means while it is proceeding that if another cog also tries to access the same bus it will cycle in I2CSTART until the bus is released by an I2CSTOP from the other cog.

However you can also use 2 other pins if you like as mentioned using I2CPINS and if that cog doesn't need to access the EEPROM then it doesn't need to switch back either nor is it strictly necessary. Normally if we do set up pins we also define them just as EEPROM is defined:

pub SENSORS 16 15 I2CPINS ;

I think though that sharing the pins will not prove to be any kind of problem really although when the dictionary in eeprom is being "searched" it basically reads in a block of up to 384 bytes if it can't find the word in the ram dictionary or in the cache. So that might be up to 4ms or so that the bus might be busy for although you will find that it is much less than this normally.

21.1. AD7993 I2C ADC

AD7993 A2D IC (PBOE) Tachyon words?

I just had a quick look at this chip but it looks very straightforward to configure and read. Have you tried to at all?

The general form of writing to this would be:

pub AD! ( byte reg -- ) I2CSTART $40 I2C! I2C! I2C! I2CSTOP ;

pub AD@ ( reg -- byte ) I2CSTART $40 I2C! I2C! I2CSTART $41 I2C! 1 I2C@ ;

22. 32-Channel Full Duplex Serial

Talking about full-duplex serial on another thread got me to thinking about ways to implement multiple serial ports.

I do believe I can write a 32-channel full-duplex object that runs in a single cog reliably at 115,200 baud. Whaaaa!? "Shirley" you're not serious you might say but seriously don't call me Shirley.

There's a technique I came up with that involves synchronous oversampling bursts feeding a majority vote with the same burst period as the baud rate that will allow me to capture serial data on up to 32 pins specified by masks. At the same time serial data can be transmitted on up to 32 pins specified with the transmit masks. I might even be able to fit the buffering in there too especially at lower baud rates anyway. There will be no jitter in the transmit and receive as samples and updates are synchronously clocked 32 I/O at a time via waitcnt.

Receiving data:

* Burst sample by reading in minimum of n longs of I/O

* Majority vote and using receive masks write filtered samples to the 32-bit "shift register" comprised of 11 longs in the cog.

note: 8 bits for data, 1 start, 1 stop, and 1 stop before start (see transmit)

* If a zero bit is in the end long then accept the data stored in that bit position over 8 longs as "data" providing the most recent sample is high (stop bit).

- reset that channel to 1's if data is accepted

- buffer data

Transmit data:

note: a channel is idle if that bit position over first 10 longs is empty (1's)

* Next data is added to the "shift register" distributed in that channel's bit position over 10 longs but from the penultimate shift register long

note: this leaves room for a previous stop bit to be transmitted before another character

Burst sampling also detects edges as middle sample in the burst involves two quick samples to help overcome metastability affecting the majority voting. (perhaps)

Burst sampling (only showing 3 samples for simplicity):

IDLE  START     BIT 0     BIT1      BIT2      BIT3      BIT4      BIT5      BIT6      BIT7      STOP
DATA:  11111100000000001111111111000000000011111111110000000000111111111100000000001111111111000000000011111111110000000000
BURST: 00011100000001110000000111000000011100000001110000000111000000011100000001110000000111000000011100000001110000000111
VOTE;  .....1.........0.........1.........0.........1.........0.........1.........0.........1.........0.........1.........0

DATA:  11111100000000001111111111000000000011111111110000000000111111111100000000001111111111000000000011111111110000000000
BURST: 000011100000001110000000111000000011100000001110000000111000000011100000001110000000111000000011100000001110000000111
VOTE;  ......1.........0.........1.........0.........1.........0.........1.........0.........1.........0.........1.........0

DATA:  11111100000000001111111111000000000011111111110000000000111111111100000000001111111111000000000011111111110000000000
BURST: 0000011100000001110000000111000000011100000001110000000111000000011100000001110000000111000000011100000001110000000111
VOTE;  .......0.........1.........0.........1.........0.........1.........0.........1.........0.........1.........0.........

DATA:  11111100000000001111111111000000000011111111110000000000111111111100000000001111111111000000000011111111110000000000
BURST: 00000011100000001110000000111000000011100000001110000000111000000011100000001110000000111000000011100000001110000000111
VOTE;  ........0.........1.........0.........1.........0.........1.........0.........1.........0.........1.........0.........

DATA:  11111100000000001111111111000000000011111111110000000000111111111100000000001111111111000000000011111111110000000000
BURST: 000000011100000001110000000111000000011100000001110000000111000000011100000001110000000111000000011100000001110000000111
VOTE;  .........0.........1.........0.........1.........0.........1.........0.........1.........0.........1.........0.........


        IDLE  START     BIT 0     BIT1      BIT2      BIT3      BIT4      BIT5      BIT6      BIT7      STOP
DATA:  11111100000000001111111111000000000011111111110000000000111111111100000000001111111111000000000011111111110000000000
BURST: XXXXXXX000000011100000001110000000111000000011100000001110000000111000000011100000001110000000111000000011100000001110000000111
VOTE;  ................0.........1.........0.........1.........0.........1.........0.........1.........0.........1.........0.........
        IDLE  START     BIT 0     BIT1      BIT2      BIT3      BIT4      BIT5      BIT6      BIT7      STOP
DATA:  11111100000000001111111111000000000011111111110000000000111111111100000000001111111111000000000011111111110000000000
BURST: XXXXXXXX000000011100000001110000000111000000011100000001110000000111000000011100000001110000000111000000011100000001110000000111
VOTE;  .................1.........0.........1.........0.........1.........0.........1.........0.........1.........0.........

It's all in my head at the moment and if it isn't fundamentally flawed it will end up as a Tachyon object which can easily be reworked to integrate with Spin or C etc but certainly a lot easier for me to develop and test in Tachyon. Got to get to work on the code now, may do it in Tachyon to test the concept at 9600 baud then implement the PASM code. Certainly there is no problem with transmitting though, at the very least this will be implemented.

Testing will involve a few Props all sending data at random intervals and checking the response. So this post is also to help me think it through and get some feedback too, like "you're mad!".

23. Compiler Directives IFDEF / IFNDEF


there is no such thing as compiler directives ...

all words are equal - and some are a bit more equal you know ;-)

IFDEF / IFNDEF are just 'dynamic' comments.

depending on the condition they just treat everything until the next matching } as a comment.

If not everything get's executed as normal and the spurious extra } is just ignored.

If you look in the code for the definition of IFDEF you can see it.

' {     Block comments - allow nested {{  }} operation
CURLY   byte    _1,REG,11,CSTORE                ' allow nesting by counting braces
CURLYlp         byte    XCALL,xWKEY             ' keep reading each char until we have a matching closing brace
       byte DUP,_BYTE,"{",EQ,_IF,04,_1,REG,11,CPLUSST  ' add up opening braces
       byte _BYTE,"}",EQ,_IF,04,MINUS1,REG,11,CPLUSST  ' count down closing braces
       byte REG,11,CFETCH,ZEQ,_UNTIL,

this is precompiled forth byte code code from the tachyon extended kernel.

But after a while you learn to read it ;-)

so you can see IFDEF / IFNDEF just continue with the { comment behaviour,

or they exit and processing the code continues.

and if there is an extra }

       byte    $20,"}",        hd+xc+im,       _NOP,EXIT

the definition of the } word just ignores it[/quote]

Oy! I had found that in the extended kernel but I couldn't read it.

Kinda wanted to skip SPIN code and just come back home to Forth . . . and then Tachyon was so sexy . . .

So just a little more he'p with this please. For what type of condition are IFDEF and IFNDEF looking.

And if the condition is true then IFDEF will compile the following code and else it will be treated as a comment until the next "}" . . . and the reverse for IFNDEF ??

Do I have that right?

Not all who wander are lost . . . but some are.

Gratitude is riches, complaint is poverty, and the worst I ever had was wonderful. - Brother Dave Gardener :innocent:


Peter JakackiPeter Jakacki Posts: 4,590

August 22 Flag0

artkennedy wrote: »

Oy! I had found that in the extended kernel but I couldn't read it.

Kinda wanted to skip SPIN code and just come back home to Forth . . . and then Tachyon was so sexy . . .

So just a little more he'p with this please. For what type of condition are IFDEF and IFNDEF looking.

And if the condition is true then IFDEF will compile the following code and else it will be treated as a comment until the next "}" . . . and the reverse for IFNDEF ??

Do I have that right?

Not all who wander are lost . . . but some are.

There aren't any "conditions" with IFDEF/IFNDEF as the only "itions" are definitions IF <this word here> is defined then continue as normal OTHERWISE treat everything up to the closing } as a comment. Simple! Isn't it?

It's a bit like in the Spin tool we didn't have a way to do conditional compile, we had to manually enclose unwanted sections in braces { } and remove or comment the braces on the sections we wanted such as '{ but that was messy and awkward. What we really wanted was to be able to say "compile this for a Quick Start" or "compile this for a BOE" and so in Tachyon if we define BOE in any form (CREATE CONSTANT : etc) it is now defined so that when we say IFDEF BOE then Tachyon will find BOE in the dictionary and proceed as normal. If it fails to find BOE it simply treats everything as a block comment up to the closing } although there is no actual opening brace, the IFDEF/IFNDEF takes its place.

So to summarize the IFDEF/IFNDEF takes the following word and depending on whether it finds it or not (true or false) will determine how to treat the following source, as a comment or to be compiled.

An example of this is in EXTEND.fth and abbreviated for this example is the lshw word to list the hardware info. We only want some of these features in the EXPLORER version as they can tend to be a bit verbose for such a small memory. So if EXPLORER is already defined then this definition will be compiled:

pub lshw        
        CR lsclk CR lsio CR lsregs CR lsi2c
        CR CR PRINT" Hardware signature = " iosig @ .LONG "," EMIT iosig 4 + @ .LONG
        i2csig 4 FOR "," EMIT DUP @ .LONG 4 + NEXT DROP
        DECIMAL ;

If the EXPLORER word in any form is not found then the equivalent of that would be:

{ pub lshw        
        CR lsclk CR lsio CR lsregs CR lsi2c
        CR CR PRINT" Hardware signature = " iosig @ .LONG "," EMIT iosig 4 + @ .LONG
        i2csig 4 FOR "," EMIT DUP @ .LONG 4 + NEXT DROP
        DECIMAL ;

That is, everything after the EXPLORER word is treat as if it were a comment.

Try this:


IFNDEF EARTH PRINT" Lost in Space" }

IFDEF EARTH PRINT" Home Sweet Home" }

24. Applications

24.1. Flash programmer (Parallel Flash/EPROM/EEPROM)

I was wondering if anyone attempted to build a Flash programmer (Parallel Flash/EPROM/EEPROM) using the propeller. For some other projects I would like to program some 29F010s.

Peter Jakacki Posts: 4,594

12:18AM Flag0

I actually started some code to access EPROMS using the 30 direct I/O method mentioned and it won't be complicated at all. It will allow me to "talk" to the EPROM via the serial console and read or dump any part as well as program it from I2C EEPROM after a hex load from the PC. It is just an outline at the moment but this is where I got up to:

--- I/O assignments
#P0        == #DATA
#P8        == #CE
#P9        == #OE
#P10        == #WE
#P11        == #ADDR        --- P29..P11 = 19 BITS, upper address bits shared with I2C.

-1 19 BITS #ADDR <<        == ADRBITS

pub ADDR ( addr -- )
pub RDBYTE ( -- byte )

pub WRBYTE ( byte -- )

pub EPROM! ( byte addr -- )                ADDR WRBYTE ;
pub EPROM@ ( addr -- byte )                ADDR RDBYTE ;

--- prefix for DUMP commmands - i.e. 0 $1000 EPROM DUMP
pub EPROM        ' EPROM@ dmm W! ;

--- Program the EPROM from src in eeprom - 100us pulse then verify etc
pub PROGRAM ( src dst cnt -- )
        ADO DUP BUFFERS $800 ELOAD I $800 ADO

25. Tachyon V3


In between projects I've started a V3 Tachyon with a few ideas I've had which also include inline LMM code although I'm not promoting that for general programming, mainly for internal use in the Prop tool compiled kernel. There's some bytecode that could be sped up by going to LMM and there is some cog kernel PASM hogging memory that could be moved out into the hub such as (EMIT), CMPSTR, and !RP etc. The EMIT opcode is a tight bit-bashed operation but if we were to limit the baud-rate to 115.2k for compatibility then it even makes sense to put this into a full-duplex serial cog instead of the high-speed receive only cog. My original rationale for bit-bashing was that is was faster to bash it out at high baud rates then it was to muck about putting it into a buffer and then extracting it again, but at slow (115.2k) baud rates it is better off buffering it.

I even have ideas to pack the dictionary names into a string list so that the "headers" are all fixed length with pointers to the string and include vocabulary masks as well. But the current method works well with the new EEWORDS hybrid dictionary so that might still be hard to beat.

Vectors are here to stay at present as that makes it possible to code the bytecode kernel relatively cleanly and directly (using all those "byte" operations) in the Spin compiler. They are also useful in reducing the length of a call operation but the vector table may be reduced from 2K down to 1K as runtime compilation allows for absolute CALL16 operations.

The end result will be that I can pack more useful opcodes into the kernel cog so that it will be more powerful and run faster overall in conjunction with selective LMM kernel hub code. Some RUNMOD modules which are internal PASM modules of up to 21 longs normally loaded into a fixed cog address to be run as a RUNMOD opcode can now free up that cog area and run as LMM if absolute speed is not an issue.

Coglets will also be implemented in this version, that is PASM objects which are loaded from hub or EEPROM into a cog via a coginit loader. These may be the PASM part of objects from the OBEX that have been adapted slightly to work with Tachyon. Coglets can be loaded by name at runtime, or anytime and include multi-port serial objects as well, just:


will load the SERIAL4 object if found in memory (including EEPROM and SD) into cog 2 with the parameter pointing to SERBUFS and run it.

This would also be a good time to introduce the MATH COG object for floating point (yuck) and transcendental functions (yay) etc.

Float is something I never need, unless I'm emulating a calculator! I've always gotten by quite fine with scaling and integer mul/divs as we always know the range we are dealing with. Besides emulated float on a processor that doesn't have mul/div instructions falls far short of anything real-time.

Now about the Spinneret, I should be able to get those samples of the IoT5500 +P8 modules off to you this week so after you forward some to WIZnet in Germany for the embedded world exhibition, you will still have some for yourself, even though you are in Spain at present! Joachim is especially interesting in the Propeller version rather than just the Ethernet module.

Once you hook them up let's see if you still get random hangs then and if so it should be fairly easy to diagnose. To grok the whole packet process I used to dump a lot of the receive and transmit buffers and socket stats to find out what I was doing wrong, or needed to do different, and of course it's easy to customise special debug words for that in Tachyon.

Tachyon could do with a nice CREATE DOES> to help extend the compiler but due to the bytecode and compile as you type methods it employs this renders the DOES> a little awkward. So the WILL is more of a mangled attempt to extend the compiler by sheer brute force. Prior to encountering the WILL word all the previous parameters have already been compiled in a temporary area at the end of code space. Once WILL is encountered it is not compiled but executes immediately however those parameters aren't on the stack yet as they have not yet been executed. This is where the GRAB (not the best choice of a name) comes in as it forces execution of everything that has been compiled up to that point which will result in the parameters being pushed onto the stack after which it can proceed as normal. In this compiler "mangulation" there are a few things that could do with a little attention and improvement which I hope to address in V3.

BTW, those "10 bits" are essentially 1 of 1,024 possible vectors which point to the code to execute, so this method allows packing into a long rather than trying to encode the full 16-bit absolute address and using more than one long (not as nice).


I've been meaning to get around to testing some SNTP protocol to fetch the current time from an NTP server but seeing how we have been parsing HTML pages it got me to thinking that all I really need is to issue a GET request to a server which will respond with the current time and date in its header response.

So to try this I just telnet to port 80 of google.com for instance and issue a incomplete GET request.


HTTP/1.0 302 Found

Cache-Control: private

Content-Type: text/html; charset=UTF-8

Location: http://www.google.com.au/?gfe_rd=cr&ei=UH_EVO24O63u8wfd74GYAQ

Content-Length: 262

Date: Sun, 25 Jan 2015 05:29:52 GMT

Server: GFE/2.0

Alternate-Protocol: 80:quic,p=0.02

<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">


<H1>302 Moved</H1>

The document has moved

<A HREF="http://www.google.com.au/?gfe_rd=cr&ei=UH_EVO24O63u8wfd74GYAQ">here</A>.


Connection closed by foreign host.

So there you have it, very easy to parse up to Date: and set the current time either in an on-board RTC or Tachyon's virtual RTC and perhaps check every hour and on power-up to resynch.

Couldn't be simpler and I will build this method into EASYNET to do just that. Does anyone see any problems with this approach?

26. SD-File System

As I am in the middle of revamping the EASYFILE module I thought I'd better give you a heads up on this one as besides improvements there has also been a shift in how I will handle the virtual memory that the file system is built on.


To date the virtual memory access words have served me well but they are limited to 4GB addressing which happens to be the smallest SD card I can now buy. When I have been accessing files whose virtual memory address is greater than 4GB I have chosen to use the sector methods instead although this has really only been for music files. The other problem with the virtual memory addressing methods is that you can just as easily write to system areas of the card as address 0 is the very first byte of the boot block on the card. So to address these concerns I am implementing new file access words which address a "file" as virtual memory so that address 0 is the first byte of the file, previously the first byte of the file was addressed with the starting address of the file in the 4GB virtual memory range. So now the address can only access the contents of the file, not beyond its last cluster as it is clipped, but that file can be anywhere on the card, even beyond the first 4GB.

The basic methods for virtual card memory are as you know XADR XC! XC@ X! etc and so the basic methods for virtual file memory are similar as FADR FC! FC@ F! etc. These words work just like the basic Forth memory operations.


Those of you who have used EASYFILE know that I essentially ignore the file allocation table and work with files as flat contiguous address space from the starting address using just the first cluster. As the card would have files created back on the PC I don't try to create files so I can get away without having to mess with the FAT or following cluster chains which is not at all conducive for flat virtual memory anyway. However there are several good reasons for needing to create files at runtime so I am in the middle of testing that except that when I create a file I do not try to use up any loose clusters but work from the end of the used area so that the new file may grow and remain contiguous which is handy for log files and copy operations etc. For other files it is advantageous to preallocate plenty of clusters at creation time.


EASYFILE has up to this point been limited to accessing files only in the root directory but selecting another directory isn't really all that complicated, just a matter of "opening" the directory as you would a file and using its starting address/sector as the current working directory. So I've added methods for this to permit access from the shell in the same way we would on DOS or Linux systems using the "cd" command. A "cd /" will also reset to the root directory while the "cd .." will of course access the ".." entry as a directory to go back to a higher level directory. The "pwd" will work the same and I may make it an option that the path becomes the prompt as it does in PCs.


As a side note the kernel was tweaked last week due to uncovering a very strange bug in one of my applications which I found nested a lot of return addresses, more in fact than I had room for in the cog, which is normally 22 levels deep. The return stack is really only ever used for implicit calls as there is the loop stack which is handy for pushing and popping from the data stack. However the return stack size was previously 22 levels as I've never had any applications touch the bottom until recently, and it wasn't even networked, just filesystem stuff but with many layers. So this has been increased to 26 levels and thoroughly tested to see whether I needed to perhaps expand the return stack into hub RAM. Thankfully I didn't need to as the extra code to do this would have to come out of other cog resources and also impact the speed of call/return.

The code is being updated on the Google pages at present and the Dropbox files will be updated after testing.

(Changing directories)

[B]ON PROMPT[/B]  ok
Tachyon@0000.0000 / $ [B]MOUNT[/B]
Mounted F603.8024-8336.5394 MSWIN4.1 NO NAME     FAT32   8,066MB (4,096/cluster) ok
Tachyon@8336.5394 / $ [B]ls[/B]
CE1372  .FTH   CE1372  .PDF   CHARLCD .JPG   IOT5500 .ROM   DRAGON  .JPG  
FSRSCH  .PNG   HCB4208 .JPG   HELP    .TXT   HOME    .HTM   HTTP404 .HTM  
IMAGE   .      IMAGE1  .      IMAGE2  .      IMAGE3  .      IOT5500 .JPG  
LOGON   .HTM   LOVE    .MP3   MENU    .TXT   P8X32A  .PDF   POPCORN .WAV  
P8PGM   .ROM   SDCARD  .FTH   TEMP    .TXT   SITE0002.LOG   SITE0003.LOG  
SITE0004.LOG   SITE0005.LOG   SITE0006.LOG   SITE0007.LOG   SITE0008.LOG  
WORDS   .DCT   WORDS2  .DCT   IOT5500 .PDF   DS3231~1.FTH   P8DEFS  .FTH  
SDWORDS .FTH   W5500   .FTH   IOT     .SET   IOT5500A.PDF   MORE    .        ok
Tachyon@8336.5394 / $ [B]cd MORE[/B]  ok
Tachyon@8336.5394 MORE $ [B]ls[/B]
.       .      ..      .      FORUM   .TXT   P1432PCB.PDF   P1432SCH.PDF  
P1455PCB.PDF   P1455SCH.PDF   PICTURES.        ok
Tachyon@8336.5394 MORE $ [B]cd PICTURES[/B]  ok
Tachyon@8336.5394 PICTURES $ [B]ls[/B]
.       .      ..      .      PC190168.JPG   PC190164.JPG   PC190163.JPG  
PC190162.JPG   PANEL   .JPG   PC110001.JPG   P1070194.JPG   PCB     .        ok
Tachyon@8336.5394 PICTURES $ [B]cd ..[/B]  ok
Tachyon@8336.5394 MORE $ [B]ls[/B]
.       .      ..      .      FORUM   .TXT   P1432PCB.PDF   P1432SCH.PDF  
P1455PCB.PDF   P1455SCH.PDF   PICTURES.        ok
Tachyon@8336.5394 MORE $ [B]cd /[/B]  ok
Tachyon@8336.5394 / $ [B]ls[/B]
CE1372  .FTH   CE1372  .PDF   CHARLCD .JPG   IOT5500 .ROM   DRAGON  .JPG  
FSRSCH  .PNG   HCB4208 .JPG   HELP    .TXT   HOME    .HTM   HTTP404 .HTM  
IMAGE   .      IMAGE1  .      IMAGE2  .      IMAGE3  .      IOT5500 .JPG  
LOGON   .HTM   LOVE    .MP3   MENU    .TXT   P8X32A  .PDF   POPCORN .WAV  
P8PGM   .ROM   SDCARD  .FTH   TEMP    .TXT   SITE0002.LOG   SITE0003.LOG  
SITE0004.LOG   SITE0005.LOG   SITE0006.LOG   SITE0007.LOG   SITE0008.LOG  
WORDS   .DCT   WORDS2  .DCT   IOT5500 .PDF   DS3231~1.FTH   P8DEFS  .FTH  
SDWORDS .FTH   W5500   .FTH   IOT     .SET   IOT5500A.PDF   MORE    .        ok


Using a standard 64 byte page I write 4096 bytes in 389ms

0 $8000 $1000 LAP ESAVE LAP .LAP 389.055ms ok

If I change the page size I get a much better 269ms - that's about 64us/byte and probably could be improved.

0 $9000 $1000 LAP ESAVE LAP .LAP 269.332ms ok


27.1. - Serial in Parallel out shift register / IO extender  595

As for what I'd like to accomplish, I'd like to write a program that receives a byte/number through the serial terminal (I'm using teraterm, per your recommendation), and sends it serially to a 595 shift register. I admit I haven't done much in terms of device driver design, and I'm sure a 595 is almost certainly compatible with the SPI function in the TF dictionary, for an excersize I think it'd be great to help introduce me to key functions and concepts. And a 595 is simple enough a device that it would be less a project on figuring out how to use a device than to use Tachyon.

Just to get started then it couldn't be simpler but first you need to setup your I/O pins with the SPIPINS word which expects one byte encoded long that specifies the clock, data out, data in, and chip select. For the 595 you could drive it with as little as 2 I/O if perhaps you were just using LEDs but let's use 3 lines so that the chip select signal ends up clocking the latch after it's done. First the setup, assume we use P0 for the clock, P1 for the data and P2 for the latch:


The & notation encodes decimal bytes in much the same manner we encode IP masks like etc where each group represents a byte. This notation is compact and efficient and it is also just as easy to make that mask a constant if we want to reuse it like this:

& == HC595

So you could just as easily say:


Now this sets up the masks and direction of the pins ready to be used by any of the SPIWR and SPIRD variants and the data in could be anything if we are not using it but in this case I set it the same as the data out. By default any reference to an SPI routine will automatically enable the chip select and when we are done with it all we can manually deselect the SPI device. For the HC595 this deselect means the "latch clock" goes from low to high which will transfer the shift-register contents to the output latch internally in the 595.

From the top and all in one interactive line let's try to send $F1 to the 595:


That was too easy, but what happened? Well after setting up the I/O mask for the SPI port we told it to write a byte to the SPI port and then take the chip select (latch clock) high. The SPIWR leaves the rotated left data on the stack so we can continue on with another byte in the long etc but in this case we discard it with DROP.

How about another one-liner after we have done this that counts up through a byte with a delay:


The ADO is basically the same as DO in Forth except it expects a start index and a count so we start from 0 and loop 256 times where "I" fetches the current loop index value onto the stack then passes that to SPIWRB after which we discard it and wait for 20 ms before looping.

If you chained four 595 shift registers together for 32-bits you could send like this:


You can build the basics into your own definition easily enough now that you can see how it works, very simple really.