XC8: .asm to C ( i.e. Lowering One's Expectations)

nsaspook

Joined Aug 27, 2009
16,334
Isn't DMA interrupt based though? How could spinning a tight "delay" loop inhibit DMA?
There are interrupt triggers at the completion of DMA events. During DMA transfers no interrupts should happen if properly coded. Spinning a tight "delay" loop doesn't inhibit DMA, it inhibits mainline code processing that could be processing data for the next DMA transfer at a completion interrupt.
 

Thread Starter

joeyd999

Joined Jun 6, 2011
6,331
Maybe, but those CODE tags are usually pretty strict... But I'll leave it there, you have your hands full
In my initial post, one of the strings is a char short. The error happened during editing of the post, but my c code is correct. Good catch, though.
 

ApacheKid

Joined Jan 12, 2015
1,762
There are interrupt triggers at the completion of DMA events. During DMA transfers no interrupts should happen if properly coded. Spinning a tight "delay" loop doesn't inhibit DMA, it inhibits mainline code processing that could be processing data for the next DMA transfer at a completion interrupt.
OK I see now, thanks.

So isn't there a potential to add a true sleep capability here? Is the cooperative code layer/manager, home grown or part of some formal library?
 

Thread Starter

joeyd999

Joined Jun 6, 2011
6,331
Is the cooperative code layer/manager, home grown or part of some formal library?
All my stuff is home-grown. Typically each module does what it can quickly and then releases control to the next module in round-robbin fashion.

Modules have their own state-machines to keep track of work that needs to be done.

Idle modules just return immediately, consuming only as many instructions as needed for a call, a test, and a return.
 

nsaspook

Joined Aug 27, 2009
16,334
OK I see now, thanks.

So isn't there a potential to add a true sleep capability here? Is the cooperative code layer/manager, home grown or part of some formal library?
1668020434420.png

In my example code (for PIC32) there is no cooperative code layer/manager because the processor has the hardware to easily handle concurrent DMA/Interrupt processes efficiently with just a few checks for concurrency overlaps if designed correctly. It's not as general as cooperative code layer/manager.

For some PIC18 8-bit processors with DMA the process is similar when using interrupt triggers and DMA concurrency but because of hardware limitations, some management of resources is needed for real-time processing
https://forum.allaboutcircuits.com/threads/pic18-q43-ntsc-b-w-video-signal-demo.175611/post-1586729
The remaining problem is dynamic updates to the video memory. All of the memory bandwidth (something in short supply with the simple DMA architectures seen in most 8-bit controllers) is being used by DMA so excessive processing during DMA will affect H/V timing causing tearing and loss of sync. The fix for that is to only process mainline code during the scanlines with no video around the H sync pulse. For that we need a simple task manager that uses the high/low priority interrupt structure and a 'hold' flag.

Setup a Low priority timer ISR as the 'idle' loop; This idle loop can be interrupted by High priority DMA completion and other module interrupt requests so those time critical processes get the cpu they need during the NTSC FSM. The 'idle' loop can also set a task time duration for the main task that will interrupt the main task back to the low priority idle loop during the critical timing periods. This means I can step into the main application code execution when I want it to run it and control how long I want one processing time slice to be.
https://raw.githubusercontent.com/nsaspook/vtouch_v2/eadogs/q43_board/q43_ntsc.X/ntsc.c
 
Last edited:

ApacheKid

Joined Jan 12, 2015
1,762
Very interesting. I can envisage a pattern where we dedicate a timer to running timer requests that we add to some list. The timer request includes a callback and a context structure pointer. The callback is in fact just the currently executing function.

Here's the pseudo code:

Code:
void system_initialization (int state, Context * context_ptr)
{

   if (state = FIRST_TIME) // called explicitly by "us"
   {
       setup_hardware_etc();
       Sleep(100, system_initialization, SECOND_TIME, context_ptr);
       return;
   }
   if (state = SECOND_TIME) // called back by timer
   {
       do_remainig_steps(context_ptr);
       return;
   }
}
The state could actually be part of the context as could the callback pointer too, but these are details. But also this is the thin edge of a wedge, one could end up gradually building a small OS which might be good but might be bad.
 
Last edited:

Thread Starter

joeyd999

Joined Jun 6, 2011
6,331
Very interesting. I can envisage a pattern where we dedicate a timer to running timer requests that we add to some list. The timer request includes a callback and a context structure pointer. The callback is in fact just the currently executing function.

Here's the pseudo code:

Code:
void system_initialization (int state, Context * context_ptr)
{

   if (state = FIRST_TIME) // called explicitly by "us"
   {
       setup_hardware_etc();
       Sleep(100, system_initialization, SECOND_TIME, context_ptr);
       return;
   }
   if (state = SECOND_TIME) // called back by timer
   {
       do_remainig_steps(context_ptr);
       return;
   }
}
The state could actually be part of the context as could the callback pointer too, but these are details. But also this is the thin edge of a wedge, one could end up gradually building a small OS which might be good but might be bad.
You're overthinking it. This might work for a simple system, where your "basic" OS is cognitive of all the various tasks that need to run (and when -- which itself is not a simple problem!).

Instead, each independent module (for example, an LCD driver), runs independently of all the other modules (and the "OS"). The main program loop just calls a polling routine for each module. The modules themselves decide if they have work to do or not. If not, they return. If so, they do their work quickly and return. Any unfinished business (or if a blocking resource becomes available in the interim) is performed during the next loop. It's really easy, fast, and efficient.

The only downside (which is not really a downside once you get good at it) is ensuring that no process consumes so much time that it starves other processes of the instruction cycles they need.
 

xox

Joined Sep 8, 2017
936
You're overthinking it. This might work for a simple system, where your "basic" OS is cognitive of all the various tasks that need to run (and when -- which itself is not a simple problem!).

Instead, each independent module (for example, an LCD driver), runs independently of all the other modules (and the "OS"). The main program loop just calls a polling routine for each module. The modules themselves decide if they have work to do or not. If not, they return. If so, they do their work quickly and return. Any unfinished business (or if a blocking resource becomes available in the interim) is performed during the next loop. It's really easy, fast, and efficient.

The only downside (which is not really a downside once you get good at it) is ensuring that no process consumes so much time that it starves other processes of the instruction cycles they need.
So basically, the "cooperative approach". Granted, it takes a bit of discipline to ensure that starvation never arises, but at the end of the day it does seem to be the cleanest method.

What about attaching these to interrupts rather than polling? For efficiency's sake, I mean. Wouldn't the application run even more smoothly? Or would the fact that you'd have to save the state of all the registers and whatnot nullify the benefits gained by switching to interrupts?
 

ApacheKid

Joined Jan 12, 2015
1,762
You're overthinking it. This might work for a simple system, where your "basic" OS is cognitive of all the various tasks that need to run (and when -- which itself is not a simple problem!).

Instead, each independent module (for example, an LCD driver), runs independently of all the other modules (and the "OS"). The main program loop just calls a polling routine for each module. The modules themselves decide if they have work to do or not. If not, they return. If so, they do their work quickly and return. Any unfinished business (or if a blocking resource becomes available in the interim) is performed during the next loop. It's really easy, fast, and efficient.

The only downside (which is not really a downside once you get good at it) is ensuring that no process consumes so much time that it starves other processes of the instruction cycles they need.
Well just to be clear, in that example there is no OS, that pseudo code could run on anything, the function is cooperative, it can "give up the CPU" by either returning or by calling Sleep and then returning. It could just as easily pass a different function into the "Sleep" method:

Code:
void system_initialization_stage_1 ()
{
    setup_hardware_etc();
    Sleep(100, system_initialization_stage_2);
}

void system_initialization_stage_2 ()
{
    do_remainig_init_steps();
}
One doesn't even need a context but that's there so the system_initialization_stage_1 function can "pass" arguments into system_initialization_stage_2 when it gets called later.

This simple pattern gives you a general purpose, genuine sleep capability that can be used anywhere and never ever spins on the processor.

Now, ideally, we'd prefer the code that runs when the timer expires to be the next statement after that Sleep, but the language has no such facility to make that easy, hence the small if/then (which can be a switch of course).
 

ApacheKid

Joined Jan 12, 2015
1,762
This suggest that a new hypothetical language could support multiple entry points, that is a language could let us write:

Code:
void my_init_code()
{

   do_phase_1_init();
   Sleep(100,resume); // execution will stop then continue at the resume label in 100 mS.
   return;

resume:

   do_phase_2_init();
}
In fact I recall now that PL/I did in fact support that feature it was called an "entry statement", very much like a goto label but with additional keyword.

Hmm, seems even C might have once envisaged this but it never did, seems K&R knew of it and considered it in some way, but alas it was not to be...
 
Last edited:

nsaspook

Joined Aug 27, 2009
16,334
Very interesting. I can envisage a pattern where we dedicate a timer to running timer requests that we add to some list. The timer request includes a callback and a context structure pointer. The callback is in fact just the currently executing function.

Here's the pseudo code:

Code:
void system_initialization (int state, Context * context_ptr)
{

   if (state = FIRST_TIME) // called explicitly by "us"
   {
       setup_hardware_etc();
       Sleep(100, system_initialization, SECOND_TIME, context_ptr);
       return;
   }
   if (state = SECOND_TIME) // called back by timer
   {
       do_remainig_steps(context_ptr);
       return;
   }
}
The state could actually be part of the context as could the callback pointer too, but these are details. But also this is the thin edge of a wedge, one could end up gradually building a small OS which might be good but might be bad.
I usually avoid actual task context switches to emulate multi-tasking on simple projects as it can be a time-bomb for hard to find bugs.

A polled loop task state machine can be an alternative for things like communications protocol data exchanges.

A simple PIC18 XC8 MODBUS master for a solar charge controller. Yes, it has a few blocking waits for each serial bytes to complete but it's a single thread, one function, sequential step device designed just for the MODBUS protocol.
1668031274205.png

C:
void main(void)
{
    init_ihcmon();
    /* Loop forever */
    while (TRUE) { // busy work
        controller_work();
    }
}

#define BusyUSART( ) (!TXSTAbits.TRMT)

int8_t controller_work(void)
{
    static uint8_t mcmd = G_MODE;

    switch (cstate) {
    case CLEAR:
        clear_2hz();
        cstate = INIT;
        modbus_command = mcmd++;
        if (mcmd > G_LAST)
            mcmd = G_MODE;
        /*
         * command specific tx buffer setup
         */
        switch (modbus_command) {
        case G_ERROR: // error code request
            req_length = modbus_rtu_send_msg((void*) cc_buffer, (const void *) modbus_cc_error, sizeof(modbus_cc_error));
            break;
        case G_MODE: // operating mode request
        default:
            req_length = modbus_rtu_send_msg((void*) cc_buffer, (const void *) modbus_cc_mode, sizeof(modbus_cc_mode));
            break;
        }
        break;
    case INIT:
        if (get_2hz(FALSE) > QDELAY) {
#ifdef LOCAL_ECHO
            RE_ = 0; // keep receiver active
#else
            RE_ = 1; // shutdown receiver
#endif
            DE = 1;
            V.send_count = 0;
            V.recv_count = 0;
            cstate = SEND;
            clear_500hz();
        }
        break;
    case SEND:
        if (get_500hz(FALSE) > TDELAY) {
            do {
                while (BusyUSART()); // wait for each byte
                TXREG = cc_buffer[V.send_count];
            } while (++V.send_count < req_length);
            while (BusyUSART()); // wait for the last byte
            cstate = RECV;
            clear_500hz();
        }
        break;
    case RECV:
        if (get_500hz(FALSE) > TDELAY) {
            uint16_t c_crc, c_crc_rec;

            DE = 0;
            RE_ = 0;

            /*
             * check received response data for size and format for each command sent
             */
            switch (modbus_command) {
            case G_ERROR: // check for controller error codes
//
                    cstate = CLEAR;
                } else {
                    if (get_500hz(FALSE) > RDELAY) {
                        cstate = CLEAR;
                        RE20A_ERROR = OFF;
                        mcmd = G_MODE;
                    }
                }
                break;
            case G_MODE: // check for current operating mode
            default:
//
                    cstate = CLEAR;
                } else {
                    if (get_500hz(FALSE) > RDELAY) {
                        set_led_blink(BOFF);
                        cstate = CLEAR;
                        V.pwm_volts = CC_OFFLINE;
                        SetDCPWM1(V.pwm_volts);
                        mcmd = G_MODE;
                    }
                }
            }
        }
        break;
    default:
        break;
    }
    return 0;
}
https://raw.githubusercontent.com/nsaspook/ihc_mon/re20a/crc.c

 

