【Embedded System】嵌入式C语言基础知识
一、基本数据类型

       C语言的基本数据类型为:整型、字符型、实数型。这些类型按其在计算机中的存储方式可被分为两个系列,即整数(integer)类型和浮点数(floating-point)类型。
这三种类型之下分别是:short、int、long、char、float、double这六个关键字再加上两个符号说明符signed和unsigned就基本表示了C语言的最常用的数据类型。
下面列出了在32位操作系统下常见编译器下的数据类型大小及表示的数据范围:
| 类型名称 | 类型关键字 | 占字节数 | 其他叫法 | 表示的数据范围 | 
|---|---|---|---|---|
| 字符型 | char | 1 | signed char | -128 ~ 127 | 
| 无符号字符型 | unsigned char | 1 | none | 0 ~ 255 | 
| 整型 | int | 4 | signed int | -2,147,483,648 ~ 2,147,483,647 | 
| 无符号整型 | unsigned int | 4 | unsigned | 0 ~ 4,294,967,295 | 
| 短整型 | short | 2 | short int | -32,768 ~ 32,767 | 
| 无符号短整型 | unsigned short | 2 | unsigned short int | 0 ~ 65,535 | 
| 长整型 | long | 4 | long int | -2,147,483,648 ~ 2,147,483,647 | 
| 无符号长整型 | unsigned long | 4 | unsigned long | 0 ~ 4,294,967,295 | 
| 单精度浮点数 | float | 4 | none | 3.4E +/- 38 (7 digits) | 
| 双精度浮点数 | double | 8 | none | 1.7E +/- 308 (15 digits) | 
| 长双精度浮点数 | long double | 10 | none | 1.2E +/- 4932 (19 digits) | 
| 长整型 | long long | 8 | __int64 | -9223372036854775808 ~ 9223372036854775808 | 
二、命名规则和关键字
命名规则
- 标识符组成:仅允许数字、字母、下划线,无其他特殊字符;
- 开头限制:明确禁止数字开头,避免与数值常量混淆;
- 关键字禁用:完全符合标准,关键字是编译器预留的 “特殊指令名”,不可被用户占用;
- 大小写敏感:是 C 语言的显著特性,num和Num会被识别为两个独立标识符。
C 语言关键字精准分类表(共 32 个)
| 分类 | 关键字列表 | 功能简述 | 
|---|---|---|
| 1. 数据类型关键字(12 个) | char、short、int、long、float、double、unsigned、signed、struct、union、enum、void | 定义基础数据类型或复合数据类型(如结构体 struct、枚举enum),void表示 “无类型”。 | 
| 2. 控制语句关键字(12 个) | if、else、switch、case、default、for、do、while、break、continue、goto、return | 控制程序执行流程,包括条件判断( if/switch)、循环(for/while/do)、跳转(break/return)。 | 
| 3. 存储类关键字(4 个) | auto、extern、register、static | 控制变量的存储位置与生命周期:- static:静态存储(全局生命周期);-extern:声明外部变量 / 函数;-register:建议存储在寄存器(快取);-auto:自动存储(默认,函数内局部变量)。 | 
| 4. 类型限定符(2 个) | const、volatile | 修饰变量的属性(不控制存储位置):- const:定义 “只读常量”(值不可修改);-volatile:提醒编译器 “变量可能被外部修改”(禁止优化)。 | 
| 5. 其他关键字(2 个) | sizeof、typedef | - sizeof:计算数据类型或变量的字节数;-typedef:给已有数据类型起 “别名”(如typedef int INT)。 | 
bool类型
- 早期C语言没有布尔类型数据,以0代表逻辑假,非0代表逻辑真 
- C99标准定义了新的关键字_Bool,提供了布尔类型,或者也可以使用stdbool.h中的bool 
三、嵌入式的数据类型
| C 语言的数据类型 | STM32 对应的数据类型 | 功能描述 | 
|---|---|---|
| unsigned char | uint8_t | 8 位无符号数据(0~255) | 
| unsigned short int | uint16_t | 16 位无符号数据(0~65535) | 
| unsigned int | uint32_t | 32 位无符号数据(0~2³²-1) | 
| unsigned long long | uint64_t | 64 位无符号数据(0~2⁶⁴-1) | 
| signed char | int8_t | 8 位有符号数据(-128~+127) | 
| signed short int | int16_t | 16 位有符号数据(-32768~+32767) | 
| signed int | int32_t | 32 位有符号数据(-2³¹~2³¹-1) | 
| signed long long | int64_t | 64 位有符号数据(-2⁶³~2⁶³-1) | 
四、嵌入式中部分关键字解释
4.1 static
- 功能:- 修饰局部变量:将其变为静态局部变量,生命周期延长至整个程序运行期间,仅在第一次调用时初始化,后续调用保留上次值。
- 修饰全局变量 / 函数:限制其作用域为当前文件(即 “文件内私有”),避免跨文件的符号冲突,增强代码模块化。
 
