Embedded C track

Lesson 02 · Part 1: Foundations (shared — verified with PC gcc)

Data Types, Number Bases, stdint & sizeof

Exact bit widths and `uint8/16/32_t`

Part 1: Foundations (shared — verified with PC gcc)10 min readFree lesson

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 the printf format (%u %x %X %o).
  • Use sizeof to check the byte size of data types and variables.
  • Understand the wrap-around of unsigned types (e.g., uint8_t 255+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 0255 / -128127
uint16_t / int16_t 16-bit 065535 / -3276832767
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., P1 on 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 of int (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 prints uint8_t/uint16_t/uint32_t because they are identical With-1/2/4 everywhere, so the results do not vary. You can see that the width of int varies 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. int is 16-bit on 8051, and On-ARM/PC is 32-bit.
  • The width of stdint.h's uint8_t/uint16_t/uint32_t is 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 leading 0 (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.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/types_radix/main.cC
#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;
}
src/types_radix/pc_test.cC
#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;
}

Assignment

This assignment is all pure logic (PC verifiable). You can solve and grade it using only gcc without a board or μVision.

gcc main.c -o prog && ./prog

Problem 1 — Radix Table for 8-bit Values

  • File name: homework_01/main.c
  • Key Concept: Number notation (10/16/2-binary), Bit extraction (>>, &)
  • Platform: Common

Requirements

  • Sequentially print the value 0, 1, 15, 16, 170, 255, with each value on a separate line in decimal·hexadecimal·binary.
  • Decimal should be right-aligned in a width of 3 (%3u), hexadecimal should be 0x + 2 uppercase digits (0x%02X), binary should be printed in 8 digits.
  • The output format must match the "Expected Output" below character by character.

Expected output

dec=  0  hex=0x00  bin=00000000
dec=  1  hex=0x01  bin=00000001
dec= 15  hex=0x0F  bin=00001111
dec= 16  hex=0x10  bin=00010000
dec=170  hex=0xAA  bin=10101010
dec=255  hex=0xFF  bin=11111111

Hint

  • If you put values in an array and loop through them, the code becomes shorter: uint8_t values[] = {0,1,15,16,170,255};
  • Zero padding for 2-digit hexadecimal is %02X. If omitted, 0x0F appears as 0xF.
  • There is no standard format for binary, so print manually. From the most significant bit (i=7) to i=0, print one bit at a time with (v >> i) & 1.
  • Verification: 170 = 0xAA = 10101010, 15 = 0x0F = 00001111.

Problem 2 — uint8_t Counter Wrap-Around

  • File name: homework_02/main.c
  • Key Concept: Wrap-around of unsigned data type (255 → 0)
  • Platform: Common

Requirements

  • Start the uint8_t counter at 250 and increment it 8 times by 1, printing the value each time after incrementing.
  • Print the starting value on the first line with start:, and the subsequent values with step N: (N is 1~8).
  • The wrap-around point, where it returns from 255 back to 0, should be visible.

Expected output

start: count = 250
step 1: count = 251
step 2: count = 252
step 3: count = 253
step 4: count = 254
step 5: count = 255
step 6: count = 0
step 7: count = 1
step 8: count = 2

Hint

  • The counter must be declared with uint8_t. Using int will increase it to 256, 257…, which prevents seeing the wrap.
  • 8-bit unsigned can only represent 0 to 255, so after 255 comes 0, not 256.
  • The initial value output is once before the loop, and afterward in the loop in the order count++ → output.

Check your answers

After solving it yourself, compare it with the reference solution below. The solution files include comments on key points and common mistakes.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

homework_01/main.cC
#include <stdio.h>
#include <stdint.h>

/*
 *Answer 1 — 8-bit value numeral table
 *
 *Key Points:
 *- View a value side by side in decimal (%3u), hexadecimal (0x%02X), and 8-digit binary.
 *- Since there is no format for binary output in the standard printf, extract the bits directly:
 *Use (v >> i) & 1 to get the i-th bit as 0/1.
 *
 *Common mistake:
 *- Forgetting the '0' padding in %02X can cause 0x0F to appear as 0xF.
 *- Printing binary output from the LSB (reversed) can flip the order of digits.
 *Starting from the most significant bit (i=7) down to i=0 creates the human-readable order.
 */

/*Print an 8-bit value in 8-digit binary (MSB → LSB order).*/
static void print_bin8(uint8_t v)
{
    int i;
    for (i = 7; i >= 0; i--) {
        putchar(((v >> i) & 1u) ? '1' : '0');
    }
}

int main(void)
{
    /*Representative values that are good for getting a sense of numerals*/
    uint8_t values[] = { 0, 1, 15, 16, 170, 255 };
    int n = (int)(sizeof(values) / sizeof(values[0]));
    int k;

    for (k = 0; k < n; k++) {
        printf("dec=%3u  hex=0x%02X  bin=",
               (unsigned)values[k], values[k]);
        print_bin8(values[k]);
        printf("\n");
    }

    return 0;
}
homework_02/main.cC
#include <stdio.h>
#include <stdint.h>

/*
 *Answer 2 — uint8_t Counter Wrap-Around
 *
 *Key Points:
 *- uint8_t can only represent values from 0 to 255. Adding 1 to 255 does not result in 256,
 *but 'wraps around' to 0. This is the principle behind hardware timer counters.
 *- Start at 250 and observe the point where the 6th increment moves from 255 → 0.
 *
 *Common mistake:
 *- If count is declared as int, it keeps increasing to 256, 257... and the wrap-around is not visible.
 *Using fixed-width uint8_t allows reproducing the wrap-around at the 8-bit boundary.
 *- One might expect it to output 256 after 255. Unsigned values never become negative,
 *and when the maximum is exceeded, it starts counting again from 0.
 */
int main(void)
{
    uint8_t count = 250;   /*Start from a value close to 255*/
    int step;

    printf("start: count = %u\n", (unsigned)count);

    for (step = 1; step <= 8; step++) {
        count++;           /*After 255, it wraps around to 0*/
        printf("step %d: count = %u\n", step, (unsigned)count);
    }

    return 0;
}

Learn this lesson with a mentor

Work through the Embedded C track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now