Software architecture using C for mid-range PIC.

Hi all,

I am new to this group but checking over recent postings seems to indicate a fair amount of Clue kicking around [1].

A bit of background before y'all tell me to RTFM or JFGI.

I've got quite a lot of experience in programming 8 bit microcontrollers with assembler; I've also done quite a lot of 32 bit embedded stuff using an RTOS with C. What's new to me is using a 16 bit mid-range PIC18 device in C.

So I'm trying to make sure that I come up with a fairly sensible architecture. Googling isn't much help - most of the "tutorial" stuff out there is aimed at a very low level, or else you find discussion at the nuts and bolts level of a particular interrupt handler.

The application is probably fairly typical - inputs / commands come in via the UART, peripheral devices need to be turned on and off in response, other devices need to be queried via I2C for data, etc.

My current thinking is to have a main() loop that is subdivided into separate sections that deal with each peripheral in turn.

Each code block will be non-blocking, and will implement a state machine for each peripheral. Incoming comms will run off interrupts. In RTOS terms, a sort-of round-robin system.

If, for example, incoming commands are ASCII sequences like "L11" for "LED 1 on" followed by an end-of-message checksum byte, in pseudo-code, something like this:

uart_isr() { // push chars into circular buffer push_char_to_uart_buf() if (char == end_of_message) message_flag = 1; }

main_loop { if (message_flag) { move_chars_from_uart_buf_to_command_buf() switch (command) { case foo: foo_state = 1; case bar: bar_state = 1; } } // foo machine if (foo_state) { switch (foo_state) { case 1: if (foo1()) foo_state = 2; case 2: if (foo2()) foo_state = 0; } // bar machine if (bar_state) { switch (bar_state) { case 1: if (bar1()) bar_state = 2; case 2: if (bar2()) bar_state = 0; } }

Does that make sense? Should the ISR do as little as possible or is it a good idea to give it some "awareness"?

The above pseudo-code is just meant to give an idea of where I'm heading, so don't take it too seriously. I'm really looking for general pointers on how to approach a mid-level architecture.

Oh go on then, rip me to shreds.

[1] I've also dug out the FAQ.
Reply to
Fevric J. Glandules
Loading thread data ...

Yes, that works fine. I've coded several things, both in-house and outside products pretty much the way you describe. It's a non-preemtive tasking system. In my projects, the main loop usually looks like:

while (1) //lint !e716 yes, it's an infinite loop! { DoCommands(); DoJobs(); }

DoJobs() iterates a list of "jobs" that can be triggered by time or by some other boolean result and executes the associated DoWork() pointer. The real time functions are handled in ISRs fired by things like UART Rx/Tx, external pins, and hardware timers.

There's typically a "housekeeping" timer interrupt that increments systime and the keypad and display scanning, reads ADCs, sends DAC values, etc. If you're careful with ISR priorities, it's even OK for the housekeeping ISR to load the CPU at 90% and above. (Of course, the main loop only gets the CPU time that's left over - UI and remote commands are usually fine that way)

Bob

Reply to
Bob
[snippety snip]

[snippage]

Opinions differ. I'm from the do as little as possible school. Handle the interrupt, set a flag, and let the main loop do what needs to be done.

There are exceptions, of course, where quite a lot has to happen within fixed time constraints and so the interrupt must accommodate it.

Quite a reasonable approach. I typically find it helpful use FIFO queues for very slow things like serial I/O.

--
Rich Webb     Norfolk, VA
Reply to
Rich Webb

PIC18s are 8 bit processors. Like PIC16s with a lot of ugly stuff kludged in to make them more powerful and slightly less unfriendly towards compiler writers.

Reply to
nospam

It is so much easier to use a basic time-slicing round robin preemptive multitasker rather then developing the complicated non-blocking state machines. Then, the tasks can be done as the straightforward linear code, and you don't have to worry about blocking.

Vladimir Vassilevsky DSP and Mixed Signal Design Consultant

formatting link

Reply to
Vladimir Vassilevsky

In a PIC18 ?

Reply to
Neil

It is a reasonable way. The interrupt usually should do as little as possible there are only two so they have to share. Your application may vary.

Reply to
Neil

No you just have to worry about finding program memory for an RTOS and RAM for task control structures and context saves and multiple stacks and worry about interrupt latencies and critical sections and resource protection and reentrant library functions.......on an 8 bit processor that has shit pointer handling and a 32 level hardware stack.

Reply to
nospam

(snip)

It's a reasonable approach. I did a multi-serial port protocol converter much the same way. Incoming messages were buffered by a bunch of serial ISRs which triggered different semaphores when a full packet was ready.

Conversion and pushing into the transmit buffer was done in the main loop. Again, serial ISRs handled the actual transmission IIRC.

As for the 2nd question, the answer is (always) "depends". Depends on the requirements of your system. The rule is, do as little as you can get away with inside the ISR.

Regards,

--
Mark McDougall, Engineer
Virtual Logic Pty Ltd, 
 Click to see the full signature
Reply to
Mark McDougall

What I did on a PIC16 was: .... in a header:

// // Most state machines follow the same form. // They have a local static variable which is their next state. // The first time they are called, they perform their own // initialisation. fFirstCall is true only on their first call. // typedef INT16 FsmStateT; #define ReturnState(Label) {FsmState = label_address(Label);return;} INT1 fFirstCall;

Somewhere in main() set fFirstCall, go round once, reset fFirstCall, then keep going round. Each finite state machine then looks something like this:

#separate VOID FSM_UserIf(void) { static FsmStateT FsmState;

PutState('v',FsmState);

if ( ! fFirstCall ) { if ( MainStatus.MotorRunning ) return; goto_address(FsmState); }

// // Initial state depends on the switches. Wait // for them to be known before deciding. // ++nInitialising; fArm = 0; fDisarm = 0;

WAITING_TO_START: if ( ! Switches[0].Known || ! Switches[1].Known ) ReturnState(WAITING_TO_START); --nInitialising;

DECIDE_NEW_STATE:

.... }

