a definite method

the realtime distributed computing system in the rambler roadster

this describes the architecture and coding scheme for a high-reliability embedded multi-processor, multi-tasking system based on Arduino. the specific system here runs a home-built automobile: lighting, engine sensors, electronic cooling system, etc. all of it distributed in the chassis, and exactly none of it "entertainment" in the cabin.

cars are a difficult environment for both code and electronics. electric power is dirty and unreliable with load-dump spikes, and not-running power drain kills the battery. temperatures vary from below freezing to water-boiling. chassis are exposed to water, oil, dirt and severe vibration. code 'can't fail'; it is not possible to manage abberant tech under the hood while driving down a busy road.

computers in the car

there are four computers in the car:

each computer has it's own electrical connections to stuff in the chassis (power, lights at the four corners of the car, fuel pump, cooling fans, water pumps, the many sensors, etc) and each computer handles a fixed set of tasks. these are mentioned below in increasing detail but you can somewhat guess from the names.

only the datalogger/display computer has anything like "typical" computer interface, but it is not a necessary part of car operation.

the panel computer has the fewest and simplest tasks: it "owns" the instrument panel switches (turn signals, hazard flashers, headlamps) and most of the analog panel meter displays (fuel level, oil pressure, etc). the panel computer however does not have direct connection to the sensors that source that information. more on this later.

the panel computer is buried in the instrument panel and has a moderate amount of wiring to the junk on the instrument panel.

the chassis computer owns most of the electrical loads and actuators in the car that are not the cooling system. this is a carbureted car; there is no fuel system ECU. the chassis computer handles all lighting (on, off, blinking), most of the chassis sensors road speed ("speedometer"), oil temperature and pressure, brake switch, tachometer, ...).

the chassis computer is attached to the firewall in the cockpit. it has a substantial amount of wiring: to all four corners of the car for lighting, all over the engine compartment for mostly sensors, to the fuel pump in the rear of the car, etc.

the cooling computer controls engine cooling only. it owns the cylinder head temperature sensor, the radiator-outlet temperature sensor, oil temperature sensor, and it controls the main coolant pump, a circulation pump, the big cooling fan in front of the radiator, and an oil-cooler fan. the code is fairly complex and documented elsewhere.

the cooling computer is also on the firewall inside the cockpit. it has wiring to the engine and chassis up front. the cooling computer's electrical loads are the most demanding.

last and least, the datalogger/display computer is a "bag on the side"; mounted on the instrument panel, visible to the driver, it has an LCD, membrane switches, a rotary encoder, and an SDcard. this box provides detailed information on the workings of all the computers above, and logs everything to the SDcard in spreadsheet format.

computer hardware

with the necessary exception of the datalogger/display, each of the computers in the car are electrically and physically identical and interchangeable. the software is different in each. this means that some computers have "extra" electronics (sensor/switch inputs, or power outputs) that aren't used but it makes testing and spares and construction much easier.

the roadster electronics is sort-of documented. the electronics is fairly generalized, and there's nothing in them that directly supports the multitasking.

why separate computers?

the reason for multiple computers is simple: wiring. there is a vast amount of wiring in automobiles. they are 15 feet long, 6 feet wide and have sensor and actuators scattered about, in places required by physics or convention. many devices consume many watts of power.

between the four computers there are 50 or so sensor and actuator (load) connections, 22 to 18 gauge wire, out of necessity in vibration/water/proof connectors. to hazard a guess there's probably 400 feet of wire in this car. and this is a very minimally-featured car, with zero in-cabin comfort or "entertainment" electrics.

it is simply not practical to have so much wiring arrive in one box, unless you are an OEM. OEMs are obsessed with lowest possible cost in largest possible quantities. my concerns are reliability, ease of construction, modularity for one constantly-evolving project, and fun. and it must work, all of the time.

how the boxes talk to each other: electrically

the computers are inter-connected with what is essentially a network. this of course necessitates an electrical connection between the computers. the simplicity of this connection belies the (possible) complexity of this, or any network, which will be covered later.

