The journey of ten thousand begins with the first step.
Preamble
According to the results of the survey in the previous post, it became clear to me that a significant part of those reading my posts would like to take a course on programming embedded systems, so I propose to consider the following text as an introductory lecture. The other, not so large, but still quite a significant part of the readers declared the opportunity to read a similar course on their own, so join, I haven’t seen the outline of your lectures on Habré, I hope they will appear there soon.
In my recent (when I started writing this, it was really recent) post about methods for building the hardware initialization module of the MC (by the way, who is interested in this topic, be sure to look at the comments to it, there is given a completely wonderful solution, which, unfortunately, is not fit in the margins of this manuscript) I promised to devote the next post to UART features in STM microcontrollers. Without rejecting this promise, I will nevertheless make some corrections - a series of posts will be created, devoted to the subject of proper software package development (for example, the implementation of the MODBUS protocol is taken), and within its framework there will be a part dedicated to the above mentioned topic.
')
Since we will study the correct development, we will need a counter-example, since there is nothing easier than showing other people's errors, so the well-known implementation of the mentioned protocol, namely FREEMODBUS, will be taken as a starting point. Without dwelling on the issues of functioning, the style of this software product does not seem to me ideal and I will try to show how to improve it.
The purpose of the planned series of posts will be an attempt to show how to design software systems for embedded devices, which considerations should be taken into account, which basic techniques should be applied and what should be the result. In the presentation, I will proceed from the implicitly implied fact that the reader of this text has a certain set of knowledge necessary to work in the field of embedded software development, and my task is to help him systematize this knowledge to obtain the best possible result.
Therefore, the statement will not always be sufficiently strict, more often I will denote the problem, not proving its significance, but merely stating and show possible solutions to it. Nevertheless, the results obtained will certainly be correct, and, moreover, are quite applicable in everyday practice. Since theoretical arguments themselves are less interesting than specific examples of using this theory (and, moreover, more difficult to write and read), we, dear reader, will design the library based on the previously mentioned.
What features should we consider when writing a library, that is, a product that is not intended for future use by us? First of all, it should be written with great care so that it would not be a shame to look into the eyes of colleagues. “How to write books for children? As for adults, only better. ” Of course, the style “Figak, Figak and Production” has recently gained a certain popularity in certain circles, but you and I will not give in to the call of sirens, who are calling us to leave the ship of reliability for the sake of momentary benefits. In addition, we should not forget about the most important reader of our code (no, this is not the president), but about itself. Expressions like "it is interesting, and why I used it here ..." are pronounced much more often than we would like, because if we have a successful library (and how else) and it will be demanded by many users, then you will sometimes have to modify it and I would like to do this without using the rich vocabulary of the Russian language, especially its profanity. But these are general considerations, and they should be valid for any development, it’s a pity that this is not always the case. Therefore, before proceeding to the actual writing of the code, you should determine the essential circumstances accompanying it.
Since we will use variables (monads, at worst), but in the language we chose (in general, we did not choose it, and I’m not sure that my choice would be such, and not the language from the Pascal family, but where do you go, C is the main language in programming embedded systems, and “if you cannot defeat your enemies, lead them”) the variables are typed, so for a start we will talk about types.
The necessary explanation about the comments in parentheses - I am not at all a staunch opponent of the C language, and even more so his enemy, but since my development as a programmer is connected with the Pascal language (in general, to be honest, then with the language of AJP in the machine “Nairi” - ah, these wonderful directives “we introduce” and “end” - then I realized that the directives in Russian are not as good an idea as it once seemed, and then with Fortran, but I am afraid that too many readers about these languages ​​are not know, and I would not risk recommending them at the moment, and Pascal in Alone, I can) that the phenomenon of capturing takes place, and I was uncomfortable when I personally experienced the length of the rope provided by C (and it’s really much longer than the help Pascal holds you with, well, that’s understandable, you’ve grown up and are capable move independently without support). But still, With as a first language, would you like such learning to walk for your children, neg?
Enumerations against definitions
So, we return to our sheep, that is, to the types, and you may be surprised, but there really is something to talk about. To begin, open the file ... and immediately come across an ad of custom types that the author plans to actively use in the library, and immediately I don’t like something written. In this case, I'm talking about the introduction of a logical type. This is not a mistake in the accepted meaning of this word, but it is a big mistake in the framework of the programming paradigm that I have adopted. I believe that compilers write intelligent people, and you should not consider yourself to be much more advanced in terms of language use than they are. The classic implementation with defines of logical constants and using the type char is given in this file, I counterpose the implementation to it through an enumerated type, namely:
typedef enum {False=0,True=1} Boolean; Boolean IsOk=True; ...; if (IsOk==False) ...;
Consider the proposed implementation in more detail, to understand all the advantages that it provides and to ensure that there are no flaws, after which the first implementation based on defines will be forgotten once and for all and its application will be attributed to unacceptable phenomena in a decent society, one of those at the mention of which “There is an ominous silence at the party, everyone turns around and looks at you reproachfully, and someone in the far corner of the hall will even drop his cocktail.”
So, first of all, for the sake of what enumerated types are created - the inadmissibility of mixing in the assignment, when you try to do this, you will receive a compiler warning. Of course, someone will say that there is nothing wrong with assigning an entire variable with a logical value, and they are equivalent, and in general warnings are different from errors, then you can ignore them and it’s better to ignore them, but we are with you. we won’t be found by man - today he mixes enumerated types, he brings pointers tomorrow, and the day after he ignores the warning about returning a pointer to a local variable, then he looks at the idle code with bewilderment and says, “It's strange, yesterday everything worked, probably e compiler glitch "and goes on Ineta seek neglyuchny compiler and does not return there, but you have to understand written them in the inimitable style of neglecting to detail the code. Everything happens exactly in this order, since there is no excuse for mixing enumerated types, except for laziness and negligence, and I have these two qualities, with all due respect to the first of these (because she is older than me, because she was born earlier, and the engine of progress is not) I can relate to the best characteristics of the programmer, whose code I would like to accompany.
By the way, I just want to warn possible coders that I have an unusually quick-tempered character, although I mask this flaw in my posts as much as possible, so you can call me a bloodthirsty maniac, I have several large kitchen knives at home that I own as well as a character Steven Seagal in a famous film, in the art of calculating a real IP address, or extracting it from code, I have no equal, but then you understand.
The next plus is the compiler itself will select the necessary primitive type for implementation, and enumerated types will not necessarily be stored as an integer, if 8 bits are enough to store the value. My IAR did just that, placing the variables of my boolean type (such as enumeration) in a byte. Generally speaking, if we want to define our logical type directly, we should use not at type char, but at_least8_t or fast8_t, and the last two should not coincide with the byte, although they can. Moreover, I do not exclude compiler implementations that are able to place the logical variable in more suitable places for them, for example, addressable bits in memory, which is undoubtedly more efficient both in memory and speed than self-made definition.
Explicit comparisons
Let us pay attention to the line in which the comparison operator is written, and I categorically insist on the version with an explicitly written comparison as opposed to the well-established practice “What is truth? “Not zero,” since this option clearly indicates the required condition and leaves no room for speculation. Well, if we take into account that 99% of modern compilers (I would write '' everything ", but you never know what happens) after seeing the comparison with zero, they will not generate code for its implementation (or rather, it will be, but exactly the same as and for the lack of comparison in the if (! error) condition, then we do not lose by efficiency. And finally, another consideration is to avoid variables with names like NoErr (I hope that the name of the variable should coincide with the meaning of the value stored by it, You have no doubts, otherwise we have big problems), because dissolved possible misunderstandings and confusion expressions such as
if (NoErr==False)
, and should be avoided by any means and the variable IsOk in this sense is preferable.
It is also advisable to write conditions in the form of an approval, that is
(IsOk == False)
rather than
(IsOk |= True)
, while the option with
~(IsOk == True)
should not even occur. Although there are people who claim (although maybe it was trolling, but it’s too thin for me, there are things that are inappropriate to joke about) that the
State|= !!ErrNumber << 5
variant is more efficiently implemented than the normal
if (ErrNumber! = 0) State = State | (1 << StateErrorBitNumber)
expression
if (ErrNumber! = 0) State = State | (1 << StateErrorBitNumber)
if (ErrNumber! = 0) State = State | (1 << StateErrorBitNumber)
. Even if it were true, and the code on my compiler did not turn out to be shorter or faster, this efficiency would not be worth even a minute stupor from such a language construct.
Probably, a person working in the field of embedded software should understand what happens as a result of double inversion of the whole argument, but still, let's save such constructions for competitions and interviews, and in practical work we will avoid them. For the simple reason - we are not writing code to appreciate the reader’s ability to solve intellectual puzzles (this is artistic, well, let me call my opuses, the text can be hinted at classics and simply liked works intended to arouse intellectual hunger for the reader) , and in the program code I personally consider such behavior inappropriate (if someone has his own, opposite to mine and, therefore, wrong opinion, then it can be stated in the comments or in response ie, I may be wrong, although it is unlikely).
Well, one more remark - they tell me here that the C standard does not guarantee the correct (from the point of view of the author of the code) the work of this fragment. Not sure that this is true, but still be better to beware.
BIT FIELDS
By the way, about bit fields. Those who read my opuses know, and for the others I will repeat it again - if you can control the order of bytes in a word in your compiler, then the bit fields are a truly delightful way to work with individual bits and bit fields in both registers and normal variables, and I highly recommend this option, since the expression
if (ErrNumber! = 0) State.ErrorBit = 1
even more understandable than the one given in the previous paragraph. If religious considerations or psychological trauma received in childhood, when you unexpectedly opened a file written by your parents and saw this naked reception there, do not allow you to use bit fields and you prefer bit masks, then I have a strong recommendation - hide operations with them behind macros and define dependent constants one through another, and not individually.
In my earlier posts I considered this question, in the comments there is even a macro that allows, along with the traditional definition of related constants like
#define StateBitNumber (12)
#define StateBitMask (1 << StateBitNumber)
#define StateBitNumber (12)
#define StateBitMask (1 << StateBitNumber)
#define StateBitNumber (12)
#define StateBitMask (1 << StateBitNumber)
use and converse in the style of
#define StateBitMask (0x00004000)
#define StateBitNumber (BITNUMBER(StateBitMask))
#define StateBitMask (0x00004000)
#define StateBitNumber (BITNUMBER(StateBitMask))
#define StateBitMask (0x00004000)
#define StateBitNumber (BITNUMBER(StateBitMask))
. Try to come up with the actual implementation of the last macro, and you can see. And, for God's sake, use only derivatives of these constants, your attitude towards people who allow themselves expressions like
*(uint *) 0x20008000 = *(uint*) 0x20008000 & 0xffff0fff | 0x00004000
*(uint *) 0x20008000 = *(uint*) 0x20008000 & 0xffff0fff | 0x00004000
, and similar expressions with the same constants are found in their text many times, I have already expressed, remember the big kitchen knives.
Note that a bit field in a register is by no means a logical type, but a whole, even if it is 1 bit long, never make its type different from the whole, but in the process state word (in a variable) you can afford such a construction - a logical type of bit length 1. And do not forget about the else branch, which I quite deliberately missed in my example, so that it fully corresponds to the criticized operator, which may be correct in the appropriate context. At the same time, I do not limit you to write something like
ProcessOK = (Boolean) (((ReadOk==True) || (WriteOK==True)) && (ErrNum ==0))
, because in this The operator clearly expresses your understanding of what is happening and declared determination to take responsibility for possible consequences.
Variable names
Another insignificant, but in certain circles, controversial circumstance, namely the names of variables, will be affected to the extent that it exists. As it is not difficult to see from my examples, I have certain preferences in this regard, but I do not impose them in any way, the style with underscores is just as good (although it’s probably worse, since I don’t use it), the Hungarian notation is quite common. widely and it has its advantages, the order of the object and the sign is possible any, so the only recommendation is to adhere to the same naming style and avoid names like IsOK6.
It is better if you personally or in your organization have an unofficially or formally established coding style and naming variables, since any, even a not very good standard, is better than its absence. On the topic of truly correct naming of a variable, one can argue endlessly and to hoarseness and personalization, so I’ll say once again that you can do anything (subject to certain rules), and I am a supporter of the phrase “Yes, every true believer egg breaks from the end from which it is more convenient for HIM ”, where my editing of a famous quotation is highlighted, although I’m not at all sure that even such a correction would save Liliputia from an internecine war.
Actually, we considered the advantages and disadvantages of enumerated types, if someone has not forgotten, following the flight of my bizarre fantasy, so let us return to this question. We analyzed the advantages of the proposed implementation, we were convinced that there were no visible drawbacks, but this is not the case, something should have worsened. "The reader is waiting for the rhymes of roses - well, then catch her soon." We have lost the ability to interpret our enumerated type as a logical expression, which by default is represented in C for built-in integer types, or rather, not completely lost, but such an attempt will be accompanied by a compiler warning, and we agreed not to ignore them.
Yes, we can not now write a charming expression like
if (~NoErr || !NoSuccess && NoRetry) ...
, which undoubtedly delivers some unpleasant minutes even to the author after a six-month break, and there is nothing even to say about the others, but, believe me, the loss is not so great. And to express logical conditions in an understandable form, our logical type is quite acceptable, for example
if (IsOk==True) || ((Success==True) && (Rerty==False))) ...
if (IsOk==True) || ((Success==True) && (Rerty==False))) ...
where everything is simple and clear, and even if this construct is executed for 2 microseconds longer, which I am not sure about personally, then it is worth it. As someone from intelligent people said, we write programs not for the compiler, but for other people.
Well, the casting has not been canceled, as I said earlier, I do not consider it a universal evil, use it on health, again avoiding the implicit cast, this is really evil. After all, it is assumed that if you wrote a cast operator, you thereby showed the compiler that you understand what you get as a result and realize that assigning a whole byte will result in dropping the most significant bits, and using a pointer to a constant variable when accessing the function Where the formal parameter is a pointer to a variable, can in some execution environments lead to an exception? You didn’t understand this, and type casting was set up just to get rid of (annoying) warnings - these are your problems, the compiler will not delve into your motivations, considering you a sufficiently responsible programmer and try not to disappoint him, because the disappointed compiler will upset you very much quite.
Express yourself clearly
Therefore, I strongly urge you to write the code as simply as possible (the well-known KISS rule), which is constantly violated, sometimes from a falsely understood sense of professional pride (there is an opinion that most often the last entry on the black box sounds “look how I can”), but more often just by habit. I read a good book “From the 21st Century”, which I was recommended in the comments to the previous post, and I see a very familiar place
static int Data=0; if (!Data) ...
static int Data=0; if (!Data) ...
; and I have a question, for what purpose the simple and understandable condition (Data == 0) is written in this form, if we decided to pervert, we could write and (~ Data & 1) (this expression is even cooler and demonstrates your a deep understanding of the behavior of integer types, and this is the main purpose of writing a program - to show off their knowledge), and you can think of something else. Is this in order to save two characters when entering text and also disk space when storing a file? Of course, all three options will work identically, and the whole type can be interpreted as a condition in C, but, nevertheless, why? To check the reader’s (user’s) knowledge of the C syntax rules is not necessary, once again, we do not have an interview.
Just don’t start talking to me about the efficiency of execution, I can state in advance that the code for a more understandable version will be no more than for both perverted (I insist on this definition) methods. By the way, it turned out that, for the first two options, the code turned out to be just the same, and the third, designed to demonstrate deep knowledge of the author of the code, naturally turned out to be longer.
Machine independent types
We go further in the text of the program and stumble upon another mysterious place, namely the definitions of the types UCHAR and UINT. Why a mysterious place? Firstly, it is not clear why these types should be entered at all, absolutely nothing follows from their names (we agreed on meaningful identifiers, howl?), Secondly, it is clear from the text that these are just aliases for standard types, but thirdly, why the standard types are taken, and the types that are not recommended for use in embedded programming with an explicit indication of the size. Is it really just an attempt to type less characters when typing?
If so, then this decision still has a right to exist, but if it was an attempt to determine machine-independent types (and I sincerely hope so, since this should be done “in the first lines of my letter”), then it clearly failed . The question of the correct names of machine-independent types is the topic of another holivar, I personally like the minimalist definitions that TI uses in its examples, namely u8, s16, and so on, but you may prefer another option.
By the way, an interesting question for an inquisitive reader - how exactly are these types defined with an explicit indication of the size in the language implementation? I suggest thinking for a minute, suggest options, and then look at the code for the stdint.h file in your compiler. Personally, in my IAR I found aliases for expressions like __INT8_T__, which are not amenable to further analysis and, obviously, should be implemented in the compiler itself. Those who read the standard C language (see how I flaunt my own ignorance, and this is not the first time) can correct me in the comments, but it seems that these internal implementation macros, as in the case of the sizeof operator, implement which I once looked for, but I never found it, having rested in a similar expression. But in Keil, these types are implemented directly through the standard ones, which makes you wonder, but how should it really be, or is the implementation completely relegated to the compiler manufacturers?
Another question for the inquisitive reader is: what types can we even have and should use in case of need an explicit indication of size? On the one hand, we are advised to use in no case (u) int8_t, the implementation of which is optional, but fastint8_t, the length of which is bounded from below, and not fixed at all, as with the type atleast8_t. Of course, experience shows that all types of types with an explicit indication of the size are implemented in all practically accessible compilers (I do not know the opposite cases, and the reader already apparently noticed my desire to generalize personal experience), but still it’s better to be safe, there is a phrase “It’s better to dig 100 meters of trench than 2 meters grave. " On the other hand, the last two types do not guarantee us the exact length, in the same Keil they are equal to a short integer and do not coincide with a byte.
My solution is that if I really need a byte, then the char type is correct, since sizeof (char) == 1 is strictly fixed by the standard, but for a 16-bit integer, most likely fast16_t should be used, although this again does not guarantee anything. Therefore, the correct solution will be to describe a packet of information as a set of bytes, and turning them into internal variables of various types and converting them back into them with explicitly prescribed operations with an array of bytes.
Unsigned types
Immediately touch on the topic of unsigned numbers. For some reason, it is a practice that unsigned types are used to determine the registers of external devices (and when describing the structure of data transmission packets).
I don’t really understand this solution, since the only difference between signed and unsigned types is the implementation of operations> and <, the most significant bit is analyzed for sign types, and the carry bit is analyzed for unsigned types, but we are not going to carry out such operations with them. There is also a slight difference in type conversion, which leads to a slight change in speed (at least for ARM stones), and in different directions.At the same time, the use of unsigned types does not lead to any negative consequences, except that we have to write an extra character u, and this goes against the main paradigm of modern programming, which is to save disk space, doesn’t it? For fields that actually contain information that is interpreted as an unsigned value, for example, the length of the data in the packet, and it cannot be negative by definition, using an unsigned type allows you to make equivalent expressions (Len> 0) and (Len! = 0), and further use the more convenient of them, but for fields that do not contain such information, the unsigned type is optional.And the last remark - it is not clear why these types are written in capital letters, it seems we don’t have a macro for which this naming was recommended, most likely a legacy of the damned past, when defaines really stood, and then they were replaced with user types. I don’t know how anyone, but for me, the names consisting of only the upper-case characters cut my eyes, and I don’t support this style, although, as we know, “all colors are different in taste”. Again, having a reasonable opposite opinion please in the comments.File structure
Now a little about the files. I don’t know how you feel, but it’s unusual for me (although more than a decade has passed) to find in the root directory of a text file project a description of the structure of modules and the places where the files that implement them are located. I understand that most likely the header files will be in a directory whose name starts with INC, and the sources in the SRC directory, although there are already options like SOURCE, but in the years of my youth the presence of such a file was considered mandatory, I see no reason to I refuse from practicing and a text file informing me about the author of this software package and the type of license under which it was developed is completely inadequate.As Jack Gansley wrote not so long ago in his blog, “when at the beginning of the program I see a two-page description of the license requirements, on which the comments almost end, I have doubts about the author’s qualifications”. This expression refers to the product under consideration slightly less than fully. And do not believe Doxygen sirens, he will not write comments for you, he can really prepare a description of the structure and composition of files, especially if you help him, but comments are exceptional Your task.And since I’m talking about comments, my advice is to not write comments in the styleCounter ++; //
. , , . « ». , , , , , . , , , , ( ), , , , .
Immediately I want to give the necessary clarification - I in no way treat the followers of this direction with disdain, there are really quite good projects at Arduino and this is a great system in its own way, it just so happened that there are not a lot of grazing on specialized sites. too experienced people, let's call them by novice programmers, who allow themselves a style of writing programs, similar to this example, and the questions they ask on the forums sometimes only cause feelings tedious bewilderment and the only possible answer to them will be a sentence to read some book on programming in general and on language C in particular.Header files
Another note about the header files, or rather two.First, do not save on files, more precisely, on their number. If your module requires internal constants or other entities that the user does not need, but you want to keep the possibility of their simple and convenient modification (although I’m not sure that the descriptions of such objects are at the beginning of the program text, and not in a separate file, so complicates their modification, but we will not save on matches), then do not drag them into the general header file, where the description of the module interface is located. There is no rule to have only one header file per module for all occasions and the fact that everyone is doing it cannot justify you.The user only needs to know what he needs and there are a lot of reasons for this, but I will give only one thing - a small file that contains important information, easier to read than a five-page sheet, the first two pages of which tell us about the license under which this the product, the following three define internal constants and objects that you will never use, and at the end of the last page, prototypes of the three functions needed by the user lonely.Not one header file can be useful in other cases. Consider an example when we have in the package some actively used variable, for example, an error code, which many modules can read, but only a few can modify it. Of course, to provide access to it in the hope of the user's prudence will be somewhat rash, so the getter is a natural solution, so we create two header files, one of which, for internal use, gives full access to this variable, and the second - only the signature of the function of reading and the user can not spoil the variable, even if he really wants, because he just knows about its existence.Second, the attached header files. At the risk of unleashing a holy war, I declare the following - their resolution was a mistake of the developers of the language, or rather, a crutch, caused by the reluctance to create truly effective mechanisms for connecting modules. Yes, at the moment C language has the ability to use nested headers, although it does not make them mandatory, but this is an ugly decision. But even being not very beautiful, this solution, nevertheless, does not require us to produce monsters, when each module in the library begins with the inclusion of a global header, which includes all the header files that are in the whole package. An example of this approach are the libraries from STM, and, based on the fact that this solution is preserved in all releases, this is the principal position of the company. Of course,Moreover, each header is provided with protection against reclosing and the compiler calmly digests such a vinaigrette, but this decision says only one thing - the interconnection of the modules in the package is not thought out by the developer and there is a doubt whether he has sufficient qualification to implement the functional part of the package.I am prompted here (of course, this person is highly qualified as a programmer, and his opinion is valuable, but I suspect that this is a somewhat protracted demonstration of independence, since he is my youngest son and argues with me from the moment his first operator wrote - no offense, Max), that we cannot do without such a decision, and we will not include all the headers for the objects we are going to work with in our module, but I answer, “why not?”In some languages ​​you must explicitly list I (and I am not alone, but I rely only on my own experience) worked with these languages ​​and nothing, “alive, healthy and cheerful”, that is, such a requirement is not tantamount to a death sentence.If you really need to place a page (or two) with the names of the included header files at the beginning of the file, then perhaps your module is too large and it should be decomposed, howl? Although in this case it would be good not to overdo it and not create 100,500 small files with code for two lines of execution. The truth, as always, is located between the two extremes, it has such a nice trait - to always lie in the middle (if you read something from this phrase over and above what is written in it, you should be ashamed). Yes, I am ready to agree with the aforementioned speaker, in many places, the attached header files are straightforward, but, nevertheless, they are just convenient and not obligatory, and, secondly,When was the last time you implemented embedded delegate pattern in embedded programming (and I am writing exclusively about this section of software development)?Keywords
Let's discuss a less important topic - keywords. I will allow myself a long quotation from the absolutely fascinating story “Entropy, ergodic source, multidimensional message space, bits, polysemy, Markov process - all these words sound quite impressive, in whatever order they are arranged. If we arrange them in the correct order, they acquire a certain theoretical content. And a real expert can sometimes with their help find a solution to everyday practical tasks. ” So, we will apply the keywords meaningfully, that isvolatile — , ,
static — , ,
const — , ,
auto register — , , , .
Other keywords are not so significant, but a reasonable use of the inline directive can really increase the speed somewhat, if we don’t forget about the features of the compiler, I strongly do not recommend the intrinsic directive, because I don’t understand very well the mechanism of its work and, accordingly, the meaning of its use. All other directives should be used with extreme caution, since they are usually compiler-dependent and may not be present in other implementations (like, for example, the warm__unused_result I like in the GNU compiler), and function somewhat differently. The raised topic allows us to smoothly move on to the next paragraph, namely the setting for the execution environment.Compiler Setup
Another section that is specific to software packages that implies alienation from the programmer is tuning to the target compiler. Unlike the usual program that is compiled here and now, the general purpose software package developed by us should work in different programming languages ​​(at least C and C ++), on different architectures, on different compilers within the same language (and they quite a few differences), in different programming environments (and they have even more differences) and on different target ICs (well, the latter directly affects the code weakly, but it would be good to take into account such a requirement when designing the structure of modules).To begin with, we will need a setting for a language in this particular place expressed in the form of an extern C directive, which will allow the C ++ program to use our functions, designed in an object file according to the rules of the C language. Applying this directive is absolutely necessary if we pass a package in the form of an object file or library, but can be excluded when using the source text for a complete assembly. I would recommend disabling this directive when fully assembled, but this is up to you, and you can leave it. But immediately a remark on the style - in the original example, this directive is written directly in the conditional expression, which I personally do not really like. I prefer to issue such directives as a separate include file, and it also manages the implementation of directives and recommend this approach.Results
Summing up: we will use talking naming in a single style everywhere, create enumerated types if possible, use machine-independent types if explicitly specifying the size of data, think through the package structure and module interconnections, carefully manage header files, comment on your code, avoid complex understanding and / or ambiguous expressions and see what happens.PS
The first part of the code, built on the basis of following the above recommendations, will be located on Githab / tGarryC / Modbus /, when I finally deal with Git.