Skip to content

Developer's Notes

Svetlin Parev edited this page Mar 15, 2017 · 11 revisions

Developer's Notes

mul runs as an interpreter, reading user's program row-by-row and executing row content. mul is written in C.

Next will trough light at main idea.

main.c

Main program has to initialize hardware, file system and mul machine. Then it runs in an infinite loop checking inputs and calling mul().

main()
    HDinit();
    initFS();
    mulInit();
    while(TRUE){
            doIns();
            mul();
    } 
}

 myInterrupts(){
    if(timerInterrupt) doTimers();			// every 250uS
    if(consoleInterrupt) rock();
 }

doIns() sets all digital (in overall count up to RANGE) and analog (in overall count up to RANGE) inputs in current state. This is done once per mul() cycle, so all the checks inside mul program can be done at same input conditions. Main program has to contain also an interrupt routine. A HD timer initialized in HDinit() should fire every 250uS to call mul's doTimers(). Console incoming data should trigger mul's rock().

mul.c

mul.c programs serve tasks named Jobs in overall count up to NUM_TASK including console Task0 in order that user stated in mul-program. It contains next programs given in order of dependence:

  • mul() main navigation cycle which performs in-row move, row-to-row and task-to-task switch.
  • mulSentence() sub, called from mul() if an mul expression encountered
  • rock() console interrupt routine
  • mulInit() loads mul program (if one presents), initial sets row and stack pointers and zeroes all the mul variables.
  • doTimers() 250uS HDTimer interrupt routine. Updates timer's values allTimers[u16] and timer's flags RANGE myTimers in overall count RANGE+NUM_TASK, as each task has a hidden extra timer used by HALT mul commands. Additional it runs the Real Time Clock.
  • Several help functions push(), pop(), loadTimer(), clearTimer(), getIndex(), breakCheck().

push() & pop()

mul sustain a 2-dimentional array named rowStack[task, struct{task, row, position}] range of [NUM_TASK][STACK_DEPTH]. STACK_DEPTH defined up most in mul.c as =8. An additional array rowStackIndex[taskNindex,..., task1index, task0index] holds each task's current index in stack. Those arrays are dedicated to track each task's load row and position-in-row when task is forced to load itself to execute.
The bottom of each task's stack (i.e. rowStack[task][0].task) holds next task to load. The first position in stack (i.e. rowStack[task][1]) holds common task entry point row and position (where first time J1...J7 found).
Each call from a task to a subroutine in user's mul program increments rowStackIndex[task] , then pushes in stack return row and in-row position after call instruction. Then loads subroutine's first row, to execute sub.
If a task is halted, its task-corresponding bit in 'halt' variable is set. The row and position where halt occur are pushed in stack too. In case halt caused by one halt instruction waiting an event or/for a time-out left-most 2 bits from task-field in

rowStack[task][rowStackIndex[task]].task<bits6,7>  

are set according to halt cause. So next cycle the task is loaded and mul detects that task is halted checking

rowStack[task][rowStackIndex[task]].task<bit 6>  

mul knows it have to check if the halt condition still persist. Same situation when a task's halt timer expire – it can check

 rowStack[task][rowStackIndex[task]].task<bit 7>  

to see if task is waiting for it, then release task if yes.
The top of each task's stack holds task's row and position to load
push(task) and pop(task) serve to make and retrieve a record to/from task's stack.

How all works: At power-up mulInit() is called. It sets all processor pins as digital inputs and clears mul variables. An interface usually an USART is initialized as console interface at 9600, 8N1. Pipe P0 is attached to it.
Next mulInit() tries to fetch mul-program “main.ml” from File System or from FLASH. The address of each row beginning is written to pointers array m_program[M_ROWS] starting from index 1 for first row address upward. At the end of this procedure variable maxRowN is set to show last row number available, so mul could prevent a jump outside user's main.ml program.
If main.ml read was done successful begins Stack load such way:

1. rowStack[0][0].task=0;	// task0 will call itself for now  

2. read m_program[] until a mark for a Job beginning found in format “Jn<spc>” or “Jn,” or “J<crlf>” 

3. set rowStack[0][0].task=n;		// task0 will call Jn  

