Fundamentals of C Programming for Microcontrollers
Data Types and Variables
Data types define the storage format and size of data. Common primitive data types in microcontroller C programming include char, int, long, float, and their signed/unsigned variants.
Type Conversion Priority: During operations, data types are automatically promoted according to this hierarchy:
bit → char → int → long → float → signed → unsigned.
For example, when a char variable operates with an int, the char is first promoted to int before the operation proceeds, yielding an int result.
Constants and Variables: Constants are immutable values, typically defined using the const keyword.
float sensor_reading;
sensor_reading = 3.14159265;
const uint8_t array_size = 10; // Must be initialized at definition
Extended Data Types (C51):
- Bit Variables: Defined using
bitorsbit. - Special Function Registers (SFRs): Accessed via
sfrorsfr16type specifiers.
bit flag_bit; // General bit variable
sbit LED_PIN = P1^0; // Bit at a specific address
sfr P0 = 0x80; // Define SFR P0 at address 0x80
sfr16 TMR0 = 0x8C; // Define 16-bit timer register
Storage Classes and Memory Types
Storage classes define the scope and lifetime of variables.
auto: Default for local variables. Allocated on stack when function enters.register: Suggests compiler to store variable in a CPU register for speed.static: Preserves value between function calls. File-scope static variables are limited to that file.extern: Declares a variable/function defined in another translation unit.
Memory Types (C51): Specify the physical memory region.
| Type | Description |
|---|---|
data |
Directly addressable internal RAM (128 bytes) |
bdata |
Bit-addressable internal RAM (16 bytes) |
idata |
Indirectly addressable internal RAM |
pdata |
Page (256 bytes) of external RAM, accessed via Ri |
xdata |
Full 64KB external RAM, accessed via DPTR |
code |
Program memory (ROM) |
Example: uint8_t xdata buffer[256];
Typedef: Creates aliases for existing types, commonly used for structures.
typedef struct {
float max_voltage;
float min_current;
} POWER_CONFIG;
struct STATUS_FLAGS {
unsigned error : 1;
unsigned ready : 1;
};
POWER_CONFIG sys_power; // No 'struct' keyword needed with typedef
struct STATUS_FLAGS dev_status;
Operators
- Arithmetic:
+,-,*,/,%(modulus). - Relational:
==,!=,>,<,>=,<=. - Logical:
&&(AND),||(OR),!(NOT). - Bitwise:
&(AND),|(OR),^(XOR),~(NOT),<<(left shift),>>(right shift). - Assignment:
=,+=,-=,*=,/=,&=,|=,^=,<<=,>>=. - Conditional (Ternary):
condition ? expr1 : expr2.
Control Structures
Selection:
if,else if,else.switch,case,default. Usebreakto exit a case.
if (adc_value > THRESHOLD_HIGH) {
set_alarm();
} else if (adc_value < THRESHOLD_LOW) {
clear_output();
} else {
normal_operation();
}
switch (menu_selection) {
case 1: display_settings(); break;
case 2: calibrate_sensor(); break;
default: show_main_menu();
}
Iteration:
while: Checks condition before loop body.do...while: Executes body atleast once before checking condition.for:for(init; condition; increment) { ... }.
uint8_t count = 0;
while (count < 10) {
if (count == 5) break; // Exit loop
if (count == 2) { count++; continue; } // Skip rest of this iteration
transmit_data(count);
count++;
}
for (uint8_t i = 0; i < ARRAY_LEN; i++) {
process_buffer[i];
}
Functions
Functions encapsulate reusable code. Return type void indicates no return value.
uint16_t calculate_checksum(uint8_t *data, uint16_t len) {
uint16_t sum = 0;
for (uint16_t i = 0; i < len; i++) {
sum += data[i];
}
return sum;
}
Interrupt Service Routines (ISRs) in C51: Use the interrupt keyword.
void ext_int0_isr(void) interrupt 0 { // Vector for external interrupt 0
if (BUTTON_PIN == 0) {
TOGGLE_LED;
}
}
Function Prototypes: Declare functions before use, especially when defined in other files.
extern void init_peripheral(void); // Declare function from another module
Arrays
Arrays store multiple elements of the same type contiguously in memory.
uint16_t adc_samples[8] = {0}; // Initialize all to zero
char welcome_msg[] = "Hello"; // Null-terminated string
Pointers
Poinetrs store memory addresses.
*: Dereference operator (access value at address).&: Address-of operator.
uint8_t data = 0xAA;
uint8_t *ptr; // Pointer to uint8_t
ptr = &data; // ptr now holds address of 'data'
uint8_t value = *ptr; // value = 0xAA
Pointer Types:
- Pointer to Array: Points to an entire array.
- Array of Pointers: Array where each element is a pointer.
- Function Pointer: Points to executable code.
- Pointer Function: A function that returns a pointer.
// Function pointer example
void (*task_ptr)(void); // Declare function pointer
task_ptr = &scheduled_task; // Assign address of function
(*task_ptr)(); // Call function via pointer
// Pointer function example
uint8_t* get_buffer_base(void) {
static uint8_t local_buf[64];
return local_buf; // Returns address of array
}
Composite Data Types
Structures (struct): Group variables of diffferent types.
struct UART_CONFIG {
uint32_t baud_rate;
uint8_t data_bits;
uint8_t parity;
uint8_t stop_bits;
};
struct UART_CONFIG uart1;
uart1.baud_rate = 115200;
// Structure pointer
struct UART_CONFIG *cfg_ptr = &uart1;
cfg_ptr->data_bits = 8; // Access member via pointer
Unions (union): All members share the same memory location. Size is that of the largest member.
typedef union {
uint32_t raw;
struct {
uint8_t byte0;
uint8_t byte1;
uint8_t byte2;
uint8_t byte3;
} bytes;
} DATA_PACKET;
DATA_PACKET packet;
packet.raw = 0x12345678;
// packet.bytes.byte0 now contains 0x78 (on little-endian systems)
Enumerations (enum): Define named integer constants.
enum OPERATION_MODE {
MODE_IDLE,
MODE_ACTIVE,
MODE_SLEEP,
MODE_ERROR = 0xFF
};
enum OPERATION_MODE current_mode = MODE_ACTIVE;
Microcontroller-Specific Concepts
Logic Levels: Digital circuits use HIGH and LOW voltages. Common standards:
- TTL: ~0V for LOW (<0.8V), ~5V for HIGH (>2.4V).
- CMOS: Wider noise margins. 5V CMOS HIGH > 3.5V, LOW < 1.5V.
Bitwise Operations for Hardware Control:
PORTB &= ~(1 << 3); // Clear bit 3 of PORTB (set to 0)
PORTB |= (1 << 5); // Set bit 5 of PORTB (set to 1)
PORTB ^= (1 << 2); // Toggle bit 2 of PORTB
Absolute Addressing (@ in IAR): Place variable at specific memory address.
uint8_t control_reg @ 0x4000; // Variable at address 0x4000
Bit Fields: Structure members with specified bit widths.
struct {
unsigned enable : 1;
unsigned mode : 2;
unsigned : 5; // Unnamed padding bits
} control_register;
volatile Qualifier: Prevents compiler optimization for variables that may change outside program flow (e.g., by hardware or ISRs).
volatile uint8_t *status_reg = (uint8_t*)0x2000;
Preprocessor Directives:
- Macro Definition (
#define): Text substitution before compilation.
#define BUFFER_SIZE 256
#define CLOCK_FREQ (16000000UL)
#define SET_BIT(reg, bit) ((reg) |= (1 << (bit)))
- Conditional Compilation (
#ifdef,#ifndef,#if):
#ifdef DEBUG_MODE
log_debug("Value: %d", sensor_val);
#endif
Data Alignment: Ensure variables are placed on specific memory boundaries for efficient access.
__attribute__((aligned(4))) uint32_t aligned_data; // GCC/MDK
alignas(4) uint8_t buffer[128]; // C11 standard
Coding Conventions
- Portability: Abstract hardware dependencies using macros.
#define LED_ON() (PORTB |= (1 << LED_PIN)) #define LED_OFF() (PORTB &= ~(1 << LED_PIN)) - Standard Integer Types: Use
<stdint.h>types for clarity.uint8_t: Unsigned 8-bit (replacesunsigned char).int16_t: Signed 16-bit.uint32_t: Unsigned 32-bit.
- Naming:
- Macros:
ALL_CAPS_WITH_UNDERSCORES. - Functions, Variables:
camelCaseorsnake_case. - Global Variables: Consider prefix like
g_or module name.
- Macros:
- Header Guards: Prevent multiple inclusions.
// peripheral.h #ifndef PERIPHERAL_H #define PERIPHERAL_H /* Header content */ #endif