Isn't DMA interrupt based though? How could spinning a tight "delay" loop inhibit DMA?No it's not meaningless where the controller has modules that don't need cpu because it has real hardware concurrency with things like DMA.
Isn't DMA interrupt based though? How could spinning a tight "delay" loop inhibit DMA?No it's not meaningless where the controller has modules that don't need cpu because it has real hardware concurrency with things like DMA.
It's a code safety check. 99.999999999999% of the time there will be no wait but not to check is bad safety code design.???C:... while (dstate != D_idle) { }; wait_lcd_done(); ...
OK that makes perfect sense then, thanks.My code runs multiple threads via OS-less cooperative multitasking. Any wait loops consume instruction cycles that can used elsewhere.
Edit: changed "preemptive" to "cooperative".
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.Isn't DMA interrupt based though? How could spinning a tight "delay" loop inhibit DMA?
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.Maybe, but those CODE tags are usually pretty strict... But I'll leave it there, you have your hands full
OK I see now, thanks.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.
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.Is the cooperative code layer/manager, home grown or part of some formal library?
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?

https://raw.githubusercontent.com/nsaspook/vtouch_v2/eadogs/q43_board/q43_ntsc.X/ntsc.cThe 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.
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;
}
}
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!).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:
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.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; } }
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.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: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.
void system_initialization_stage_1 ()
{
setup_hardware_etc();
Sleep(100, system_initialization_stage_2);
}
void system_initialization_stage_2 ()
{
do_remainig_init_steps();
}
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();
}
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.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:
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.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; } }

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;
}
In my case, "power" is just another "device", and has it's own driver.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:
I agree here, ultimately a well defined state machine more less does what's needed here.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
https://raw.githubusercontent.com/nsaspook/ihc_mon/re20a/crc.cC: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; }
Nice, so it's all governed by a finite state machine. I like the design!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
https://raw.githubusercontent.com/nsaspook/ihc_mon/re20a/crc.cC: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; }
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.(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.