Possibilities for allocating memory for modular firmware design in C
I program small micro-controllers in C++, which achieves exactly what you want.

What you call a module is a C++ class, it can contain data (either externally accessible or not) and functions (likewise). The constructor (a dedicated function) initilializes it. The constructor can take run-time parameters or (my favourite) compile-time (template) parameters. The functions within the class implicitly get the class variable as first parameter. (Or, often my preference, the class can act as a hidden singleton, so all data is accessed without this overhead).

The class object can be global (so you know at link-time that everything will fit), or stack-local, presumably in the main. (I don't like C++ globals because of the undefined global initialization order, so I prefer stack-local).

My preferred programming style is that modules are static classes, and their (static) configuration is by template parameters. This avoids nearly all overhad and enables optimization. Combine this with a tool that calculates the stack size and you can sleep without worries :)

My talk on this way of coding in C++: Objects? No Thanks!

A lot of embedded / microcontroller programmers seem to dislike C++ because they think it would force them to use all of C++. That is absolutely not neccessary, and would be a very bad idea. (You probably don't use all of C either! Think heap, floating point, setjmp/longjmp, printf, ...)



In a comment Adam Haun mentions RAII and initialization. IMO RAII has more to do with deconstruction, but his point is valid: global objects will be constructed before your main starts, so they might work on invalid assumptions (like a main clock speed that will be changed lateron). That is one more reason NOT to use global code-initialized objects. (I use a linker script that will fail when I have global code-initialized objects.) IMO such 'objects' should be explicitly created and passed around. This includes a 'waiting' facility 'object' that provides a wait() function. In my setup this is 'object' that sets the clock speed of the chip.

Talking about RAII: that is one more C++ feature that is very usefull in small embedded systems, although not for the reason (memory deallocation) it most used for in largere systems (small embedded systems mostly don't use dynamic memory deallocation). Think of locking a resource: you can make the locked resource a wrapper object, and restrict access to the resource to be only possible via the locking wrapper. When the wrapper goes out of scope, the resource is unlocked. This prevents access without locking, and makes it much more unlikely to forget the unlocking. with some (template) magic it can be zero-overhead.



The original question didn't mention C, hence my C++-centric answer. If it realy must be C....

You could use macro trickery: declare your stucts publicly, so they have a type and can be allocated globally, but mangle the names of their components beyond usability, unless some macro is defined differently, which is the case in your module's .c file. For extra security you could use the compile time in the mangling.

Or have a public version of your struct that has nothing usefull in it, and have the private version (with usefull data) only in your .c file, and assert that they are the same size. A bit of make-file trickery could automate this.



@Lundins comment about bad (embedded) programmers:


The type of programmer you describe would probably make a mess in any language. Macro's (present in C and C++) are one obvious way.
Tooling can help to some extent. For my students I mandate a built script that specifies no-exceptions, no-rtti, and gives a linker error when either the heap is used or code-initialized globals are present. And it specifies warning=error and enables nearly all warnings.
I encourage using templates, but with constexpr and concepts metaprogramming is less and less required.
"confused Arduino programmers" I would very much like to replace the Arduino (wiring, replication of code in libraries) programming style with a modern C++ approach, which can be easiere, more secure, and produce faster and smaller code. If only I had the time and power....