C语言(嵌入式方向)
1. 数据存储与转换
1.1 内存存储方式
(1) 基本单位
Bit(二进制位):最小存储单位,值为
0
或1
Byte(字节):8 bit
Word(字/字长):处理器一次可处理的数据宽度
8位 MCU:1 Word = 8 bit
32位 MCU:1 Word = 32 bit
嵌入式意义:
MCU 寄存器、RAM、Flash 都是按字节/字访问。
Bit 常用于标志位或寄存器控制位。
(2) 字节序(大小端)
大端(Big Endian)
高字节存储在低地址,低字节存储在高地址
例如:
int a = 0x12345678;
地址 数据
0x00 12
0x01 34
0x02 56
0x03 78
小端(Little Endian)
低字节存储在低地址,高字节存储在高地址
地址 数据
0x00 78
0x01 56
0x02 34
0x03 12
嵌入式应用场景:
外设寄存器:访问寄存器要按硬件要求处理字节序
通信协议:SPI、I2C、UART、CAN 数据可能需要大小端转换
Flash/EEPROM:存储多字节数据时注意读写顺序
C语言示例:
#include <stdio.h>int main() {unsigned int a = 0x12345678;unsigned char *p = (unsigned char*)&a;printf("存储顺序:%02x %02x %02x %02x\n", p[0], p[1], p[2], p[3]);return 0;
}
输出结果不同表示系统大小端方式
(3) 对齐与填充
内存对齐:编译器将数据放到特定地址,提升访问效率
规则(以 32 位 MCU 为例):
char
→ 对齐到 1 字节short
→ 对齐到 2 字节int
、float
→ 对齐到 4 字节结构体整体大小也会按最大成员对齐
嵌入式注意:
寄存器映射结构体必须
#pragma pack(1)
或__attribute__((packed))
避免填充
例子:
struct Test { char a; int b; };
不加
packed
:sizeof(Test)
= 8(1字节 + 3字节填充 + 4字节)加
packed
:sizeof(Test)
= 5
1.2 基本数据类型大小
类型 | 默认大小(32位MCU) | 说明 |
---|---|---|
char | 1字节 | 0255 / -128127 |
short | 2字节 | -3276832767 / 065535 |
int | 4字节 | -2^31~2^31-1 |
long | 4字节 | 同 int |
long long | 8字节 | -2^63~2^63-1 |
float | 4字节 | 单精度浮点 |
double | 8字节 | 双精度浮点(部分 MCU 可优化成4字节) |
嵌入式应用提醒:
小型 MCU 浮点运算慢,尽量用整数处理
uint8_t, int16_t, uint32_t
等标准整型可保证大小一致
1.3 类型转换
(1) 隐式转换
小类型 → 大类型
整数 → 浮点
int a = 10;
float b = a; // int → float
(2) 强制类型转换
用
(类型)
显式指定可能丢失精度或溢出
float x = 3.14;
int y = (int)x; // y = 3
(3) 嵌入式应用示例
ADC 转换为电压:
uint16_t adc_val = 2048;
float voltage = (float)adc_val * 3.3f / 4095;
通信协议数据打包/拆包:
uint16_t data = 0x1234;
uint8_t low = (uint8_t)data; // 低字节
uint8_t high = (uint8_t)(data>>8); // 高字节
1.4 未决问题 & 嵌入式注意事项
溢出:
有符号整型溢出行为未定义
无符号整型溢出回绕(模2^n)
浮点精度:
小数运算可能不精确
MCU可用定点运算替代
内存优化:
位域、压缩结构体可减少 RAM 占用
2. 代码与基本数据类型
2.1 基本数据类型
(1) 整数类型
有符号(signed):默认,能表示正数、负数
无符号(unsigned):只能表示正数,范围更大
👉 在嵌入式中,无符号类型常用于计数器、定时器、寄存器值
uint16_t timer_cnt = 65535; // 定时器计数寄存器
(2) 浮点类型
float
(单精度,4字节)double
(双精度,8字节,有些 MCU 仍然按 4 字节处理)
⚠️ 嵌入式注意:
STM32F1 没有 FPU(硬件浮点单元),浮点运算非常慢
在定时、PWM、滤波中,能用整数计算就尽量用整数
例如:
// 用浮点计算电压
float voltage = (adc_val * 3.3f) / 4096.0f;// 用整数计算(效率更高)
uint32_t voltage_mV = (adc_val * 3300) / 4096; // 单位:mV
(3) 特殊关键字类型
volatile
告诉编译器:该变量随时可能被 硬件/中断修改,不要优化。
常用于寄存器映射、全局标志位。
volatile uint32_t TIM_FLAG; // 定时器中断标志void TIM_IRQHandler(void) {TIM_FLAG = 1; // 中断修改
}int main() {while (!TIM_FLAG); // 主程序轮询等待
}
如果不用 volatile
,编译器可能认为 TIM_FLAG
永远不变,直接优化掉循环。
const
定义只读变量
嵌入式常用于查表、存储常量参数(放在 Flash,而不是 RAM)
const uint8_t sine_table[256] = { /* 正弦波表 */ };
typedef
为类型起别名,提高可读性
typedef unsigned int uint32_t;
2.2 指针与数组
(1) 指针
存储变量地址,可以间接访问数据
嵌入式最重要的用途:寄存器映射
#define GPIOA_ODR (*(volatile unsigned int*)0x4001080C)
GPIOA_ODR = 0x01; // 直接操作寄存器
👉 volatile
必须加,否则优化掉寄存器访问。
(2) 数组
一段连续内存存储同类数据
嵌入式常用于 数据缓存(buffer)
uint8_t rx_buffer[128]; // 串口接收缓存
(3) 字符串(C风格)
实际上是
char
数组,以'\0'
结尾
char msg[] = "Hello STM32";
2.3 结构体与位域
(1) 结构体
把相关数据打包
嵌入式常用于 传感器数据打包、通信数据帧
struct Sensor {int temperature;int humidity;int pressure;
};
(2) 位域(嵌入式重点 ⚡)
可以按位定义变量,节省空间
常用于寄存器操作
struct GPIO_CR {unsigned int MODE : 2; // 2位unsigned int CNF : 2; // 2位unsigned int RES : 28; // 保留
};
使用方式:
struct GPIO_CR *reg = (struct GPIO_CR*)0x40010800;
reg->MODE = 0x3; // 设置模式
⚠️ 注意:不同编译器对位域存放规则可能不同,不适合跨平台寄存器映射,常用 宏定义+位操作 更可靠。
2.4 枚举与宏
(1) 枚举(enum)
用于定义状态机、模式切换等
enum State { IDLE, RUNNING, ERROR };
enum State sys_state = IDLE;
(2) 宏(#define)
用于常量、寄存器、位操作
#define LED_PIN (1<<5) // PA5
#define LED_ON (GPIOA_ODR |= LED_PIN)
#define LED_OFF (GPIOA_ODR &= ~LED_PIN)
📌 小结(代码与基本数据类型)
基本类型:整数优先、浮点尽量少用
关键字:
volatile
(寄存器、中断标志)、const
(查表)指针:核心在于寄存器映射
结构体与位域:提高可读性,寄存器操作常用宏更安全
宏 & 枚举:便于定义寄存器位、系统状态
3. 数据运算符
3.1 算术运算符
(1) 基本运算
+
:加法-
:减法*
:乘法/
:除法%
:取余(整型才有,浮点不能用%
)
嵌入式注意点:
整型除法 → 舍弃小数部分:
int a = 7/3; // a = 2
除以 0 → 未定义行为,MCU 可能异常。
%
运算效率低,可以用位运算代替(如%2
可以用&1
)。
优化示例:
// 判断奇偶数
if (x % 2 == 0) { } // 慢
if ((x & 1) == 0) { } // 快
3.2 关系运算符
==
:相等!=
:不相等>
<
>=
<=
👉 在嵌入式中常用于 传感器判断、标志位检测
if (adc_val > 2000) {// 电压过高
}
3.3 逻辑运算符
&&
:逻辑与||
:逻辑或!
:逻辑非
嵌入式应用:
多条件检测(如按键+传感器同时有效)
if (button_pressed && adc_val > 1000) {// 条件同时满足
}
3.4 位运算符(嵌入式重点 ⚡)
(1) 与(&)
保留某些位,其余清零
value &= ~(1<<3); // 清零第3位
(2) 或(|)
设置某些位为 1
value |= (1<<5); // 置位第5位
(3) 异或(^)
相同为 0,不同为 1 → 用于翻转某些位
value ^= (1<<2); // 翻转第2位
(4) 取反(~)
0 → 1,1 → 0
uint8_t a = 0b10101010;
a = ~a; // 结果:01010101
(5) 移位(<< >>)
<<
左移:乘 2>>
右移:除 2(有符号数右移时,最高位补符号位)
uint8_t a = 1 << 3; // 结果 = 8 (00001000)
(6) 嵌入式应用实例
GPIO 输出控制
#define LED_PIN (1<<5) GPIOA_ODR |= LED_PIN; // PA5 置高(LED亮)
GPIOA_ODR &= ~LED_PIN; // PA5 置低(LED灭)
寄存器配置(置位/清零)
// 使能 TIM2 时钟 (RCC_APB1ENR寄存器第0位)
RCC_APB1ENR |= (1<<0); // 关闭 TIM2 时钟
RCC_APB1ENR &= ~(1<<0);
翻转引脚状态
GPIOA_ODR ^= (1<<5); // PA5 电平翻转
3.5 赋值与自增运算符
(1) 复合赋值运算符
+= -= *= /= &= |= ^= <<= >>=
a += 5; // 相当于 a = a + 5
(2) 自增自减运算符
++a
:先加再用a++
:先用再加
int a = 5;
int b = a++; // b=5, a=6
int c = ++a; // c=7, a=7
嵌入式常用场景:循环计数、数组索引、延时函数。
3.6 条件运算符
三目运算符:
条件 ? 值1 : 值2
int min = (a < b) ? a : b;
👉 嵌入式里常用于 寄存器快速判断赋值。
📌 小结(数据运算符)
算术运算:注意整型除法和溢出
逻辑运算:常用于多条件判断
位运算:嵌入式开发核心,寄存器、GPIO 必备
赋值/自增:常用于循环和计数
条件运算:简洁高效,适合快速判断
4. 总结与应用实例
4.1 LED 闪烁程序解析
这是嵌入式 C 语言学习的第一个“Hello World”。
#include "stm32f10x.h"int main(void) {// 1. 开启 GPIOA 时钟 (RCC_APB2ENR bit2 = 1)RCC->APB2ENR |= (1<<2);// 2. 设置 PA5 为推挽输出 (GPIOA_CRL MODE5=11, CNF5=00)GPIOA->CRL &= ~(0xF << (5*4)); // 清零GPIOA->CRL |= (0x3 << (5*4)); // 输出模式 50MHz// 3. 循环点亮和熄灭 LEDwhile(1) {GPIOA->ODR |= (1<<5); // LED ONfor (volatile int i=0; i<500000; i++); // 延时GPIOA->ODR &= ~(1<<5); // LED OFFfor (volatile int i=0; i<500000; i++); // 延时}
}
知识点应用:
数据存储
volatile
用于延时变量,避免编译器优化掉循环
数据类型
使用
int
和uint32_t
,保证寄存器访问一致
运算符
|
、&~
用于寄存器位操作移位
<<
用于快速定位寄存器位
4.2 定时器配置实例
利用 TIM2 让 LED 每秒闪烁一次。
#include "stm32f10x.h"int main(void) {// 1. 开启 GPIOA、TIM2 时钟RCC->APB2ENR |= (1<<2); // GPIOARCC->APB1ENR |= (1<<0); // TIM2// 2. 设置 PA5 为输出GPIOA->CRL &= ~(0xF << (5*4));GPIOA->CRL |= (0x3 << (5*4));// 3. 配置 TIM2 (1Hz 定时)TIM2->PSC = 7200 - 1; // 72MHz/7200 = 10kHzTIM2->ARR = 10000 - 1; // 10kHz/10000 = 1HzTIM2->CR1 |= 1; // 使能定时器while (1) {if (TIM2->SR & 1) { // 溢出标志 UIF=1TIM2->SR &= ~1; // 清除 UIFGPIOA->ODR ^= (1<<5); // 翻转 LED}}
}
知识点应用:
存储与转换:
PSC
、ARR
用整数计算替代浮点运算数据类型:
uint16_t
确保定时器参数不会溢出运算符:
&
检查标志位&~
清除标志位^
翻转 LED
4.3 通信协议打包与解析
模拟 UART 发送温度和湿度数据。
#include <stdint.h>typedef struct {uint8_t header;uint16_t temperature;uint16_t humidity;uint8_t checksum;
} DataPacket;uint8_t calc_checksum(DataPacket *pkt) {return (uint8_t)(pkt->header + (pkt->temperature&0xFF) + (pkt->temperature>>8)+ (pkt->humidity&0xFF) + (pkt->humidity>>8));
}int main(void) {DataPacket pkt;pkt.header = 0xAA;pkt.temperature = 253; // 25.3℃pkt.humidity = 600; // 60.0%pkt.checksum = calc_checksum(&pkt);// 此处可通过 UART 发送 pktreturn 0;
}
知识点应用:
数据存储与转换
uint16_t
拆分为高低字节
基本类型
typedef struct
用于打包数据帧const
可以用于协议固定帧头
运算符
&
、>>
提取高低字节+
计算校验和
4.4 小结
数据存储与转换 → 解决了 MCU 的 字节序、寄存器映射、类型大小 问题
基本数据类型 → 通过
volatile
、typedef
、struct
让代码更可靠和可读数据运算符 → 位运算是嵌入式的“灵魂”,直接控制硬件
通过 LED、定时器、通信这几个例子,我们可以看到 所有理论知识都能落到寄存器和外设操作上,这就是 C 语言在嵌入式开发中的精髓。