用 Keil5 理解 C 语言指针(二):指针的操作与硬件控制

前言

哈喽大家好,这里是 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的存储空间。
我们用一张存储空间示意图来辅助理解:

变量地址
a0000000d0x2000 040c
p2000040c0x2000 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 中通过调试来观察这一过程:

  1. int *p = (int *)0x20000000;语句前打上断点
  2. 将p添加进watch1窗口观察,方便观察指针的值;
    请添加图片描述
  3. 在Memory 1窗口搜索0x20000000地址观察,观察该地址初始的存储内容;
    请添加图片描述
  4. 单步运行Step(F11),执行int *p = (int *)0x20000000;语句,此时 Watch1 窗口中p的值变为 0x20000000;
    请添加图片描述
  5. 单步运行Step(F11),执行*p = 0x12345678;语句,此时 Memory 1 窗口中 0x20000000 地址的内容被成功改写为 0x12345678。
    请添加图片描述
    上面的示例展示了通过指针操作内存地址的过程,下面我们通过指针操作单片机的寄存器地址,实现 LED 灯的闪烁,这就是嵌入式开发中常用的寄存器编程方式。

现在我们以STM32F103C8T6最小系统板的PB5为例说明。要通过指针控制STM32F103C8 单片机的 PB5 引脚(连接 LED 灯),需先计算出控制该引脚的寄存器地址,具体推导如下:

  1. 确定 GPIOB 基地址
    打开数据手册找到文档中存储器映像信息,GPIOB 的基地址为 0x40010C00
    请添加图片描述

  2. 明确 PB5 对应的寄存器
    PB5 是 GPIOB 端口的第 5 个引脚,其电平状态由 GPIOB 的输出数据寄存器(ODR)或输入数据寄存器(IDR)控制,其中最常用的输出控制寄存器为 ODR,其偏移量为 0x0C(即从基地址偏移 12 字节)。
    请添加图片描述

  3. 计算寄存器地址
    GPIOB 的 ODR 寄存器地址 = GPIOB 基地址 + ODR 偏移量,即:
    0x40010C00 + 0x0C = 0x40010C0C

  4. 定位 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,一个在嵌入式路上慢慢摸索的小白,咱们下篇笔记再见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值