【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 语言的显著特性,numNum会被识别为两个独立标识符。

C 语言关键字精准分类表(共 32 个)

分类 关键字列表 功能简述
1. 数据类型关键字(12 个) charshortintlongfloatdoubleunsignedsignedstructunionenumvoid 定义基础数据类型或复合数据类型(如结构体struct、枚举enum),void表示 "无类型"。
2. 控制语句关键字(12 个) ifelseswitchcasedefaultfordowhilebreakcontinuegotoreturn 控制程序执行流程,包括条件判断(if/switch)、循环(for/while/do)、跳转(break/return)。
3. 存储类关键字(4 个) autoexternregisterstatic 控制变量的存储位置 与生命周期:- static:静态存储(全局生命周期);- extern:声明外部变量 / 函数;- register:建议存储在寄存器(快取);- auto:自动存储(默认,函数内局部变量)。
4. 类型限定符(2 个) constvolatile 修饰变量的属性 (不控制存储位置):- const:定义 "只读常量"(值不可修改);- volatile:提醒编译器 "变量可能被外部修改"(禁止优化)。
5. 其他关键字(2 个) sizeoftypedef - 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;,避免地址被意外覆盖导致硬件操作错误。

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

功能 :为已有的数据类型定义一个 "别名",本质是 "类型重命名",不创造新类型。作用:简化复杂类型的书写、提高代码可读性、增强跨平台兼容性(统一类型定义)。

用法示例:
cpp 复制代码
// 为基本类型起别名(嵌入式中最常见,如标准库的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;
嵌入式应用场景:
  1. 统一跨平台类型 :不同 MCU 的int可能是 16 位或 32 位,用typedef定义int32_t(固定 32 位有符号)、uint16_t(固定 16 位无符号),确保代码在不同平台上的类型长度一致(如 STM32 的stdint.h中大量使用)。
  2. 简化复杂类型 :外设配置结构体(如 SPI 配置)通常较长,用typedef起别名后,变量定义更简洁(如SPI_HandleTypeDef实际是typedef struct _SPI_HandleTypeDef SPI_HandleTypeDef)。
注意:

typedef是 "类型别名",有类型检查;与#define的 "文本替换" 不同(见 4.6)。

4.6 #define

功能 :预处理指令,用于 "文本替换"(宏定义),在编译前由预处理器完成替换,不参与编译的语法检查。作用 :定义常量、简化重复代码、实现条件编译(配合#ifdef等)。

用法示例:
cpp 复制代码
// 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
嵌入式应用场景:
  1. 硬件地址映射 :MCU 的外设寄存器地址是固定的,用#define定义后,代码中直接使用别名(如#define USART1_SR (*(volatile uint32_t*)(USART1_BASE + 0x00))),避免硬编码数字。
  2. 引脚与参数配置 :板级外设的引脚(如按键、LED)、配置参数(如波特率#define UART_BAUD 115200)用#define定义,修改时只需改一处,便于维护。
  3. 调试开关 :通过#define DEBUG控制调试信息的打印,发布时关闭DEBUG即可移除调试代码,不占用运行资源。
注意:
  • 宏替换是 "纯文本替换",无类型检查,复杂宏需加括号避免歧义(如MAX(a,b)的括号)。
  • typedef的区别:typedef针对类型,#define针对文本(可用于非类型的替换,如引脚、常量)。

4.7 inline

功能 :建议编译器将函数 "内联展开"(即把函数体直接插入到调用处,而非通过函数调用指令跳转),减少函数调用的开销(如压栈、跳转、返回的时间)。作用:优化频繁调用的小函数的执行效率,适合实时性要求高的场景。

用法示例:
cpp 复制代码
// 定义内联函数(在函数声明前加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);  // 可能被展开,无函数调用开销
}
嵌入式应用场景:
  1. 高频调用的小函数 :如传感器数据的位解析(如从 16 位数据中提取温度的高 8 位)、GPIO 电平读取(inline uint8_t led_state(void) { return GPIO_ReadPin(LED_PORT, LED_PIN); }),这些函数被频繁调用,内联后可减少实时性开销。
  2. 中断服务程序(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)可直接复用,大幅提升开发效率。

相关推荐
 梦晓天明6 小时前
12.集合介绍以及数组的使用选择
linux·开发语言·python
千里镜宵烛6 小时前
Lua--协程
开发语言·lua
m0_748231316 小时前
深入JVM:让Java性能起飞的核心原理与优化策略
java·开发语言·jvm
Jackson@ML6 小时前
在macOS上搭建C#集成开发环境指南
开发语言·macos·c#
嵌入式-老费7 小时前
Easyx图形库应用(python+opencv的图形库开发)
开发语言·python·opencv
Vaclee7 小时前
JVM超详解
开发语言·jvm
Ialand~7 小时前
深度解析 Rust 的数据结构:标准库与社区生态
开发语言·数据结构·rust
在坚持一下我可没意见7 小时前
Java 网络编程:TCP 与 UDP 的「通信江湖」(基于TCP回显服务器)
java·服务器·开发语言·笔记·tcp/ip·udp·java-ee