📜 ⬆️ ⬇️

On the issue of standard libraries

This story we begin with riddles,
Even Alice can hardly answer
What remains of the tale afterwards,
After being told?


This essay will be devoted to various topics, among which there is a place and an answer to the question in the subtitle, and the narration will be developed mainly around the problems associated with the initialization of the periphery of the modern MC.
So, let's designate the main problems associated with setting up the MK hardware: the need to set a significant number of parameters, most of which are not specified in each specific case, but, nevertheless, cannot be left arbitrary, but should take some predefined values . If you believe that such a simple statement of the problem can cause a stream of consciousness and lead to some not entirely obvious solutions, then

Let us consider an example related to setting up a fanged interface, namely, the IRPS (Radial Follow-Up Interface - it was under this name that it appeared in girlhood, now known as the UART character). I’ll immediately answer the question why the Russian abbreviation is because I write notes partially on the road, and I wouldn’t call the keyboard part of the keyboard switching to me (by the way, if anyone knows a comfortable keyboard with cursor control buttons and not a braking one, drop in a personal, but it is so inconvenient to poke into the screen).
Of course, the raised range of questions is not limited only to the IRPs, it is necessary to tweak other MK hardware, but I took it just for example. So, to tune the work of the IRPS, we must, at a minimum, specify the format of the parcel, which is determined by the following parameters - the transmission rate, the number of transmitted data bits, the presence and type of parity bit, the number of stop bits. And this is only the beginning, in fact there is also an extended configuration and it is much longer, but now this is not about that. It should also be noted that in 70 percent of cases when using IRPS the standard configuration 9600-8-0-1 will be set, in another 25 percent only the transmission speed will change, and all other configurations will share the remaining 5 percent, but it is for them that there should be ability to customize.

Before we begin to consider various options for solving this problem, we should determine the criteria for evaluating the success of one or another option, otherwise the choice of the most suitable of them will turn into a discussion of taste preferences. In my post, I proceed primarily from the reliability criterion, because I consider the following circumstances to be decisive: 1. it is human to err, 2. the compiler’s task is (including) to point out these errors as early as possible, and ideally to prevent their appearance. It is from such positions that I will consider the acceptability of this or that decision, but if you disagree with at least one of the above two postulates, then most likely you will not like this post very much and you should stop reading it, although this is not forbidden. continue and (necessarily argued) state their position in the comments.
')
To begin with, we will understand why such a problem arose, because in previous generations MK (PIC, AVR, 51) we did an excellent job with setting up the equipment by direct entries to the appropriate registers? Well, first of all, for this you need to know the registers, or, as is customary to put it, smoking manuals, and there are a lot of letters and, although for me personally, this occupation does not seem to be particularly straining, nevertheless, especially considering the quality of the current manuals, for a significant part Community such an approach can really be a problem.

A small (hahaha I really thought so) note about the quality of modern documentation - maybe the grass was greener before, but I fully agree with the phrase of Jack Gansley in one of his recent blogs: “Recently, I increasingly see instruments for which the word “insufficient” itself would be insufficient, although earlier it would be defined as absent. ” Of course, Inet is a great thing (and there is no sarcasm here, this is really a new wonder of the world) and you can ask a question on obscure places both directly to developers and on forums and often get a lot of answers, including even true with some luck. but why not write the documentation in such a way that it leaves no room for discrepancies? They usually answer me that developers should be involved in writing good documentation, and their time is too valuable, and technical writers cannot cope with the task, but, in my opinion, these are not my problems, howl?

I can only recommend trying to write documentation for an outsourcing, and it is desirable for developers of devices that your devices use, because the developers of the device itself consider too much obvious, and what is obvious to the developer is not always (more precisely, always not) obvious to the user. Perhaps, to create an office for the provision of this type of service, although, given the attitude of our manufacturers to the documentation, they are unlikely to use such an expensive and unnecessary service.

