前言
在最初学习STM32的过程中,由于知识不进脑子,经常边学边忘,并且C语言学习的也比较浅,涉及到指针地址等方面的知识,内心就有点排斥。
第一次遇到 -> 和 . 这两种操作符时,我只是知道按照示例“照着用”,但并不清楚它们之间的具体区别,也没有深入理解它们的内在逻辑。这样的学习方式让我很容易陷入“知道怎么用,却不知道为什么”的状态。
今天在实际开发中再次遇到这两个操作符时,我发现仍旧无法清晰地解释它们的区别和适用场景。于是,我决定花时间好好研究一番。通过梳理这些基础概念,希望不仅能彻底搞清楚 -> 和 . 的用法,还能深化对指针和地址的理解,夯实自己在嵌入式开发中的基础知识。
接下来的文章将通过 TIM2->CCR2 与 TIM2.CCR2 的区别这一实际问题为切入点,详细解析 -> 和 . 两种操作符的用法和应用场景,帮助初学者更直观地理解它们在 STM32 开发中的具体用途和背后的逻辑。
TIM2->CCR2 与 TIM2.CCR2 的区别与对比说明
在 STM32 的开发中,寄存器操作通过定时器的基地址映射和结构体定义实现。两种操作符 ->
和 .
的使用情况需要具体区分,分别应用于不同的环境。以下将详细说明它们的区别、基地址映射的原理,以及应用场景。
概念
. 操作符:用于访问结构体实例
的成员。在使用它时,结构体变量必须是直接定义的实例,而不是一个指针。
-> 操作符:用于访问指针指向的结构体的成员。在使用它时,必须有一个结构体指针
,该指针指向一个有效的结构体对象或内存区域。
1. 基地址映射原理
在 STM32 中,每个外设(如定时器 TIM2)都有一个固定的基地址,用于唯一标识硬件寄存器的起始地址。为了便于开发,CMSIS(Cortex Microcontroller Software Interface Standard)框架通过结构体定义寄存器布局,并将这些基地址与结构体进行映射。
基地址映射的具体过程
-
寄存器基地址的定义:
- 每个外设在芯片手册中都有对应的基地址。例如,TIM2 的基地址为:
#define TIM2_BASE (APB1PERIPH_BASE + 0x0000)
APB1PERIPH_BASE
是 APB1 总线的基地址,而0x0000
是 TIM2 的偏移量。
- 每个外设在芯片手册中都有对应的基地址。例如,TIM2 的基地址为:
-
基地址与结构体的结合:
- 通过将 TIM2 的基地址强制转换为指向
TIM_TypeDef
结构体的指针,实现基地址到寄存器的映射:#define TIM2 ((TIM_TypeDef *) TIM2_BASE)
- 这样,
TIM2
成为一个指针,指向定时器 2 的寄存器映射区域。
- 通过将 TIM2 的基地址强制转换为指向
-
寄存器结构体的定义:
- 以
TIM_TypeDef
为例,结构体定义了定时器寄存器的布局,每个成员代表一个寄存器,其偏移量按芯片手册的规定排列:typedef struct { __IO uint32_t CR1; // Offset: 0x00 __IO uint32_t CR2; // Offset: 0x04 __IO uint32_t SMCR; // Offset: 0x08 ... __IO uint32_t ARR; // Offset: 0x2C __IO uint32_t CCR2; // Offset: 0x38 ... } TIM_TypeDef;
- 以
-
访问寄存器的地址计算:
- 例如,访问
TIM2->CCR2
实际等价于访问物理地址:TIM2_BASE + 0x38
注意
TIM2->ARR 的值不是一个指针,而是寄存器地址的直接映射
直接用于赋值操作即可。不需要*(TIM2->ARR) = 1000;
- 编译器会根据 TIM2->ARR = ARR_Value;
翻译成对具体硬件地址的写操作。 - 翻译前
TIM2->ARR = ARR_Value;
- 编译器将翻译为:
*(TIM2_BASE + 0x2C) = ARR_Value;
- 例如,访问
2. TIM2->CCR2 的应用
TIM2->CCR2
是标准的寄存器访问方式,用于裸机开发环境。特点如下:
语法和功能:
TIM2
是一个指针,指向TIM_TypeDef
结构体。->
是指针成员访问操作符,用于访问指针指向的结构体成员。TIM2->CCR2
直接访问硬件的捕获/比较寄存器 2。
示例代码:
TIM2->CCR2 = 200; // 设置捕获/比较寄存器 2 的值为 200
uint32_t value = TIM2->CCR2; // 读取捕获/比较寄存器 2 的值
底层工作机制:
TIM2->CCR2
实际上访问的是寄存器的物理地址TIM2_BASE + 0x38
。
适用场景:
- 裸机开发中直接操作寄存器,例如配置 PWM 信号的占空比。
3. TIM2.CCR2 的应用
TIM2.CCR2
使用点操作符 .
,用于非指针的结构体实例。与 TIM2->CCR2
相比,它不能直接应用于裸机开发环境,但在以下情况中可能使用:
语法和功能:
.
是结构体实例的成员访问操作符,用于访问结构体的成员。- 适用于非指针结构体变量的场景。
示例代码:
TIM_TypeDef tim2_instance; // 创建一个结构体实例
tim2_instance.CCR2 = 200; // 设置实例的 CCR2 值为 200
uint32_t value = tim2_instance.CCR2; // 读取实例的 CCR2 值
注意事项:
- 在裸机开发中,
TIM2
是一个指针而非结构体实例,因此不能使用点操作符。 TIM2.CCR2
会导致编译器报错:error: request for member 'CCR2' in something not a structure or union
适用场景:
- 模拟外设寄存器的逻辑测试环境(非硬件环境)。
- 某些高级抽象层(如 HAL 或 LL 库)可能会间接使用结构体实例。
4. TIM2->CCR2 与 TIM2.CCR2 的对比
特性 | TIM2->CCR2 | TIM2.CCR2 |
---|---|---|
语法合法性 | 合法,标准用法 | 非法,裸机开发中不能使用 |
定义要求 | TIM2 是指针(TIM_TypeDef * ) | TIM2 是非指针结构体实例 |
适用场景 | 裸机开发,直接操作寄存器 | 模拟环境或高级封装的测试 |
访问方式 | 通过基地址指针访问寄存器(直接与硬件交互) | 通过结构体实例访问(不直接与硬件交互) |
效率 | 高效,直接访问硬件寄存器 | 不适用裸机寄存器访问 |
5. 扩展说明:HAL 与 LL 库的封装
在实际开发中,为了简化寄存器操作,STM32 提供了 HAL 和 LL 库,对寄存器访问进行了封装。例如:
HAL 封装:
HAL 使用定时器句柄间接访问寄存器:
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, 200); // 设置捕获/比较寄存器 2 的值
uint32_t value = __HAL_TIM_GET_COMPARE(&htim2, TIM_CHANNEL_2); // 获取捕获/比较值
LL 封装:
LL 库提供更接近寄存器的访问方式:
LL_TIM_OC_SetCompareCH2(TIM2, 200); // 设置捕获/比较寄存器 2 的值
uint32_t value = LL_TIM_OC_GetCompareCH2(TIM2); // 获取捕获/比较值
尽管封装简化了开发,但底层依然依赖于 TIM2->CCR2
的直接操作。
6. 结论
-
TIM2->CCR2
是裸机开发中的合法写法,直接操作硬件寄存器,是高效的寄存器访问方式。 -
TIM2.CCR2
是非法的,因为TIM2
是指针,不能用点操作符访问其成员。 -
高级开发中可以通过 HAL 或 LL 库封装间接访问寄存器,但最终仍依赖于
TIM2->CCR2
的底层实现。 -
了解基地址映射和操作符的使用区别,有助于在不同开发场景中选择合适的方式。
-
选择操作符的原则:
-
使用
.
:当操作的是一个普通的结构体变量时。 -
使用
->
:当操作的是一个结构体指针时。
嵌入式开发中的推荐:
- 寄存器访问:绝大部分场景下需要通过指针进行访问,推荐使用
->
操作符。 - 模拟或测试:在非硬件场景中,可以使用普通结构体变量和
.
操作符。
通过正确选择 .
和 ->
操作符,可以有效避免语法错误,提升代码的可读性与可靠性。特别是在嵌入式开发中,理解这两种操作符的区别和适用场景是入门开发的重要一步。