4. set rowStack[n][0].task=0;		// taskN will roll back to task0  

	4.1. rowStack[n][1].row=row;		// taskN common in-point row  

	4.2. rowStack[n][1].position=0;		// taskN common in-point position in row  

as this is first user Job, we wold like to make it those Job which will execute all rows until current row i.e. the common beginning initialization job in mul program. So we assign

1. stopPos.task = n;  // default task will start from row 1 and will do the set-up jobs  
2. stopPos.position = 0;  
3. stopPos.row = 1;  
4. stopPos is a structure used when debugging a mul program, executing it step-by-step.  

We continue reading m_program[ ] looking for other jobs. Each time we found a new one we rewrite

* rowStack[pre-task][0].task=newTask;	// previous task will load new one
* newTask[newTask][0]=task0;		// new one will roll back to task0

Next runs mul() program. Mul() works over a RAM buffer named myRow[] in which current task loads appropriate m_program row. Each time mul() was called it starts with loading task0. Most the time task0 is blocked, except when user sends data to console and rock() do not know what to do with this data. Then rock() releases task0. Task0 loads console buffer to myRow and executes it. No mater is task0 blocked or not finally it passes execution to task written it its

rowStack[0][0] task.  

When mul encountered that should pass to task0 (roll-over) it returns.
One cycle was done.
In general mul() sustains in-row, row-to-row and task-to-task navigation. If it don't knows what to do with current row content, it calls mulSentence(). MulSentence() covers all the mul instructons as per User manual. To process math and string expressions it uses math$Expressions(). More over if a sentence does not fit to any instruction pattern it passes sentence 'as is' to math$Expressions() to produce a result back to mul().

Mul does not use standard C-lib functions, rather those from mySubroutines.c. So you are not forced to include most standard C-libs.  

math$Expressions.c

Once a expression encountered mul passes it to math$Expression calling Ex(). Ex() takes 2 parameters – a pointer to pointer that currently processes a row. Pointer points to beginning of expression so Ex() will be in able to modify it. The second parameter is an extra char, which user sets according to expected mark for end-of-expression or 0 if a trivial end-of-expression. More over setting this char MSB signals Ex() to do a recursive call not zeroing its structures in the beginning. So Ex() can call itself.

How Ex() works? I will illustrate giving an example. Suppose we have next sentence:

1000-0x($1+”%axV1 completed\r\n”)]6-(V2*F1-20)  
Where:
$1=”Test ”, V1=200, V2=-12, F1=1  

First we will explain strings processing. For temporary strings we have to use an array named workS[max$Chars] (all defined in math$Expressions.c) and a table[max$Chars] which contains pointers to length and value of all the strings we will need. We use an index to fill workS named wIndex and an other index to fill table named nextSchar both initial zero. We start from left to right exploring our sentence, and if not a string we leave it 'as is':

1000-0x(
	   $1 we will change to a new letter ASCII=127+'a'+nextSchar. As we have not sign for this ASCII we will write it as 'a' keeping in mind that this is not really 'a' but 127+'a'.  

Also

		table[wIndex].len=$1.len;		// as $1.len is pointer too
		table[wIndex].val=$1.val;
		wIndex++;  

So we don't need to waste a workS for $1. Our expression becoms:

1000-0x(a+  

Next we have the string

 “%axV1 \r\n”.   

% is a special char that means 'convert to', 'a' means ASCII and 'x' means hexadecimal, lower-case. So this string leads to

“c1 \r\n”          // as V1=200=0xC1

We have to use a work string for it, so:

		workS[wIndex].len=5
		workS[wIndex].val=”c1 \r\n”

Then table[wIndex].len=&workS[wIndex].len table[wIndex].val=workS[wIndex].val wIndex++;

and for our expressoin:

		1000-0x(a+b)]6-(V2*F1-20)

No more strings, so we start a similar procedure with other variables. This time we will use a table with signed values Val[maxEchars] and an index named nextChar. We will rename digits and variables using ASCII from 127+'A' up to 126+'a' as in 127+'a' remapped strings (see above).

		Val[nextChar]=1000;		//A
		nextChar++;  

Once more we will write 'A', keeping in mind that this is not 'A' rather 'A'+127!

A-0x(a+b)]