A little reminiscence on the topic of documentation. I recently studied the description for one crystal (by Intel, by the way) and there is an input with a talking name PE_RST_N, which is described as a + 3.3Vdc input, which, when asserted, signals the presence of power and a clock frequency. How exactly the input is claimed is high or low, is not indicated, there are no time diagrams for this signal (although there is a PERST # signal on the time diagram and it is mentioned with the same name in the device state diagram), later in the text there is a phrase that that the value of 0b indicates the activity of the reset, in the table of modes of operation, if there is a reset, there is a tick in the column for the value that hints. In general, from the totality of indirect signs, we can conclude that the active level on this leg, which leads to the device reset, is yes, low, but why should I guess instead of just reading the corresponding clear, clear and understandable description in the documentation?
Why are I being forced to come to the shaky ground of guesswork and assumptions? Probably for me personally, this is a punishment for bad karma in past lives (well, I was not an angel in this one), but what about the other developers? One of my colleagues expressed an interesting hypothesis that such documentation is made specifically to make it difficult for us to raise from our knees, but why then is it written in English? Or is it made specifically for us, and in the West they use real, correct documentation? Although in this case the phrase “We shouldn’t explain with malicious intent something that can be explained by simple laziness” (I won’t offend the creators of such documentation by literal quoting).

But the real masterpiece in my personal collection is the description of one, in general, quite good MK produced by a domestic company, the documentation for which described the control bits for connecting pull-up resistors as follows: “0- pull-up resistors fall out”, 1- ... guess how it is written there? - "pull-up resistors do not fall out" - a good attempt, but did not guess ... the drum roll in the studio ... "the opposite value is 0". Everyone applaud, the curtain.

But all of the above (I’m talking about the unimportant quality of the documentation, if anyone has already forgotten) is only part of the problem, its second component
is that modern MK is really more difficult than its predecessors. I have my own point on why the hardware is becoming more complex and “more and more complete satisfaction of the continuously growing needs of developers” is not in the first place in the list of reasons for this phenomenon, but my thoughts on this matter do not change the situation as a whole - modern MCs are really more difficult their predecessors, they have much more hardware blocks and the blocks themselves have become much more difficult, they implement additional functions that developers can (and should) use for business. I here conceived a post on some of the features (really useful) in the UART family of STM, when I finish this, I will definitely write, just when implementing these features, I thought about the problems that gave rise to this post.

The next part of the problem is connected with the growth of the nomenclature of both the MK families from different manufacturers and the subspecies of the MK in the same series from one manufacturer, completing a significant variety of devices within the same subspecies. Simultaneously with the expansion of the nomenclature, the life cycle of a specific representative of the family is shortened, which raises the question of a permanent transition to new devices. Again, this is far from always really necessary, but the tendency is obvious and it is very difficult to deal with it, so it is advisable to develop software in such a way that switching to another MC is as painless as possible and ideally comes down to replacing one line.
#define device xxxxx 
what undoubtedly contributes to the use of standard libraries, so I am writing this post about the correct (from my point of view, but you already know about the other) writing them.

We will begin to consider possible options for implementing the IRPS setup, and the first of them will be just an initialization function with all possible parameters (here I struggled for a while with the temptation to write texts of examples in the ANP, an algorithmic programming language, but then I decided that this would be on the other side of the border, separating good from evil and easy trolling from bullying). And we get something like
 UARTInit(9600,8,0,1); 
for the above standard case (hereinafter, we leave behind the brackets the question of choosing one configurable channel of equipment from the existing ones in the MC). It seems that everything is normal here and we don’t have to write anything supernatural, but we’ll try to find flaws in this version and (of course, otherwise why look) eliminate them.

First of all, we need to decide what we all want from the hardware setup procedure and what requirements we place on it. In my opinion, the program must first of all be reliable, secure, and understandable. Efficiency requirements are not so significant, since initialization, as a rule, is carried out once and may not be very fast, but if it is compact in memory and undemanding in speed, this is an additional plus in the assessment. By the way, at least one technology, namely, Charlipleking, requires an operative change of the operating mode of the MK's legs, so you really shouldn’t neglect the speed.

What disadvantages we see in this technology (and we see them, otherwise what to write about further) - first of all, it is the need to enumerate a large number of parameters, and in a strictly defined order, and if we make a mistake somewhere, the result will not be at all the one we counted on. The issue with the order of parameters can be somewhat weakened, if we define custom data types for each parameter, then we get:
 typedef enum {…UARTSpeed9600…} UARTSpeedT; void UARTInit(UARTSpeesT UartSpeed,…}; 
, and if we try to make a mistake, we will receive a warning from the compiler. This approach does not cost anything at the execution stage and quite a bit at the compilation stage, so I can strongly recommend it and at the same time express bewilderment about the fact that the authors (including proprietary) libraries in such a simple and at the same time effective way are neglected.

Their only justification is the need to use expressions that need to be remembered, as opposed to the magic number 9600, which is intuitively understandable (it was sarcasm). The alternative is a large number of assertions at the entrance to the function, which will check the correctness of the parameters for the compiler. In principle, the approach is not so bad (even a small fish is better than a large cockroach), but it requires a debug release from us and translates error messages to the execution stage, which is worse than getting them when compiling.

Having a little smoothed over the requirements for the order of the parameters (now they are being cursed when trying to violate it), we, nevertheless, still have to list them all, which is annoying. If we work with advanced language, then we have the default parameter values, but if we stay within the framework of the classical C (and we stay in them, didn’t I have warned before), then this path is not for us. In addition, this method has a very significant limitation, namely, all the default parameters must follow the ones defined, therefore, to set the last parameter explicitly in the list, we must indicate all the previous ones.

If we again work with an advanced C ++ type language, then we have another method - overloading the assignment operator, although personally the view of writing (4 + 4 * 3 + 4 + 1 = 21) assignment functions with hard the order of the arguments and an even more unimaginable number of assignment functions with arbitrary order. Nevertheless, such a possibility exists, and the rules of propriety require it to be mentioned, although it is not obligatory to use.

If we had a good preprocessor that really provided us with macro language capabilities, then we could write a macro function call with a variable number of typed arguments and get the preprocessor generated function call, but we don’t have it (well, we don’t have a good preprocessor in C we have no middle ground either). If someone from the readers of this post considers the standard preprocessor for the C language to be really a macro language, then I strongly recommend reading the description of the macro processor of the assembler for DEC machines, and then it will be possible to discuss this topic in more detail. But only in this order, and I do not exclude acquaintance with other developed means of code generation. Nevertheless, such an approach is not available to us due to the limited means of expression, and we will not write our own preprocessor, although sometimes we want to. On this pessimistic note, we conclude with options using an initialization function with direct parameter passing.

Another option is to use some kind of control structure and separate the process of setting the value of the fields of this structure (including the default ones) from the initialization process itself. What gives us such a separation? First of all, the long-awaited opportunity to modify only those parameters that should differ from the default values.
Of course, at the same time, we should be very well aware of and remember these very meanings, but the two-command compiler still remains an unattainable ideal. But we have to pay for everything in this world, and we have to pay for this opportunity by guaranteeing these same default values ​​to be guaranteed, which, when using a direct function call, was somehow guaranteed by the compiler.

Of course, if we write the function to set the default values ​​and call it for the control structure we plan to use, then there will be no problems. And if we forget to do it? At best, we will get a little bit not what we expected, in the case of moderate severity we will get well not at all what we expected, in a very bad case we will get so not that we damage the equipment, but in the worst case we will get everything The above is sudden, that is, in some rare and poorly repeated situations. This is all to the fact that the task is not far-fetched.

Again, if we use C ++, then the constructor is the natural answer to our aspirations, and if we also fasten smart pointers, then the solution is close to ideal, but we still remain within the framework of pure C, since this is the way of the samurai ( constant readiness path for Segmentation Fault).

I will define my attitude to a single issue related to the use of the control structure, namely the format of the structure itself and the method of changing its components.
First of all, I will immediately declare that I personally am against providing the user with information about the internal structure of the library in general and the control structures of this library in particular. The reasons for this deviation in behavior are far from the principles of the PLO, due to the damned past and were laid in the distant past, when computers were large, and the small memory and the large size of the name table could really slow down the compilation. Therefore, I calmly, without internal resistance, adopted at one time the concept of encapsulation and interface, although it was created entirely from other considerations than saving memory at the compilation stage.
By the way, I recommend the book “Programming for Mathematicians”, based on the course readable (once, I don’t know how it is now) on the VMC, which perfectly expresses the principles of OOP without using this term from the standpoint of operations.

And that my behavior is a deviation and it is a deviation from the mainstream, confirmed by the study of the source code of well-known software packages, including STM and TI. Due to incomprehensible considerations, the authors of these packages believe that everyone should know everything about everything, which is achieved using nested inclusions of header files and protection against re-inclusion. That is, if the module of work with IRPS is not able to recognize the distribution of bits in the USB host control register, it will “suffer, wither, and even ... uh ... die."

A small digression on the topic touched - I really consider the use of the include directive in h files to be evil, and I consider the advice on placing at the beginning of the header file of a conditional macro to protect it from re-enabling as tips on how to reduce the harm from smoking. The correct and simple solution is not to smoke - the authors of the recommendations seem not to be considered, that is, a priori it is implied that to think over the architecture of the software package, determine the interconnection of the modules and build their average hierarchy (well or above average, it’s all about the products of well-known companies) programmer embedded systems is not capable by definition, so it can only be a matter of minimizing the damage from its curvature.
Well, I don’t know how true this is, but personally I have such nested header files that cause doubts in the ability of their creator to write good (reliable and convenient) code. But this is a personal opinion expressed in frequent conversation, and, as they say, “I didn’t even know that it was possible.”

So they (STM and TI) as they want, and I will continue to adhere to the principle “The less you know, the better you sleep”, or, in another formulation, “What you don’t know cannot disturb you”. Therefore, I believe that the library user didn’t give up information about its internal structure, although we are obliged to provide it within C (and how cool it was in Turbo Pascal with its Unit concept, but we already said that it’s impossible to dream about unrealizable that they will not be in C ++ 17). So we will have an expression like
 Typedef struct { UARTSpeedT UARTSpeed; … } UARTConfigT; 
and its appearance is as inevitable as the victory of communist labor. But no one forces us to take another step along the road to the quagmire and set the values ​​of the control information in the style
 UARTConfigT UARTConfig; UARTConfig.UARTSpeed=UARTSpeed9600; 
because the user is completely uninterested in our specific fields, and he just needs confidence that what is needed will happen. Therefore, the use of this or that SET-tera seems preferable, and its implementation as a separate function or as a member function remains a matter of taste, as shown in the following code fragment
 void UARTSetSpeed(UARTConfigT *UARTConfig, UARTSpeedT UARTSpeed); UARTSetSpeed(&UartConfig, UARTSpeed9600); 
I apologize for how heavy the naming styles of functions are, but if the extra 20 characters in the name allowed you to save half an hour of debugging, then you won.
This approach, in addition to following the principles of OOP, also has a utilitarian meaning - if we use C ++ (but do we not use it, do not forget?), Then we can write one overloaded function and use it to set various parameters in the style
 void UARTSetParam(UARTConfigT *UARTConfig, UARTSpeedT UartSpeed); void UARTSetParam(UARTConfigT *UARTConfig, UARTParityT UartParity); 
and so on, which allows us to reduce the load on the user's brain by reducing the number of functions needed to memorize the names.
Of course, we need to understand that “DarSaBe” (separate hello to those who appreciate Heinlein) and accessing the setter will take more time to execute and more memory to store code compared to directly assigning a value to a field (although for inline functions this statement and no doubt), but, from my point of view, the advantages outweigh.

Another interesting aspect is that the user doesn’t need our control structure and he shouldn’t know about it (whatever he thinks about it), so it would be good to make it invisible to the user and here a global static variable would fit if we take into account that it imposes certain restrictions on the method of its use, or an anonymous instance, but more on that later.

So, in any method of setting the values ​​of individual fields, the question arises about the values ​​of fields that are not explicitly set, and, accordingly, are set by default. Here it is necessary to distinguish two aspects: the guarantee of the absence of garbage in the fields and the values ​​remaining from the previous use of the structure. And if the first one can still be dealt with by using initialized variables, then the second one cannot be solved without an explicit and direct call to the initialization function, even with constructors (which we don’t have).

As for the initial initialization, this is either a direct initialization at the point of declaring the structure, which in principle is correct and permissible, because it is controlled, or the structure is located in the global variables area, which ensures its zeroing, and I consider this method unacceptable in principle, since it is uncontrolled and imposes significant restrictions on the default values, they must be equal to 0. Well, in any case, this method does not solve the issue of reuse, so even the initial one Personalization can only be viewed as a demonstration of good style, but not as a solution.

What solution do I consider acceptable after mixing all the rest with sparrow food? This solution is complex, that is, it allows both to provide default values, and allows selective change of parameters, eliminates user errors and reduces cholesterol levels in the blood, and does a whole bunch of useful and necessary things, as is typical of a truly comprehensive solution. And one more important property of it is written in pure C, that is, the path of the bushido did not suffer damage.

After all, you can talk as much as you like about the bad properties of Japanese steel and, due to this, the impossibility of classical fencing a blade into a blade, but it is very beautiful to solve the fight with one blow, starting with pulling the blade out of its scabbard and culminating in returning it back in one continuous movement with shaking off the blood drops unlucky opponent on the road. As for critics, I recently read the wonderful phrase “criticism of impotent Don Juan may be objectively fair, but it still has an unpleasant tinge”.
To be honest, I feel the strongest temptation at this moment to interrupt the post and leave the reader in the strongest bewilderment and disappointment, but still subject it to further trials and let us get another kind of disappointment because the so beautifully described solution turned out to be awkward and inconvenient.

So, here it is.
 UARTConfigT UARTConfigInit(void) { UARTConfigT UARTConfig; UARTConfig.UARTSpeed=UARTSpeed9600; return UARTConfig; }; 

So really it is possible, in C we can return any type, except for an array, and we can return a structure, an array containing, which surprises me a little, but apparently Kernighan and Richie had reason to make such a decision, it’s a pity that they are incomprehensible to me. In this case, no bad things can happen, such a solution is absolutely reliable and meets the standard of the language. But this is only an initialization procedure, and how will we carry out the assignment of significant parameters? The variant with the use of an intermediate variable is dismissed with indignation, since it does not guarantee us the exclusion of user errors and create cascading use in the following style:
 UARTConfigT UARTConfigSpeed(UARTSpeedT UARTSpeed,UARTConfigT UARTConfig) { UARTConfig.UARTSpeed=UARTSpeed; return UARTConfig; }; 
Pay attention to the order of the parameters, the advantages of this solution we see in the line
 UARTConfigSpeed(UARTSpeed4800,UARTConfigParity(UARTParityEven,UARTConfigInit())); 

where the parameter value immediately follows the function name, which is more observable compared to the following expression
 UARTConfigSpeed(UARTConfigParity(UARTConfigInit(),UARTParityEven),UARTSpeed4800)); 

