Source: Internet
After reviewing various embedded books available in the market, including MCS-51, AVR, ARM, etc., I found that none specifically introduce design principles, and even if there are, they are quite rare.Writing code is not difficult, but writing good and efficient code requires some experience.The idea of structured and modular program design is the most basic requirement.However, how do we apply this abstract concept to engineering practice?It requires experiencing hardships during project development, summarizing some aspects, and abstracting them into theories, which greatly benefits the accumulation of experience and the dissemination of technology.So, I would like to share some summaries based on my experience.
From my personal experience, there are two very important design principles.
The first is the “time-slice round design principle,” which is very useful in solving multitasking problems in practice. It can typically be used to distinguish whether someone is a microcontroller learner or a microcontroller engineer. This must be mastered. (It will be introduced below).The second is the “layered masking design principle,” which is the layered concept. Below, I will use the keyboard scanning program as an example to introduce today’s topic.
Layered Thinking
The concept of layering is not mysterious; in fact, many engineers working on projects will use it themselves.I’ve seen many posts that do not mention this concept, yet the layered structure is indeed very useful, and understanding it can lead to a sense of enlightenment.If I don’t understand how to drive an LCD, that’s manageable; I can look at the datasheet and refer to other people’s code to quickly implement it.However, if I don’t understand the principles of program design, it will bring a lot of confusion during the project development process.
▎Problem Statement
Microcontroller learning boards usually simplify things by properly assigning keys; for example, an entire 4*4 keyboard matrix is assigned to port P1, with 8 control lines, just right.This makes the program very easy to write.All that is needed is a simpleKEY_DAT = P1; to read the data from the port.Indeed, reality is not so straightforward.In actual project applications, the pin multiplexing of microcontrollers is quite significant, which differs greatly from those so-called microcontroller learning boards.Another reason is that the general design flow is “software cooperating with hardware”; simply put, the hardware schematic and wiring are determined first, and only then is software development done, as modifying hardware is relatively cumbersome, while software is easier to modify.This aligns with the traditional Chinese philosophy of yin and yang balance.Hardware design and software design are inherently at odds; making hardware design easier can create significant challenges for software development.Conversely, making software design easier can lead to considerable difficulties in hardware design.If both hardware and software design are made easier, there are only two possibilities: either the design solution is very simple, or the designer has reached a very high level of expertise.Without considering too many scenarios, let’s look at the problem purely from the perspective of common practical applications. To facilitate wiring, hardware often assigns I/O ports to different ports; for example, the aforementioned 4*4 keyboard may have its 8 wires assigned to ports P0, P1, P2, and P3.In that case, the scanning keyboard programs for development boards can be disregarded.How do we scan the keys?I remember when I first started learning, I had to break it down into three very similar programs for scanning each key…Perhaps some people are reluctant, saying, “Those things I spent a long time learning and using well, how can you say they are useless?”Although it sounds harsh, I still want to say, “Brother, accept reality; it is cruel…”However, what distinguishes humans from lower animals is our ability to create and find solutions when faced with difficulties, leading us to reflection…Finally, we introduce the concept of “mapping” learned in middle school mathematics to solve the problem.The basic idea is to map keys from different ports to the same port.Thus, the key scanning program is divided into three layers.1) The lowest layer is the hardware layer, which completes port scanning, 20ms delay for debouncing, and maps the port data to a KEY_DAT register, with KEY_DAT serving as an interface to the upper driver layer.2) The middle layer is the driver layer, which only operates on the values of the KEY_DAT register.Simply put, regardless of how the hardware is wired at the lower level, the driver layer does not need to care; it only needs to focus on the value of the KEY_DAT register.This indirect effect “masks the differences in underlying hardware,” allowing programs written in the driver layer to be reused.Another function of the driver layer is to provide a message interface for the upper layer.We use a concept similar to window messages here.Some key messages can be provided, such as: press message, release message, long press message, and long press step message, etc.3) The application layer. This is where key function programs are written according to different project requirements, belonging to the top layer of programs.It uses the message interface provided by the driver layer.The idea of writing programs at the application layer is that I do not care how the lower layer works; I only care about key messages.When a key message comes, I execute the function; when no message comes, I do nothing.Below is a simple and commonly used example to illustrate the application of this design principle.When adjusting the time on a stopwatch, it requires holding down a certain key to continuously increase the time.This is very practical and widely used in household appliances.Before looking at the following, everyone can think about whether this is difficult.I believe everyone would answer loudly, “Not difficult!”However, I will ask again:“Is this troublesome?”I believe many people would definitely say, “Very troublesome!”This reminds me of when I first learned microcontrollers and wrote such key programs with chaotic structures.If you don’t believe it, try writing it with 51; you will better appreciate the superiority of the layered structure discussed in this article.
▎Project Requirements:
Two keys, assigned to P10 and P20, respectively, are the “increase” and “decrease” keys, requiring continuous increase and decrease functions when long-pressed.
▎Practical Application:
Assuming the keys are pulled up, with a high level when no key is pressed and a low level when a key is pressed, in addition, to highlight the problem, the debounce program is not included here; it should be added in actual projects.There are various ways to pass parameters in C language functions; here, for simplicity, I used global variables to pass parameters. Of course, you could also use unsigned char ReadPort(void) to return a key reading result, or even void ReadPort(unsigned char* pt) to use a pointer variable to pass the address to directly modify the variable.There are many methods, depending on each person’s programming style.
1) Start writing the hardware layer program to complete the mapping
#define KEY_MIN 0x01
#define KEY_PLUS 0x01
unsigned char KeyDat;
void ReadPort(void) {
if (P1 & KEY_PLUS == 0) { KeyDat |= 0x01; }
if (P2 & KEY_MIN == 0) { KeyDat |= 0x02; }
}
C language should be easy to understand, right? If KEY_PLUS is pressed, the P10 port reads a low level, so P1 & KEY_PLUS results in 0 (xxxx xxx0 & 0000 0001), satisfying the if condition, entering KeyDat |= 0x01 sets bit 0 of KeyDat to one, meaning that KEY_PLUS is mapped to bit 0 of KeyDat.KEY_MIN is mapped to bit 1 of KeyDat in the same way.If bit 0 of KeyDat is 1, it indicates that KEY_PLUS is pressed, and vice versa.There’s no need to think of it as very mysterious; mapping is just like that.If there are other keys, use the same method to map them all to KeyDat.2) Write the driver layer program If we imagine KeyDat as port P1, then isn’t this the same as the standard scanning program of the learning board?That’s right; this is the purpose of the underlying mapping.3) Write the application layer program
The hardware layer must be separated, but the requirements for the driver layer and application layer are not so strict; in fact, for some simple projects, it is unnecessary to separate these two layers; it can be flexibly handled according to practical applications.In fact, writing programs this way is very convenient for portability; by appropriately modifying the hardware layer’s ReadPort function according to the board’s differences, much of the code in the driver layer and application layer can be used directly without modification, which greatly improves development efficiency.Of course, this key program will have certain issues, especially in scenarios where normally closed keys and momentary keys are mixed. This is left for everyone to think about; after all, problems always have solutions, even if some methods are better than others.
Time-Slice Round Design Principle
Let’s start with a small example to introduce today’s theme. Imagine a basic home appliance control board, which will inevitably include: LEDs or digital tube displays, keys, and outputs of relays or thyristors.The digital tube requires dynamic scanning every 10ms to 20ms, and the keys also need about 20ms of debounce delay. Have you realized that these times are actually happening simultaneously?Recall how our textbooks taught us to debounce keys?That’s right, a dead loop, absolutely a dead loop, using instructions to time.This naturally raises a question: if the microcontroller is stuck in a dead loop, what about other tasks?What about the dynamic scanning of the digital tube?It can only wait until the key scanning is done, resulting in flickering of the digital tube; shortening the debounce time is not a solution. Imagine if we have many other tasks to perform simultaneously?One solution is today’s theme, the idea of time-slice scanning.Of course, this is not the only solution; it is just something I have been using and find to be a very good idea that can solve many practical problems.Let me boldly state that the idea of time-slice scanning is also one of the core concepts in microcontroller programming; whether you believe it is up to you.
▎Implementation of Core Ideas
Actually, it involves several steps:First, use RTC interrupts for timing; I prefer a short RTC interrupt time of 125us, needed for decoding infrared remote control signals. RTC timing is quite accurate, so it should be utilized as much as possible.Second, place three (number can be adjusted) timers in the RTC interrupt service routine (essentially counters). My preference is 2ms, 5ms, and 500ms as reference times provided for the entire system to call, so they must be accurate. Adjusting with an oscilloscope is sufficient; it’s not difficult.Third, insert a dedicated time handling subroutine in the main program loop. (Note: Microcontrollers do not stop; they are always running in a loop, which differs from what we learned in school; I was asked this question in an interview…)All time handling should be done in the time handling subroutine, which is very convenient. A microcontroller system needs to handle at least 10 to 20 different times, requiring 10 to 20 timers, many of which need to work asynchronously. If each one is handled separately, it will be quite troublesome.Fourth, “Programs run to wait, rather than stand still to wait.” This may sound a bit cryptic; an engineer I discussed this with at my company mentioned this, and I think it is also a relatively important concept of the time-slice system, hence the name.Fifth, let’s illustrate with code; the comments should be detailed enough that you can understand without looking at the code.▎(1) First, the interrupt service routine part:Interrupts every 125us
——————- Generate several reference times —————————
(1) The ref_2ms register continuously decrements by 1 every interrupt, reducing 16 times, so the elapsed time is 125us × 16 = 2ms, which is the so-called timer/counter. Thus, we can use one system’s RTC interrupt to achieve many timing needs.(2) Set the 2ms timing end flag, which is used by the time handling program; this is a framework for a timer. The 5ms timer works in exactly the same way.This program also uses a block framework, which is quite convenient, but is not related to today’s topic; I will write about this when I feel frustrated later. The above program is the timer in the interrupt service routine, timing 2ms, 5ms, and 500ms, with overflow flags recorded by flag_time. The program can read this flag to know when the timing has reached.▎(2) Now let’s look at the unified time handling subroutine
Above, I used the key debounce timer of 20ms as an example. If you understand it, you will find that we can completely mimic that timer and place many, many more timers below; each counts as it comes in every 5ms. Each timer counts simultaneously, and whoever finishes first turns itself off, setting the corresponding flag for other programs to call, without affecting the other timers! Thus, we can place many timers here; generally, ten or twenty is not a problem, fully meeting the needs of a microcontroller system for multiple timing requirements.The structure of a single timer is quite simple; first, check whether the timing flag is allowed to enter timing, and then a dedicated register increments or decrements by 1. After adding/subtracting the corresponding value, when the time arrives, the timer turns off and sets the corresponding flags needed.At this point, we can generate the required timing; isn’t this very convenient? Let’s take a look at what the microcontroller is doing during this time. Only when the interrupt timing reaches 5ms or 500ms, the overflow flag becomes valid, allowing entry into the above timing program. At other times, it is performing other tasks. Moreover, when entering the above timer, it is not stuck in a dead loop; it merely increments or decrements the register and exits, consuming extremely little time—about 5us to 20us—without affecting the execution of the main program.▎(3) Now let’s see how to call it specificallyThe key debounce timing handling issue discussed earlier is now addressed using the method introduced above. Key handling is also an essential foundational knowledge, but it is beyond the scope of this discussion, so we will only discuss how to solve timing issues, and we can continue discussing key-related issues next time.
Roughly speaking: determine when a key is pressed. If not, exit; if yes, start the debounce timing. When entering for the second time, directly use the flag to control whether the timing is sufficient.Similarly, in waiting, this is the last point mentioned: we are running to wait, not standing still to wait. Compared to dead loop timing, what is the microcontroller doing during the time before the timing reaches 20ms? In a dead loop, it would certainly be waiting in place, doing nothing, but look at the above program; it merely checks whether the timing is sufficient. The specific timing is handled in the unified time subroutine. If the timing has not reached, it exits and continues executing other programs until the timing is reached, and the microcontroller determines that the conditions of flag_delay and key_flow are met, then begins the key handling program. During this time, the microcontroller can run other programs without wasting time on debouncing.
▎(4) Let’s take a look at my main program loop
This is the loop body I use, with all functions structured as subroutines, making it convenient to call as needed. This way, in a general loop body, the microcontroller continuously executes this loop. If the entire program adopts the time-slice scanning idea mentioned above, the time for one loop cycle is quite short; doesn’t it resemble the concept of a computer? A computer, no matter how fast, does not process multiple tasks simultaneously; rather, it processes one at a time at a very rapid speed, giving us the impression that it is handling multiple programs at once. I think that’s what I ultimately want to express.
I’ve rambled on, and I don’t know if it makes sense or if anyone will read it. I wonder if I misunderstood the concept of time-slice scanning? In my view, with this idea supporting it, programming for microcontrollers becomes relatively easy to grasp; the remaining task is to concentrate on using programming to implement our ideas. Of course, this is just one feasible method, not the only one. If anyone has better ideas, please share them! Writing programs is an art; it’s easy to write, but writing well and elegantly is quite challenging.
-END-
Recommended Reading
