前言
哈喽大家好,这里是 Hello_Embed 的新一篇学习笔记。前面我们聊了变量的地址特性和指针的基本概念,这一次咱们要把指针 “落地”—— 从代码里的地址操作,讲到它如何实实在在地控制单片机硬件。
嵌入式开发的魅力就在于 “软硬结合”,而指针正是打通软件和硬件的关键。接下来会通过具体例子,先讲清指针怎么操作变量,再一步步演示如何用指针直接访问硬件地址、配置寄存器,最后实现 LED 闪烁。跟着例子走,你会发现 “用代码控制硬件” 其实没那么神秘~
指针的简单使用
#include <stdio.h>
int mymain(void)
{
int a = 13;
int *p;
p = &a;
printf("a = 0x%x\n\r", a);
printf("a address = 0x%x\n\r", &a);
printf("p address = 0x%x\n\r", &p);
printf("p = 0x%x\n\r", p);
*p = 14;
printf("a = %d\n\r", a);
return 0;
}
从内存分配的角度来看,任何变量被定义后,系统都会为其分配相应大小的存储空间,用于存放变量的值,因此变量a
(int 型)和指针p
(int * 型)都有各自独立的存储空间和地址。
从运行结果能清晰地看到,当我们给*p
赋值 14 时,最终这个赋值操作会直接作用在变量a
上。这是因为p
中存储的是a
的地址,而*p
表示 “访问p
所指向地址对应的存储空间”,本质上就是访问a
的存储空间。
我们用一张存储空间示意图来辅助理解:
变量 | 分 | 配 | 空 | 间 | 地址 |
---|---|---|---|---|---|
a | 00 | 00 | 00 | 0d | 0x2000 040c |
p | 20 | 00 | 04 | 0c | 0x2000 0408 |
表格说明:a
的首地址是 0x2000040c,它作为 int 型变量占用 4 字节存储空间,存储的值为 00 00 00 0d(十六进制 13);p
作为指针变量,其存储的内容是a
的地址 0x2000040c;当执行*p = 14
时,实际上是将p
所存储的地址(0x2000040c)对应的 4 字节存储空间写入 14(十六进制 0e),因此a
的值会被修改为14。
为了加深理解,我们完善测试代码并运行:
#include <stdio.h>
int mymain(void)
{
int a = 13;
int *p; //定义int类型指针
printf("a = %d\n\r", a);
printf("a address = 0x%x\n\r", &a);
printf("p address = 0x%x\n\r", &p);
p = &a; //指针p指向a
printf("p = 0x%x\n\r", p);
*p = 0x12345678; //通过指针修改a的值
printf("a = %d\n\r", a);
printf("*p = %d\n\r", *p); //*p与a的值一致
*p = 'C'; //字符'C'的ASCII码为67
printf("a = %d\n\r", a);
return 0;
}
输出结果进一步验证:对*p
的所有操作,最终都会作用在它所指向的对象(变量a
)上。
那如果我们对*p
赋一个超出其指向变量数据类型大小的值,会发生什么呢?我们将上述代码中的int
都换成char
类型(1 字节),再次运行程序,结果如下图:
可以看到,a
与*p
的值都为 120(0x78)。这是因为char *p
限制了*p
操作的存储空间大小为 1 字节,而a
作为char
类型变量也只能存储 1 字节的数据。同时,p = &a
这一语句要求等式两边的数据类型必须一致,这种类型约束能确保通过*p
操作目标变量时,不会出现数据大小不匹配的情况,保证了操作的安全性。
指针与硬件的联系
在嵌入式领域,指针的重要性不仅体现在软件层面的数据操作,更在于它能直接与硬件交互,实现对硬件的控制。指针与硬件交互的简单工作流程如下:
定义指针变量(如int *p;)→ 将硬件寄存器的地址赋值给指针(p = 硬件地址;)→ 通过指针写入数据控制硬件(*p = val;)→ 通过指针读取硬件状态(v = *p;)
我们可以通过指针直接操作具体的硬件地址,例如:
#include <stdio.h>
//我们还可以直接操作具体地址
int mymain(void)
{
int *p = (int *)0x20000000;
*p = 0x12345678;
return 0;
}
我们在 Keil5 中通过调试来观察这一过程:
- 在
int *p = (int *)0x20000000;
语句前打上断点 - 将p添加进watch1窗口观察,方便观察指针的值;
- 在Memory 1窗口搜索0x20000000地址观察,观察该地址初始的存储内容;
- 单步运行Step(F11),执行
int *p = (int *)0x20000000;
语句,此时 Watch1 窗口中p
的值变为 0x20000000;
- 单步运行Step(F11),执行
*p = 0x12345678;
语句,此时 Memory 1 窗口中 0x20000000 地址的内容被成功改写为 0x12345678。
上面的示例展示了通过指针操作内存地址的过程,下面我们通过指针操作单片机的寄存器地址,实现 LED 灯的闪烁,这就是嵌入式开发中常用的寄存器编程方式。
现在我们以STM32F103C8T6最小系统板的PB5为例说明。要通过指针控制STM32F103C8 单片机的 PB5 引脚(连接 LED 灯),需先计算出控制该引脚的寄存器地址,具体推导如下:
-
确定 GPIOB 基地址
打开数据手册找到文档中存储器映像信息,GPIOB 的基地址为0x40010C00
。
-
明确 PB5 对应的寄存器
PB5 是 GPIOB 端口的第 5 个引脚,其电平状态由 GPIOB 的输出数据寄存器(ODR)或输入数据寄存器(IDR)控制,其中最常用的输出控制寄存器为 ODR,其偏移量为0x0C
(即从基地址偏移 12 字节)。
-
计算寄存器地址
GPIOB 的 ODR 寄存器地址 = GPIOB 基地址 + ODR 偏移量,即:
0x40010C00 + 0x0C = 0x40010C0C
-
定位 PB5 在寄存器中的位
ODR 寄存器是 32 位寄存器,每一位对应一个引脚(bit0 对应 PB0,bit1 对应 PB1……bit5 对应 PB5)。因此,PB5 对应 ODR 寄存器的第 5 位(bit5)。
综上,通过操作 GPIOB 的 ODR 寄存器(地址0x40010C0C
)的 bit5,即可控制 PB5 的电平状态(如置 1 输出高电平,清 0 输出低电平)。
//main函数
#include "stm32f10x.h"
#include <stdio.h>
#include "../Peripherals/usart.h"
#include "text.h"
// 定义寄存器地址宏
#define GPIOB_ODR (*(volatile uint32_t*)0x40010C0C)
// 初始化PB5为推挽输出
void Init_LED_GPIO(void) {
// 使能GPIOB时钟
*(volatile uint32_t*)0x40021018 |= (1 << 3);
// 配置PB5为推挽输出(50MHz)
*(volatile uint32_t*)0x40010C00 |= (3 << 20); // 直接设置MODE5[1:0]=11
}
// 延时函数(根据CPU频率调整)
void delay_ms(uint32_t ms)
{
for (uint32_t i = 0; i < ms * 10000; i++);
}
int main(void)
{
Init_LED_GPIO();
while (1)
{
// 方法A:直接操作ODR(需注意可能影响其他位)
GPIOB_ODR &= ~(1 << 5); // PB5置低
delay_ms(500);
GPIOB_ODR |= (1 << 5); // PB5置高
delay_ms(500);
}
}
虽然过程中会遇到不少问题,但最终我们成功点亮了 LED 并实现了闪烁功能。这段代码涉及地址宏定义、端口输出模式配置、延时函数及while(1)
循环的应用等多个知识点。即使现在对代码细节不熟悉也没关系,我也是借助了 AI 的帮助才完成的。我们只需明白:在数据手册的指导下,通过指针操作硬件地址的方式,能够配置单片机的端口输出高低电平,从而控制 LED 灯的亮灭。随着学习的深入,当我们掌握了标准库或 HAL 库后,配置硬件端口将会变得更加简单便捷。
结尾
在这篇笔记里,我们见证了指针从 “操作变量” 到 “控制硬件” 的跨越:通过*p
能修改变量的值,通过指向寄存器地址的指针,居然能直接让 LED 灯闪烁起来。这背后的核心逻辑其实很统一 —— 指针存储地址,*
操作符访问地址对应的空间,而硬件寄存器本质上就是一段特殊的内存地址。
下一篇,我们会继续深挖指针的用法,比如用指针操作数组、访问结构体成员,这些技巧在解析传感器数据、配置复杂外设时特别有用。如果你在调试指针控制硬件时遇到过奇怪的问题,欢迎在评论区分享,咱们一起找原因~
我是 Hello_Embed,一个在嵌入式路上慢慢摸索的小白,咱们下篇笔记再见!