Actually, those who programmed in TurboVision learned this unforgettable style with a lot of closing brackets at the end, but it’s not necessary to try to build a single-line expression and the alternative already looks less nightmarish
 UARTConfigSpeed(UARTSpeed4800, UARTConfigParity(UARTParityEven, UARTConfigInit() ) ); 
but this is a matter of taste and is not subject to discussion by definition - they do not argue about tastes.

What are the advantages of this option - it excludes the possibility of skipping the initialization of the control structure, it excludes the user’s familiarity with this structure, because it is anonymous, it checks the function parameters (due to enumerated types) and it can check one more possible error if we all they did it right, but they forgot to use the structure (we remember that it is human nature to make mistakes). After all, we lost sight of the fact that all our manipulations have not yet led to the setting up of the IRPS proper and we still need a function.
 int UARTConfigUse(UARTConfigT UARTConfig) { return DO_something(); //      }; 

and our example in the final form will look like
 UARTConfigUse( UARTConfigSpeed(UARTSpeed4800, UARTConfigParity(UARTParityEven, UARTConfigInit() ) ) ); 

As a cherry on the cake, we will show that the possibility of the last error can be controlled - skipping the use of the formed structure, unfortunately, this possibility does not apply to standard language tools and is present only in the GCC family - a warning about ignoring the return value, for which the initialization and configuration functions should be described as __attribute __ ((warn_unused_result)). , , , KEIL , IAR ( IAR, , ). , , , , .

, ? , , .
, , - . , ( , , ).
? , , (, , ), , , IAR . ( main), , - , , . . , .
( ) ( , ) , , , .

— , . , . , ( , ).

, , , ( ), - ( , , ). , .

, . , , . , , . , , - , , , , , , , .

, :
 typedef UARTConfigT *UARTConfigPT; UARTConfigPT UARTConfigInit(void) { static UARTConfigT UARTConfig; UARTConfig.UARTSpeed=UARTSpeed9600; return &UARTConfig; }; UARTConfigPT UARTConfigSpeed(UARTSpeedT UARTSpeed,UARTConfigPT UARTConfigP) { UARTConfigP->UARTSpeed=UARTSpeed; return UARTConfigP; }; int UARTConfigUse(UARTConfigPT UARTConfigP) { return DO_something(); //      }; 
.
, , , . , , , ( , , ) , , .

, , , , , & L-. , , , , , . .

- , , , , , .

Source: https://habr.com/ru/post/273863/


All Articles