each computer has two ports, named Downlink and Uplink. these names aren't very good, as there is no "up" nor "down", and no heirarchy at all; each computer is a peer. since each does its own separate set of tasks, there's nothing to compete for or contend with.

each computer is connected to it's neighbor, one boxes' Uplink connected to the other's Downlink. each link is symmetrically bi-directional; each computer can send and receive on both Uplink and Downlink.

physically the links are little waterproof four-pin cables, male and female, wired such that one plugs directly into the other. if the connectors mate up, the link works. electrically the links are three wires; ground, RX and TX. RS-232 serial data at 9600 bits/second.

the four computers are connected together like this:

.------------. .---------. .---------. .---------. --->| datalogger |--->| panel |--->| chassis |--->| cooling |---> <---| |<---| |<---| |<---| |<--- '------------' '---------' '---------' '---------'

however everything would work, the car would drive and act exactly the same, with the computers connected in any other combination. the unused cables just dangle in the air, bits sent to them spill out onto the floor and stain the carpet.

to summarize, each computer:

how the boxes talk to each other: informationally

within a driving automobile the computers need to work with each other, the rest of the stuff in the car, and the driver and the road.

some automotive functions are distributed across multiple boxes; for example, the panel computer hosts the turn-signal switches, but the chassis computer controls the lighting. the panel displays oil pressure originated by the chassis computer, and engine temperature originated by the cooling computer. some information is about when (brakes applied now) rather than a quantity.

inter-computer communication and action is done by a software concept called inter-processor communication, aka IPC. IPC takes place over the Uplink/Downlink network and within each box itself.

inter-processor communication (external)

the inter-processor system here is a messaging system. all data exchange is done by sending (receiving) simple messages, a metaphor for what can be visualized as a written post-it type note containing two components:

.--------------.------------------. | message name | message contents | |--------------|------------------| | OILPRESSURE | 61 | '--------------'------------------'

all messages have the exact same format, with no exceptions. the message name says what the message is about, and the payload is a single number, 0 to 65535 decimal, the meaning of which depends on what the name "is" (or means). some examples:

MSGOILPRESSURE MSGRPM MSGENGINETEMP MSGOILTEMP

a message "MSGRPM 2300" arriving at a computer means that when the message was generated, the engine ws turning that 2300 revolutions per minute. in a driving car data changes; most messages are sent out at a pre-determined rate appropriate for the datum. (engine RPM changes rapidly; fuel tank level slowly).

there are also messsage names that trigger an action rather than contain data. for example, "short left turn signal sequence" (sent by the panel, to the chassis computer means 15 blinks on and off) and reset the main trip odometer (sent by the datalogger/display to the chassis computer, sets the trip odometer to 0).

IPCR_BLINKLEFTSHORT IPCR_ODOTRIPRESET

(message names starting with MSG are payload-carrying, and IPC_R are IPC, REMOTE (inter-computer) events.)

there are approximately 100 different message/event names (message types) in use, and the "traffic" averages 20 to 50 messages per second or so into and out of each box.

message passing

all computers send and receive messages. messages are produced internally (originated), and arrive from neighbors.

message origination

generally speaking each box (computer) originates a subset of possible message types. this is consistent with the dedicated tasks of each box; eg. the cooling computer originates all messages regarding engine cooling: cylinder head and radiator outlet temperatures, fan and pump speeds, internal status, etc.

computers originate messages either on a schedule (eg. oil pressure measured and sent a few times per second) or upon some event (switch press) or a particular state of the metal (oil temperature exceeds some threshold). messages originating in a computer are sent out simultaneously on both Uplink and Downlink. the fuel (tank) level is probably the simplest; every 20 seconds the current level is read, smoothed and filtered, and sent Uplink and Downlink.

originated messages are delivered in both directions.

message transit

messages originated by one computer then received by that computer's neighbor(s) are in transit -- they are sent onward to the next neighbor. messages arriving from Uplink are sent Downlink; messages received from Downlink are sent Uplink. generally speaking computers transit messages that were not originated within the computer. it would be a programmer error if the panel computer originated (for example) a cylinder head coolant temperature.

transiting messages are delivered unidirectionally, to the left or to the right.

