Right. I figured some things out and got it to work.
The guiding principles behind this experiment
- no RAM usage
- smallest code
- predicatable execution. no wiggle room for the optimizers.
- no libraries that call call call.
- symbolic names
- everything hardcoded. no mathematical acrobatics at runtime. compile time is fine
- no bleeping pointers or pointer arithmetic.
- no room for user to things like writing to nonvolatile or loading data of wrong type in system.
- things like switch case , == and != need to work correctly
Think about a register that consists of 8 flipflops.
in an FPGA i can write REGISTER[7:6] = 2'b01 and this will write those two flipflops in one clocktick.
The equivalent code in a CPU
becomes
MOV B Register // get contents of register
AND B 0b0011_1111 // msk off 2 top bits
MOV A 0b0000_0001 // load 01 in accumulator
SHL A,6 // shift accumaltor left 6 positions
OR A,B or b and accumulator
MOV Register A // send it back into 'REGISTER'
this is sheer insanity. just to twiddle some bits ... oodles of code, moves , or register usages.
The functions i need are
- test a bit for true
- test a bit for false
- set a bit
- clear a bit
- toggle a bit
- check a subfield for a value from a predefined list , kind of like an enumerated list , allowing usage of standard flow control like 'switch' , if == / != operators
- set a subfield to a value of a predefined list.
I want to approach this as close as possible to what is happening in an FPGA
- compiler should be able to resolve most arithmetic at compile time as everything is constant based.
- shortest possible code.
- no ram usage
- inline compilation so no calls, push pop and other overhead
- interrupt safe. no risk of context switch loss. no usage of stack to preserve intermediates.
Why all of this ? well , to create 'virtual devices' in a microcontroller.
Think of a peripheral like an I2C or SPI chip.
Those things are essentially number of registers with data in them. some registers pack multiple fields of varying size.
To code these kind of things it is nice to have these registers as a continuous block of memory so you can access it using a read and write pointer from the transport handler routines.
The incoming commands tell you what byte to write and what the byte is. nothing more , nothing less. short and good.
To code the logic that forms the devices it would be nice to have symbolic access to the individual chunks without every time having to write all the bit-twiddling code.
consider a virtual device with a memory map like this :
0x00 : CONFIG [GLOBAL_INT],-,-,-,-,[ADC_INT],[GP2INT][GP1INT]
0x01 : TEMPERATURE [d7..d0]
0x02 : GPCONF1 -,-,-,INTMODE1,INTMODE0,PULLUP_ENABLE,PINMODE0,PINMODE1
0x03 : GPCONF2 -,-,-,INTMODE1,INTMODE0,PULLUP_ENABLE,PINMODE0,PINMODE1
0x04 : GPDATA1 8 bits
0x05 : GPDATA2 8 bits
the config bits are flags. If a flag is SET then that device will generate an interrupt_on_change
For example :
if GP2INT is set , and the pinmode is input , and intpmode it is set to rising edge then an interuupt will be generated if that pin goes from low to high.
possible modes for INMODE
- none
- rising
- falling
- both
pinmode :
digital input -> state is stored in GPDATAx as 0x00 or 0xFF
analog input -> adc result is stored in GPDATAx as a number between 0x00 and 0xff
digital output -> pin is follows bit0 of GPDATAx
PWM output -> pin has duty cycle set by GPDATAx
all in all something that is easily cobbled up in an FPGA , but in a microcontroller becomes spaghetticode unless you have symbolic access to the 'flipflops'
SO, here we go:
First, lets make our registerbank so the transport routines can read and write based on a pointer. The simplest construction : an array of unsigned chars.
unsigned char REGISTERSET[6]
So far so good. We have allocated all the ram we ever need.
Let's create some symbolic names so we can easily access these things from the logic side.
unsigned char REGISTERSET[5];
#define CONFIG REGISTERSET[0]
#define TEMPERATURE REGISTERSET[1]
#define GPCONF1 REGISTERSET[2]
#define GPCONF2 REGISTERSET[3]
#define GPDATA1 REGISTERSET[4]
#define GPDATA2 REGISTERSET[5]
let's set up the interrupt flag mapping.
#define INTENABLE_MASK 0b10000000 // bit 7 of CONFIG register
#define GP1INT_MASK 0b00000001 // bit 0 of CONFIG register
#define GP2INT_MASK 0b00000010 // bit 1 of CONFIG register
#define ADCINT_MASK 0b00000100 // bit 2 of CONFIG register
and the mapping for the INTmode and PINMODE of the GPCONFx registers.
#define PINMODE_MASK 0b000000011
#define PULLUP_MASK 0b000000100
#define INTMODE_MASK 0b000011000
Next let's define the possible states INTMODE and PINMODE and pullup
#define PINMODE_IN_ANALOG 0b00000000
#define PINMODE_IN_DIGITAL 0b00000001
#define PINMODE_OUT_PUSHPULL 0b00000010
#define PINMODE_OUT_PWM 0b00000011
#define PINMODE_PULLUP 0b00000100
#define INTMODE_NONE 0b00000000
#define INTMODE_RISING 0b00001000
#define INTMODE_FALLING 0b00010000
#define INTMODE_BOTH 0b00011000
Note that the above are NOT masks, they are the settings of possible fields, aligned to their mask. This is done intentionally as i don't want any risk of the compiler injecting unnecessary >> and << operations. the fields are pre-aligned.
now we need some code to 'extract' the individual fields.
// reading
#define GPCONF1_PINMODE (GPCONF1 & PINMODE_MASK)
#define GPCONF1_INTMODE (GPCONF1 & INTMODE_MASK)
#define GPCONF2_PINMODE (GPCONF2 & PINMODE_MASK)
#define GPCONF2_INTMODE (GPCONF2 & INTMODE_MASK)
// writing
#define GPCONF1_INTMODE_SET (GPCONF1 & ~INTMODE_MASK)|=
#define GPCONF2_INTMODE_SET (GPCONF2 & ~INTMODE_MASK)|=
#define GPCONF1_PINMODE_SET (GPCONF1 & ~INTMODE_MASK)|=
#define GPCONF2_PINMODE_SET (GPCONF2 & ~INTMODE_MASK)|=
and some for the individual bits
#define GP1INT_SET FLAGS |= GP1INT_MASK
#define GP1INT_ISSET (FLAGS & GP1INT_MASK)!=0
#define GP1INT_CLEAR FLAGS &= ~GP1INT_MASK
#define GP1INT_ISCLEAR (FLAGS & GP1INT_MASK)==0
#define GP1INT_TOGGLE FLAGS ^= GP1INT_MASK
#define GP2INT_SET FLAGS |= GP1INT_MASK
#define GP2INT_ISSET (FLAGS & GP1INT_MASK)!=0
#define GP2INT_CLEAR FLAGS &= ~GP1INT_MASK
#define GP2INT_ISCLEAR (FLAGS & GP1INT_MASK)==0
#define GP2INT_TOGGLE FLAGS ^= GP2INT_MASK
#define ADCINT_SET FLAGS |= GP1INT_MASK
#define ADCINT_ISSET (FLAGS & GP1INT_MASK)!=0
#define ADCINT_CLEAR FLAGS &= ~GP1INT_MASK
#define ADCINT_ISCLEAR (FLAGS & GP1INT_MASK)==0
#define ADCINT_TOGGLE FLAGS ^= GP2INT_MASK
#define INTENABLE_SET FLAGS |= GP1INT_MASK
#define INTENABLE_ISSET (FLAGS & GP1INT_MASK)!=0
#define INTENABLE_CLEAR FLAGS &= ~GP1INT_MASK
#define INTENABLE_ISCLEAR (FLAGS & GP1INT_MASK)==0
#define INTENABLE_TOGGLE FLAGS ^= GP2INT_MASK
This seems like a lot of work to build, but it is easily creatable using a script that reads a definition table.
using a simply notation like
// MEMMAPPER
// REG 0, CONFIG [7] INTENABLE, [6:3] - , [2] ADC_INT , [1] GP2INT , [0] GP1INT
// REG 2, GPCONF1[7:5] - , [4:3] INTMODE , [2], PULLUP , [1:0] PINMODE
// REG 3, GPCONF2[7:5] - , [4:3] INTMODE , [2], PULLUP , [1:0] PINMODE
// MODE INTMODE {NONE:00, RISING:01,FALLING:10,BOTH:11}
// MODE PINMODE {IN_ANALOG:00, IN_DIGITAL:01,OUT_PUSHPULL:10, OUT_PWM:11}
// TARGET
#include "registermap.inc"
i can have a simple script that scans my source files for the magic keyword 'MEMMAPPER' then parses the comment lines until it hits the keyword TARGET , and spits the data into the filename given in the include.
all the #define stuff is automatically generated from a simple notation syntax.
But why you ask. Well . becasue now i can write the logic for this virtual devices using symbolic names and not have to bother if i got the mapping of the fileds right, the correct shift offsets , i dont need ot dig in header files to see what is what as autocomplete will suggest me possible settings.
An example :
GP1INT_SET; // set the GP1INT flag true
GP1INT_CLEAR; // clear the GP1INT flag
if (GP1INT_ISCLEAR) { // if GP1int is clear
go_do_this;
}
if (GP1INT_ISSET) { // if set
go_do_that;
}
switch (GPCONF1_PINMODE)
{
case PINMODE_IN_ANALOG : GPDATA1= ADCread; break;
case PINMODE_IN_DIGITAL : GPDATA1 = porta.1; break;
case PINMODE_OUT_PUSHPULL : PORTa.mode = pushpull; PORTa.1 = GPDATA1; break;
case PINMODE_OUT_PWM : PORTa.mode = MODE_pmwy; PORTaPWMvalue = GPDATA1; break;
};
....
and so on
This the process of coding the control logic very simple. If in the future the memory map changes ( i allocate the individual fields and registers differently ) my logic still keeps working correctly !