- 嵌入式应用场景:- 驱动开发中,用static修饰仅在当前模块(.c 文件)内调用的函数,如某传感器驱动的内部处理函数static void sensor_process(void),防止被其他模块误调用。
- 实现模块内的 “持久化” 状态,如按键消抖函数中,用static uint8_t debounce_cnt记录消抖计数,每次调用时保留计数状态。
 
- 驱动开发中,用
4.2 const
- 功能:定义只读变量(常量),编译时会检查对其的修改操作,防止程序运行中意外篡改。(首先定义一个变量,只进行一次赋值,使用const,使其带上后续不能改变其值的特性。)
- 嵌入式应用场景:- 配置硬件寄存器的固定参数,如串口波特率、GPIO 模式等,例如:const uint32_t UART_BAUDRATE = 115200;,确保波特率在初始化后不会被错误修改。
- 定义硬件地址映射(如外设寄存器基地址),如const uint32_t GPIOA_BASE = 0x40010800;,避免地址被意外覆盖导致硬件操作错误。
 
- 配置硬件寄存器的固定参数,如串口波特率、GPIO 模式等,例如:
4.3 extern
- 功能:声明外部变量 / 函数,表示该变量或函数在其他文件中定义,当前文件可直接使用(实现跨文件的变量 / 函数共享)。
- 嵌入式应用场景:- 多模块协作的工程中,如 “按键模块” 定义全局变量uint8_t key_state,在 “主控制模块” 中用extern uint8_t key_state;声明后,即可读取按键状态进行逻辑处理。
- 驱动层与应用层的接口共享,如传感器驱动定义extern float sensor_read(void);,应用层包含头文件后可直接调用该函数获取传感器数据。
 
- 多模块协作的工程中,如 “按键模块” 定义全局变量
4.4 volatile
- 功能:告诉编译器 “该变量的值可能被硬件 / 中断 / 其他异步操作意外修改”,禁止编译器对其进行优化(如缓存变量值),确保每次都从内存(或硬件)中读取最新值。
- 嵌入式应用场景:- 读取硬件寄存器(如状态寄存器),如volatile uint32_t *status_reg = (volatile uint32_t*)0x40001000;,因为寄存器值可能被硬件随时更新(如外设完成一次传输后自动置位),必须用volatile确保每次读取的是真实当前值。
- 中断标志位,如volatile uint8_t irq_flag;,在主循环中轮询该标志,而中断服务程序会修改它,volatile保证主循环能及时检测到标志变化。
 
