Embedded Systems: Creating a System Architecture

A system architecture diagram is a good way to start viewing and designing the software. This would result in both a hardware diagram and a draft schematic. Unstable hardware leads to buggy software. We need to work out the system, software, and hardware architectures in order to design this. 


The 3 diagrams to consider are the architectural block diagram, the hierarchy of the control organization chart, and the software layering view.

We can first think with an object-oriented mindset. A communication object is a processor, and each peripheral is attached to a communication object. 

Flash Memory is a relatively inexpensive memory used in many devices, which most of them will communicate through SPI protocol.

The flash box is outside the processor, acting as a peripheral,  while the SPI box is inside the processor. It is imperative to separate the main communications boxes from the peripherals. We need to consider the performance and resource contention that sharing brings. The following 2 diagrams with show the schematic and the hardware/software architecture diagrams, respectively.




The next step is to add higher-level functionality. Flash is used to have bitmaps to put on the screen. Add any other software structures (databases, state machines, algorithms). Look for poor spots with critical bottlenecks, usually represented by the boxes that change between different diagrams that can lead to a good solution.


The next type of software model enumerated here is the hierarchy of control. The MAIN is at the highest level, and we can fill in subsequent levels with algorithm-related objects. Fill out the lower-level objects that are used by higher-level objects. SPI is used by Flash object, which is used by display object, and so on. 

We don't want to combine as many functions as possible in one peripheral.


Since text and images share the Flash and stuff, we need to avoid contention. Luckily, the rendering code controls both and will likely successfully partition both of these areas. 

We need to have flash large enough to fit serial numbers. Software will have the to avoid collisions and get the extra assets. Adding more things that a parent module has to control makes the system a little more robust. Shared resources cause problem in design, implementation and maintenance phases. All shared resources should make you think about the consequences. 

The last drawing looks for layers and recommends things for size. I make the object size to depend on its complexity, and make it bigger if the object has multiple users. The box gets bigger when I have a resource that is shared by many things. 

Initially I had the rendering box much smaller, but it becomes larger once I have to control all the bits and pieces of it.

The layered view allows me to bundle groups of resources together if they are used together. We want to think of the implications of combining the boxes, and this can possibly result in a simpler design/mechanism. 

Don't have several modules try to tough a lower level module, this means we should break apart that resource. A good design can save both time and money in both implementation and maintainability.


If multiple higher level module uses a lower level module, then it's difficult since we need to splice the modules to work in different time slots. 

The next thing that I want to discuss is transferring from a diagram to an architecture. 

Encapsulation is an extremely important design principle. These are interfaces that don't depend on the content of the modules which they connect to. Our goal is to reduce complexity without compromising flexibility too much.

Look for objects that are only used by 2 objects, and consider combining these 2 objects. Think of grouping related objects into a higher level interface, and try to break apart or combine together objects weight many interdependencies. If an interface can be described in a few sentences, then what I can do is combine them. 

LCD connecting to a parallel interface is simple, and we can encapsulate this interface with the LCD module. Encapsulation makes for a cleaner architecture drawing. 


We can also apportion our work to minions. 


I want to reduce interdependencies whenever possible. If I can't reduce these, at least I want to be wary of them when coding. We can also look for things that can be split and accomplished by another person. How can we keep data safe nd usable? What defensive structure can be pictured erecting, and the amount of data that can travel through a module?


We want the minions to be boxed into their own code with their own architectures.

We can also go from bottom up. Many drivers are based on an API used to call devices in unix systems. 

The command open opens the driver for use.
The command close cleans up a driver so another subsystem can call open.
The command read reads data from the device.
The command write sends the data to the device. 
The command ioctl stands for input output control, and handles features not handled by other part of the interface. 

The driver is part of the kernel. We want to model our driver upon Unix drivers. 

These command are for specific functionality:

spi.open()
spi_open()
SpiOpen(WITH_LOCK)

spi.iotcl_changeFrequency(THIRTY_MHz)
SpiIoctl(kChangeFrequency, THIRTY_MHz)

Don't put the round peg into the square hole. This standard interface can make the design a bit easier to maintain. 

An adapter is another traditional software design pattern, converting the interface of an object into one that is easier for client, and written over APIs. The hardware interface can change without the upper level software changing. 

Drivers are also stackable. Open will call the initialization code for a subsystem, which is a self-contained system in a larger system, and then call the method in that subsystem's code. 

If interfaces of levels are consistent, then then higher level might not need to change. 

If Spi changes to I2CEEProm, the display driver or higher level functionality may not change. You can also do Automated systems as well.

The following diagram shows a consistent interface for the client. 

The definition of a module depends on the specifics of a system. We want to retain encapsulation of a the module, the minion contribution and the driver module, and work out the responsibilities of each box. 

We also want to implement a robust and usably logging system, such is the goal of the logging module. We want to code the interface considering hypothetical limitations. Logging subsystems are invaluable tools during maintenance. 

The logging interface hides the details on how logging is accomplished. There are not enough resources to get everything I want. We want to output a lot of information with a relatively small pipe. It can connect through RS-232 or be a special debug package. The data can also be another RAM source. 