the cache, and shared state

messages aren't simply moved about; they are stored. each computer maintains a cache, a storage mechanism for each type of payload-bearing message (52 "slots"). given the origination and transiting scheme every interconnected computer receives, over time, every message ever sent. by implication storage in the cache means therefore every computer maintains the complete functional operational state of the whole system. this is a critical feature of this system and a major component of it's reliability features.

(here i will mention only briefly that internally the semantics are that messages have payloads and are stored in the cache, but events, eg. those messages whose names begin with IPCR_ or IPCL_, are acted upon immediately and are not stored in the cache.)

reset, power on, crash recovery

many positive effects fall out of this scheme. here is one example.

a key reliability mechanism is built upon the MSGCOLDBOOT message.

every box has it's own independent system clock (a simple millisecond counter) that starts counting from zero at power on/reset.

each computer issues a MSGCOLDBOOT message immediately upon startup or reset, and again after a fraction of a second.

any time a computer receives a MSGOLDBOOT message it examines it's own system clock; if this receiving box has been "up" for less than a second, the message is ignored and no action is taken. however if the receiving box has been running for at least a few seconds, it triggers a re-send of its own originated messages (from it's cache) and forwards the cold-boot message normally (see the message transit section).

the net effect is that if during normal operation (driving down the road) any computer crashes/resets its neighbors re-deliver state to it from their caches, restoring the reset computer's state within tens of milliseconds.

implications

though the scale and scope in this system is modest, it has some major advantages. foremost, it answers one of John Day's "seven unanswered questions" regarding connectionless statefulness. extending the cache to include timestamp and maximum data age (implemented then removed!) brings in most of the advantages of the proposed delta-t/Watson scheme, which achieves reliable connection management without TCP-type state overhead.

messages, the cache, IPC implementation details

let's look at the cache and messaging and IPC delivery system in more detail. the multi-tasking code structure described in the next section is designed around it.

messages composition

this scheme is built around messages as defined by my SRMessage library. messages are profoundly simple and rely on the nature of character coding defined by ASCII/ECMA6.

messages are character streams or can be thought of as text strings, ASCII letters [A-Za-z] ASCII digits [0-9] and the ASCII space character. some of the ASCII "graphic" characters are used for special purposes, and some "high bit" characters are used to extend the "letters" space. the details are summarized below with the library code being authoritative.

messages are in postfix notation, eg. payload first message type last. payload is zero or more digits and the message type is a single ASCII letter. if there are no payload digits then the payload is 0. the followign are all valid messages:
message text message type payload
1234M M 1234 typical
65535w w 65535 largest payload
0U U 0 smallest payload
U U 0 implicit payload 0

postfix notation is decomposed by a state machine of utmost simplicity. as characters arrive (or pulled from a string) digits (if any) accumulate into the payload and when a letter arrives, code dispatches the payload to the appropriate routine and clears the payload to zero. no buffering of incoming data is used or needed. (assembly of the decimal payload is of course a form of buffering.)

the ASCII space character resets the payload accumulator. some ASCII control characters (CR and LF) are mapped to the space character. this character structure means that messages adhere to what humans think of as "text", including spaces and line control for readability.

there is a message checksumming scheme to help detect transmission error, built using a message of type (ASCII . or period) that is the checksum of the message that follows. see the code for details.

implications and side effects

there are many advantages to this scheme. messages are human-readable and typeable (in the Arduino IDE debug window for example). 7-bit ASCII/ECMA6 passes through any serial character system, modern or legacy. message generation and decomposition are extremely simple, fast and robust.

SRMessage format is used as the inter-box IPC communication scheme, as inter-task IPC within a given processor, as the addressing scheme for storing machine state (odometers, etc) and settable parameters in EEPROM. SRMessage format is also used as a manual command language via the Arduino USB interface.

(when used for inter-task communication within a single processor, the form

result= sendMessage (TASKNAME, message-type, message-payload)
is used, rather than the ASCII text format, but for transmission outside of the box, for display or storage in EEPROM, it is simply "printed".)

the cache