Thread Starter

joeyd999

Joined Jun 6, 2011
6,331
Well just to be clear, in that example there is no OS, that pseudo code could run on anything, the function is cooperative, it can "give up the CPU" by either returning or by calling Sleep and then returning. It could just as easily pass a different function into the "Sleep" method:
In my case, "power" is just another "device", and has it's own driver.

The power module brings the system up and down, monitors input power (like battery levels, presence of A/C, etc.), and puts the processor to sleep when possible (a few global flags determine whether or not to sleep, and multiple devices can wake the processor via interrupts or timers).
 

ApacheKid

Joined Jan 12, 2015
1,762
I usually avoid actual task context switches to emulate multi-tasking on simple projects as it can be a time-bomb for hard to find bugs.

A polled loop task state machine can be an alternative for things like communications protocol data exchanges.

A simple PIC18 XC8 MODBUS master for a solar charge controller. Yes, it has a few blocking waits for each serial bytes to complete but it's a single thread, one function, sequential step device designed just for the MODBUS protocol.
View attachment 280286

C:
void main(void)
{
    init_ihcmon();
    /* Loop forever */
    while (TRUE) { // busy work
        controller_work();
    }
}

#define BusyUSART( ) (!TXSTAbits.TRMT)

int8_t controller_work(void)
{
    static uint8_t mcmd = G_MODE;

    switch (cstate) {
    case CLEAR:
        clear_2hz();
        cstate = INIT;
        modbus_command = mcmd++;
        if (mcmd > G_LAST)
            mcmd = G_MODE;
        /*
         * command specific tx buffer setup
         */
        switch (modbus_command) {
        case G_ERROR: // error code request
            req_length = modbus_rtu_send_msg((void*) cc_buffer, (const void *) modbus_cc_error, sizeof(modbus_cc_error));
            break;
        case G_MODE: // operating mode request
        default:
            req_length = modbus_rtu_send_msg((void*) cc_buffer, (const void *) modbus_cc_mode, sizeof(modbus_cc_mode));
            break;
        }
        break;
    case INIT:
        if (get_2hz(FALSE) > QDELAY) {
#ifdef LOCAL_ECHO
            RE_ = 0; // keep receiver active
#else
            RE_ = 1; // shutdown receiver
#endif
            DE = 1;
            V.send_count = 0;
            V.recv_count = 0;
            cstate = SEND;
            clear_500hz();
        }
        break;
    case SEND:
        if (get_500hz(FALSE) > TDELAY) {
            do {
                while (BusyUSART()); // wait for each byte
                TXREG = cc_buffer[V.send_count];
            } while (++V.send_count < req_length);
            while (BusyUSART()); // wait for the last byte
            cstate = RECV;
            clear_500hz();
        }
        break;
    case RECV:
        if (get_500hz(FALSE) > TDELAY) {
            uint16_t c_crc, c_crc_rec;

            DE = 0;
            RE_ = 0;

            /*
             * check received response data for size and format for each command sent
             */
            switch (modbus_command) {
            case G_ERROR: // check for controller error codes
//
                    cstate = CLEAR;
                } else {
                    if (get_500hz(FALSE) > RDELAY) {
                        cstate = CLEAR;
                        RE20A_ERROR = OFF;
                        mcmd = G_MODE;
                    }
                }
                break;
            case G_MODE: // check for current operating mode
            default:
//
                    cstate = CLEAR;
                } else {
                    if (get_500hz(FALSE) > RDELAY) {
                        set_led_blink(BOFF);
                        cstate = CLEAR;
                        V.pwm_volts = CC_OFFLINE;
                        SetDCPWM1(V.pwm_volts);
                        mcmd = G_MODE;
                    }
                }
            }
        }
        break;
    default:
        break;
    }
    return 0;
}
https://raw.githubusercontent.com/nsaspook/ihc_mon/re20a/crc.c

