On componentisation... I think this is the first time I have been really trying to put any kind of structure into the layout of functions and there data.
It was going well. I started with each module allocating it own memory in c files, correctly exporting only "publc" functions and variables. Making c variables private and static etc. etc. I even passed in pointers for the timers and dma streams etc.
It didn't carry on quite that well. I found allocating the memory in the modules is not the way to go if you want to say, use that module twice!
So, at this stage the hygene level is about 80% and some pointers have been borrowed and there is a bit of "invasive code" in at least one callback because it's shared with another timer. Stuff like that, which needs a designed, interface around it, but have instead just "grown into place".
Ah yes...
There are levels, at least as it's traditionally done in C.
It's a bit easier, or more obvious, with OOP, but in C you have to do all that by hand without all the syntactic sugar to help you along.
Usually the next step up is to stuff all the locals into a struct. You can do this trivially already, what with your state variables being local to the module, that is. Then just allocate one static/local struct for the module and be done. (The compiler will probably resolve the struct accesses to direct pointers, no overhead computing offsets. Alternately, maybe those are faster, and it'll choose that way instead!)
Now with everything packed up, you can make a new struct, and call the same functions but on it instead, and boom, you've got a general purpose module that can repeat.
Basically OOP under the hood, is a collection of functions associated to a struct, and maybe the function pointers are members of that struct too (you can put them there if you like!), and together that's called a class. And the constructor/destructor are done somewhat automatically, at least up to having the place to put them (usually special named functions) but you still have to fill in all the init/free/etc. yourself (as in C++), but maybe a bit more is done automatically (Java etc.), whatever.
Which also makes inheritance a very natural thing to do, all the base elements are there, and just append to the struct for whatever junk you add onto it, and call the base con/de/structors first then handle your part of it and you're set.
Which also maybe explains why multiple inheritance isn't trivial and a lot(?) of languages (Java) don't do it, you can't just glue two disparate structures together and make their potentially completely different internal logic play nice together. Maybe they can be resolved with certain restrictions, or with symbolic representation (keeping source, or some representation of it, rather than a compiled binary, of the base classes), I don't know, but yeh.
But I digress, also, if you haven't used Java or whatever, this probably isn't any familiarity at all... but in that case, maybe knowing roughly how they do it, maybe is still something of a clue.
Anyway, for C purposes, who's responsible for allocating those structs, and where and when, is an open question. Often, the module using them declares/allocates them, then calls the constructor to init those structs, and so on and so forth. The pointers to these objects can be passed freely between modules and any can operate upon them (just don't loose track of who's doing what, of course!). Given headers for the module in question of course (so, you can use
#include "header.h" as equivalent to Java's
import my.Module.Class.Subclass or whatever).
And of course you don't get the syntactic sugar of
object.doStuff(params)*, but that's just saying
doStuff(object, params), basically any non-static method on an object has an implicit first parameter
this which links to the object it was called from (via "." member operator).
*Unless you put the function pointers in the struct, but you still need to call
object.doStuff(object, params).
You can even do, at least certain kinds of parameterization, by passing in callback functions (to the constructor). So you can do the basic data structures and algorithms gimmicks of, here's an array of X, here's a linked list of Y, here's... whatever. The constructor and internal logic handle the size of the objects (which would have to be dynamic (heap alloc) in this case) and the callbacks handle the actual operations on them (so, in C, you have to implement and pass around every piddly function that does basic things like add, subtract, concatenate, etc., on the data type). This is all more straightforward in C++ of course (including operator overloading so you can use e.g. "+" for string concatenation, and the compiler being able to optimize callbacks down to static calls or inlined functions even*).
*I think? I don't use C++ so I haven't seen personally, I'm just assuming it has this kind of visibility.
It really helps to see a couple projects that work this way; I've used few myself, but for example the freemodbus module I used recently has some callbacks in it, making it flexible for ASCII, RTU, TCP, etc. It's not fully object-oriented in the above sense, but it wouldn't be hard I think to put all its state objects into a struct and instantiate it that way. (Which would then have to include hardware interface, which they have as a file you have to fill out the implementation of. Having a e.g. serial port object would be handy anyway, good way to introduce buffering, general allocation of identical resources (you want how many ports with Modbus? You got it!..), and only modest overhead. Downside: most platforms you can't resolve interrupts by object.)
Also, as these things go, maybe some of the above context helps you with the HAL itself:
Ah... very good point. There ARE holes in my state machines. I am not catch ANY of the UART error conditions. Error conditions which are highly likely to occur on a breadboard power off unisolated USB power and a Wifi adapter on it.
All it takes is an overrun, or a RESET or a BREAK condition detected and HAL is very likely to call the Error callback and not the complete call back.
There are places where that would be immediately evident. And others where it wouldn't.
The HAL is largely written in that sort of way, i.e. you set up a state object, insert callbacks, activate the instance, etc. etc., and there you go. You're constructing and instantiating a whole object!
Well, handling all the status and error conditions may feel more natural too when you consider them part of an object, too.
And you could even make, for example, a common error handler that just iterates over all the objects fed to it of that type, and does whatever it needs to. Maybe/probably the error handling will be different for each one so this wouldn't be useful, but just to say, it could be, if it were. You don't have to check each one with repeated code (eliminate code duplication)!
You may also find -- I mean, maybe not, since this is a fairly early point on the learning curve I guess, with respect to these things; but something to think about / look forward to in the future -- you just don't like the way the HAL objects allocate and interact with the hardware, and what API they expose for your program to work with. Well, you have all the source (HAL is nothing but headers), you can run the hardware yourself bare-bones if you like -- and maybe you work with that directly, or at the CMSIS level instead, and implement your own, preferred, perhaps higher level too (buffering and fault logic and etc.?), interface.
The downside of course, of cooking your own, is it's almost certainly going to be very narrowly scoped, tailored to your immediate need, and may not be very general with respect to other parts in the family, or other families; so, there is a lot of potential there for keeping things general, or for scope creep and overgeneralizing.
Also, maybe you're not using all the HAL tools that are provided; the API is massive, you might just be missing something. I don't recall offhand but maybe there's buffering and stuff already there? Could be usable directly, or adaptable.
---
Oh, on another note, be careful of when to decode and parse, versus perform IO. That mention of blocking (a thread? an interrupt..?!!) waiting for a char or timeout, seems devilishly nasty, at least from the least favorable interpretation of that description. I like to do those by keeping internal state in a function (static locals) or struct (object) and parsing what's available then leaving immediately.
A... I guess fairly simple example is here:
https://github.com/T3sl4co1l/Reverb/blob/master/console.c It's a basic command prompt sort of thing, so it basically takes line input, persistently / no timeout delay, but it also has editing features (backspace/delete, cursor move, insert). The GetInputLine function is non-blocking and returns immediately in almost all cases (except when executing an actual command, in which case execution is passed to the function found in the list, if one is found). So you can just spin-loop on it and it takes little overhead in main().
Speaking of wait for timeout, Modbus does that -- might want to look at the freemodbus implementation to see how they handle that. It's (IIRC offhand) just a one-shot timer reset on last char rx or something like that, and when it times out, it clears a flag in the state machine. Simple as that. Modbus in general is quite simple and effective, and may be interesting just to study. (Or that's exactly what you're doing and trying to roll your own, heh, in which case I would recommend just going with this codebase, it seems fairly mature and I didn't find any problems with it.)
Tim