1230 views|2 replies

28

Posts

0

Resources
The OP
 

Senior programmer talks: My opinion on the reliability design of embedded C language [Copy link]

Senior programmer talks: My opinion on the reliability design of embedded C language

Preface

The reliability of equipment involves many aspects: stable hardware, excellent software architecture, strict testing, market and time testing, etc. Here we focus on some understanding of embedded software reliability design, and improve software reliability through certain techniques and methods. The embedded devices mentioned here refer to measurement and control or industrial control systems that use microcontrollers, ARM7 , Cortex -M0 , M3 , etc. as the core.

The reliability design of embedded software should be considered from three aspects: error prevention, error detection and fault tolerance . In addition , you also need to understand the characteristics of the compiler you are using.

This article is just a starting point for discussion.

Good software architecture, clear code structure, mastery of hardware, and in-depth understanding of C language are the key points to error prevention. Here we will only talk about C language.

" People's thinking and experience accumulation have a great impact on software reliability . " C language is weird and has many traps and defects. It takes programmers many years of experience to reach a relatively perfect level. " The quality of software is determined by the quality of programmers and their cooperation with each other . " Therefore, the author believes that the key to error prevention is to consider the human factor.

" Learn a programming language in depth, not superficially . " The reliability of software is closely related to the depth of your understanding of the language, especially embedded C. In addition to the language, the author believes that embedded development must also have a deep understanding of the compiler.

This section will provide an initial exploration of the pitfalls and flaws of the C language.

1. Traps Everywhere

When you first start programming, in addition to mistakenly writing English punctuation as Chinese punctuation, you may also encounter the mistakenly writing the comparison operator == as the assignment operator = , as shown in the following code:

if (x = 5 ) { … }

The original intention here is to compare whether the variable x is equal to the constant 5 , but '==' is mistakenly written as '=' , and the if statement is always true. If the assignment operator appears in a logical judgment expression, most compilers now will give a warning message. Not all programmers will pay attention to such warnings, so experienced programmers use the following code to avoid such errors:

if ( 5 == x ) { … }

Put the constant on the left side of the variable x . Even if the programmer mistakenly writes '==' instead of '=' , the compiler will generate a syntax error message that no one can ignore: You cannot assign a value to a constant!

It is also easy to confuse += with =+ , -= with =- . Although compound assignment operators ( += , *= , etc.) can make expressions more concise and potentially generate more efficient machine code, some compound assignment operators can also bring hidden bugs to the program , such as the following code:

tmp=+ 1 ;

The code was meant to express tmp=tmp+1 , but the compound assignment operator += was mistakenly written as =+ : the positive integer constant 1 is assigned to the variable tmp . The compiler will happily accept this type of code without even generating a warning.

If you can find this bug during the debugging phase , you should really celebrate, otherwise it is likely to become a major hidden bug that is not easy to detect.

The same is true for -= and =- . Similar to this are logical AND && and bitwise AND & , logical OR || and bitwise OR | , logical NOT! and bitwise inversion ~ . In addition, the letter l and the number 1 , the letter O and the number 0 are also easily confused, which can be corrected with the help of the compiler.

Many software bugs come from typos. When you search on Google , some of the results come with a warning that Google thinks it contains malicious code. If you searched Google early in the morning on January 31 , 2009 , you would have seen that for 55 minutes that morning, Google 's search results marked every site as harmful to your PC . This included every site on the entire Internet , including all of Google 's own sites and services. Google 's malware detection feature identifies dangerous sites by looking them up on a list of known attackers. On the morning of January 31 , an update to this list accidentally included a slash ("/") . All URLs contain a slash, and the anti-malware feature interpreted this slash as meaning that all URLs were suspicious, so it happily added a warning to every site in the search results. It's rare to see such a simple typo have such strange and widespread consequences, but programs are like that and can't afford to be careless.

Arrays are often an important factor that causes program instability. The confusing nature of C language arrays is closely related to the fact that array subscripts start at 0. You can define int a[30] , but you can never use array element a[30] unless you know exactly what you are doing.

The switch...case statement can easily implement a multi-branch structure, but be careful to add the break keyword at the appropriate position. Programmers often forget to add break , which causes multiple case statements to be executed sequentially. This may be a defect of C. For the switch...case statement, from a probability perspective, most programs only need to execute one matching case statement at a time, and each such case statement must be followed by a break . It is somewhat unreasonable to complicate high-probability events.