The combination '-0x..' we rewrite as '-1*0x..' so we insert a new char in string which is 'B+127'. We will illustrate it as 'B':

		Val[nextChar]= -1;		// B
		nextChar++;

		A+B*0x(...

'6' – we will put in table too

		Val[nextChar=6;		        // C
		nextChar++;

and the expression:

A+B*0x(a+b)]C-(

As soon as we encounter '-(' we will insert an extra '-1*(' as in case '-0x' above:

		Val[nextChar]= -1;		// D
		nextChar++;

		A+B*0x(a+b)]C+D*(...

Further there is nothing new, so finally we get:

		A+B*0x(a+b)]C+D*(E*F+G)	// E=V2= -12, F=F1=1, G= -20

Next we will process the expressions in parentheses. The job will do noBracket() subroutine. We pass it a+b.
It will return a

string=”Test c1 completed\r\n” 

and a

value=0  

which is value of the string converted to integer base 10. We put the string in a new work string – this will be 'c'

		A+B*0xc]C+D*(E*F+G)  

The second parentheses passed to noBracket() will return an empty string and value=-12*1-20= -32 which we put in a new Val[nextChar]=-32 named H:

		A+B*0xc]C+D*H  

As we have no more parentheses we may pass the whole expression to noBracket().

Firs it will process strings that is: will get the content of string's 'c'=Test c1 completed\r\n” staring from position B=6 in string which get to “c1 completed\r\n”, try to produce a hexadecimal value from it

		0xc1			// =200 decimal  

put it to a new

I= Val[nextChar]= -200  

and continue with

                A+B*I+D*H

which finally gives:

		1000+(-1)*(200)+(-1)*(-32)=832  

In fact Ex() always produces:
* a string, named 'outS'
* a signed value, named 'result'
both declared in 'globals.h' so they are visible from everywhere in mul. 
Mul decides which part of Ex() output to use.

Interface Units and Pipes

There is a strong indexed list of interfaces for mul that developers must obey. As the list may be extended in time, an actual version is always maintain at mul site. Developers are free to make propositions for new interfaces to be included in list. After discussion in community if approved they be included in list, so everyone can use it.

For example the interface list looks like this:  

type sub-type

 1  Serial           1               UART/RS232      
                     2               UART/RS485   
                     3               SPI Master
                     4               SPI Slave
                     5               I2C Master
                     6               I2C Slave

 2  USB              1               HUB
                     2               Device

 3  BT               1               Master
                     2               Slave

 4  GSM              1               Voice call
                     2               Video call
                     3               CSD
                     4               GPRS
                     5               SMS

 5  WiFi             1               Station
                     2               AP

 6  Stream           1               File
                     2               RAM
                     3               Encrypted RAM
                     4               Audio
                     5               Video

 7  NET              1               Station
                     2               AP  

 8  CAN              1               Master
                     2               Slave  

 9  Device           1               Analog Output
                     2               PWM
                     3               3-phase PWM
                     4               Pulse Coder
                     5               Manchester coder/decoder
                     6               DTMF coder/decoder
                     7               Tone Generator

In their mul program users use indexes for type and sub-type to manage an interface unit. This may seems inconvenient for users than using a mnemonic expression but saves space. Of course a processor should not support all the interfaces in interface list. A help command U{?} will provide all the interface units available to user. To get familiar with any interface unit handle we will look closer to a common Serial interface (type=1, sub-type=1) in mul for ESP8266 WiFi module.

In ProcessorSpecific.h first we have to uncomment row 32:

	#define USE_SERIAL

At row 134 we may see

	#ifdef USE_SERIAL
	void 	initSerial(u8, u8*);
	#define U1	initSerial

and an array of function pointers is filled, according to developer's interfaces choice (row 196):

void(*allInterfacesByType[])(u8, u8*)={0, U1, U2, U3, U4, U5, U6, U7, U8, U9};

