Your 10-Step C Programming Journey
- Linux/Raspberry Pi:
sudo apt install gcc - Mac:
brew install gccor install Xcode Command Line Tools - Windows: Install WSL (Windows Subsystem for Linux) or MinGW
/* Step 1: Setup & Your First C Program
Compile: gcc step1_hello.c -o step1_hello
Run: ./step1_hello */
#include <stdio.h> // Standard I/O — gives us printf()
int main() {
printf("Hello, Robot World!\n");
printf("C is compiled, fast, and runs on everything.\n");
printf("Let's learn to build robots!\n");
return 0; // 0 = success; any other number = error
}
What gcc flag specifies the name of the output executable?
What does `return 0` at the end of main() signal to the operating system?
- int — whole numbers (4 bytes on most systems)
- float — decimal numbers (~7 digits precision)
- double — high-precision decimals (~15 digits)
- char — a single character or 1-byte integer
- uint8_t / int16_t — fixed-width types from
<stdint.h>(essential for hardware!)
sizeof()sizeof(type) to check how many bytes anything uses:
sizeof(int) = 4, sizeof(char) = 1, sizeof(double) = 8.
On a microcontroller with only 2KB of RAM, knowing this is critical!
/* Step 2: Variables & Data Types
Compile: gcc step2_types.c -o step2_types */
#include <stdio.h>
#include <stdint.h> // Fixed-width integer types
int main() {
/* Basic types */
int motor_speed = 75; // whole number
float sensor_angle = 45.5f; // decimal (f suffix = float literal)
double precise_dist = 3.14159265; // high-precision decimal
char drive_mode = 'A'; // single character
/* Fixed-width types — use these for hardware code! */
uint8_t pwm_duty = 200; // 0–255 (1 byte, unsigned)
int16_t encoder = -1024; // -32768–32767 (2 bytes, signed)
uint32_t timestamp = 0; // 0–4 billion (4 bytes, unsigned)
/* Print them — notice the format specifiers */
printf("Motor speed: %d%%\n", motor_speed);
printf("Sensor angle: %.1f deg\n", sensor_angle);
printf("Drive mode: %c\n", drive_mode);
printf("PWM duty: %u\n", pwm_duty);
printf("Encoder ticks: %d\n", encoder);
/* Check sizes — important on microcontrollers! */
printf("\nsizeof(int)=%zu sizeof(float)=%zu sizeof(uint8_t)=%zu\n",
sizeof(int), sizeof(float), sizeof(uint8_t));
return 0;
}
Which type is best for a PWM duty-cycle value that must be 0–255 and always exactly 1 byte?
What operator tells you how many bytes a type or variable occupies?
- Arithmetic:
+ - * / %(% = remainder/modulo) - Comparison:
== != < > <= >=(returns 0 or 1) - Logical:
&& || !(AND, OR, NOT) - Bitwise:
& | ^ ~ << >>(operate on individual bits)
flags |= (1 << 3) sets bit 3 without touching any other bits.
flags &= ~(1 << 3) clears it. flags ^= (1 << 3) toggles it.
This is the exact pattern used in every Arduino and embedded C driver ever written.
/* Step 3: Operators & Bitwise Operations
Compile: gcc step3_operators.c -o step3_operators */
#include <stdio.h>
#include <stdint.h>
int main() {
/* ── Arithmetic ────────────────────────────────── */
int speed = 100;
printf("Half speed : %d\n", speed / 2); // 50
printf("Remainder : %d\n", speed % 3); // 1
speed++; // increment (speed = 101)
speed -= 10; // speed = 91
/* ── Comparison ────────────────────────────────── */
int is_fast = (speed > 50); // 1 = true
int is_zero = (speed == 0); // 0 = false
printf("is_fast=%d is_zero=%d\n", is_fast, is_zero);
/* ── Bitwise — GPIO / hardware registers ──────── */
uint8_t flags = 0b00000000; // all bits off
flags |= (1 << 3); // SET bit 3 → 0b00001000
printf("After set : 0x%02X\n", flags);
flags &= ~(1 << 3); // CLEAR bit 3 → 0b00000000
printf("After clear : 0x%02X\n", flags);
flags ^= (1 << 2); // TOGGLE bit 2 → 0b00000100
printf("After toggle : 0x%02X\n", flags);
/* Check if a specific bit is set */
int bit2_on = (flags >> 2) & 1;
printf("Bit 2 is on : %d\n", bit2_on);
return 0;
}
What does `flags |= (1 << 3)` do to the variable flags?
What does the % (modulo) operator compute?
- if / else if / else — branch on any boolean condition
- switch / case — clean way to handle a fixed set of states (perfect for robot state machines!)
- Ternary
cond ? a : b— compact single-line if/else
break in switch!switch cases fall through to the next case unless you write break.
This is different from Python and a very common beginner bug.
Without break at the end of case 1, the code will keep running into case 2,
which is almost never what you want. Always add break (or return) to every case.
/* Step 4: Control Flow
Compile: gcc step4_control.c -o step4_control */
#include <stdio.h>
int main() {
/* ── if / else if / else ───────────────────────── */
int distance = 15; // cm
if (distance > 30) {
printf("✅ Path clear — moving forward\n");
} else if (distance > 10) {
printf("⚠️ Obstacle nearby — slowing down\n");
} else {
printf("🛑 Too close! Stopping.\n");
}
/* ── switch / case — robot state machine ───────── */
int state = 2;
switch (state) {
case 0: printf("State: IDLE\n"); break;
case 1: printf("State: DRIVING\n"); break;
case 2: printf("State: AVOIDING\n"); break;
case 3: printf("State: CHARGING\n"); break;
default: printf("State: UNKNOWN\n"); break;
}
/* ── Ternary: cond ? value_if_true : value_if_false */
int speed = 75;
char* status = (speed > 50) ? "fast" : "slow";
printf("Robot is moving: %s\n", status);
return 0;
}
What happens if you forget `break` at the end of a switch case in C?
When x = 3, what does the expression `x > 5 ? 'big' : 'small'` evaluate to?
- for — best when you know how many times to repeat
- while — repeats while a condition is true
- do-while — always runs at least once, then checks the condition
- break — immediately exits the loop
- continue — skip to the next iteration
while(1) as the main control loop — it runs forever until you cut power. This is standard in embedded C!while(1) is the Robot Heartbeatwhile(1) { ... } is the main control loop —
it reads sensors, makes decisions, and drives motors on every iteration.
Unlike application software that runs and exits, robots need to run continuously.
On microcontrollers there's no OS to return to, so the infinite loop is intentional and correct.
The loop frequency (how fast it runs) is often carefully tuned for the application.
/* Step 5: Loops
Compile: gcc step5_loops.c -o step5_loops */
#include <stdio.h>
int main() {
/* ── for loop — count from 0 to 4 ─────────────── */
printf("for loop:\n");
for (int i = 0; i < 5; i++) {
printf(" Sensor reading %d\n", i);
}
/* ── while loop — run until condition is false ─── */
printf("while loop:\n");
int distance = 50;
while (distance > 20) {
printf(" Moving... distance = %d cm\n", distance);
distance -= 8;
}
printf(" Stopped! Distance = %d cm\n", distance);
/* ── do-while — runs body FIRST, checks after ─── */
printf("do-while loop:\n");
int attempts = 0;
do {
printf(" Attempt %d\n", ++attempts);
} while (attempts < 3);
/* ── break and continue ─────────────────────────── */
printf("break/continue:\n");
for (int i = 0; i < 10; i++) {
if (i == 3) continue; // skip i=3
if (i == 6) break; // stop at i=6
printf(" i = %d\n", i);
}
/* ── Main robot control loop (simulation) ───────── */
printf("\nSimulating robot loop (5 ticks):\n");
int tick = 0;
while (1) { // runs forever on real hardware
printf(" [tick %d] read sensors → decide → drive\n", tick++);
if (tick >= 5) break; // simulation exit only
}
return 0;
}
What does `while(1)` do in C?
What keyword immediately exits the innermost loop or switch?
- Return type — what type the function gives back (
void= nothing) - Parameters — inputs the function needs, with their types
- Prototype — a declaration before
main()so the compiler knows the function exists
void as the return type when a function just does an action and doesn't need to send a value back..h header files.
The actual code lives in .c files. Other files #include the header to
use your functions without seeing their implementation.
This is how all large robot codebases (ROS, ArduPilot, etc.) are organised —
headers define the interface, source files define the behaviour.
/* Step 6: Functions & Prototypes
Compile: gcc step6_functions.c -o step6_functions */
#include <stdio.h>
/* ── Prototypes (declarations before main) ─────────
The compiler needs to know the signature before use. */
float calc_speed(int ticks, float seconds);
void print_status(const char* label, float value);
int clamp(int value, int min, int max);
int main() {
float speed = calc_speed(512, 0.5f);
print_status("Wheel speed (cm/s)", speed);
int raw_input = 300;
int safe_speed = clamp(raw_input, 0, 255);
printf("Raw: %d Clamped: %d\n", raw_input, safe_speed);
return 0;
}
/* ── Function definitions (after main is fine) ───── */
float calc_speed(int ticks, float seconds) {
float wheel_circumference = 20.1f; // cm (6.4 cm diameter)
float rotations = ticks / 512.0f; // 512 encoder ticks per revolution
return (rotations * wheel_circumference) / seconds;
}
void print_status(const char* label, float value) {
printf("[STATUS] %s: %.2f\n", label, value);
}
int clamp(int value, int min, int max) {
if (value < min) return min;
if (value > max) return max;
return value;
}
What is a function prototype in C?
What return type means a function performs an action and returns nothing?
- Arrays start at index 0 — the last valid index is length - 1
- C strings are just
chararrays ending with a null terminator'\0' - There is no bounds checking — going past the end is a dangerous bug called a buffer overflow!
char array where the last element
is '\0' (the null character, value 0) — this marks the end.
strlen() counts characters up to (but not including) that null terminator.
Functions like strcpy and strcat from <string.h>
always write the null terminator for you — but you must ensure the destination array
is large enough. Buffer overflows in C strings are one of the most common security vulnerabilities in history.
/* Step 7: Arrays & Strings
Compile: gcc step7_arrays.c -o step7_arrays */
#include <stdio.h>
#include <string.h> // strlen, strcpy, strcat, snprintf
int main() {
/* ── Float array — sensor distance readings ─────── */
float distances[5] = { 25.0f, 18.3f, 42.1f, 8.7f, 31.0f };
int count = 5;
float min_dist = distances[0];
for (int i = 1; i < count; i++) {
if (distances[i] < min_dist)
min_dist = distances[i];
}
printf("Closest obstacle: %.1f cm\n", min_dist);
/* ── 2D array — motor speed table ───────────────── */
int speed_table[3][2] = {
{ 100, 100 }, // forward
{ 60, -60 }, // turn left
{ -60, 60 } // turn right
};
printf("Forward: L=%d R=%d\n", speed_table[0][0], speed_table[0][1]);
printf("Turn left: L=%d R=%d\n", speed_table[1][0], speed_table[1][1]);
/* ── C strings (char arrays) ─────────────────────── */
char robot_name[32] = "Rover-01";
printf("Name: %s Length: %zu\n", robot_name, strlen(robot_name));
char full_name[64];
strcpy(full_name, robot_name); // copy
strcat(full_name, "-Alpha"); // append
printf("Full name: %s\n", full_name);
/* snprintf — safer than sprintf, prevents overflow */
char log_msg[64];
snprintf(log_msg, sizeof(log_msg),
"Robot %s at %.1f cm", robot_name, min_dist);
printf("%s\n", log_msg);
return 0;
}
What is the last valid index of `int arr[5]`?
What special character marks the end of a C string?
&x— the address of variable xint* p— declare a pointer to an int*p— dereference: read or write the value at the address
& gives you the address, * gets you the value. Read int* p as "p is a pointer to an int."0x40021000
might turn on an LED. Pointers are how C accesses these addresses directly —
no operating system needed. At a higher level, passing a pointer to a large sensor
struct is much faster than copying the whole struct into a function — critical
when your control loop must run in microseconds.
/* Step 8: Pointers
Compile: gcc step8_pointers.c -o step8_pointers */
#include <stdio.h>
/* Pass by pointer — allows function to modify the caller's variable */
void set_speed(int* speed, int value) {
*speed = value; // write through the pointer
}
void swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
/* ── Basic pointer mechanics ─────────────────────── */
int motor_speed = 0;
int* ptr = &motor_speed; // ptr holds the address of motor_speed
printf("Address : %p\n", (void*)&motor_speed);
printf("Value : %d\n", motor_speed);
set_speed(&motor_speed, 75); // pass address so function can modify it
printf("After set_speed: %d\n", motor_speed);
*ptr = 100; // write directly via pointer
printf("After *ptr=100: %d\n", motor_speed);
/* ── Pointers and arrays ─────────────────────────── */
int readings[4] = { 10, 20, 30, 40 };
int* p = readings; // array name IS a pointer to element 0
for (int i = 0; i < 4; i++) {
printf("readings[%d] = %d (via pointer: %d)\n",
i, readings[i], *(p + i));
}
/* ── Swap demo ───────────────────────────────────── */
int left = 60, right = 80;
printf("Before swap: left=%d right=%d\n", left, right);
swap(&left, &right);
printf("After swap : left=%d right=%d\n", left, right);
return 0;
}
What does `&x` return when x is an int variable?
Given `int* p = &x;` — what does `*p = 99` do?
SensorData struct. This mirrors how real robotics frameworks model the world.
struct.member— access a field when you have the struct directlyptr->member— access a field through a pointer (most common in practice)typedef— give the struct a clean type name so you don't writestruct Xeverywhere
SensorData, MotorState, Position, PIDController. Clean structs = clean code.Robot* pointer to every function, every part of your code
can read and update the robot's world model consistently — a fundamental pattern
in robotics software design.
/* Step 9: Structs & Custom Types
Compile: gcc step9_structs.c -o step9_structs */
#include <stdio.h>
/* ── Define custom types for robot components ───────── */
typedef struct {
float front, left, right; // distances in cm
int battery_pct; // 0–100
} SensorData;
typedef struct {
int left_speed; // -255 to 255
int right_speed; // -255 to 255
} MotorState;
typedef enum {
STATE_IDLE, STATE_DRIVING, STATE_AVOIDING, STATE_CHARGING
} RobotState;
typedef struct {
SensorData sensors;
MotorState motors;
RobotState state;
float x, y; // position
} Robot;
/* ── Functions that operate on robot state ──────────── */
void print_sensors(const SensorData* s) {
printf("Sensors | Front:%.1f Left:%.1f Right:%.1f Batt:%d%%\n",
s->front, s->left, s->right, s->battery_pct);
}
void update_motors(Robot* robot, int l, int r) {
robot->motors.left_speed = l;
robot->motors.right_speed = r;
printf("Motors set → L:%d R:%d\n", l, r);
}
int main() {
Robot rover = {
.sensors = { 42.0f, 15.3f, 88.7f, 87 },
.motors = { 0, 0 },
.state = STATE_IDLE,
.x = 0.0f, .y = 0.0f
};
print_sensors(&rover.sensors);
printf("State: %d Position: (%.1f, %.1f)\n",
rover.state, rover.x, rover.y);
rover.state = STATE_DRIVING;
update_motors(&rover, 200, 200);
/* Arrow operator: rover.sensors is a struct, not a pointer,
so we use dot (.) not arrow (->) here */
printf("Front distance: %.1f cm\n", rover.sensors.front);
return 0;
}
You have `Robot* r`. How do you access its `speed` field?
What does `typedef struct { float x; } Point;` let you write instead of `struct Point p;`?
- Stack — automatic, fast, limited size. Local variables live here.
- Heap — manual, flexible, large.
malloc()allocates here. - malloc(n) — request n bytes from the heap. Returns a pointer (or NULL on failure).
- free(ptr) — release heap memory. Every malloc must have exactly one free.
NULL immediately
after freeing it — then an accidental second free is a no-op instead of a crash.
/* Step 10: Memory Management & Complete Robot Program
Compile: gcc step10_complete.c -o step10_complete -lm */
#include <stdio.h>
#include <stdlib.h> // malloc, free, exit
#include <string.h> // memset
#include <math.h> // sqrt (compile with -lm)
#define MAX_LOG 64
#define SAFE_DIST_CM 20.0f
/* ── Types ───────────────────────────────────────────── */
typedef struct { float front, left, right; int batt; } Sensors;
typedef struct { int left, right; } Motors;
typedef enum { IDLE, DRIVING, AVOIDING } State;
typedef struct {
float* readings; // dynamic array on the heap
int count;
int capacity;
} SensorLog;
/* ── SensorLog helpers ───────────────────────────────── */
SensorLog* log_create(int capacity) {
SensorLog* log = (SensorLog*)malloc(sizeof(SensorLog));
if (!log) { fprintf(stderr, "Allocation failed!\n"); exit(1); }
log->readings = (float*)malloc(sizeof(float) * capacity);
if (!log->readings) { free(log); exit(1); }
log->count = 0; log->capacity = capacity;
return log;
}
void log_add(SensorLog* log, float v) {
if (log->count < log->capacity)
log->readings[log->count++] = v;
}
float log_average(const SensorLog* log) {
if (log->count == 0) return 0.0f;
float sum = 0.0f;
for (int i = 0; i < log->count; i++) sum += log->readings[i];
return sum / log->count;
}
void log_free(SensorLog* log) {
if (!log) return;
free(log->readings); log->readings = NULL; // nullify after free!
free(log); log = NULL;
}
/* ── Robot decision logic ────────────────────────────── */
void tick(Sensors* s, Motors* m, State* state, SensorLog* log) {
log_add(log, s->front);
switch (*state) {
case IDLE:
printf(" [IDLE] Waiting for mission...\n");
*state = DRIVING; break;
case DRIVING:
if (s->front > SAFE_DIST_CM) {
m->left = m->right = 200;
printf(" [DRIVING] Forward. Front=%.1fcm\n", s->front);
} else {
m->left = m->right = 0;
*state = AVOIDING;
printf(" [DRIVING] Obstacle! → AVOIDING\n");
}
break;
case AVOIDING:
m->left = -100; m->right = 100; // spin right
printf(" [AVOIDING] Turning. Front=%.1fcm\n", s->front);
if (s->front > SAFE_DIST_CM) *state = DRIVING;
break;
}
}
int main() {
printf("=== Robot Simulator (C) ===\n\n");
Sensors s = { 50.0f, 30.0f, 40.0f, 95 };
Motors m = { 0, 0 };
State state = IDLE;
SensorLog* log = log_create(MAX_LOG);
/* Simulate 6 ticks with a changing environment */
float sim_front[] = { 50.0f, 40.0f, 15.0f, 10.0f, 35.0f, 60.0f };
for (int t = 0; t < 6; t++) {
s.front = sim_front[t];
tick(&s, &m, &state, log);
}
printf("\n--- Session Summary ---\n");
printf("Readings logged : %d\n", log->count);
printf("Average distance: %.2f cm\n", log_average(log));
log_free(log); // free heap memory
printf("Memory freed. Program complete.\n");
return 0;
}
What must you call for every successful malloc() call?
What is a memory leak?