the cache is simply a table, with one entry for each possible message type; given the 26 upper and lower ASCII letters, 52 entries each containing an unsigned 16 bit integer. arriving SRMessages are decomposed to an index (0..51) and a payload, which is stored in the cache:

cache [message-type]= payload

more or less. the cache is addressed only through two access methods described later on and so other constraints on the cache such as data-age timers, checksums or other validations can be applied.

the cache also affords many optimization and reliability enhancements. IPC autonomously re-sends cache contents to both neighbors at a low data rate; though rare transmission/reception errors do happen in serial links. the cache is consulted before a message transits; if the payload has not changed since last transit and that transit was recent, the message can be dropped. the MSGCOLDBOOT mechanism described above "falls out" of the design; it simply triggers a rapid (vs slow) background resend of the cache.

IPC task loop .---------------------------------. ---------->| Uplink | |---------------------------------| | SRMessage decomposer | | 1) store in cache | | 2) send Downlink |-------------------------------> '---------------------------------' UPLINK | DOWNLINK | .---------------------------------. | | Downlink |<----------- | |---------------------------------| | | SRMessage decomposer | | | 1) store in cache | <---------------|--------------| 2) send Uplink | | '---------------------------------' | | | | | | | | | | | | | | | .----------------. | | | cache | | | |----------------| | '--------->| datums |<------------' | A..Z | | a..z | .--->| |<---. | | | | | | | | | '----------------' | | | | | | | | | | | .--------------------------------. | internal methods | |--------------------------------| | fetchDatum (type) | | storeDatum (type, payload) | '--------------------------------'

a definite method: cooperative multi-tasking

each computer contains multiple independently executed programs, each of which performs some defined job or task. in this system these are called task loops. a rough summary for illustration purposes is:

each computer also has overhead or system task loops: IPC, power (on/off) management, task (end result) monitoring.

what multi-tasking means

though this isn't a programming tutorial it is worth making explicit what this system does and what it doesn't.

a foundational assumption made about all multitasking schemes is that they work with a single-threaded CPU because few things in the world need 100% of a processor's attention. this is especially true of embedded systems controlling things that have mass -- especially substantial mass like automobile subsystems and human beings.

humans simply don't move that fast -- something responding to human command within 10 milliseconds is perceived as instantaneous. the most time-critical human interface in a car is the brake light, lit when the pedal is pressed. this system easily achieves an average-worst-case of 5 millisconds. in contrast moving coolant through a 500 lb engine takes nearly a second to detect the smallest perceptible change in the temperature of the metal. at the other extreme, truly rapid bit-level control (motor speed or lamp brightness) is already done by hardware (PWM and the like).

the slow processor i use executes 16 million instructions/second. there is no system in a car (or most of the world) that cannot be operated smoothly by frequent but brief bursts of CPU attention to the task at hand.

a better name for this technique would be time slicing -- this is a round-robin time slice scheme, where each task loop gets attention, one at a time, in bursts, so htat each task can proceed along it's execution path. there is no task priority per se, though task loops frequently coordinate their activity to that of other task loops through shared IPC data (the cache).

here, "task" means some purposeful job, goal, end result, etc. engine cooling, light brightness and blinking, are jobs in a car needing doing.

here, task loop refers to code that performs some job or task arranged into a particular structure described below.

the fundamental mechanisms for task loop multitasking

multitasking is done here through a strategy, not a realtime kernel or routine. these are the basic requirements:

the central role of IPC: inter-process communication

before discussing how this multi-tasking works, the role of the IPC needs to be understood. at the highest level of abstraction, functional tasks (lighting, cooling, etc) are coded as isolated programs. cooling worries about cooling; lighting about lighting. task loops do not need to inter-communicate explicitly. exceptions are rare.

when a task needs to know something outside of local data that data is stored in the cache. data is either generated internally within a task (eg. as local memory variables) or it is in the cache. data in the cache are the payloads of originated and transiting messages handled by the IPC.

by maintaining complete system state within each computer via IPC and the cache, there is no need for addressing "nodes" in the network. data is either trivially local to the task (local compiler variables) or it is in the cache.

