We recommend following the public account below to learn more about electronic technology knowledge!
The idea of layering is not a mysterious concept; in fact, many engineers working on projects already use it themselves. I have seen many posts that do not mention this, yet the layered structure is indeed very useful, and once you understand it, you will have an epiphany.
If I don’t understand how to drive an LCD, that’s easy; I can look at the datasheet and refer to someone else’s program, and I can quickly get it done. However, if I do not understand the principles of program design, it will bring a lot of confusion during the project development process.
I have referenced various embedded books available on the market, such as MCS-51, AVR, ARM, etc., but I have not found any that introduce design thinking. Even if there are, they are rare. Writing programs is not difficult, but writing good and efficient programs requires experience. The principles of structured and modular program design are fundamental requirements.
However, how do we apply this abstract concept in engineering practice? It requires experiencing hardships during project development, summarizing some things, and abstracting them into theories, which greatly benefits the accumulation of experience and the dissemination of technology. Therefore, I will humbly summarize some points.
From my personal experience, there are two design philosophies that are very important.
One is the “time slice round-robin design philosophy,” which is very useful for solving multitasking problems in practice. This can typically be used to determine whether someone is a microcontroller learner or a microcontroller engineer. This must be mastered (will be introduced later). The second is the “layered masking design philosophy,” which is the layered thinking. Below, I will use the keyboard scanning program as an example to introduce what I want to discuss today.
The microcontroller learning board is generally designed simply, with keys well assigned, for example, the entire 4*4 keyboard matrix is assigned to port P1, with 8 control lines, just right. In this case, the program is also very easy to write. You only need to simply do: KEY_DAT = P1;
The data from the port is read in.
Indeed, reality is not so good. In practical project applications, the multiplexing of microcontroller pins is quite significant, which is very different from those so-called microcontroller learning boards.Another reason is that the general design process is “software cooperating with hardware”; simply put, first determine the hardware schematic and wiring, and only then develop the software, because modifying hardware is relatively troublesome, while modifying software is easier. This is the traditional yin-yang balance philosophy in China. Hardware design and software design are inherently a relationship of fish and bear’s paws; you cannot have both. Making hardware design convenient may cause significant trouble for writing software.
Conversely, making software design convenient may also make hardware design quite troublesome. If both hardware and software design are made convenient, there are only two possibilities: either the design scheme is very simple, or the designer has reached a very high level. We will not consider so many situations; let’s purely look at the problem from the perspective of common practical applications.For the convenience of wiring, hardware often may allocate IO ports to different ports; for example, as mentioned above, the 4*4 keyboard has 8 wires allocated to P0, P1, P2, and P3. Therefore, the scanning keyboard program of the development board can go to hell. How to scan the keys? I remember when I first started learning, I had to write three very similar programs to scan each key one by one…
Perhaps someone is unwilling, “Those things I spent a long time learning and using well, how can you say not to use them?” Although it’s a bit cruel, I still want to say, “Brother, accept reality; reality is cruel…”However, the difference between humans and lower animals is that humans can create. When faced with difficulties, we will find ways to solve them, and thus we begin to ponder…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.This way, the key scanning program is divided into three levels:
1) The lowest level is the hardware layer, which completes port scanning, 20ms delay for debouncing, and maps the port data to a KEY_DAT register, where KEY_DAT serves as an interface for 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 underlying hardware is wired, in the driver layer, we do not need to care; we only need to care about what the value of the KEY_DAT register is. This indirect effect is that it “masks the differences in the underlying hardware,” so the programs written in the driver layer can be universally applied.The other function of the driver layer is to provide a message interface for the upper layer. We use a concept similar to the message in window programs. Here, we can provide some key messages, such as: key press message, key release message, long press message, and step message during a long press, etc.3) The application layer. This is where we write key function programs based on different project requirements, belonging to the top-level program. It uses the message interface provided by the driver layer. The idea of writing programs in the application layer is that I do not care how the lower layers work; I only care about the key messages. When key messages come, I execute functions, and when there are no messages, I do nothing.Below, I will use a simple and commonly used example to illustrate the application of this design philosophy.
When adjusting the time of a stopwatch, it is required to hold down a certain key to continuously increase the time. This is very practical and widely used in actual household appliances.
Before looking at the following things, everyone can think about whether this is difficult? I believe everyone will answer loudly, “Not difficult!!” However, I will ask again: “Is this troublesome?” I believe many people will definitely say, “Very troublesome!!” This reminds me of the messy structure of the program I wrote when I first learned microcontrollers. If you don’t believe it, you can write one using 51; that will allow you to better appreciate the superiority of the layered structure mentioned in this article.
Two keys are assigned to P10 and P20, respectively, which are the “Add” and “Subtract” keys, requiring that when the key is pressed for a long time, it implements continuous addition and continuous subtraction functions.
Assuming the key is pulled up, when there is no key pressed, it is high-level, and when the key is pressed, it is low-level. In addition, to highlight the problem, I have not written the debounce program here; it should be added in actual projects. The C language function parameter passing varies; here, as an example, I used the simplest global variable to pass parameters. Of course, you can also use unsigned char ReadPort(void) to return a read key result, or even void ReadPort(unsigned char* pt) to use a pointer variable to pass the address to modify the variable directly. The methods are diverse, 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 low-level, then P1 & KEY_PLUS results in 0 (xxxx xxx0 & 0000 0001), satisfying the if condition, and entering KeyDat |= 0x01, which sets bit0 of KeyDat to one, meaning that KEY_PLUS is mapped to bit0 of KeyDat.KEY_MIN is similarly mapped to bit1 of KeyDat; if bit0 of KeyDat is 1, it indicates that KEY_PLUS is pressed, and vice versa.There is no need to think of it as mysterious; mapping is just that. If there are other keys, use the same method to map them all to KeyDat.2) Writing the driver layer program
If you think of KeyDat as the P1 port, then isn’t this the same as the standard scanning program of the learning board? Correct, this is the purpose of the low-level mapping.3) Writing the application layer program
According to the message, the hardware layer must be separated; however, the requirements for the driver layer and application layer are not so strict. In fact, some simple projects do not need to separate these two layers; they can be flexibly addressed based on practical applications.
In fact, writing programs this way is very convenient for portability; by slightly modifying the hardware layer’s ReadPort function according to the different boards, the driver layer and application layer can often use a lot of code without modification, which greatly improves development efficiency. Of course, this key program will have certain issues, especially when encountering mixed use of normally closed keys and momentary keys. This is left for everyone to think about; anyway, problems can always be solved, although the methods may vary in quality.
2. Time Slice Round-Robin Design Philosophy
Let’s start with a small example to introduce today’s topic. Imagine a basic home appliance control board, which will certainly include: LED or seven-segment display, keys, and outputs from relays or thyristors in more or less quantity. The seven-segment display needs dynamic scanning every 10ms to 20ms, and the keys also require about 20ms of debounce delay. Have you realized that these times are actually happening simultaneously?
Recall how our textbooks taught us to debounce keys? Yes, 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, then how will other tasks be handled? What about the dynamic scanning of the seven-segment display?
The only solution is to perform the scanning after the key scanning; the result is that the seven-segment display will certainly flicker, as the scanning time is too long, and shortening the debounce time for the keys does not solve the problem. Imagine if we have many other tasks that need to be done simultaneously?
One solution is today’s topic, the time-sliced scanning philosophy. Of course, this is not the only solution; however, I have been using it and find it to be an excellent philosophy to solve many practical problems. Boldly stating, the time-sliced scanning philosophy is also one of the core ideas in microcontroller programming; whether you believe it is up to you.
Implementation of Core Ideas:
There are actually several stepsFirst, use RTC interrupts for timing. I prefer a short interrupt time of 125us, which is required to decode infrared remote control codes. RTC timing is quite accurate, so it should be utilized as much as possible.Second, place three (quantity can be defined) timers in the RTC interrupt service routine (simply put, these are counters). My preference is for 2ms, 5ms, and 500ms as the base times provided for the entire system to call, so they must be accurate. In practice, you can use an oscilloscope to adjust them, which is not difficult.Third, place a dedicated time handling subroutine in the main program loop. (Note: The microcontroller will not stop; it continuously runs in a loop. This is somewhat different from what we learned in school; I was asked about this in an interview…). All timing handling is 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, and many need to work simultaneously and asynchronously. If each were handled separately, it would be quite cumbersome.Fourth, “the program runs to wait, not stands to wait”; this saying seems a bit profound. An engineer who joined the company with me mentioned this issue, and I believe it is also a relatively important idea in time-sliced systems, thus I call it that. Below, I will let the program speak; the comments should be as detailed as possible. You can understand the program without looking at the code, just by reading the comments.
(1) Interrupt Service Program Part:Interrupts every 125us
————Generating several base times—–
(1) The ref_2ms register keeps decrementing by 1 with each interrupt. It decrements 16 times, so the elapsed time is 125us × 16 = 2ms; this is the so-called timer/counter. Thus, we can use a system-wide RTC interrupt to achieve many required timing intervals.(2) Set the 2ms timing end flag, which is used by the time handling program; this is a framework for a timer. The 5ms timing is completely identical.This program also uses a block framework, which is quite convenient, but it is not related to today’s topic. The above program is the timer in the interrupt service program, which respectively times 2ms, 5ms, and 500ms. The overflow is recorded by the flag_time flag, and the program can read this flag to know whether the timing has reached.
(2) Now let’s look at the unified time service subroutine
Above, I used the key debounce timer of 20ms as an example; upon understanding, you can find that we can completely mimic that timer and place many timers below, each counting simultaneously every 5ms. Whoever finishes counting first will turn off itself and set the corresponding flags for other programs to call, without affecting the other timers!
Thus, we can place many timers here; generally speaking, having ten or twenty is not a problem, fully meeting a microcontroller system’s need for multiple timing intervals.The structure of a single timer is quite simple: first, check whether the timing flag is allowed to enter timing; then a dedicated register increments or decrements by 1. After adding or subtracting the corresponding value, when the respective time arrives, the timer is turned off, and the corresponding flags are set for use.At this point, we can achieve the timing we need; isn’t this very convenient? Let’s see what the microcontroller is doing during this time!
With the interrupt timing enough for 5ms or 500ms, the overflow flag is only valid then, allowing entry into the above timing program. Other timings are doing other tasks. Moreover, when entering the above timer, it is not stuck in a dead loop; it simply increments or decrements a register and exits shortly, consuming very little time, perhaps around 5us to 20us, which does not affect the execution of the main program.
(3) Now let’s look at how to call it specifically
We previously discussed the key debounce timing handling issue; now we will use the above method to see how to solve the problem. Key handling is also important foundational knowledge, but it is not within the scope of this discussion, so we will only discuss how to solve the timing problem. We can continue discussing the key issues next time, hoho~~~
It’s roughly like this: determine when there is a key; if not, jump out; if there is, start the debounce timing. On the second entry, directly control the judgment of whether the time is sufficient by the flag bits.Again, waiting here is the last point mentioned; we are running to wait, not standing to wait. Compared to dead loop timing, what is the microcontroller doing during the time before the debounce reaches 20ms?
In a dead loop, it will definitely be waiting in place, doing nothing. However, looking at the program above, it only checks whether the timing is sufficient; the specific timing is handled in the unified time subroutine. If the time has not reached, it jumps out and continues executing other programs until the timing arrives, at which point the microcontroller determines that flag_delay and key_flow meet the conditions and begins processing the key. During this time, the microcontroller can execute other tasks, merely checking once in the main loop.
(4) Let’s look at my main program loop body
This is the loop body I used; all functionalities are written in subroutine form, which makes it convenient to attach when needed. This way, in a total loop body, the microcontroller continuously executes this loop. If the entire program adopts the aforementioned time-sliced scanning philosophy, the time taken for one loop return is quite short. Isn’t this somewhat similar to the computer’s philosophy?
The computer, no matter how fast, does not process multiple tasks simultaneously; rather, it processes one at a time at a very fast speed, creating the illusion that it is processing multiple programs at once. I want to express that this is the essence of the idea.
In my view, with this idea supporting it, programming the microcontroller becomes relatively easy; the remaining task is to focus on using the program to implement our ideas. Of course, this is just one feasible method; it does not mean that there is only this method. If anyone has good ideas, please share them! Writing programs is an art; it is easy to write them, but writing them well and elegantly is quite difficult.
Differences between MOSFETs and IGBTs
Several common communication interfaces for microcontrollers, such as I2C, SPI, UART, etc.
Can microcontrollers replace PLCs?
How to learn microcontrollers from scratch? A beginner’s progression path for reference.
How to understand the rail-to-rail characteristics of operational amplifiers? This article will open the door for you!
What are the differences between SRAM and DRAM?
Click “Read the original text” to see more shares. Welcome to share, collect, like, and view.