The developer have to provide function initSerial() and most obvious place to do this is to collect all Serial-needed functions in a separate file uartESP.c. Take in account that all interfaces init-functions are same parameter format(intf_num, list_of_param's), so mul can open any interface unit same way. 'intf_num' is the index of U, user want to create and 'list_of_param's' is a pointer to string, where initial parameters for concrete interface are given in strict order and format. This format is also provided at mul site. This is essential for the ability a mul program written for one processor to migrate to other without changes. Users have not forced to remember this format, as developer provides a help command for reminder.

parameters=sub_type, uart_number, u16 baud rate/100, bits_parity_stopB , hd_flow_c 

Sub-type in our case will be 1-UART
uart_number=0               // as many processors have several UART ports we have to specify one
baud rate=96                // for 9600. We wold like to stay in 16-bits range.
bits_parity_stopB=8N1	// common case
hd_flow_c=0			// we will not use hardware flow control 

User should use command:

U1{1,1,0,96,8N1,0}

to open Serial port 0 as UART, 9600,8N1, no hardware control as interface Unit1. How to deal with opening UART with this parameters is processor specific. What for mul developer essential is, is that after successful opening of interface a interface structure named 'oneInteface' in 'myInterfaces[ ]' array must be filled in:

struct oneInterface{
    enum intf_state state;					/// all Interfaces state
    u8 type;
    u8 subtype;
    u8 id;
    u8 pipes_attached;
    void(*openPipe)(u8 pipe, u8 intf_num, enum pipe_type, u8*);  /// pointer to void openPipe(pipe, intf_num, u8* params)
    void(*closePipe)(u8);					/// pointer to void closePipe(pipe)
    void(*sendInterface)(u8);				/// pointer to void sendInterface(pipe)
    void(*closeInterface)(u8);				/// pointer to void closeInterface(intf_num)
}volatile myInterfaces[INTERFACES_NUM]={0};		        /// collection of all the processor's interfaces opened

In our case it will look such way:

myInterfaces[intf_num].id=uart_no; // RS Port0 myInterfaces[intf_num].closeInterface =uart_close; // call ex: x=(*uart0_close)(0) myInterfaces[intf_num].sendInterface=uart_send; myInterfaces[intf_num].openPipe=uartAddPipe; myInterfaces[intf_num].closePipe=uartRemovePipe; myInterfaces[intf_num].type=1; myInterfaces[intf_num].subtype=1; myInterfaces[intf_num].state=INTF_OPEN; myInterfaces[intf_num].pipes_attached=0;

As you may see initSerial() defines all the functions involved in interface handle and attaching/ removing Pipes to it. Certainly providing all this functions is developer's responsibility.
User can see and use interface's U1 state using U1 in an expression

!U1                         // print U1 state

Pipes are used close to Interface Units. In order to send and receive data trough an interface mul should use a pipe. You may thing to pipe as couple of strings – one for outgoing and the other for incoming data. Those strings length is MAX_BUFFER defined in ProcessorSpecific.h, but no less than a string length MAX_L$. As each type of interface needs specific pipe, openPipe() function(see interface structure) is different in concrete interface-handle functions. Call to openPipe() is standard for all interfaces as per interface open functions discussed above. Once opened, pipe handle does not differ for different interfaces. In our example opening a pipe to one UART port is quite trivial:

P1{1}			// open Pipe1 to Unit1

and as you see – no parameters needed to open a pipe to UART. Some interfaces however need a parameters queue to be passed in their openPipe() function. For example openPipe() to one Net interface (U2) may look like:

P1{2,C,”TCP”,”192.168.1.10”,1234}

to open a pipe to interface 2 as C(lient), proto “TCP”, IP 192.168.1.10, port 1234. Opening a pipe to one interface includes its index in pipesAttached[ PIPE_COUNT] list which each interface maintains. For a ESP8266 which owns only one UART the list needs only pipe_id:

struct {
    s8 pipe_id;											
}LOCAL pipesUart0[PIPE_COUNT]={-1,0};			// Every UART pipe goes to this array 
// For many ports use pipesAttachedUart[port][PIPE_COUNT]

while a WiFi Net interface fills such a structure for each pipe in its list:

typedef struct {
    s8 pipe_id;			// to which pipe attached
    u8 sIndex;			// index of Server a server-pipe belongs to
    u16 try_count;
    enum pipe_mode	mode;		// Client/Server, UDP/TCP
    u8 hostName[NAME_LENGHT];       // for DNS URL
    ip_addr_t IPfromDNS;
    union{
         esp_tcp tcpIP;
         esp_udp udpIP;
    };
    struct espconn pipe_con;	// user-defined when pipe created
    struct espconn *con_p;		// pointer to current active espconn
} net_pipe;

LOCAL net_pipe *pipesWiFi[PIPE_COUNT]={NULL};  /// Every net pipe goes here

If your processor has sufficiently memory, opening and closing pipes may be done using malloc() and free() memory functions. During opening a pipe next structure have to be filled:

/** Current state of Stream.*/
enum pipe_state {
    PIPE_CLOSE,
    PIPE_READY,
    PIPE_SEND,
    PIPE_RCV,
    PIPE_ERR
};
enum pipe_type{
    TYPE_NONE,
    TYPE_CONSOLE,
    TYPE_DIGITAL,
    TYPE_BUFFER,
    TYPE_FILE,
    TYPE_MAX
};

typedef struct{
    t_Buffer *r;                    /// r=os_malloc() so we will use it in os_free()
    uX* len;                        /// pointer to pipe length
    u8 * val;                       /// pointer to string with data
}p_Buffer;				/// structure pointers to Buffer

struct oneStream{
    enum pipe_type type;
    u8 intf_num;                   /// interface index to which pipe attached
    enum pipe_state state;         /// enum pipe_state
    s16 txVal;                     /// value of tx/rx string or a process result
    s16 rxVal;                     /// value of tx/rx string or a process result
#ifdef USE_STRINGS
    p_Buffer rxPipe;                // pointers to receive value[] and length
    p_Buffer txPipe;               /// pointers to send value[] and length
#endif
} static myStreams[sizeof(RANGE)*8+1] = {0};     /// Array of Pipe's buffers for send/receive +1 for load file

As you may see each interface knows which pipes attached to it (pipe_id), and each pipe knows to which interface belongs (intf_num). If you can't use malloc() and free() you should define an array[t_Buffer] of buffers for Pipes

typedef struct{				     /// Type 't_Buffer' definition
     uX len;
     u8  val[MAX_BUFFER];
}t_Buffer;

and set pointers in mySreams[] to them.

TODO: In current mul version there is no way user to retrieve Pipe's state in order to use it in an expression or to see it.

File system

As a type of interface unit, openFile() is much to same as other interfaces. Pipe attach/release – too. The only difference is that both pipes coincide and point to memory where file opened. We will throw light on FS basic principles so one can convert it to a new processor. FS is situated usually in FLASH memory. A quantum of memory we name PAGE. We can use no less than a PAGE and many PAGEs for a file. Let's start with some constant definitions inProcessorSpecific.h.

    // File System set
    #define roEMPTY	0xff		/// read value of an empty flash cell
    #define NAME_LEN	11	/// file name length, incl. \0
    #define FS_SIZE 	65536	/// file system size max used memory	64k
    #define BLOCK_SIZE	4096	/// min erase size block, HD depend
    #define PAGE_SIZE	256	/// one page size i.e. 256 pages in 64k. No more or pageN: will exit 0xff!
    #define FILE_NUM 	(sizeof(RANGE)*8)	/// max file number if RANGE=u16 =16 -> 0- dir, 1...15- files
    #define FAT_SECTOR	0x10	/// File system start sector
    #define FAT_ADDRESS	0x10000	/// File system start address

u32 me_correct = FAT_ADDRESS % 0x1000;	// integer correction if Start FS Address not at the beginning of a sector (usually 0)

Usually non-volatile memory NAND or NOR adopt 1 for erase state, so roEMPTY=0xff. If this is not the case, change to roEMPTY=0. File name length in our FS is reduced to 10 chars + '\0' (Ex: “Test1.txt” is 10 chars length). FS_SIZE depends on memory you have, but take in account, that in our FS pageN is limited by its type - u8, so it can adopt max. value= 255. BLOCK_SIZE is hardware defined for your memory so look at Datasheet PAGE_SIZE is the quantum unit of size you can manage. It is very natural to set this parameter = string length MAX_L$ as read and write to a file is done using strings. If this parameter is smaller you will gain in memory usage when handle with small files(20-100 bytes). In this case you have to provide sufficiently gap of pages after open a file, so a write operation of a string to not overflow the memory your file occupy. Lowest page size leads to more often file-re-size(see below) when you work with file.

In FLASH, FS spreads such way:

    FAT_TABLE			// for example 1 Page length=256 bytes, Page0
    FAT_MAP				// for example 2 Page length=256 bytes, Page1
    PAGE_2
    PAGE_3
    .......
    PAGE_LAST                       // Page 255

In initFS() FAT_TABLE and FAT_MAP are copied from FLASH to RAM, so we have a quick access to them. FATtable in fact is an array of structures, one per each file(0..15) in FS or empty if no file with this index.

  typedef struct {					// KEEP THIS 4 byte aligned!
       u8 name[NAME_LEN];				// 11
       u16 length;
       u16 checksum;
       struct{					// TODO
            u8 state : 4;
            u8 flags : 4;
       }attr;
  }t_file;						// size of 16 bytes

static t_file FATtable[FILE_NUM] = { roEMPTY };	/// if RANGE u16 => max 15 files

FATmap is an array of u8. Each element index corresponds to page number it present and = index of file which owns this page .

static u8	 FATmap[FAT_MAP_LEN] = { roEMPTY };/// represents file pages used in ROM

As sounds too complicate see the next Example:

Suppose we write to FS next files: “Text2.txt”, “Text1.txt”, “main.ml”

Then our FATtable will look like:

main.ml...,0120,F0EC,0000,Text2.txt..,1024,CC12,0000,Text1.txt..,728,0C01,0000,FF,FF,FF,FF,...FF

As you see, “main.ml” always resides at index 0, other files – in order they appear. Each file's length and check sum are just for illustration.

Our FATmap will show where over pages each file is situated:

0xfd, 0xfd, 1,  1,  2,  1,  2,  2,  0,  1, 0xff, 0xff, 0xff,.....
 pg0   pg1 pg2 pg3 pg4 pg5 pg6 pg7 pg8 pg9 pg10 ...

You may see that Page0 and Page1 are marked as 0xfd=(roEMPTY-2) – this was done in initFS() and we know that those pages are reserved for FATtable and FATmap, so they are not empty (0xff or 0x00) nor belong to a file (0...15). Take in account that roEMPTY-2 works as if roEMPTY=0xff and also if roEMPTY=0x00. Then we see that

file 1 (“Text2.txt”) starts in page 2, then page 3, page 5 and finally ends in page 9.
file 2 (“Text1.txt”) starts in page 4, then 6 and 7
file 0 (“main.ml”) is in page 8
all other pages are empty (0xff)

When you open a file a RAM memory pool malloc(size of file + 2 Pages extra buffer) is reserved , a file check sum is calculated and compared against CheckSum from FAT table and if equals, file is copied from FLASH to RAM pool. Original pointers from malloc() are stored in 'ping' pointer of

// All the files have to be resized dynamic
struct{
    u8* ping;     /// just one active instant, switches to other when file resized
    u8* pong;
}static fileMountAddress[INTERFACES_NUM] = {0};						
/// fill in when a file open or create to direct the Pipes to it

static u16	fileSize[INTERFACES_NUM] = { 0 };/// holds current malloc size of a file

defined in globals.h so everywhere in mul program you may free() memory a file occupies. When you use writing to or removing from the file's content, its size may become critical for next write operation to spread outside memory boundaries reserved for file or to memory-waste if you delete most of it content. Every mul cycle a program, called fileResize() is called. It decides if a file memory block needs resizing and opens a new pool for file as 'pong' pointer. If operation successful it sets 'ping' to NULL, so everyone will know that should use 'pong' for file handle. Next time when the file needs re-size, roles will change.
When closing a file a CheckSum is calculated for check upon next file opening, then file is stored in FLASH looking for first available free page and its RAM buffer released.
When an empty file forced to close it will be removed from FS, FATtable and FATmap.

TODO:
1. Check sum currently uses simple sum. You may wont to use CRC instead.
2. File attributes and flags not used as mul do not need them. Those are attributes as Read-only, Write-only, and all others.