When writing C on a PC, we hardly ever worry about how many bytes int takes. Memory is plentiful, and anyway, the compiler handles it well. But embedded systems are different. The registers of an MCU have a width exactly defined (8-bit, 16-bit, 32-bit), and RAM is only a few KB. Whether "this variable has exactly how many bits" determines whether it fits properly in a register and whether memory is wasted.
In this chapter, using pure C logic without a board, we look into the actual width of data types. Why fixed-width types of stdint.h are practically the standard in embedded systems, how to view the same number in decimal, hexadecimal, and binary, checking the size using sizeof, and even checking on a PC with gcc how unsigned values wrap around when exceeding the maximum.
Learning Objectives
- Understand that the size of basic C data types can differ depending on the compiler and chip, and explain the width differences between the 8051 and of-ARM/PC
int. - Explain why fixed-width types of
stdint.h(uint8_t/uint16_t/uint32_t, etc.) are used by linking them to register width. - Represent the same number in decimal, hexadecimal (
0x), octal, and binary, and output it in theprintfformat (%u %x %X %o). - Use
sizeofto check the byte size of data types and variables. - Understand the wrap-around of unsigned types (e.g.,
uint8_t255+1=0) and the differences in sign interpretation for the same bits.
Target Environment
- Compiler: Keil C51 (8051) / Keil MDK-ARM (STM32) — This time it's pure logic, so either one or even just a PC is sufficient.
- Device: Pure logic — PC + gcc or μVision simulator (actual board not required).
- Verification:
gcc(main.c,pc_test.c) on PC, or Watch window in μVision simulator.
Key Concepts
1) Basic C data types and the problem of "size varies by chip"
The C standard only guarantees the minimum size of each data type, and the exact byte size is determined by the compiler and target chip. So even a single int can have different widths depending on the environment.
| Data Type | 8051 (Keil C51) | ARM Cortex-M (MDK) | PC (x86-64 gcc) |
|---|---|---|---|
char |
1 byte | 1 byte | 1 byte |
int |
2 bytes (16-bit) | 4 bytes (32-bit) | 4 bytes (32-bit) |
long |
4 byte | 4 byte | 8 byte |
| Pointer | 1~3 bytes (depending on memory area) | 4 bytes | 8 bytes |
int is 16-bit on 8051, and 32-bit on ARM. If you tried to count up to 50000 with int count; on 8051, it would hit the 16-bit limit (if signed, -32768~32767), resulting in incorrect values. The assumption "because it’s int it should be enough" is a typical point where it breaks in embedded systems.
2) stdint.h — a data type with width built into its name
The solution is to use types with the size specified in their name. Using #include <stdint.h>, you can use the following.
| Type | Width | Range |
|---|---|---|
uint8_t / int8_t |
8-bit | 0 |
uint16_t / int16_t |
16-bit | 0 |
uint32_t / int32_t |
32-bit | 0~4294967295 / about ±2.1 billion |
Just by looking at the name, you can know the width has the same meaning on any chip. Furthermore, registers have a precisely defined width, so if you choose the exact type, your intention becomes clear.
- 8-bit Port/Register (e.g.,
P1on 8051) →uint8_t - 16-bit timer counter →
uint16_t - 32-bit STM32 peripheral register →
uint32_t
Memory is also saved. If you use
uint8_t(1 byte) instead ofint(2 bytes on 8051) for a counter in the 0~200 range, you save RAM. On chips with only a few KB, this difference adds up.
3) Number Notation — Decimal, Hexadecimal, Octal, Binary
Hexadecimal is natural when dealing with registers and bits because one hex digit corresponds exactly to 4 bits (a nibble).
| Notation | Example | Meaning |
|---|---|---|
| Decimal | 42 |
Human-counted numbers |
| Hexadecimal | 0x2A |
0x prefix, one digit = 4 bits |
| Octal | 052 |
0 prefix — if added by mistake, it becomes octal (caution!) |
| Binary | 0b00101010 |
0b is a compiler extension (standard from C23) — avoid for portability |
printf format characters also differ depending on the base.
| Format | Output | Example (value 42) |
|---|---|---|
%u |
Unsigned decimal | 42 |
%d |
Signed decimal | 42 |
%x / %X |
Hexadecimal (lowercase/uppercase) | 2a / 2A |
%o |
Octal | 52 |
The standard printf has no binary output format. Therefore, for binary, bits are printed directly: with (v >> i) & 1, the 0/1-th bit is obtained from i (bit operations are covered fully in part 3).
4) sizeof — Verifying the actual type of a data type
sizeof is an operator that gives the number of bytes a data type or variable occupies (it is not a function).
sizeof(uint16_t) /* 2 */
sizeof(reg) /*Size of variable reg*/
The result type is size_t (unsigned integer), so when printing with printf, you cast to (unsigned) or use %zu. In this example, for C90 compatibility, it is cast to (unsigned) and printed with %u. If the chip changes, you can directly check with sizeof whether sizeof(int) is 2 or 4.
5) Signed None/Exists-and Wraparound
Unsigned types do not represent negative numbers. Instead, if the maximum value is exceeded, it wraps around to 0.
uint8_t c = 255;
c = c + 1; /*0, not 256*/
This is not a bug but defined behavior, and it is also how hardware timer counters operate. Meanwhile, the same bit pattern can have different values depending on how it is interpreted as signed. 0xFF (all 8 bits set to 1) reads as 255 in uint8_t, but as -1 (two's complement) in int8_t. If a register value is mistakenly assigned to a signed variable, this is where it can unexpectedly become negative.
Viewing in μVision (This time, a PC is sufficient)
This section deals with pure logic not tied to any specific device. To check in μVision, add main.c to an empty project and enter debug mode (Ctrl+F5). Then add variables like reg in the Watch window, right-click → change the radix to Number Base → Hexadecimal, and observe how the same value appears differently. The step-by-step procedure for creating official device-specific projects is covered in sections 6 (8051) and 14 (STM32).
Checking text output is much easier on a PC. Try it directly with "Verification on PC" below.
Examples
Example 1 — main.c: Multiple Radices for the Same Value with Fixed-width Types
Print the size of a fixed-width type using sizeof, and output one value (0x2A) side by side in decimal, hexadecimal, octal, and binary. The goal is to visually confirm that the number is the same, differing only in radix.
#include <stdio.h>
#include <stdint.h>
/*Print an 8-bit value as an 8-digit binary number.
*The standard printf does not have a binary output format, so the bits are extracted and printed directly.*/
static void print_bin8(uint8_t v)
{
int i;
for (i = 7; i >= 0; i--) {
putchar(((v >> i) & 1u) ? '1' : '0');
}
}
/*
*Lesson 02 Example 1 — Look at multiple numeral representations of the same number, including the actual width of data types.
*The width-fixed types in stdint have the same size on any chip (8051/STM32/PC).
*/
int main(void)
{
uint8_t reg = 0x2A; /*Hex 0x2A = Decimal 42 = Binary 00101010*/
/*The size of the width-fixed types in stdint is exactly as their name indicates (the same everywhere).*/
printf("sizeof(uint8_t) = %u byte\n", (unsigned)sizeof(uint8_t));
printf("sizeof(uint16_t) = %u byte\n", (unsigned)sizeof(uint16_t));
printf("sizeof(uint32_t) = %u byte\n", (unsigned)sizeof(uint32_t));
/*Look at a single value in decimal, hexadecimal, octal, and binary.*/
printf("\n");
printf("dec = %u\n", (unsigned)reg);
printf("hex = 0x%02X\n", reg);
printf("oct = 0%o\n", reg);
printf("bin = ");
print_bin8(reg);
printf("\n");
return 0;
}
Example 2 — pc_test.c: Wrap-around and Signed Interpretation
It checks when an unsigned value exceeds the maximum value and wraps around to 0, and also how the same 0xFF bits diverge to with-255/1 depending on whether they are signed or unsigned.
#include <stdio.h>
#include <stdint.h>
/*
*Lecture 2 Example 2 — Observing the 'wrap-around' of unsigned data types and
*how the same bits are interpreted differently depending on whether they are signed or unsigned.
*It is a phenomenon you encounter daily in registers and counters, so you get a feel for it first on the PC.
*/
int main(void)
{
uint8_t u = 255; /*Maximum value of uint8_t*/
uint8_t bits = 0xFF; /*Let's interpret the same 8 bits in two ways*/
/*Unsigned overflow is a 'wrap-around': when it exceeds the maximum value, it returns to 0.*/
u = (uint8_t)(u + 1);
printf("uint8_t 255 + 1 = %u\n", (unsigned)u); /* 0 */
/*The same 0xFF bits are read differently as unsigned/signed.
*(int8_t)0xFF is -1 in a two's complement environment (most MCUs and PCs).*/
printf("0xFF as uint8_t = %u\n", (unsigned)bits); /* 255 */
printf("0xFF as int8_t = %d\n", (int)(int8_t)bits); /* -1 */
return 0;
}
Verification on a PC
Since this is pure logic, you can check it directly without a board.
gcc main.c -o main && ./main
Expected output (stdout):
sizeof(uint8_t) = 1 byte
sizeof(uint16_t) = 2 byte
sizeof(uint32_t) = 4 byte
dec = 42
hex = 0x2A
oct = 052
bin = 00101010
gcc pc_test.c -o pc_test && ./pc_test
Expected output (stdout):
uint8_t 255 + 1 = 0
0xFF as uint8_t = 255
0xFF as int8_t = -1
Note:
sizeof(int)outputs 4 on PC·ARM and 2 on 8051. The above example only printsuint8_t/uint16_t/uint32_tbecause they are identical With-1/2/4 everywhere, so the results do not vary. You can see that the width ofintvaries by environment in the table (Core Concept 1).
Checking with a Simulator (Reference)
In a μVision debug session, if you put reg in the Watch window and use the right-click menu to change the display between Decimal ↔ Hexadecimal, you will see that the same value is shown as 42 ↔ 0x2A. By stepping through one line at a time with F11, you can also visually confirm that the uint8_t variable wraps from 255 back to 0. (Because the simulator output of printf requires UART retargeting settings, it is easier to check the text on PC gcc — UART is covered in Parts 11 and 19.)
Common Mistakes
Q. I counted up to 50,000 with int count;, but the value is strange.
A. On 8051 (Keil C51), int is 16-bit, so if it is signed, the range is -3276832767, and unsigned, 065535. When dealing with large numbers, use a fixed-width uint32_t (or unsigned long). The habit on PC of "int is enough" is a representative case that fails on 16-bit chips.
Q. I used int x = 010;, but why is it 10 instead of 8?
A. If you prefix a number with 0, it is interpreted as octal. 010 is octal, so in decimal it is 8. Unintended leading zeros can cause bugs, so decimal numbers should not start with 0, and for hexadecimal, explicitly prefix with 0x.
Q. for (uint8_t i = 10; i >= 0; i--) does not end.
A. An unsigned i never becomes negative. If i is 0 and you i--, it wraps around to 255 instead of -1, making i >= 0 always true. Use a signed type for countdowns, or redesign the condition like while (i-- > 0).
Q. I read 0xFF from the register and stored it in a variable, but it shows -1.
A. If the receiving variable is int8_t/signed char, 0xFF will be -1 in two's complement. Bit fields like register values should be received as unsigned types (like uint8_t) to read 255 as intended.
Summary
- The size of basic C data types differs between chips.
intis 16-bit on 8051, and On-ARM/PC is 32-bit. - The width of
stdint.h'suint8_t/uint16_t/uint32_tis fixed in the name and consistent everywhere — the de facto standard in embedded systems. - Registers and bits are naturally expressed in hexadecimal (
0x). Be careful with leading0(octal) and width assumptions. - You can check the actual byte size of a data type using
sizeof. - Unsigned values wrap around to 0 when exceeding the maximum value, and the same bits can represent different values depending on how the sign is interpreted.
Try it yourself
Solve the assignment below. The reference solution is shown below.