Sharing Two Programming Philosophies for Microcontrollers

Layered Thinking The concept of layering is not a mysterious thing; in fact, many engineers working on projects are already using it. I have seen many posts that do not mention this concept, yet layered structures are very useful, and understanding them can lead to a moment of enlightenment. If I don’t understand how to drive an LCD, that’s manageable; I can look at the datasheet and reference others’ programs to get it done quickly. However, if I don’t understand the philosophy of program design, it can lead to a lot of confusion during the project process. I have referenced various embedded books on the market, including MCS-51, AVR, ARM, etc., but I have not found any that introduce design philosophies; even if there are, they are rare. Writing programs is not difficult, but writing them well and quickly requires some experience. The philosophy of structured and modular program design is the most basic requirement.Regarding articles on programming philosophyRecommendation:Programming Philosophy in Embedded Development with C Language 5️⃣ However, how do we apply this abstract concept to engineering practice? It requires experiencing hardships during project work, summarizing some things, and abstracting them into theories, which greatly benefits the accumulation of experience and the dissemination of technology. Therefore, I would like to share some insights. Based on 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 is usually a criterion to distinguish whether someone is a microcontroller learner or an engineer. This must be mastered (to be introduced later). The second is the “layered masking design philosophy,” which is the layered thinking. Below, I will use a keyboard scanning program as an example to introduce today’s topic.Problem Statement Microcontroller learning boards are generally designed for simplicity, with keys well allocated; for example, an entire 4*4 keyboard matrix is allocated to port P1, with 8 control lines, just right. This makes the program very easy to write. You only need to simply:

KEY_DAT= P1;

and the port data is read in. Indeed, reality is not so favorable. In actual project applications, the multiplexing of microcontroller pins is quite significant, which greatly differs from those so-called microcontroller learning boards. Another reason is that the general design process is “software cooperating with hardware”; simply put, the hardware schematic and wiring are determined first, and only then is software development done, because modifying hardware is relatively troublesome, while software modifications are easier. This reflects the traditional Chinese philosophy of yin and yang balance. Hardware design and software design are inherently at odds; facilitating hardware design can lead to significant troubles in software writing. Conversely, facilitating software design can also complicate hardware design. If both hardware and software designs are made easy, there are only two possibilities: either the design is very simple, or the designer has reached a very high level. We will not consider so many situations; we will purely look at the problem from the perspective of common practical applications. For the convenience of wiring, hardware often allocates IO ports to different ports; for example, the aforementioned 4*4 keyboard may have its 8 lines allocated to P0, P1, P2, and P3. Thus, the scanning keyboard programs for development boards can go to hell. How do we 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 some are unwilling to accept, “Those things I spent a long time learning and using well, how can you say they are useless?” Although it sounds a bit harsh, I still want to say, “Brother, accept reality; reality is cruel…” However, what distinguishes humans from lower animals is our ability to create. When faced with difficulties, we 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.How to Divide the Key Scanning Program into Three Layers 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, which serves as an interface to the upper driver layer. 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, the driver layer only needs to care about the value of the KEY_DAT register. The indirect effect of this is that it “masks the differences in underlying hardware,” allowing the programs written in the driver layer to be universally applicable. Another function of the driver layer is to provide a message interface for the upper layer. We use a concept similar to window program messages here. Some key messages can be provided, such as: press message, release message, long press message, long press step message, etc. The application layer is the topmost program, where key function programs are written according to different project requirements. 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 key messages. When a key message comes, I execute the function; when there is no message, I do nothing. Below is a simple common example to illustrate the use 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 household appliances. Before looking at the following, everyone can think about whether this is difficult? I believe everyone will answer loudly, “Not difficult!!” However, I ask again: “Is this troublesome?” I believe many people will 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, you can try writing it with 51; that way, you can better appreciate the advantages of the layered structure discussed in this article.Project Requirements: Two keys, allocated to P10 and P20, are the “increase” and “decrease” keys, requiring continuous increase and decrease functions when the keys are long-pressed.Practical Implementation: Assuming the keys are pulled up, with a high level when no key is pressed and a low level when a key is pressed. Additionally, to highlight the problem, the debounce program is not included here; it should be added in actual projects. The C language function parameter passing is diverse; here, as an example, the simplest global variable is used 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 directly modify the variable. There are many methods, and this depends on each person’s programming style. 1)Start writing the hardware layer program to complete the mapping