The logging methods can be subsystem specific. A priority level can also debug the minutiae of the subsystem without losing critical information from all other parts of the code.  We want a complete meld when the system is acting oddly. 

The following displays interfaces for a display subsystem.



There are a lot of words for something that can be summed up in 1 line of code:

void Log(enum eLogSubSystem sys, enum eLogLevel level, char *msg)

This provides a good shorthand for other developers. The log levels include none, information debugging, warning error and critical. A subsystem is a group of interacting or interrelated entities that form a unified whole. These include communications, display, system, sensor, and updating firmware. 

The input to log contains a string, and we have to print out at least one number at a time.

void LogWithNum(enum eLogSubSystem  sys, enum eLogLevel level, char *msg, int number)

Start a subsystems at a verbose priority and raise its priority level when it is cleared. This way I get the messages I want when I want them. 

void LogSetOutputLevel(enum eLogSubSystem sys, enum eLogLevel level)

We want the interface to allow for this flexibility. The code should not have direct access to this implementation. The log initialization function should always call one of these. 

The logging function has to turn off and on and a global way, to make sure that this system is not interfering with any other system. 

void LogGlobalOn();
void LogGlobalOff();

Try to make version available for the primary communication path (UART, I2C, etc.) or a simple query A.B.C, where A is the major version (1 byte), B is the minor version (1 byte) and C is the build indicator (2 bytes). There should be a interface for displaying the version of the code properly.

void LogVersion(struct sFirmwareVersion *v)

Each piece that is built in the code should have its own version. We need to make sure that all the moving pieces are compatible. We just want to make sure that none of the interfaces depend on each other.

Finish designing the interfaces before going into the implementation. The less state held in each module, the better. There are ways to access the logging objects besides opening the modules completely.

A private variable can be described in the C programming language as a static variable. This keyword means "hide me so no one else can see this". If we want C code to be more like an object, structure would be malloc during initialization, and of course we need a functionality to free the object.


struct sLogStruct* LogInit(){
    int i; 
    struct sLogStruct *logData = malloc(sizeof(*logData));
    logData->logOn = FALSE;
    for (i = 0; i < NUM_LOG_SUBSYSTEMS; i++){
        logData->outputLevel = eNoLogging;
    }
    }
    return logData;
}

A singleton is a design pattern to make sure that every part of a system has access to the same log object. This is when it is important that a class have exactly one instance. It intercepts new object requests and the function looks like the following in C++:

class Singleton {
    public:
        static Singleton* Instance() {
            if (mInstance == 0){
                mInstance = new Singleton;
            }
            return mInstance;
}
protected:
    Singleton();
private:
    static Singleton* mInstance = 0;
}

A singleton can help with conflicts within the system. They also permit lazy allocation and initialization so that modules never used don't consume resources. This protects a module's variables, giving us a more robust solution. 

We can return a pointer to private variables only if absolutely needed.

static struct sLogStruct gLogData;
struct sLogStruct* LogInternalState() {
    return &gLogData;

and this is to guard against the pointer in normal development:

static struct sLogStruct gLogData;
struct sLogStruct* LogInternalState(){
    #if PRODUCTION
        #error "Internal state of logging protected!"
    #else
        return &gLogData;
    #endif
}

Keep in mind we also need methods to verify a system, and a good architecture is to keep the algorithm boxes as separately as possible, and we can do this by implementing the Model View Controller to separate the backend code from the user interface. 

The View is the interface to the user, which can also be hardware sensors and the screen. The model is the domain-specific data and logic. The controller handles how to get the input to the model for processing and data to display.

The model holds the state, data, and application logic. In a weather station the model is the predictive diagrams and the temperature modifying code. 

The view is the display handling functions, or what the user sees of the model, for example the picture of graphs/of a sun etc. The controller is between the model and the view, and it makes the view and model independent to help them work together. For example this is transforming a user button press into an action of the model. I test this model until any anomalies are resolved. 

In the sandbox, the files are in the view, the algorithm is tested, and the controller is the part that changes when the algorithm is in the PC. The first field is the time in a view file and the second field is the method in the system.


And the following is an efficient timebox of input view:

Time, Action, Variable arguments
0:00.00 PowerOnClean
0:01,00 PlayPressed

etc.

You may also have multiple output files to describe the state and the changes sent to the user interface. 
We can abstract the MVC to many things, including having a filename and information bitstream as the view, the logic as the model, and the handler as the controller. There is a tradeoff between building time and debugging time, but you need to figure out how to get a sandbox from design. 


A schematic is still a good way to start in terms of developing a diagram, and getting a list of code files and put these into modules. There should be both local and global modules, and the boxes may need to be bundled up as the drawing gets more complex.

One common interview question is to describe the architecture for something, and the interviewer wants to see the process of an interviewee, how he mentally deconstructs an object as inputs and outputs. Call the boxes drivers for the lowers level and objects for the next level, and try to encapsulate objects in the system that can be used easier. Ask questions about how it works, and think about the data structures. Draw something, even if it is only slightly legible. 

Comments

Popular Posts