The break keyword is used to jump out of the nearest loop statement or switch statement, but programmers often do not pay enough attention to this.

On January 15 , 1990 , a switch in AT&T 's telephone network in New York crashed and restarted, causing its neighboring switches to crash, and then one after another. Soon, 114 switches crashed and restarted every six seconds, and 60,000 people could not make long-distance calls for nine hours. The solution at the time was that engineers reinstalled the previous software version. The subsequent accident investigation found that this was caused by the misuse of the break keyword. " C Expert Programming" provides a simplified version of the problem source code:

network code (){ switch (line) { case THING1: doit1(); break ; case THING2: if (x==STUFF) { do_first_stuff(); if (y==OTHER_STUFF) break ; do_later_stuff(); } /* Code is intended to jump here … …*/ initialize_modes_pointer(); break ; default : processing(); } /*… … but it jumps here instead. */ use_modes_pointer(); /* causes modes_pointer to be uninitialized */ }

The programmer wanted to jump out of the if statement, but he forgot that the break keyword actually jumps out of the nearest loop statement or switch statement. Now it jumps out of the switch statement and executes the use_modes_pointer() function. However, the necessary initialization work is not completed, which lays the groundwork for future program failure.

Assign an integer constant to a variable. The code is as follows:

int a= 34 , b= 034 ;

Are variables a and b equal? The answer is no. We know that hexadecimal constants are prefixed with '0x' , and decimal constants do not need a prefix, so what about octal ? It is different from both decimal and hexadecimal representations. It is prefixed with the number '0' , which is a bit strange: the representation methods of the three bases are completely different. If octal is also prefixed with numbers and letters like hexadecimal , it may be more helpful to reduce software bugs . After all, you may not use octal more times than you use it incorrectly! The following is an example of misusing octal , with the last array element assigned incorrectly:

a[0]= 106 ; /* decimal number 106 */ a[1]= 112 ; /* decimal number 112 */ a[2]= 052 ; /* actually decimal number 42 , intended to be decimal number 52 */

Pointer addition and subtraction operations are special. The following code runs on a 32 -bit ARM architecture. After execution, what are the values of a and p ?

int a=1;int *p=(int*)0x00001000;a=a+1;p=p+1;

It is easy to judge that the value of a is 2 , but the result of p is 0x00001004 . After adding 1 to pointer p , the value of p increases by 4. Why is this? The reason is that pointer addition and subtraction operations are performed in units of the pointer data type. p+1 is actually p+1*sizeof(int) . If you don't understand this, it is very easy to make mistakes when using pointers to directly operate data. For example, the following code initializes the continuous RAM to zero :

unsigned int *pRAMaddr; // define address pointer variable for (pRAMaddr=StartAddr;pRAMaddr<EndAddr;pRAMaddr+= 4 ){ *pRAMaddr= 0x00000000 ; // specify RAM address to clear }

Since pRAMaddr is a pointer variable , the code pRAMaddr+=4 actually makes pRAMaddr offset by 4*sizeof(int)=16 bytes, so each execution of the for loop will cause the variable pRAMaddr to offset by 16 bytes, but only 4 bytes will be initialized to zero. The contents of the other 12 bytes will be random numbers in most architecture processors.

For sizeof() , there are two points to emphasize here. First, it is a keyword, not a function, and it returns unsigned integer data by default (remember it is unsigned); second, when using sizeof to obtain the length of an array, do not apply the sizeof operator to a pointer, such as the following example:

void ClearRAM( char array []) { int i ; for (i= 0 ;i< sizeof ( array )/ sizeof ( array [ 0 ]);i++) // The usage here is wrong, array is actually a pointer { array [i]= 0x00 ; }} int main( void ) { char Fle[ 20 ]; ClearRAM(Fle); // Only the first four elements in array Fle can be cleared }

We know that for an array array[20] , we can use the code sizeof(array)/sizeof(array[0]) to get the elements of the array (here 20 ), but array names and pointers are often easily confused, and there is only one case where an array name can be used as a pointer, that is, when the array name is used as a function parameter, the array name is considered a pointer. At the same time, it can no longer serve as an array name. Note that only in this case can the array name be used as a pointer, but unfortunately this case is prone to risks. In the ClearRAM function, the array[] parameter is no longer an array name, but a pointer. sizeof(array) is equivalent to finding the number of bytes occupied by the pointer variable. In a 32 -bit system, the value is 4 , and the result of sizeof(array)/sizeof(array[0]) is also 4. Therefore , calling ClearRAM(Fle) in the main function can only clear the first four elements in the array Fle .

The increment operator ++ and the decrement operator -- can be used as both prefixes and suffixes. The difference between a prefix and a suffix is that the time at which the value is increased or decreased is different. As a prefix, it first adds or decrements itself and then performs other operations. As a suffix, it first performs the operation and then adds or decrements itself. Many programmers do not know enough about this, which can easily lead to hidden dangers. The following example can well explain the difference between prefixes and suffixes.

int a=8,b=2,y;y=a+++--b;

What is the value of y after the code executes ?

This example is not a C puzzle designed to make you rack your brains (if you feel confident that you have mastered the details of C , it is a good choice to test it by doing some C puzzles. Then, you must not miss " The C Puzzle Book ".), you can even use this difficult statement as a negative example of unfriendly code. But it can also help you better understand the C language. According to the operator priority and the greedy method of the compiler to identify characters, the code y=a+++--b; can be written in a more explicit form:

y=(a++)+(--b);

When assigning values to variable y , the value of a is 8 and the value of b is 1, so the value of variable y is 9 ; after the assignment is completed, variable a is incremented and the value of a becomes 9. Do not assume that the value of y is 10. This assignment statement is equivalent to the following two statements:

y=a+(--b);a=a+ 1 ;

2. Toy compiler semantic checking

In order to design compilers more simply, the semantic checks of almost all compilers are relatively weak. In addition, in order to achieve faster execution efficiency, the C language is designed to be flexible enough and performs almost no runtime checks, such as array out-of-bounds checks, whether pointers are legal, whether calculation results overflow, etc.

C language is flexible enough. For an array a[30] , it allows the use of a[-1] to quickly get the data before the address of the first element of the array; it allows a constant to be cast into a function pointer, and the code (*((void(*)())0))() is used to call the function at address 0. C language gives programmers enough freedom, but programmers also bear the responsibility for abusing freedom. The following two examples are both infinite loops. If similar codes appear in uncommon branches, it will cause seemingly inexplicable freezes or restarts.

a. unsigned char i ; b. unsigned char i;

for(i=0;i<256;i++) {… } for(i=10;i>=0;i--) { … }

For unsigned char type, the range of representation is 0~255 , so the unsigned char type variable i is always less than 256 (the first for loop executes infinitely) and always greater than or equal to 0 (the second for loop executes infinitely). It should be noted that the assignment code i=256 is allowed by C language, even if this initial value has exceeded the range that variable i can represent. It can be seen that C language will do everything possible to create opportunities for programmers to make mistakes.

If you accidentally add a semicolon after the if statement and change the program logic, the compiler will also help cover it up and not even give a warning. The code is as follows:

if (a>b); //A semicolon was added here by mistake a=b; //This code is always executed

Not only that, the compiler will also ignore extra spaces and newlines, just like the following code will not give enough hints:

if (n< 3 ) return //A semicolon is missing here logrec.data = x[ 0 ]; logrec.time = x[ 1 ];logrec.code = x[ 2 ];

The original intention of this code is that the program returns directly when n<3 . Due to the programmer's mistake, the return lacks a semicolon. The compiler translates it into returning the result of the expression logrec.data=x[0] . Even an expression after return is allowed in C language. In this way, when n>=3 , the expression logrec.data=x[0]; will not be executed, which buries a hidden danger to the program.

It can be said bluntly that weak compiler semantic checking has largely allowed unreliable code to exist with impunity.

As mentioned above, arrays are often an important factor that causes program instability. Programmers often write arrays out of bounds without noticing. A colleague's code ran on hardware, and after a while, he found that a number on the LCD display was abnormally changed. After a period of debugging, the problem was located in the following code:

int SensorData[ 30 ]; … for (i= 30 ;i> 0 ;i--){ SensorData[i]=…; …}

Here, an array with 30 elements is declared . Unfortunately, the for loop code mistakenly uses the non-existent array element SensorData[30] , but the C language allows this usage and happily changes the value of the array element SensorData[30] according to the code. The location of SensorData[30] was originally an LCD display variable, which is why the value on the display was changed abnormally. I am so glad that I found this bug so easily .

In fact, many compilers will generate a warning for the above code: the assignment exceeds the array bounds. But not all programmers are sensitive enough to compiler warnings, and the compiler cannot check all cases of array out of bounds. For example, you define an array in module A :

int SensorData[ 30 ];

Reference the array in module B. Since the code you reference is not standardized, the array size is not declared here, but the compiler allows this:

extern int SensorData[];

If the same code as above exists in module B :

for (i= 30 ;i> 0 ;i--){ SensorData[i]=…; …}

This time, the compiler will not give a warning message, because the compiler does not know the number of elements in the array. Therefore, when an array is declared with external linkage, its size should be explicitly declared.

Here is another example where the compiler cannot detect array out-of-bounds errors. The parameter of the function func() is an array, and the function code is simplified as follows:

char * func( char SensorData[ 30 ]) { unsignedint i; for (i= 30 ;i> 0 ;i--) { SensorData[i]=…; … }}

The compiler does not give any warning for the statement that assigns an initial value to SensorData[30] . In fact, the compiler implicitly converts the array name Sensor into a pointer to the first element of the array. The function body uses the pointer to access the array, so it certainly does not know the number of array elements. One of the reasons for this is that the authors of the C compiler believe that using pointers instead of arrays can improve program efficiency and simplify the complexity of the compiler.

Pointers and arrays can easily cause confusion in programs, so we need to carefully distinguish their differences. In fact, if you think about it from another angle, they are also easy to distinguish: there is only one case where an array name can be equated with a pointer, which is when the array is used as a function parameter in the above example. At other times, the array name is the array name, and the pointer is the pointer.

In the following example, the compiler also fails to detect array out of bounds.

We often use arrays to cache a frame of data in communication. In the communication interruption, the received data is saved in the array until a frame of data is completely received before processing. Even if the defined array length is long enough, array out-of-bounds may occur during the data reception process, especially when interference is severe. This is because external interference destroys certain bits of the data frame, the data length of a frame is misjudged, the received data exceeds the array range, and the excess data overwrites the variables adjacent to the array, causing the system to crash. Due to the asynchronous nature of the interrupt event, the compiler cannot detect this type of array out-of-bounds.

If the local array is out of bounds, it may cause an ARM architecture hardware exception. A colleague's device is used to receive data from wireless sensors. After a software upgrade, it was found that the receiving device would crash after working for a period of time. Debugging shows that a hardware exception occurred in the ARM7 processor, and the exception handling code is an infinite loop (the direct cause of the crash). The receiving device has a hardware module for receiving the entire packet of data from the wireless sensor and storing it in its own hardware buffer. When a frame of data is received, an external interrupt is used to notify the device to fetch data. The simplified external interrupt service program is as follows:

__irq ExintHandler (void){ unsignedchar DataBuf[ 50 ]; GetData(DataBug); //Get a frame of data from the hardware buffer ...}

Because there are multiple wireless sensors that may send data almost simultaneously and the GetData() function is not well protected, the array DataBuf goes out of bounds during data retrieval. Since the array DataBuf is a local variable, it is allocated in the stack, along with the operating environment when the interrupt occurs and the interrupt return address. The overflow data destroys these data, and the PC pointer may become an illegal value when the interrupt returns, resulting in a hardware exception.

If we carefully design the overflow data and convert the data into instructions, we can use the array out-of-bounds to modify the value of the PC pointer to point to the code we want to execute. In 1988 , the first network worm infected 2,000 to 6,000 computers in one day . This worm program took advantage of an array out-of-bounds bug in a standard input library function . The cause was a standard input and output library function gets() , which was originally designed to get a piece of text from a data stream. Unfortunately, the gets() function did not specify the length of the input text. A 500- byte array was defined inside the gets() function . The attacker sent data larger than 500 bytes and used the overflowed data to modify the PC pointer in the stack , thereby obtaining system permissions.

A program module usually consists of two files, a source file and a header file. If you define a variable in a source file:

unsigned int a;

And declare the variable in the header file: extern unsigned long a;

The compiler will prompt a syntax error: the declared type of variable 'a' is inconsistent. But if you define the variable in the source file:

volatile unsigned int a,

Declare variables in the header file: extern unsigned int a; /* Missing volatile qualifier */

The compiler will not give an error message (some compilers only give a warning). Here, volatile is a type qualifier. Another common type qualifier is the const keyword. The qualifier volatile is crucial in embedded software. It is used to tell the compiler not to optimize the variable it modifies. Here is a deliberately constructed example, because most of the real-world volatile usage bugs are implicit and difficult to understand.

In the source file of module A , define the variable:

volatile unsigned int TimerCount= 0 ;

This variable is used for software timing in a timer service program:

TimerCount++; //Read the value of IO port 1

In the header file of module A , declare the variable:

extern unsigned int TimerCount; //The type qualifier volatile is missing here

In module B , use the TimerCount variable for precise software delay:

#include "...Ah" //First include the header file of module A ...TimerCount= 0 ; while (TimerCount>=TIMER_VALUE); //Delay for a while ...

In fact, this is an infinite loop. Since the volatile qualifier was omitted when declaring the variable TimerCount in the header file of module A , the variable TimerCount is treated as an unsigned int type variable in module B. Since registers are much faster than RAM , the compiler will first copy the variable from RAM to the register when using non- volatile qualified variables . If the same code block uses the variable again, it will no longer copy the data from RAM but directly use the previous register backup value. In the code while(TimerCount>=TIMER_VALUE) , the variable TimerCount is only used during the first execution, and the register backup value is used afterwards. This register value is always 0 , so the program loops infinitely. The following flowchart illustrates the execution process of the program with and without the volatile qualifier .

The compiler under the ARM architecture frequently uses the stack to store function return values, registers that must be protected as specified by the AAPCS , and local variables, including local arrays, structures, unions, and C++ classes. The initial value of a local variable allocated from the stack is uncertain, so the variable needs to be explicitly initialized at runtime. Once the local variable leaves its scope, the variable is immediately released and can be used by other code, so a memory location in the stack may correspond to multiple variables in the entire program.

Local variables must be explicitly initialized unless you know what you are doing. The temperature value obtained by the following code will be very different from the expected value, because when using the local variable sum , its initial value is not guaranteed to be 0. The compiler will clear the stack area when it runs for the first time, which aggravates the hiddenness of such bugs .

unsigned intGetTempValue(void){ unsigned int sum; //Define local variables to save total value for(i=0;i<10;i++) { sum+=CollectTemp(); //Function CollectTemp can get the current temperature value} return (sum/10);}

Since local variables are released once the program leaves their scope, it is meaningless for the following code to return a pointer to a local variable. The area pointed to by the pointer may be used by other programs and its value may be changed.

char * GetData( void ) { char buffer[ 100 ]; //local array ... return buffer;}

The good news is that more and more compilers are aware of the importance of semantic checking, and compiler semantic checking is becoming more and more powerful. For example, the famous Keil MDK compiler has added dynamic syntax checking and strengthened semantic checking in its V4.47 or above, which can provide more friendly warning messages.

3. Improper Prioritization

C has 32 keywords but 34 operators. It is difficult to remember the precedence of all operators. Improper #define will aggravate the priority problem and make the problem more hidden.

#define READSDA IO0PIN&(1<<11) //define macro, read the port status of IO port p0.11 //determine whether port p0.11 is high level if (READSDA==( 1 << 11 )) { …}

The compiler brings in the macro after compilation , and the original if statement becomes :

if (IO0PIN&( 1 << 11 ) ==( 1 << 11 )){ …}

The priority of the operator '==' is greater than that of '&' . The code IO0PIN&(1<<11) ==(1<<11)) is equivalent to IO0PIN&0x00000001 : to determine whether port P0.0 is at a high level, which is far from the original intention.

In order to create more software bugs , the number of operators in C language is certainly not limited to a large number. On this basis, there are many operators that may cause misunderstandings when used in the conventional way! As shown in the following table:

4. Implicit conversion and forced conversion

This is another one of C 's weirdnesses, and it's as dangerous as arrays and pointers. A statement or expression should usually use only one type of variable or constant. However, if you mix types, C uses a set of rules to automatically convert between types. This can be convenient, but it can also be dangerous.

a. When appearing in an expression, signed and unsigned char and short types are automatically converted to int type, and, if necessary, to unsigned int ( when short and int have the same size). This is called type promotion. Promotion usually does no harm in arithmetic operations, but if the bitwise operators ~ and << are applied to operands of primitive type unsigned char or unsigned short , the result should be immediately cast to unsigned char or unsigned short type (depending on the type used in the operation).

uint8_t port =0x5aU; uint8_t result_8; result_8= (~port) >> 4;

If we don't understand the type promotion in the expression, we think that the variable port is always of unsigned char type during the operation. Let's take a look at the operation process: ~port results in 0xa5 , 0xa5>>4 results in 0x0a , which is the value we expect. But in fact, the result of result_8 is 0xfa ! Under the ARM structure, the int type is 32 bits. The variable port is promoted to int type before the operation : ~port results in 0xffffffa5 , 0xa5>>4 results in 0x0ffffffa , and is assigned to the variable result_8 . Type truncation occurs (this is also implicit!), result_8=0xfa . After such a weird implicit conversion, the result is very different from the value we expect! The correct expression statement should be:

result_8=( unsigned char ) (~port) >> 4 ; /*forced conversion*/

b. In any operation involving two data types, the two values will be converted to the higher level of the two types. The order of type level from high to low is long double , double , float , unsigned long long , long long , unsigned long , long , unsigned int , int . This type promotion is usually a good thing, but many programmers often do not really understand this sentence and do some things for granted, such as the following example, the int type represents 16 bits.

uint16_t u16a = 40000; /* 16-bit unsigned variable*/uint16_t u16b= 30000; /* 16-bit unsigned variable*/uint32_t u32x; /* 32-bit unsigned variable*/uint32_t u32y;u32x = u16a +u16b; /* u32x = 70000 or 4464? */u32y =(uint32_t)(u16a + u16b); /* u32y = 70000 or 4464? */

The results of u32x and u32y are both 4464 ( 70000%65536 )! Don't think that if there is a high-category uint32_t type variable in the expression, the compiler will help you promote all other low-category variables to uint32_t type. The correct way to write it is:

u32x = (uint32_t)u16a + (uint32_t)u16b; or: u32x = (uint32_t)u16a + u16b;

The latter is correct in this expression, but not necessarily in other expressions, for example:

uint16_t u16a,u16b,u16c;uint32_t u32x;u32x= u16a + u16b + (uint32_t)u16c;/*Wrong writing, u16a+ u16b may still overflow*/

c. In the assignment statement, the final result of the calculation is converted to the type of the variable to which the value is assigned. This process may result in type promotion or type downgrade. Downgrade may cause problems. For example, the value of 321 is assigned to an 8 -bit char type variable. The program must properly handle data overflow during calculation.

Many other languages, such as Pascal (funny thing is that one of the designers of C language once wrote an article severely criticizing Pascal language), do not allow mixed use of types, but C language will not limit your freedom, even if this often causes bugs .

d. When passed as function arguments, char and short are converted to int , and float is converted to double .

The eC language supports forced type conversion. If you must perform a forced type conversion, make sure you have enough knowledge about the type conversion:

Not all casts are risky; converting an integer value to a wider type with the same signedness is perfectly safe.

When a type with higher precision is cast to a type with lower precision, the result is obtained by discarding an appropriate number of the most significant bits, which means that data truncation occurs and the sign bit of the data may be changed.

When a low-precision type is cast to a high-precision type, there is no problem if the two types have the same sign; it should be noted that when a negative signed low-precision type is cast to an unsigned high-precision type, sign extension is performed unintuitively, for example:

unsigned int bob; signed char fred = -1 ;bob=( unsigned int )fred; /*Sign extension occurs, now bob is 0xFFFFFFFF*/

Some programming advice:

In-depth understanding of embedded C language and compiler

Careful and careful programming

Use good style and reasonable design

Don't rush into writing code, think twice before writing each line of code: What kind of errors might occur? Have you considered all the logical branches?

Turn on all compiler warnings

Analyze code using static analysis tools

Read and write data safely (check all array bounds ... )

Check the validity of the pointer

Check the validity of function entry parameters

Check all return values

Initialize all variables where they are declared

Use brackets appropriately

Be careful with casts

Use good diagnostic information logs and tools

This post is from ARM Technology

Latest reply

I didn't use the code snippet function to post, so the formatting is all messed up.   Details Published on 2020-4-13 11:44
 

1w

Posts

25

Resources
2
 

I didn't use the code snippet function to post, so the formatting is all messed up.

This post is from ARM Technology
 
 
 

28

Posts

0

Resources
3
 

Today's sharing ends here. If you have any questions, please feel free to discuss with me at 3250395686

This post is from ARM Technology
 
 
 

Just looking around
Find a datasheet?

EEWorld Datasheet Technical Support

EEWorld
subscription
account

EEWorld
service
account

Automotive
development
circle

Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号
快速回复 返回顶部 Return list