#define KYE_MIN  0X01#define KEY_PLUS  0X01unsigned 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 the result of P1 & KEY_PLUS is 0 (xxxx xxx0 & 0000 0001), satisfying the if condition, entering KeyDat |= 0x01, which sets bit0 of KeyDat to one, meaning that KEY_PLUS is mapped to bit0 of KeyDat. KEY_MIN is mapped to bit1 of KeyDat in the same way; 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)Write the driver layer program If we think of KeyDat as port P1, then this is just like the standard scanning program of the learning board, right? Yes, this is the purpose of the underlying mapping.3)Write the application layer program The hardware layer must be separated, while 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; flexibility can be applied according to actual applications. In fact, writing programs this way is very convenient for portability. By appropriately modifying the hardware layer’s ReadPort function according to different boards, the driver layer and application layer can use much of the code without modification, greatly improving development efficiency. Of course, this key program will have certain issues, especially when dealing with mixed use of normally closed keys and momentary keys. This is left for everyone to think about; after all, problems can always be solved, even though methods may vary in quality.Time-Slice Round-Robin Design Philosophy First, let’s introduce today’s theme with a small example. Imagine a basic home appliance control board, which will inevitably include: LED or seven-segment display, keys, relays, or SCR outputs in these three parts. The seven-segment display requires dynamic scanning every 10ms to 20ms, and keys also need about 20ms of debounce delay. Have you realized that these times are actually occurring simultaneously? Recall how our textbooks taught us to debounce keys? That’s right, a dead loop, absolutely a standstill dead loop, using instructions to time. This naturally raises a question: if the microcontroller is stuck in a dead loop, what about other tasks? How about the dynamic scanning of the seven-segment display? The only way is to wait until the key scanning is done, resulting in flickering of the seven-segment display. Shortening the debounce time for keys 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 method; I have been using it and find it to be a very good idea that can solve many practical problems. Boldly speaking, the time-slice scanning idea is also one of the core ideas in microcontroller programming; whether you believe it is up to you.Implementation Process of Core IdeasFirst, use RTC interrupts for timing. I prefer a short RTC interrupt time of 125us, which is necessary for decoding infrared remote control codes. RTC timing is quite accurate, so we should utilize it as much as possible. Second, place three (number can be defined) timers in the RTC interrupt service routine (essentially counters). My habit is to use 2ms, 5ms, and 500ms as benchmark times, which are provided for the entire system to call, so they must be accurate. In practice, adjust with an oscilloscope to ensure accuracy; it’s not difficult. Third, place a dedicated time-handling subroutine in the main program loop. (Note: The microcontroller does not stop; it is always running in a continuous loop, which is somewhat different from what is taught in school. I was asked this question in an interview…) All time 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 of them need to work simultaneously and asynchronously. If each were handled separately, it would be quite troublesome. Fourth, “the program runs to wait, not stands still to wait”. This may sound a bit esoteric; an engineer I discussed this with when I joined the company mentioned this, and I think it is also a relatively important idea of the time-slice system, so I call it that. Below, I will elaborate. Fifth, let’s let the program speak; comments should be as detailed as possible. You can understand the code without looking at it, just by reading the comments.First, the Interrupt Service Routine Interrupt every 125us to generate several benchmark times.Sharing Two Programming Philosophies for Microcontrollers(1) The ref_2ms register continuously decrements by 1, reducing by 1 each interrupt, a total of 16 times, so the elapsed time is 125us × 16 = 2ms, which is the so-called timer/counter. Thus, we can use a single system RTC interrupt to achieve many required timing intervals.(2) Set the 2ms timer end flag, which is provided for the time-handling program; this is a framework for a timer, and the 5ms timer is exactly the same. This program also uses a block framework, which is quite convenient, but it is unrelated to today’s topic. The above program is the timer in the interrupt service routine, which times 2ms, 5ms, and 500ms, with the overflow flag_time marking the completion of timing. The program can read this flag to determine whether the timing has reached.Now let’s look at the unified time service subroutineSharing Two Programming Philosophies for MicrocontrollersWe used the 20ms debounce timer for keys as an example. Once understood, you can find that we can completely mimic that timer and place many timers below it, each counting simultaneously every 5ms. Whoever finishes counting first will turn off itself and set the corresponding flag for other programs to call, without affecting other timers! Thus, we can place many timers here; generally, having ten to twenty is not a problem, fully meeting a microcontroller system’s demand for multiple timing needs. 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, the timer is turned off, and the corresponding flags that need to be used are set. At this point, we can obtain all 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 does the overflow flag become valid, allowing entry into the above timing program. At other times, it is performing other tasks. Moreover, when entering the above timer, it is evident that it is not stuck in a dead loop; it simply increments or decrements a register and exits, consuming very little time, around 5us to 20us, which does not affect the execution of the main program.Now let’s see how to call it specifically The key debounce time handling problem discussed earlier can now be addressed using the method introduced above.Sharing Two Programming Philosophies for MicrocontrollersIt looks something like this: determine when a key is pressed; if not, exit; if yes, start the debounce timing. On the second entry, directly control the judgment of whether the time is sufficient by the flag bit. Similarly, 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? If it were a dead loop, it would be stuck waiting, doing nothing. However, looking at the above program, it only checks whether the timing is sufficient; the specific timing is done in the unified time subroutine. If the timing is not yet reached, it exits and continues running other programs until the timing is reached, at which point the microcontroller determines that flag_delay and key_flow meet the conditions and begins to enter the key handling program. During this period, the microcontroller is free to run other programs, and it does not waste time on debouncing.Main Program Loop BodySharing Two Programming Philosophies for Microcontrollers

This is the loop body used, where all functions are made into subroutine forms, and they can be hooked up as needed, which is quite convenient. Thus, in a total loop body, the microcontroller continuously executes this loop. If the entire program adopts the time-slice scanning idea discussed above, the time for a complete loop is quite short. Isn’t this somewhat similar to the concept of computers?

No matter how fast a computer is, it does not process multiple tasks simultaneously; rather, it processes one at a time, and at a very fast speed, creating the illusion that it is processing multiple programs simultaneously. I want to express that this is the essence of the idea. This is also a function of the operating system, relatedArticle:What Are the Advantages of Using RTOS in Embedded Development?With this idea supporting it, programming for microcontrollers becomes relatively easy; the remaining task is to focus on using programs to realize our ideas. Of course, this is just one feasible method, not the only one.

Programming is an art; it is easy to write, but difficult to write elegantly.

Leave a Comment