- 读取硬件寄存器(如状态寄存器),如
4.5 typedef
功能:为已有的数据类型定义一个 “别名”,本质是 “类型重命名”,不创造新类型。作用:简化复杂类型的书写、提高代码可读性、增强跨平台兼容性(统一类型定义)。
用法示例:
// 为基本类型起别名(嵌入式中最常见,如标准库的uint8_t等)
typedef unsigned char uint8_t;  // 后续可用uint8_t代替unsigned char// 为结构体类型起别名(简化结构体变量定义)
typedef struct {uint8_t year;uint8_t month;uint8_t day;
} Date;  // 后续可直接用Date定义变量:Date today;// 为指针类型起别名(避免复杂指针声明的混淆)
typedef uint8_t* PtrUint8;  // PtrUint8等价于uint8_t*,定义指针:PtrUint8 buf_ptr;嵌入式应用场景:
- 统一跨平台类型:不同 MCU 的int可能是 16 位或 32 位,用typedef定义int32_t(固定 32 位有符号)、uint16_t(固定 16 位无符号),确保代码在不同平台上的类型长度一致(如 STM32 的stdint.h中大量使用)。
- 简化复杂类型:外设配置结构体(如 SPI 配置)通常较长,用typedef起别名后,变量定义更简洁(如SPI_HandleTypeDef实际是typedef struct _SPI_HandleTypeDef SPI_HandleTypeDef)。
注意:
typedef是 “类型别名”,有类型检查;与#define的 “文本替换” 不同(见 4.6)。
4.6 #define
功能:预处理指令,用于 “文本替换”(宏定义),在编译前由预处理器完成替换,不参与编译的语法检查。作用:定义常量、简化重复代码、实现条件编译(配合#ifdef等)。
用法示例:
// 1. 定义常量(硬件相关的固定值,如寄存器地址、引脚)
#define GPIOA_BASE 0x40010800  // GPIOA寄存器基地址
#define LED_PIN    GPIO_PIN_5  // LED连接的引脚// 2. 定义“函数宏”(简化重复操作,带参数)
#define MAX(a, b)  ((a) > (b) ? (a) : (b))  // 求两数最大值// 3. 条件编译(控制代码是否参与编译)
#define DEBUG 1  // 定义DEBUG后,下面的调试打印生效
#ifdef DEBUG#define LOG(msg) printf("Debug: %s\n", msg)
#else#define LOG(msg)  // 不定义DEBUG时,LOG为空
#endif嵌入式应用场景:
- 硬件地址映射:MCU 的外设寄存器地址是固定的,用#define定义后,代码中直接使用别名(如#define USART1_SR (*(volatile uint32_t*)(USART1_BASE + 0x00))),避免硬编码数字。
- 引脚与参数配置:板级外设的引脚(如按键、LED)、配置参数(如波特率#define UART_BAUD 115200)用#define定义,修改时只需改一处,便于维护。
- 调试开关:通过#define DEBUG控制调试信息的打印,发布时关闭DEBUG即可移除调试代码,不占用运行资源。
注意:
- 宏替换是 “纯文本替换”,无类型检查,复杂宏需加括号避免歧义(如MAX(a,b)的括号)。
- 与typedef的区别:typedef针对类型,#define针对文本(可用于非类型的替换,如引脚、常量)。
4.7 inline
功能:建议编译器将函数 “内联展开”(即把函数体直接插入到调用处,而非通过函数调用指令跳转),减少函数调用的开销(如压栈、跳转、返回的时间)。作用:优化频繁调用的小函数的执行效率,适合实时性要求高的场景。
用法示例:
// 定义内联函数(在函数声明前加inline)
inline uint8_t get_bit(uint8_t data, uint8_t pos) {return (data >> pos) & 0x01;  // 提取data的第pos位
}// 调用时,编译器可能直接展开为:(value >> 3) & 0x01,而非函数调用
int main() {uint8_t value = 0x88;uint8_t bit = get_bit(value, 3);  // 可能被展开,无函数调用开销
}嵌入式应用场景:
- 高频调用的小函数:如传感器数据的位解析(如从 16 位数据中提取温度的高 8 位)、GPIO 电平读取(inline uint8_t led_state(void) { return GPIO_ReadPin(LED_PORT, LED_PIN); }),这些函数被频繁调用,内联后可减少实时性开销。
- 中断服务程序(ISR)中的辅助函数:ISR 要求执行速度快,其内调用的小函数(如数据校验、标志位处理)用inline可避免函数调用的延迟。
注意:
- inline是 “建议” 而非 “强制”:编译器可能忽略(如函数体过大、包含循环 / 递归时),由编译器根据优化策略决定是否展开。
- 内联会增加代码体积(函数体被多次复制),因此仅适合短小简单的函数(通常 1-5 行代码),避免滥用导致程序体积膨胀。
4.8 inline补充说明
内联函数(inline function)和宏(macro)在代码展开和效率优化上有相似的表象(都能减少函数调用开销),但本质、功能和安全性有显著区别,尤其在嵌入式开发中,理解这些区别对代码可靠性和性能至关重要。
核心区别对比表
| 对比维度 | 宏( #define) | 内联函数( inline) | 
|---|---|---|
| 本质 | 预处理阶段的文本替换(无语法 / 类型检查) | 编译阶段的函数展开建议(有完整的函数特性) | 
| 处理阶段 | 预编译( cpp阶段),在代码编译前完成替换 | 编译阶段( cc1阶段),由编译器决定是否展开 | 
| 类型安全 | 无类型检查,参数类型错误不会报错(如 int和float混用) | 有严格的类型检查,参数类型不匹配会编译报错 | 
| 代码展开方式 | 强制替换(无论是否合理,预处理器都会执行文本替换) | 编译器 “建议”(可忽略,如函数体过大时不展开) | 
| 调试支持 | 无法调试(预处理后宏名消失,只剩替换后的代码) | 可调试(保留函数特性,能设置断点、查看调用栈) | 
| 复杂逻辑支持 | 处理复杂逻辑(如循环、多语句)容易出错(需加 do-while包裹) | 支持任意复杂逻辑(循环、分支、递归等),结构清晰 | 
| 参数副作用 | 可能产生副作用(如 MAX(a++, b++)会导致a或b多自增) | 无副作用(参数先计算再传入,与普通函数一致) | 
| 作用域 | 无作用域限制(宏定义后全局有效,除非用 #undef取消) | 遵循函数作用域(如 static inline限制在文件内) | 
| 代码体积影响 | 强制展开可能导致代码体积急剧膨胀(尤其多次调用时) | 编译器会平衡效率与体积(大函数可能不展开) | 
关键差异详解
1. 本质与处理阶段不同
- 宏是文本替换工具,由预处理器处理:比如#define MAX(a,b) ((a)>(b)?(a):(b)),预处理器会在编译前将代码中所有MAX(x,y)直接替换为((x)>(y)?(x):(y)),不理解 “函数”“参数” 的含义,仅做字符串替换。
- 内联函数是真正的函数,由编译器处理:inline int max(int a, int b) { return a>b?a:b; }本质是函数,编译器会先检查参数类型、函数体语法,再根据优化策略决定是否将函数体插入到调用处(替代函数调用的跳转 / 压栈操作)。
2. 类型安全与错误检查不同
- 宏无类型概念:如果用MAX(3.14, 5)(浮点数)和MAX(10, 'a')(整数与字符),预处理器都会无脑替换,即使类型不匹配也不会报错,可能导致隐藏 bug。
- 内联函数有严格类型检查:如果定义inline int max(int a, int b),调用时传入max(3.14, 5)会因 “浮点转整数” 报警告,传入max("a", "b")(字符串)会直接编译报错,安全性更高。
五、了解有STM32CubeMX产生的部分文件结构

为了更清晰体现嵌入式开发的分层架构逻辑(从底层驱动到上层应用),以下按 “底层支撑→中层适配→上层应用” 的顺序重新整理各模块定义,同时补充模块间的依赖关系,便于理解各部分的定位与作用:
(1)嵌入式开发各模块整理(按分层逻辑排序)
| 模块名称 | 核心定义 | 关键特性 / 作用 | 典型内容 / 示例 | 
|---|---|---|---|
| 1. Drivers(厂商 SDK 程序) | MCU 厂商(如 STM32、ESP32)或 MCU 内部 CPU 厂商提供的底层驱动库,是硬件编程的基础支撑。 | 1. 为 CPU 和 MCU 内部外设(如内核、时钟、IIC/SPI 接口)提供标准化操作接口;2. 屏蔽硬件底层细节,避免用户直接操作寄存器,降低开发难度;3. 官方维护,兼容性与稳定性强。 | 1. 寄存器定义文件(如 stm32f10x.h);2. 外设驱动函数库(如stm32f10x_spi.c);3. 内核相关接口(如 CMSIS 标准库)。 | 
| 2. Core(MCU 驱动程序) | 基于 Drivers 层,面向 MCU 内部外设的初始化与管理,是连接底层 SDK 与上层板级逻辑的中间层。 | 1. 负责 MCU 内部核心功能的配置(如系统时钟、中断优先级、IIC/SPI 外设参数初始化);2. 是整个程序的 “入口载体”,包含主函数,统筹 MCU 整体运行逻辑;3. 不涉及板上外部外设(仅管 MCU 内部)。 | 1. main.c(程序入口,包含主循环);2. 时钟初始化函数(如SystemClock_Config());3. 内部外设配置代码(如IIC_Init())。 | 
| 3. BSP(板级支持驱动程序) | Board Support Package,负责 MCU 与板上外部外设(非 MCU 内部)的通信与交互,是硬件板卡的 “专属适配层”。 | 1. 分两种场景适配:- 裸机(如有限状态机):直接编写外设驱动逻辑,无操作系统依赖;- 操作系统下:编写适配 OS 的驱动(如驱动线程、中断回调绑定);2. 隔离板级硬件差异,使上层应用无需关心具体板卡的外设连接方式。 | 1. 板上传感器驱动(如 MPU6050、DS18B20 驱动);2. 板载外设接口适配(如 LCD 屏、按键的 GPIO 映射);3. 裸机 / BSP 驱动函数(如 mpu6050_read_data())。 | 
| 4. Middlewares(中间件) | 抽象程度高、可跨项目复用的通用功能模块,不依赖具体硬件,专注于 “通用能力支撑”。 | 1. 通用性强:可在不同硬件平台、不同项目中直接移植(如 LVGL 在 STM32/ESP32 上均可使用);2. 补充核心功能:提供业务无关的通用能力,减少重复开发。 | 1. 图形库(LVGL、TouchGFX);2. 算法库(数学库、快速傅里叶变换 FFT 库);3. 通信协议栈(如 Modbus、MQTT 客户端库)。 | 
| 5. OS/SYSTEM(操作系统层) | 负责系统资源管理与任务调度的核心层,SYSTEM 是 OS 层的 “配置补充目录”。 | 1. OS(如 FreeRTOS、RT-Thread):提供任务管理、内存分配、中断管理、信号量等核心能力;2. SYSTEM:存放全局系统配置(如系统宏定义、全局参数、跨模块共享的头文件),影响整个系统的运行参数。 | 1. OS 内核文件(如 freertos_kernel.c);2. SYSTEM 目录下的system.h(全局宏定义)、sys_config.c(系统参数配置);3. 任务创建与调度代码(如xTaskCreate())。 | 
| 6. 应用层 | 基于所有下层模块,实现具体业务逻辑,是开发的最终目标(即 “产品要做的事”)。 | 1. 不关心底层硬件细节,仅通过调用上层接口(如 BSP 的传感器读取、OS 的任务接口)实现功能;2. 与产品需求强绑定,不同项目的应用层逻辑完全不同。 | 1. 智能家居的 “温湿度监测与上报逻辑”;2. 工业设备的 “参数采集与控制逻辑”;3. 消费电子的 “UI 交互与功能触发逻辑”(如按键控制 LED 亮灭)。 | 
(2)模块间核心依赖关系
   Drivers(底层 SDK)→ Core(MCU 内部驱动)→ BSP(板级外设适配)→ 应用层(业务逻辑)Middlewares(通用功能)、OS/SYSTEM(系统管理)→ 跨层支撑(为 Core、BSP、应用层提供通用能力 / 资源管理)
这种分层结构的优势是:硬件变更时仅需修改 Drivers/Core/BSP 层,应用层无需改动;通用功能(如 LVGL)可直接复用,大幅提升开发效率。
