Avoiding global variables in embedded programming

In the type of embedded programming that I enter, the determinism and transparency of running code are much appreciated. What I mean by transparency is, for example, the ability to browse through arbitrary sections of memory and know which variable is stored there. Therefore, as I am sure, embedded programmers expect that the new should be avoided, if at all possible, and if it cannot be avoided, then it is limited to initialization.

I understand the need for this, but I disagree with how my colleagues have already done this, and I do not know a better alternative.

We have several global arrays of structures and some global classes. There is one array of structures for mutexes, one for semaphores and one for message queues (they are initialized mainly). For each thread that runs, the class that owns it is a global variable.

The biggest problem I am facing is unit testing. How can I insert a mock object when the class I want to check for #include global variables that I don't have?

Here's the situation in the pseudo code:

foo.h

 #include "Task.h" class Foo : Task { public: Foo(int n); ~Foo(); doStuff(); private: // copy and assignment operators here } 

bar.h

 #include <pthread.h> #include "Task.h" enum threadIndex { THREAD1 THREAD2 NUM_THREADS }; struct tThreadConfig { char *name, Task *taskptr, pthread_t threadId, ... }; void startTasks(); 

bar.cpp

 #include "Foo.h" Foo foo1(42); Foo foo2(1337); Task task(7331); tThreadConfig threadConfig[NUM_THREADS] = { { "Foo 1", &foo1, 0, ... }, { "Foo 2", &foo2, 0, ... }, { "Task", &task, 0, ... } }; void FSW_taskStart() { for (int i = 0; i < NUMBER_OF_TASKS; i++) { threadConfig[i].taskptr->createThread( ); } } 

What if I want more or less tasks? Different set of arguments in foo1 constructor? I think I will have to have a separate bar.h and bar.cpp, which seems a lot more work than necessary.

+7
c ++ global-variables embedded
source share
3 answers

If you want to unit test such code first, I would recommend reading Effectively work with legacy code. Also see this .

Basically, using the linker to insert false / fake objects and functions should be a last resort, but still fine.

However, you can also use control inversion, without a framework, this may push some responsibility to the client code. But it really helps testing. For example, to check FSW_taskStart()

 tThreadConfig threadConfig[NUM_THREADS] = { { "Foo 1", %foo1, 0, ... }, { "Foo 2", %foo2, 0, ... }, { "Task", %task, 0, ... } }; void FSW_taskStart(tThreadConfig configs[], size_t len) { for (int i = 0; i < len; i++) { configs[i].taskptr->createThread( ); } } void FSW_taskStart() { FSW_taskStart(tThreadConfig, NUM_THREADS); } void testFSW_taskStart() { MockTask foo1, foo2, foo3; tThreadConfig mocks[3] = { { "Foo 1", &foo1, 0, ... }, { "Foo 2", &foo2, 0, ... }, { "Task", &foo3, 0, ... } }; FSW_taskStart(mocks, 3); assert(foo1.started); assert(foo2.started); assert(foo3.started); } 

Now you can transfer the fraudulent version of your threads to "FSW_taskStart" to make sure that the function actually starts the threads as needed. Unfortunately, you have to rely on the fact that the original FSW_taskStart passes the correct arguments, but now you are testing a lot more of your code.

+4
source share

Did dependency injection help you in your situation? This can get rid of all global variables and provide easy dependency replacement in your unit tests.

Each main stream function passes a map containing dependencies (drivers, mailboxes, etc.) and stores them in classes that will use them (instead of accessing some global variable).

For each environment (target, simulator, unit test ...), you create one "configuration" function that creates all the necessary objects, drivers, and all threads, providing the flows with their list of dependencies. For example, the target configuration can create a USB driver and insert it into some message flow, while the comms unit test configuration can create a USB stub driver that controls the tests.

If you need this "transparency" for an important variable, create classes for them that will contain them at a known address and add these classes if necessary.

This is a bit more than static object lists, but the flexibility is fantastic, especially when you encounter some complex integration issues and want to exchange components for testing.

Rough:

 // Config specific to one target. void configure_for_target_blah(System_config& cfg) { // create drivers cfg.drivers.push_back("USB", new USB_driver(...)) // create threads Thread_cfg t; t.main = comms_main; // main function for that thread t.drivers += "USB"; // List of driver names to pass as dependencies cfg.threads += t; } // Main function for the comms thread. void comms_main(Thread_config& cfg) { USB_driver* usb = cfg.get_driver("USB"); // check for null, then store it and use it... } // Same main for all configs. int main() { System_config& cfg; configure_for_target_blah(cfg); //for each cfg.drivers // initialise driver //for each cfg.threads // create_thread with the given main, and pass a Thread_config with dependencies } 
+3
source share

You can allocate memory with malloc and then get a new statement to make the object at that position

 void* mem = malloc(3*sizeof(SomeClass)); SomeClass *a = new(mem) SomeClass(); mem += sizeof(SomeClass); SomeClass *b = new(mem) SomeClass(); mem += sizeof(SomeClass); SomeClass *c = new(mem) SomeClass(); 

so that you can malloc all the memory and then allocate it as you wish. Note: make sure you invoke deconstruction manually, as usual, when calling delete

-one
source share

All Articles