this is the purpose of the connectionless/stateless IPC design. tasks go about their internal business without any knowledge of the topology, electrical or otherwise, of other tasks, or even knowledge of which computer a given task resides in. all "global" system data is accessed via local method

a major and intentional side effect of this code structure is that there are no global datums for tasks to erad, or corrupt. the one intentional exception is a "debug" bit flag array referred to by a manually invoked print routine.

conversely, when a task wants to change a datum (a "global" datum) it uses a local method that updates the cache (which the IPC in turn originates outgoing messages to it's neighbors).

the task loop: code structure for cooperative multi-tasking in Arduino

i rely on the Arduino IDE system for the simple reason that it is more available than the tools of any particular operating system (and one major operating system doesn't have any tools). it is adequate to the task, and it is portable across operating systems. the Arduino libraries are generally excellent and when they are not, you write your own.

i have done what i think of as a literal extention of the "Wiring" model implied in the Arduino code and examples, bringing the setup()/loop() structure and implied event-loop, non-blocking methodology outward. the Arduino Wiring model enforces the basic setup() and loop() functions and implies that code within loop() be treated like an event handler, and not block, but gives few useful examples of how to do this. this is unfortunate.

my code i think of as "C within C++ objects". C++ objects are used only to containerize C code, specifically to limit scope of in-memory variables and local and generally non-portable local methods.

without exceptionall inter-task communication is done via the IPC, within or outside the computer.

the methodology in a definite method is that non-blocking event loop treatment, taken literally. with this simple idea is built extremely fast, low overhead, easy to debug and develop cooperative single-threaded multi-tasking.

this method is inherently reliable -- stack use is minimal and predictable. ordinary tools can be used to debug and develop it. other advantages will appear as this discussion continues.

task loop schematic construction in source

the examples below are correct, but schematic and incomplete. the Arduino IDE and the gcc/g++ compiler impose restrictions on source code format that requires a bit of care to construct, and this is covered in detail later (and is available in my code on github). these examples here leave out some of the important details, such as source code modularity (tabs in the IDE), etc, in order to get across the larger ideas of structure and organization.

the canonical Arduino sketch example is essentially:

// global data void setup () { // one-time initialization here } void loop () { // implied non-blocking code in loop, executes repeatedly. if (someEvent) { // do something } }

this structure gently encourages "event loop" type programming; the hint is there, but nothing else in the Arduino suite eexands on it. this is unfortunate. so what follows is to me an obvious expansion: to put little "jobs" or tasks into separate Arduino tabs, for example, so that that code is self-contained and independent of others like it, and arrange invocation of them like so:

// global data void setup () { // one-time initialization here Loop1.setup(); Loop2.setup(); Loop3.setup(); Loop4.setup(); } void loop () { // implied non-blocking code in loop, executes repeatedly. Loop1.loop(); Loop2.loop(); Loop3.loop(); Loop4.loop(); }

note that the Arduino setup() and loop() are present as required, and those in turn invoke each task loops' setup() and loop() in turn.

this skeletal example is not adequate for real programs, but the structure is decent: if objects Loop1 etc are, for example, "sub-programs" each contained in its own source tab, it becomes a nice way to partition complex programs.

implicit here, but explicit later, is that code is in Loop1.loop(), ... Loop4.loop() executes rapidly (without blocking) so that each loop (task loop) gets round-robin attention. "often enough".

what this structure needs is a way for the task loops to communicate (with each other, the outside world, etc) and that is the purpose of IPC. in this scheme IPC is implemented as simply another task loop:

// NO global data // global IPC dispatcher int IPCDispatcher (source-loop, dest-loop, type, payload) { switch (dest) { case IPCLOOP: IPCLoop.recvMessage (type, payload); break; case LOOP1: Loop1.recvMessage (type, payload); break; case LOOP2: Loop1.recvMessage (type, payload); break; case LOOP3: Loop1.recvMessage (type, payload); break; case LOOP4: Loop1.recvMessage (type, payload); break; } } void setup () { // one-time initialization here IPCLoop.setup(); Loop1.setup(); Loop2.setup(); Loop3.setup(); Loop4.setup(); } void loop () { // implied non-blocking code in loop, executes repeatedly. IPCLoop.loop(); Loop1.loop(); Loop2.loop(); Loop3.loop(); Loop4.loop(); } // ----------------------------------------------------------- // example task loop #1. class { // begin enclose task loop private: // private data ... // private methods... public: int recvMessage (type, payload) { switch (type) { // case: do some things etc } } void setup() { // one time etc } void loop () { // task NON-BLOCKING code here } } Loop1; // end enclose task loop // TASK LOOPS 2, 3, 4, AS ABOVE WITH NAMES Loop2, Loop3, Loop4.

as before, each task loop's loop() is run one by one in turn. IPC has it's own task loop covered in technical sections below. the bulk of the inter-process communication work is done by the global IPC dispatcher. it is deceptively simple, providing the linkage between protected C++ objects.

all C++ objects are instantiated within the default global C space of the Arduino main program. all C++ objects, the task loops, are lateral peers and cannot access each others data. this is by intent.

(code could still use a pointer to smash another task's data. please don't do that; better yet, don't user pointers; i no longer do, as general (good) practice.)

the IPC dispatcher is in global space and so all task loops contained in the C++ objects can reach it. however it is constructed as simply a big nested switch() statement which allows as fine a control over intercommunication as desired. in general i use one more level of switch to enforce (enable) allowed source-loop-->dest-loop combinations, but it is possible to control or limit (or grant) access at the message type or even payload level. while i don't find that particularly useful it is extremely handy for debugging or displaying transient data.

the IPC dispatcher restricts inter-task-loop interactions to be entirely determinate. it allows top-level structural enhancements with zero impact on the main task loops -- for example, EEPROM storage of certain datums (eg. odometers) was included by adding an EEPROM task loop, and having IPC send it messages. EEPROM task loop refers to global state (via IPC) to see imminent power off so that it can cache EEPROM writes to minimize wear.

blocking is bad! timers are good!

the final and most important software construct in this cooperative multitasking methodology is a timer abstraction. the metaphor is a stopwatch, or wall clock.

a definite method is designed around the simple stopwatch timers in the SRTimer library. SRTimer in turn relies only on the millis() free-running millisecond counter built into every Arduino.

the example skeletal four task loop example above is useful to analyze. first consider execution flow. because each task loop's loop() contains no code, it is called and then immediately returns. therefore the main, top-level loop (the customary Arduino "loop()") executes some 100,000 times/second.

though this example does nothing useful, it at least avoids doing something that is harmful to all realtime operation: it does not block.

blocking is any sequence of code that loops back onto itself in any manner that either (1) consumes "significant time" or (2) waits for a resource to become available (eg. a pushbutton switch). blocking is tolerable if and only if the time spent blocking is gauranteed to be "brief" -- usually on the order of tens of microseconds. for() loops that initialize a block of data or a device are fine; awaiting an event that might not happen is unacceptable, and universally, is better done in a state machine.

timers are your friend

the task loops in the the skeletal example above are the lowest possible computational "load" possible -- as soon as they enter, they exit. on an actual 16 MHz UNO, these skeletal tasks take 10 microseconds to complete.

nearly all physical things in the world are slow and need only occasional attention to "feel" smooth and continuous and immediate. human interface controls are a perfect example. many "fast" devices only need a lot of attention in bursts.

with human interface as an example, to feel "instantaneous" a pressed switch needs to produce a detectable result in under 25 milliseconds or so; for most casual things (turning on a lamp, motor, etc) 20 to 50 milliseconds is unnoticable, and 75 to 100 tolerable for things not involving feedback.

in this automobile multi-processor system turning the headlamps on and off is a good example. the panel computer examines the headlamp switch every 7 milliseconds, and when pressed for a sufficient amount of time (switch function overload produces multiple events from switch presses; short press, long press, release, etc) originates a message via IPC to (say) turn on the headlamps. that message wends its way to the chassis computer, where the message is received and stored. let's stop here for discussion.

the panel computer does many things, including scanning multiple switches. they change rarely, looking at one every 7 milliseconds seems "instant" to the driver (me). code that does so is embedded in a task loop named Switches (loosely similar to the Loop1 example above).

the code necessary to detect and deal with the "headlamp switch has changed" event is "wrapped" in a timer. timers are like repeating stopwatches. once set (in setup(), usually) they "fire" or return true once 7 milliseconds has passed since the last time it fired or was set. here is a somewhat more detailed example:

void setup () { // ... setTimer (SWITCHTIMER, 7); // set timer to fire every 7 milliseconds // ... } void loop () { // ... if (timer (SWITCHTIMER)) { // see if the headlamp switch is different than last time... if (headlamp.switch.something.something) { // code here } } // ...

in this skeletal example each task loop (this might be Loop1.loop()) executes about 100,000 times/second, loosely every 10 microseconds. nearly every time, the timer (SWITCHTIMER) method returns false, and loop() exits. but every 7000 or so iterations (in setup() it was set to fire every 7 mS), the method timer(SWITCHTIMER) returns true and only then is the enclosed code run.

you might point out that in this example so far, this deference of already infrequent code via software timer is unnecessary; the if() code won't operate very often under nearly any circumstance, anyway, since its requires a sluggish human to press a physical switch. a partial answer is: maintain perspective! -- this is not the only task loop -- and this is not the only if(), and most things are far more complex than this. and strategically, you need to think about time as an inherent part of control.

in fact a "simple switch press" is not simple at all. real life controls have function overload. especially in an automobile, switch presses want to be intentional; a "short press" in this case at least 50 milliseconds, not an accidental brush of a finger. discerning short press ("on") from a long press ("off") means not acting on the switch press, but on it's release, and after having examined the time it spent in the pressed state. all of these calculations involve time. timers do this nicely.

(when the headlamp switch is pressed and released, the panel sends a message via IPC, which is delivered to the chassis computer in about two milliseconds. the Lighting task loop in the chassis computer sees the changed headlamp state usually a millisecond later, though if all the planets align badly, and multiple task loops are each getting attention in turn because their timers have somehow all fired at once, it might be deferred by 5 or so milliseconds. the lamp-controlling hardware is then turned on and the headlamp lights.)

in the cooling computer, a computationally-heavy cooling calculation occurs every 2.5 seconds -- not faster! because the passage of time is requird to stay in sync with the slow thermal change of a large mass. however, precise temperature measurement, which includes despeckling noise and applying smoothing must be done 100 times per second, with a carefully chosen time constant for smoothing.

time management is a critical function in real-time control.

other computers in other cars

there are very many DIY auto/computer projects. most have gone down a much more traditional path and are more OEM performance oriented, which is far more popular than the stark sort of first-principle stuff i do. AutoSportLabs is a good place to start (i use a lot of their stuff).

another sort of auto/computer approach you decidedly do not want to follow is this deadly (but hilarious to me of course outside of the corporate-sponsored death involved) "toyota unintended acceleration" debacle.

because the U.S. court system is open and public the full transcripts of the highly technical evidence is now public record. and it is fascinating. i read it on a long air flight. the judge is no dummy, but he's not a nerd; talented consultants rendered the truly horrific coding practices of toyota's programmers visible.

reading this sent me down a long path that has culminated in me reading and modifying all of my Roadster code work to meet most of the appropriate MISRA-C:2004 coding standards. they're actually very good recommendations; in fact i found a number of errors in my own code simply reading it. (most were due to implied casts and size of variable issues).

most of my code now at least approximates the MISRA-C:2004 spec, as far as coding style goes.

here are copies of the Toyota disaster documentation:

and here is a copy of MISRA-C:2004 document (PDF) at least until someone tells me to take it down.


footnotes

"Patterns in Network Architecture: A Return to Fundamentals", John Day, 2008, Pearson Education Inc.

https://github.com/tonyg/delta-t-udp/blob/master/watson-delta-t-protocol-specification-1981.md

actually the "local method" is to use IPC to send a message fetches the payload referenced by the passed message type. writing a value to the cache is done by sending the message to the IPC task. it's messages everywhere.