Welcome to the Embedded Systems Programming course. My name is Miro Samek and in this lesson I'll talk about the fixed-width integer types stdint.h, endianness, mixing data types in expressions and a little about the floating point data types. For those of you, who watch this video in preparation for a job interview in embedded programming, this lesson will provide answers to some tricky questions. As usual, let's get started with making a copy of the previous "lesson10" project and renaming it to "lesson11". If you are just joining the course, you can download the previous projects from state-machine.com/quickstart. Get inside the new "lesson11" directory and double-click on the workspace file to open the IAR toolset. If you don't have the IAR toolset, go back to "lesson0". So far in this course, you have been only using the C built-in integer types, such as 'int' or 'unsigned'. The problem with these built-in types is that the C Standard does not actually prescribe their size. For example, 'int' or 'unsigned' can be 32-bit wide on a 32-bit machine, such as ARM, but they can be only 16-bit wide on a 16-bit or 8-bit machines, such as MSP430 or AVR. Beyond 'int' and 'unsigned', the C language offers also 'short' and 'unsigned short', 'long' and 'unsigned long', as well as 'char' and 'unsigned char' built-in data types. But, here again the C standard prescribes only that the size of 'short' must not be bigger than int and int must be not bigger than long. The plain 'char' data type is typically one byte, but it can be either signed or unsigned, depending on the compiler options. All these ambiguities are actually intentional in the original C standard to give the compiler vendors more flexibility, but in embedded work it is often imperative to know exactly the size, signedness, and dynamic range of your variables, so that your code runs the same way, regardless of the target processor. This is obviously not a new problem and solutions have been around for decades. The approach generally taken is to define a new data type for each fixed-width integer you plan to use in your programs. For example, the popular MicroC/OS-II Real-Time Operating System uses the typedef statements to define the fixed width integer types as follows: Each typedef statement defines a new type and, as most definitions in C, should be read backwards. For example, the new type CPU_INT32U, is defined as the unsigned int built-in type. Once you typedef a new type name, you can use it in the same way as a built-in type to define your own variables, such as x, y, and z defined here. Of course, in a real project, you would put all these typedef statements in a header file with the .h extension, and then include this header file in all your .c files. However, please note that such a hand-made header file cannot be universal for two reasons. First, it uses some made-up names, which aren't standardized in the same sense as the built-in types are. And second, the definitions are correct only for a specific processor and a specific compiler. For example, the type CPU_INT32U is defined in terms of the 'int' type, which can have different size on different processors. It sure would have been nice if the authors of the C standard had defined some standard names and made compiler vendors responsible for providing the appropriate typedefs in a standard library header file. Interestingly, this is exactly what finally happened in the C99 ISO Standard, which provides the standard header file 'stdint.h'. For an embedded programmer, the 'stdint.h' header file is one of the most valuable features of the C99 Standard. The IAR compiler supports the C99 Standard, if you select it in the project options. You can actually look inside the stdint.h file, because it is inside the IAR toolset on your disk. As you can see, the file uses typedefs to define the following fixed-width integer types: int8_t: for signed 8-bit integer uint8_t: for unsigned 8-bit integer int16_t: for signed 16-bit integer uint16_t: for unsigned 16-bit integer and finally int32_t: for signed 32-bit integer uint32_t: for unsigned 32-bit integer The file defines more types, but these six are the most important. The value of the stdint header file is in standardizing the type names and the fact that it is now the responsibility of the compiler vendor to provide the right definitions for your CPU. The compilers do it frequently by means of macros, such as __INT32_T_TYPE__, which in turn are defined in therms of the built-in types. Obviously, with the ISO standard in place, inventing your own integer type names is counter-productive. Even if your compiler is not C99 compliant, you should provide the stdint.h header file with the standard names. For example, the Microsoft Visual C++ compiler is not C99 compliant, so if you wish to use it for developing embedded code on the PC, you can still do it by providing your own stdint.h header file. So, now, let's use the standard fixed-width integer types to take a bit closer look at how the ARM Cortex-M processor will handle them. Here I define a few variables of each fixed-width type, whereas I name the variables so that the type is reflected in the name. The unsigned variables start with 'u' and signed variables start with 's'. First, let's check that the fixed-width integers have really the expected size. The C language provides the sizeof() operator that returns the size in bytes either of a variable, such as u8a, or the data type, such as uint16_t or uint32_t. Let's build the code by pressing F7 to make sure that the compiler likes it so far. While you can run the code on the LauchPad board, I will today use the simulator so that anybody without the board can follow along as well. So let's start debugging in the simulator. Open the watch1 view and type the names of the variables you want to watch. At this point, some variables that you have defined are not available, because they are not yet used anywhere in the code and the compiler has eliminated them. When you step through the code, you can see that the reported sizeof(u8a) is 1 byte, the size of the uint16_t type is 2 bytes, and the sizeof the uint32_t type is 4 bytes, as expected. I'd also like to point your attention to the addresses of your variables, because their order in memory is different than the order in which you have defined them in the code. The compiler allocated the 4-byte variable u32e at a lower address than the 2-byte variable u16c, and left the 1-byte variable u8a at the end. The point to remember here is that the compiler can and will change the order of your definitions, so you should never assume that the order will be preserved. Next, let's initialize the variables to hex constants, but in such a way that you can distinguish each byte. For example, the first byte of u16c is hex-c1 and the second is hex-c2. Also, let's use some assignment statements to see how the CPU reads the values of one variable and writes it to another variable. Before you start debugging, please prepare the memory view to show the raw bytes, as they are naturally ordered in memory. For this you need to set the address of beginning of RAM and select the "1-times units" view. Now, take a look at the disassemly view as you single-step through the code. Notice that the processor uses the LDRB instruction to load the byte-wide variable u8a into R1 and the STRB instruction to store R1 into the 1-byte variable u8b. Similarly, the instruction LRDH is used to load u16b into R1 and STRH to store it into u16d. And finally, the instruction LDR is used to load u32e into R1 and STR to store it into u32f. The interesting point to remember here is that the ARM processor has special instructions LDRB and STRB to read and write bytes, LDRH and STRH to read and write half-words, and plain LDR and STR to read and write whole words into memory. Now, let's take a closer look in the memory view. The interesting thing here is that the order of bytes in the multi-byte variables, such u32e, is reversed compared to the order of these bytes in the register R1. This is because ARM is a so called little-endian machine, or strictly speaking the ARM core has configurable endianness, but virtually all silicon vendors, such as Texas Instruments, choose the little-endian configuration. Little-endian means that lower-order bytes from a register are placed at lower-addresses than higher-order bytes. For example, the least-significant byte e4 from the R1 register has been stored by the last STR instruction at a lower address than the e3, e2, and e1 bytes. I don't have a big-endian machine to show you the difference, but I can manually change the order of bytes to big-endian in the memory, so that you can see how it would look like on a big-endian processor. An example of such a big-endian processor is PowerPC used in the Mac computers before they switched to x86. Once you start using the various integer data types, you will inevitably encounter situations when you will have to to mix them in expressions and assignments. This will require conversions from one type to another and the good news is that the C language does it automatically through implicit conversions. The bad news is that the rules for the implicit conversions are rather complicated, counter-intuitive, and often lead to developer confusion. I can't possibly cover all the nuances of type conversions in C, but I will explain a couple of the most important principles. I'd like you to remember that this is perhaps the most tricky part of the C language, and often leads to subtle bugs. In the first example, I want to illustrate that C always automatically promotes any smaller-size integers to the built-in 'int' or 'unsigned int' type before performing any computations. Consider the following expression: When you run this example you can see that the result is 70000, which obviously is 40000 + 30000. Nothing surprising here. But this is because you have executed this code on an ARM processor, which is a 32-bit machine. In order to see a problem, you need to execute the code on a machine, where the standard 'int' data type is only 16-bit wide, such as MSP430. In fact, I have here the MSP-EXP430G2 LaunchPad board, with is quite similar to the Tiva LauchPad, except it has the MSP430 microcontroller instead of the ARM Cortex-M. I have also prepared a blinky program version for the MSP430 LauchPad, that I would like to demonstrate first. Since MSP430 is entirely different than the ARM, I am using here a version of the IAR toolset for MSP430. In fact, I have the IAR toolset for ARM still open on my screen in the background. So, let me show you how the blinky code executes on the MSP430 Lauchpad. Before I let the program run free, I first step through the code to demonstrate that the LED on the MSP430 board is turned on and off from the code. OK, so now I will just copy and paste the example code from the IAR for ARM to IAR for MSP430. I need to type again the names of the variables into the watch1 view. And finally, I can step through the code and see how the expression is evaluated. Oops, as you can see the result is 4464, instead of the expected 70000. Now, I don't think that you've expected this, did you? To understand what has just happened, you need to go back to my original statement. I said that C always automatically promotes any smaller-size integers to the built-in type 'int' or 'unsigned int' before performing any computations. On the 32-bit ARM machine, the promotion was to 32-bits, because this is the size of 'int' on the ARM. However, on MSP430, there was no real promotion, because the type 'int' is only 16-bit wide on MSP430. So, the computation happened in 16-bits, which overflew. So, the result 4464 that you saw is just the truncated value to 16-bits. Please check this with your calculator. The tricky aspect of this example is that the result of the computation is eventually assigned to a 32-bit wide number, which has enough dynamic range to represent 70000. This is something that throws many developers off. But I hope that this example will help you remember that the precision in which the computation is performed does *not* depend on the left-hand side of the assignment. So, knowing this, how would you fix the problem? Well, you need to enforce promotion to a 32-bit precision of at least one of the operands. That way, the other implicit conversion rule of the C language will apply, which is that the computation is performed at the largest precision of the involved operands. Specifically, if one of the operands is 32-bit wide, the other will be promoted to 32-bits and the whole computation will be performed at 32-bits. So, the solution is to explicitly cast one, or both of the operands to uint32_t, as follows. As I step through this code on the MSP430 LaunchPad, please watch the values of the operands and the result u32e. I specifically single-step through the disassembly to show you the additional work the 16-bit MSP430 CPU needs to do to promote both arguments to 32-bits. In particular, note the familiar partial value 4464, which is subsequently extended to 32-bits. In the end, the final result is 70,000, as expected. So, this final version is the truly portable code. You should copy and paste it to the IAR for ARM, because the version left there is not portable. Of course, on a 32-bit machine, the explicit cast to uint32_t will not cost any additional CPU cycles. Just to confirm, I run the code and verify the disassembly is the identical as before and the result is 70,000. In the next example, I would like to show you problems that can arise with changing the signedness of an arithmetic operation. Consider the following expression: Here I mix signed 10 with unsigned u16c. The expected result of this computation is 10 minus 100 equals -90, which is stored in the signed s32 variable. When you run this code on ARM, the result is indeed -90. However, when you copy this code to MSP430, and run it on the MSP430 LauchPad, you get a different result. The value in s32 in this case is 65446. The implicit conversion rule that applies in this case is that when you mix a signed and unsigned operands, both are promoted to 'unsigned int' and the result is 'unsigned int'. Subsequently, the result is converted to signed 32-bit, because this is the type of the s32 variable on the left-hand side of the assignment. When this expression is evaluated on the 32-bit ARM, the 'unsigned int' value is 32-bit wide, so it fills completely the s32 variable and the value is re-interpreted in the 2s-complement representation as negative. If you don't remember the 2s-complement representation of negative numbers, please go back to lesson 1. However, when the same expression is evaluated on the 16-bit MSP430, the 'unsigned int' is only 16-bit wide and fills up only the lower half of the s32 variable. Because the value is unsigned, it is not sign-extended to 32-bits, and is interpreted as a big positive number. The best way to avoid such non-portable code is to avoid mixing signed and unsigned. Instead, you should explicitly cast the unsigned variable to a signed type. Note also that in this case the problem was not with insufficient number of bits, so you can cast to the signed int16_t type, because it is sufficient. When you run the corrected code on MSP430, you can see that the value in s32 is sign-extended, so it is -90, as expected. The problem of mixing signed and unsigned integers arises often in comparisons. Consider the following if statement: When you compile this code, you get a warning that you have a pointless integer comparison. When you think about it, it seems to makes sense, because the smallest value an unsigned number can hold is zero, which is always bigger than -1. But wait a minute, the warning says that the comparison is always false, which is exactly the opposite what you would think. But this is apparently exactly what the compiler thinks, because you cannot event set a breakpoint in the if branch, only in the else branch. Indeed, when you run the code, you hit the breakpoint in the else branch. So how can that be? Well, this makes sense only when you recall that in the mixed singed-unsigned expressions, the C standard promotes the signed operand to 'unsigned int'. The -1 value promoted to 'unsigned int' is 0xFFFFFFFF, which is the largest value a 32-bit unsigned integer can hold. That's why the comparison is always false. Again, you can fix the problem by an explicit cast from unsigned to signed. The last frequent confusion I'd like to discuss is with binary operations on small integers. Consider the following comparison: You compare here a bit-wise 1s-complement of a byte with zero. Something like this could come up, for example, when your byte represents a checksum over some data and you want to make sure that the checksum adds up. At first glance, with value 0xff in u8a, the comparison should be true, because 1s-complement simply inverts all the bits. But in fact the comparison will always be false, although the compiler does not report a warning in this case. However, you cannot even set a breakpoint inside the if or even on the if itself, because the compiler has eliminated the whole thing. The compiler knows that the comparison will never be true, because the byte u8a will be promoted to int, so the most significant bytes will be zero. When 1s complement is taken, the most significant bytes will be all ones, so the result can never be zero. The remedy in this case is to specifically revert the promotion to int by casting the inverted value back to a byte. This concludes this lesson about the standard fixed-width integers, endianness and mixing types in expressions. I haven't covered the floating point types, co I have to leave that subject for another time. In the next lesson, I will talk about C structures and the Cortex Microcontroller Software Interface Standard (CMSIS) If you like this channel, please subscribe to stay tuned. You can also visit state-machine.com/quickstart for the class notes and project file downloads. --- Course web-page: http://www.state-machine.com/quickstart YouTube playlist of the course: http://www.youtube.com/playlist?list=PLPW8O6W-1chwyTzI3BHwBLbGQoPFxPAPM