2

My question

I built an inverted pendulum on an Arduino using C (ie. everything was done procedurally). I'm trying to self study application design and would like to refactor my code into a more OO approach with SOLID, loose coupling, and testability in mind.

How can I improve this design to achieve a more loosely coupled system and better testability?


UML

UML-class-diagram


Brief summary of classes

MotorController - An interface for motor controllers.

DrokL928 - Is the motor controller I use, implements MotorController.

Cart - Is just a convenient wrapper for DrokL928. Allows for more intuitive control of the cart.

Encoder - An external library for reading values from rotary encoders.

EncoderWrapper - A wrapper for Encoder. That way I encapsulate the external API into one place.

StateVector - Holds the current state data.

StateUpdater - Processes encoder values from EncoderWrapper and assigns them to StateVector.

LQRController - Computes the PWM signal (based on the current state) to send to the Cart in order to stabilize the pendulum. see: Wikipedia's linear-quadratic-regulator.


Specific design questions

  1. StateUpdater uses constants IDLER_PULLEY_RADIUS and SYSTEM_LOOP_RATE. These smell like they don't belong inside StateUpdater, but they are relevant to the calculation of the state. I suppose all of those private methods inside StateUpdater could be put into a separate class StateCalculator?

  2. LQRController uses the constant pendulumBound. That is, the LQR controller should only calculate the input if the pendulum angle is within a certain bound. For some reason I feel like this doesn't belong here, but maybe I'm wrong on that. For the sake of completeness, maybe I should add a bound for each variable in StateVector.

  3. As of right now I can't instantiate EncoderWrapper into a test harness because it requires a reference to an Encoder. And by extension, I can't instantiate a StateUpdater into a test harness. How can I fix this?

  4. In the most ideal case, I would like for StateVector to not be so hardcoded and the variables should be open to modification. Therefore, LQRController should have a gainVector of any length (right now it is hardcoded to 4). Is this too much abstraction? If not, how can I go about achieving this? Now that I think about it, I believe it would be too much abstraction because then I'm not sure how the StateUpdater would calculate the state on arbitrary state variables, because the algorithm is very specific.

2 Answers 2

5

One of the hallmarks of good design is that it is mostly self-explanatory, even to someone without much knowledge about the domain. It so happens that I had no idea what an inverted pendulum was before reading your question, so I tried to gather some insights from your diagram, but unfortunately Cart seemed to be the only object that was possibly related to your problem domain.

After quickly reading about inverted pendulums on Wikipedia, the details of your design started to make more sense but then I realized that you were missing abstractions such as Pendulum, and instead had the state and behavior scattered between StateVector and StateUpdater.

My suggestion would be to think about your system outside-in and focus on the concepts you are trying to model rather than how they are implemented. For example, you mentioned:

Cart - Is just a convenient wrapper for DrokL928. Allows for more intuitive control of the cart.

Cart isn't just a "convenient wrapper", it is one of the primary things you are trying to model. A cart has a position and velocity, and its movement can be controlled. Encoder and MotorController are implementation details used to obtain the current state and control the movement, respectively.

Similarly, you would have a Pendulum class with angle and velocity properties, again using an Encoder internally to obtain these values.

And finally, an InvertedPendulum class that represents the entire system consisting of a Cart and a Pendulum, and uses a Controller to stabilize the system.

This eliminates StateVector and StateUpdater from your design since all the pieces have been moved into more meaningful objects.

As of right now I can't instantiate EncoderWrapper into a test harness because it requires a reference to an Encoder. ... How can I fix this?

By creating an interface just like you did for MotorController. The real implementation will call the external library, and you can create one or more fake implementations that will simply return canned responses for your tests.

4
  • Awesome, thanks so much for this answer it was extremely helpful. One question I have is how do I handle the onion-style class creation? Specifically, Cart requires an EncoderWrapper which requires an Encoder. So it would essentially look like Cart(new EncoderWrapper(new Encoder)). Is this good practice? Should I be handling this in a different way?
    – Hunter
    May 16, 2020 at 17:20
  • @Hunter: Yes, you've basically described dependency injection, which is considered a best practice. For an embedded system such as yours, you can probably just do it by hand in your main function. In larger applications, you might use a DI framework.
    – casablanca
    May 16, 2020 at 19:52
  • Would you be able to clarify what you mean when you say "by hand"? I planned on doing something like Encoder = ...; EncoderWrapper = ....; Cart(EncoderWrapper);. That is, instantiate each object and then pass them in where needed, instead of doing it all inside the constructor in one-go like in my first comment.
    – Hunter
    May 18, 2020 at 12:46
  • "By hand" simply means you're writing the instantiation code manually, and what you've described is totally fine. I was just trying to point out that there also exist libraries to aid in wiring up application components, but that's probably overkill for your scenario.
    – casablanca
    May 19, 2020 at 2:41
1

Not a full-fledged answer. But since you are designing an embedded system (and by coincidence I just worked on an Arduino supported pendulum): These are usually large state machines. The class design is rather trivial since the controllers have anyway similar interfaces. So that's the minor part. The big one is the desgin of the state machine. And of course since you are dealing with a real-time application you will find that you can not always follow the gods of good design in favor of needed performance :-/

Regaring test: This is especially tricky for RT applications. By adding test you distort the behavior of the system. E.g. I added a fast buffer logger that could eventually be read post mortem. Doing fancy debugging is simply not possible since physics does not allow to stop motion when you want to look at it.

2
  • How much does changing the structure from C to C++ change the performance? Is there a way I can measure that? Also, this isn't a state machine. My system stabilizes the pendulum using an LQR controller, so there is in fact separate objects with their own responsibilities. Lastly, I wouldn't be running tests during the real time scenario, I would create fake classes for things like Encoder and use that to test locally on my computer.
    – Hunter
    May 16, 2020 at 16:56
  • Changing between C and C++ would not really affect pure calculation performance. It's just that C++ can create objects from language constructs. You can simply measure performance using the HW clock. Simulation is fine, but be warned that reality is different. Reacting on sensors can be time critical and influence from an inert motor might give you other head aches.
    – user188153
    May 16, 2020 at 18:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.