Reply to
Bill Davy

What's the use of PICs or 8051x misery when AVRs, HC08s and other processors with the flat memory and the traditional stack are available?

Vladimir Vassilevsky DSP and Mixed Signal Design Consultant

formatting link

Reply to
Vladimir Vassilevsky

I like this approximate approach, but I think you may find the specific implementation may break if you get complete messages queuing up in the receive buffer while you are handling a time-consuming one - your flag won't show how many, so you may miss the subsequent ones. I'd either change it to a count, or preferably detect the ends of messages when copying to the evaluation buffer. In the latter case, the ISR just becomes a software extensions of whatever tiny (typically

1 to 16 byte) buffer exists in the fifo peripheral.
Reply to
Chris Stratton

So they are. I've spent enough time with the datasheet and looking at example assembler and my own disassembled C code that you'd think it would have percolated back to my consciousness by now. Mind, it was quite late when I wrote that.

Reply to
Fevric J. Glandules

Thanks to you and all the other respondents; sounds like I'm not straying too far into thedailywtf.com territory, so far.

Reply to
Fevric J. Glandules

I'm bearing this in mind - the pseudo-code was as simple as I could make it, in order to make clear the general gist.

Reply to
Fevric J. Glandules

This looked interesting when I first saw it, but I haven't tried it:

formatting link
_requestid=61933.

Writing for it will be a lot like the task loop you describe, except that you'll get prioritized tasking. Beware the stack -- the PIC 18xxx parts let you monitor the stack and move things off to RAM when it fills up, but making it work is your job.

--
www.wescottdesign.com
Reply to
Tim Wescott

I have found there are several coding constructs that help the MPLAB compiler create tighter code. Define a bit field for logical flags. The compiler will emit BTFSS, BTFSC instructions. If you use a full byte the compiler will emit a MOVF to W and test instructions. If you have something like: flag = 0; .... flag = 1; .... if (flag != 0) ...

you can save a couple of instruction words by using flag += 1 instead of the flag=1. This is because the compiler will generate a INCF instruction instead of a MOVLW and MOVWF.

Many times a switch statement will generate cleaner code than using a if/elsif/else

The compiler does a lot of work dealing with pointers and pushing arguments on the stack.

If I start running low on program memory I'll usually try different coding constructs and look at the assembly listing to find the best generated code.

--
Joe Chisolm
Marble Falls, Tx.
Reply to
Joe Chisolm

Thanks for that - it looks well worth reading.

Reply to
Fevric J. Glandules

I've written some code to parse incoming strings; 'AB1' triggers foo(), 'AB2' triggers bar(), and so on. Is the following sensible?

// Coded with nested switches in the belief that this will // be far more code and RAM efficient than a strcmp... switch (*ptrUartRxBuf++) { case 'A': switch (*ptrUartRxBuf++) { case 'B': switch (*ptrUartRxBuf++) { case '1': foo(); break; case '2': bar(); break; } break; } break; }

Reply to
Fevric J. Glandules

Whether it is more efficient depends on a lot of things - compile it w/ assembly output and compare it to other implementations. It's not the way I would do it unless I had to.

I'm always amazed at how hung-up programmers get about parsing. If you have the authority to define the command set, you can make it easy on yourself by having fixed length commands; Even a single character (A - Z, e.g.) might suffice. A query has a '?' in buf[1] (or whatever your command identifier length is), and a list of comma separated numeric arguments is trivial to collect. Since a small project often has no floating point, the numbers are all integers.

Bob

Reply to
Bob

ElectronDepot website is not affiliated with any of the manufacturers or service providers discussed here. All logos and trade names are the property of their respective owners.