I agree here, ultimately a well defined state machine more less does what's needed here.
 

Thread Starter

joeyd999

Joined Jun 6, 2011
6,331
(Previous post cont'd):

My goal is to provide extreme abstraction between the various modules, to the extent that they are truly independent of each other and literally have no knowledge that they exist or are operating (or not). Yet they fully cooperate in a sane way once plugged into the basic framework.
 

xox

Joined Sep 8, 2017
936
I usually avoid actual task context switches to emulate multi-tasking on simple projects as it can be a time-bomb for hard to find bugs.

A polled loop task state machine can be an alternative for things like communications protocol data exchanges.

A simple PIC18 XC8 MODBUS master for a solar charge controller. Yes, it has a few blocking waits for each serial bytes to complete but it's a single thread, one function, sequential step device designed just for the MODBUS protocol.
View attachment 280286

C:
void main(void)
{
    init_ihcmon();
    /* Loop forever */
    while (TRUE) { // busy work
        controller_work();
    }
}

#define BusyUSART( ) (!TXSTAbits.TRMT)

int8_t controller_work(void)
{
    static uint8_t mcmd = G_MODE;

    switch (cstate) {
    case CLEAR:
        clear_2hz();
        cstate = INIT;
        modbus_command = mcmd++;
        if (mcmd > G_LAST)
            mcmd = G_MODE;
        /*
         * command specific tx buffer setup
         */
        switch (modbus_command) {
        case G_ERROR: // error code request
            req_length = modbus_rtu_send_msg((void*) cc_buffer, (const void *) modbus_cc_error, sizeof(modbus_cc_error));
            break;
        case G_MODE: // operating mode request
        default:
            req_length = modbus_rtu_send_msg((void*) cc_buffer, (const void *) modbus_cc_mode, sizeof(modbus_cc_mode));
            break;
        }
        break;
    case INIT:
        if (get_2hz(FALSE) > QDELAY) {
#ifdef LOCAL_ECHO
            RE_ = 0; // keep receiver active
#else
            RE_ = 1; // shutdown receiver
#endif
            DE = 1;
            V.send_count = 0;
            V.recv_count = 0;
            cstate = SEND;
            clear_500hz();
        }
        break;
    case SEND:
        if (get_500hz(FALSE) > TDELAY) {
            do {
                while (BusyUSART()); // wait for each byte
                TXREG = cc_buffer[V.send_count];
            } while (++V.send_count < req_length);
            while (BusyUSART()); // wait for the last byte
            cstate = RECV;
            clear_500hz();
        }
        break;
    case RECV:
        if (get_500hz(FALSE) > TDELAY) {
            uint16_t c_crc, c_crc_rec;

            DE = 0;
            RE_ = 0;

            /*
             * check received response data for size and format for each command sent
             */
            switch (modbus_command) {
            case G_ERROR: // check for controller error codes
//
                    cstate = CLEAR;
                } else {
                    if (get_500hz(FALSE) > RDELAY) {
                        cstate = CLEAR;
                        RE20A_ERROR = OFF;
                        mcmd = G_MODE;
                    }
                }
                break;
            case G_MODE: // check for current operating mode
            default:
//
                    cstate = CLEAR;
                } else {
                    if (get_500hz(FALSE) > RDELAY) {
                        set_led_blink(BOFF);
                        cstate = CLEAR;
                        V.pwm_volts = CC_OFFLINE;
                        SetDCPWM1(V.pwm_volts);
                        mcmd = G_MODE;
                    }
                }
            }
        }
        break;
    default:
        break;
    }
    return 0;
}
https://raw.githubusercontent.com/nsaspook/ihc_mon/re20a/crc.c

Nice, so it's all governed by a finite state machine. I like the design!
 

ApacheKid

Joined Jan 12, 2015
1,762
(Previous post cont'd):

My goal is to provide extreme abstraction between the various modules, to the extent that they are truly independent of each other and literally have no knowledge that they exist or are operating (or not). Yet they fully cooperate in a sane way once plugged into the basic framework.
Well if it works, its valuable! It's an interesting exercise too to try and define precisely what is meant by "truly independent of each other" I guess it means that the execution path of each of them is never ever influenced by the execution of the others. That will likely extend to any hardware that they might share.

Having them totally "unaware" of each other is indeed a very good design point.
 
Top