Human-Machine Interface Prototyping Strategies for Embedded Systems
2026-04-06 01:03:03··#1
To simulate a human-machine interface (HMI) before the target hardware is completed, design engineers need to build an HMI prototype on a PC using software. This article analyzes the tools, languages, and coding styles used in building HMI prototypes, as well as the interface issues between files written in different languages, providing valuable guidance for simulation designers. Building an HMI prototype helps design engineers understand the interface's requirements and usability early in the design process. Below, we will explore a method for building an HMI prototype on a PC when the target hardware is far from being realized. The main purposes of building such prototypes are twofold: 1. To allow other members of the same design team to see how the device works. When designing an interactive device on paper, judging whether the described interactivity can be practically achieved requires considerable imagination. Building a working prototype clarifies the situation and allows more observers to comment on the planned interface design. Often, experimenting with interface prototypes also helps design engineers determine how many buttons, LEDs, digital displays, or text displays the actual hardware design needs. 2. To use the interface prototype to write software for the HMI when the hardware is not operational. To achieve this, the interface prototype appearing on the PC monitor must be controlled using C, C++, or other languages suitable for embedded development. For other parts, we can assume C is the language used for the final target hardware. Then consider which part of the software needs to be simulated. In the simplest case, the software could be used to turn an LED on or off, or to output a string to a small character display. Controlling physical elements on a human-machine interface is a very common function, so the advantage of being able to write such software on a PC is negligible. Turning an LED on or off might only require one line of code, and displaying a text string on an LCD text display might only require calling a 10- or 20-line function. The real difficulty lies in writing the software to decide whether to turn the LED on or off, and what string to display. For example, when a measured sensor value consistently exceeds a warning threshold for a period of time, and a set of conditions that make the warning effective are met, the software might choose to turn the LED on. Similarly, when a user presses a button to select the next item in a menu, the software might consult a string table and an operation table describing the menu to determine which item should be displayed next. Software like control menus can have code lengths exceeding those of the underlying software. In this example, our goal is to write a simulation software for text display and LED control to represent changes on a PC screen. We can write alert check code and menu control code that runs on both the PC and the target device. This simulation method isn't new, but it's commonly used when writing software for target devices like PDAs and game consoles that don't have their own development environments. Tools Required for Writing Simulation Software Displaying a few buttons and two lines of text on a PC using Visual Basic isn't difficult, but interfaceing that prototype with C code becomes quite cumbersome. Many prototyping tools for embedded development exist, often forcing design engineers to rely on their event models, leading to excessive tool dependency. While these tools can generate code if the design engineer follows their interface design style, they lack sufficient flexibility across platforms, and the generated code may not be suitable for small microcontrollers. The tool I used was Borland C++ (hereinafter referred to as CPB). Borland C++ isn't specifically designed for embedded systems, but I've found it well-suited to design needs, and using Borland C++ doesn't restrict the design to any particular processor or software architecture. CPB has a set of predefined graphical components, most of which are not designed for embedded projects but for desktop applications (like dropdown menus). However, a small sub-component is available for the purposes discussed in this article. UI elements like LEDs can be simulated using images. CPB comes in three editions: Standard, Professional, and Enterprise. The Standard edition is sufficient for the interfaces we'll be discussing. Buttons, sliders, labels, and other UI elements can be inserted into a table (a simple dialog window) using a drag-and-drop environment. Creating such a table generates a framework of C++ classes. For example, whenever a user clicks an image or moves a slider, a set of events is generated, and each element in the table has its own set of events corresponding to it. The programmer chooses which events to respond to. These responses are written as member functions of the class generated by the table. If the front panel was designed by an industrial design team, then the entire display image would be available. Alternatively, if a physical prototype already exists, a digital photograph of that prototype can be used as a background. I use image targets (also called Timages in CPB) to display most physical components. Using image targets allows for the import and display of bitmaps. For example, an image of a light-emitting diode (LED) can be imported. In this application, an interface prototype containing five buttons and four LEDs is displayed, as shown in Figure 1. The LEDs in the background image are in the off state. Once the software decides that one of the LEDs should be turned on, the visibility property of that LED image is set to true, and the image of the lit LED covers the image of the unlit LED. With this simple trick of overlaying multiple images, we can simulate other parts of a physical display. For example, suppose we use the CPB IDE to create a label containing the word "ALARM" and name this element AlarmIndicator, then we can write a function to control it: void setAlarmState(Boolean state) { PanelForm->AlarmIndicator->Visible = state; } The panel form contains all the graphical objects we use in the simulation. Alarm-Indicator is the name we assign to a label after placing it on the panel table. When we add the label to the table by dragging and dropping it into the table window, it becomes a data member of that table. In CPB, all properties of an element on the display can be used as public data members representing the class of that element. Therefore, the Visible property can be changed with a simple assignment operation. Public data members can be changed anywhere in the program through assignment. In CPB, each property also has its own special state, allowing the property to be changed in the IDE through this state. Developers can click on a label and set the Visible property in the property window. The displayed color and font can also be changed in a similar way. Now let's look at a setAlarmState() program, which is used to drive CPB-based simulation. The following code is CPB-specific code and will not run on the final target. Before long, we will have to write another version of this function for the target interface, in the following form: `void setAlarmState(Boolean state) { if (state) { ledRegister |= 0x02; } else { ledRegister &= ~0x02; } }` Sometimes, programming style can lead to small functions incurring function call overhead. This issue is more concerning in smaller systems, and some of these functions can be written as macros or inline functions. I usually only begin this type of optimization in the final stages of a project. Code Organization If we have written two versions of the `setAlarmState()` function, we must ensure that only one is compiled at a time. One way to achieve this is to use the CPB code until the target hardware is designed, and then replace all the CPB-specific code with target-specific code. If we do this, then we will no longer be able to run simulations after we begin developing the target hardware. The reader may think this is not a problem, but in fact, simulations are useful even after the hardware design is complete. For example, the PC-based debugging environment in simulation is often better than the development environment on the target hardware. This is because the download speed on the target hardware may be slower, or a one-time programmable chip must be reprogrammed every time the software is modified. Furthermore, the debugging environment on the target hardware may not support single-step debugging and breakpoint debugging. Even if the debugging environment on the target hardware is better, PC simulation still has other advantages. Developers can email the .exe file to colleagues in different locations to obtain their feedback. Once developers decide to keep two versions of functions throughout the entire project development cycle, separating them is easy. Macros can be defined under Project/Options in CPB. I usually define USING_CPB and then use a #ifdef in my source code to distinguish between different function versions. Another way to distinguish function versions is to store the target code and simulation code in different files, but share the same header file to ensure that they use the same set of function tags. The CPB environment is a C++-based environment, but many embedded targets hardly support C. In this case, developers can only use a subset of C++ supported by the cross-compiler in the shared code, which is not as difficult as one might imagine. One way to solve this problem is to compile code specifically for the embedded target, even if there is currently no hardware available to run it. This brings to mind features that are available on a PC but might be invalid on the target hardware. For example, some smaller processors do not support recursion. Furthermore, inspecting the software with an embedded compiler can quickly identify CPB-specific code that might accidentally be included in the target executable. I personally found this method very useful for tracking software size, as the CPB library is so large that it completely distorts the program size, making the size given during PC compilation inaccurate. Three types of code are used here. Some are CPB-specific code that can only be compiled on a PC; some are target-specific code that can only be compiled on the target platform; and the rest are public code that should run on both PC and target platforms. Ideally, each source file should contain only one type of code. The design engineer's IDE or Makefile should allow them to choose which files to include each time the executable is created. It is recommended that all CPB-specific files be named .cpp, and all target-specific and shared files be named .c. This way, during compilation in the target environment, only files with the .c extension will be compiled, not those with the .cpp extension. However, if the design engineer follows this style, another problem arises during compilation in the CPB environment. The CPB environment assumes .c files are written in C code and .cpp files are written in C++ code. When a call occurs from one file to another, a linking error will occur due to the different way C++ generates broken function names. We can circumvent this problem by using the "extern C" construct, but this is cumbersome, especially when the call occurs from C to C++ or from C++ to C. A flag could be set for the Borland compiler to tell it to compile all filenames as C++ files, regardless of the file extension. Unfortunately, such a flag is not available in the IDE. Therefore, we must manually edit the project configuration file to achieve this functionality. Code Example Readers can find an executable file, five.exe, at www.panelsoft.com/cpb. This file contains a line of five buttons and a set of LEDs. Pressing any of the first four buttons will turn on the corresponding LED. The fifth button is the RESET button; pressing it will turn off all LEDs. Of course, simulation is not required when building such a project. However, this example aims to illustrate that with an initial interface image, simulation can easily achieve results similar to those of a real device with minimal effort. This example also demonstrates that the code in the key.c module can run in both the target environment and the simulation environment, and that the code does not require any conditional code to run due to differences between the two platforms. All source code and initial bitmaps used to build this application can be downloaded from this site. Building such a simulation requires design engineers to have some C++ knowledge, and learning the CPB development environment also takes time, especially if the design engineer has never used this object-oriented, event-driven environment before. However, once a simulation is established, other tasks can be performed using the same steps. Design engineers with prior experience writing PC-based programs that utilize GUIs will find this experience helpful in learning CPB. I previously used such a program to complete a simple download application, enabling serial communication with an embedded target.