STM32F103C8T6开发板入门学习——寄存器和库函数介绍
学习目标:STM32F103C8T6开发板入门学习——寄存器和库函数介绍
学习内容:
1. 寄存器介绍
1.1 存储器映射
存储器本身无固有地址,是具有特定功能的内存单元。它的地址是由芯片厂商或用户分配,给存储器分配地址的过程就叫做存储区映射。给内存单元分配地址之后,就可以通过指针去操作内存地址。
1.2 存储器映射表
STM32作为32位单片机,地址范围为2³²(即4GB)。为降低相同应用场景下的软件复杂度,其存储映射按Cortex-M4处理器规则预先定义:
- 部分地址空间由Arm Cortex-M4的系统外设占用,且不可更改;
- 其余地址空间可由芯片供应商定义使用。
1.3 什么是寄存器
寄存器是具有特定功能的内存单元,通过操作这些单元可驱动外设工作。按功能可分为:
- 指令寄存器、地址寄存器、数据寄存器;
- 处理器可通过相互独立的总线读取指令、加载/存储数据。
1.4 寄存器映射
程序存储器、数据存储器、寄存器和I/O端口位于同一4GB线性地址空间内:
- 每个寄存器对应不同功能,操作相应寄存器可配置对应功能;
- 控制外设时,可通过起始地址以C语言指针方式访问内存单元;
- 给已分配地址、有特定功能的内存单元取别名的过程,称为寄存器映射,该别名即寄存器。
1.5 寄存器重映射
给寄存器再次分配地址的过程称为寄存器重映射。
1.6 总线基地址
片上外设区域分为三条总线,按外设速度不同挂载相应外设:
- AHB总线:最高时钟可达72MHz;
- APB1总线:最高时钟可达36MHz;
- APB2总线:最高时钟可达72MHz。
根据外设速度的不同,不同的总线挂载着不同的外设。总线的最低地址我们称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。
1.7 外设基地址
每条总线上挂载多个外设,每个外设均有自己的地址范围。
1.8 外设寄存器地址
在外设的地址范围内,分布着该外设的多个寄存器。以GPIO外设为例:
- GPIO外设地址范围内有多个寄存器,每个均有特定功能,通过操作对应寄存器可配置GPIO功能;
- 每个寄存器为32位,占4个字节。
1.9 如何操作寄存器
以“让GPIOA端口的16个引脚都置1”为例,操作步骤如下:
- 确定目标寄存器:需配置端口输出寄存器GPIOx_ODR;
- 计算寄存器地址:
- 由中文参考手册第115页可知,GPIOx_ODR寄存器的地址偏移量为0x0C;
- GPIOA端口的基地址为0x40010800;
- 因此,GPIOx_ODR寄存器的地址为:0x40010800 + 0x0C = 0x4001080C。
通过上图可以了解到要想使能所有引脚配置为1,只需要将ODRy(y=0…15)对应的位置1即可。也就是配置GPIOA_ODR寄存器的高16位为0,低16位为1,换成十六进制就是0x0000FFFF。
1.通过绝对地址访问内存单元
// GPIOA 端口的16个引脚全部输出高电平
*(unsigned int*)(0x4001080C) = 0xFFFF;
说明:
(unsigned int*)
:将立即数0x4001080C
强制转换为无符号整型指针,告诉编译器这是一个内存地址;*
:对该地址进行解引用,访问地址对应的内存空间;- 整体含义:向
0x4001080C
(GPIOA_ODR寄存器)对应的内存空间写入0xFFFF
,实现GPIOA所有16个引脚输出高电平(低16位置1,高16位为0)。
2. 通过别名访问内存单元
为简化操作并增强可读性,可给寄存器地址定义别名:
方式一:定义地址指针
// 定义 GPIOA_ODR 地址指针
#define GPIOA_ODR (unsigned int*)(0x4001080C)// GPIOA 端口的16个引脚全部输出高电平
*GPIOA_ODR = 0xFFFF;
方式二:直接定义解引用的地址(更常用)
// 定义 GPIOA_ODR 并直接解引用
#define GPIOA_ODR (*(unsigned int*)(0x4001080C))// GPIOA 端口的16个引脚全部输出高电平
GPIOA_ODR = 0xFFFF;
- 优势:通过
GPIOA_ODR
这样的别名,可直观理解操作的是GPIOA的输出数据寄存器,无需记忆复杂的十六进制地址,提升代码可读性和可维护性。
2. 库函数介绍
2.1 为什么要使用库函数
从上一节可知,通过寄存器驱动外设虽可行,但STM32寄存器数量极多:
- 仅定义寄存器就需耗费大量时间,还需逐一查找其功能、地址并配置对应值,在开发难度和时间成本上均不划算。
库函数的出现正是为解决这一问题,其优势在于:
- 由STM32官方开发,可直接移植到工程中使用,大幅提升开发效率;
- 无需深入了解硬件底层机制,只需根据需求查找对应函数并调用,降低了开发门槛。
2.2 库函数简单介绍
在创建的工程模板中,包含STM32F1x的标准外设库,可通过打开stm32f10x_gpio.h
和stm32f10x_gpio.c
等文件查看具体实现。
本质上,库函数是在寄存器操作的基础上进行了封装:
- 底层仍通过操作寄存器实现功能,但上层提供了更简洁的函数接口,使开发操作更简单。
3. 寄存器和库函数的区别
寄存器和库函数最终均通过对地址操作实现功能,但二者存在明显差异,适用场景也不同:
- 原理与直观性:寄存器操作更易理解底层原理,过程直观;库函数则屏蔽了底层细节,直接面向应用功能。
- 代码量与冗余:库函数代码量相对更大,因函数需考虑多种使用场景,可能存在代码冗余;寄存器操作代码更精简。
- 开发难度与效率:库函数使用简单、易上手,可快速开发应用,能显著提高开发效率;寄存器操作需熟悉硬件细节,开发门槛更高。
- 资源占用与速度:寄存器操作占用内存少、执行速度快,在资源有限(如内存紧张)或对执行速度有高要求的场景下更具优势。
学习时间:8月25日
1. 寄存器介绍
1.1 存储器映射
原文核心:存储器本身无固有地址,分配地址的过程称为存储区映射,映射后可通过指针操作内存单元。
个人理解:存储器就像一堆无编号的储物箱,“存储器映射”就是给每个箱子贴上限号(地址)。有了编号后,CPU才能通过“地址指针”准确找到并操作对应的存储单元。这是硬件与软件交互的基础——没有地址映射,软件就无法“定位”硬件资源。
1.2 存储器映射表
原文核心:STM32作为32位单片机,地址范围为4GB,部分地址由Cortex-M4内核占用(不可改),其余由芯片厂商定义。
个人理解:4GB地址空间是32位处理器的“理论上限”,就像一个巨大的“地址版图”。其中,Cortex-M4内核预留了部分区域(如系统外设),相当于“国家划定的保护区”,不可占用;剩下的区域由STM32厂商分配给片上外设(如GPIO、UART等),形成了固定的“地址规划”。这种标准化的映射表让不同开发者能基于同一套地址规则操作硬件,降低了兼容性成本。
1.3 什么是寄存器
原文核心:寄存器是具有特定功能的内存单元,按功能分为指令、地址、数据寄存器,处理器通过独立总线访问。
个人理解:寄存器是硬件的“控制面板”。比如,指令寄存器存放当前要执行的指令,地址寄存器存放要访问的内存地址,数据寄存器临时存储运算数据。独立总线设计(如指令总线、数据总线)就像“专用通道”,避免了指令读取和数据传输的冲突,提高了CPU效率。简单说,寄存器是CPU与外设“对话”的直接窗口——操作寄存器就是在“告诉”硬件该做什么。
1.4 寄存器映射
原文核心:给已分配地址、有特定功能的内存单元取别名的过程,称为寄存器映射,别名即寄存器。
个人理解:假设某块内存单元的地址是0x4001080C,其功能是控制GPIOA的输出,我们给它起个名字“GPIOA_ODR”,这个过程就是寄存器映射。它的本质是“地址→功能别名”的映射,让开发者不用死记硬背冗长的十六进制地址,而是通过“GPIOA_ODR”这样直观的名字操作硬件,大幅降低了代码的理解难度。
1.6 总线基地址
原文核心:片上外设分为AHB(72MHz)、APB1(36MHz)、APB2(72MHz)三条总线,总线基地址是总线上首个外设的地址。
个人理解:总线就像“高速公路”,外设是“车辆”。AHB和APB2是“快车道”(72MHz),适合高速外设(如GPIO、SPI);APB1是“慢车道”(36MHz),适合低速外设(如I2C、UART)。总线基地址是“高速路的起点”,比如AHB总线基地址是0x40010000,总线上的所有外设地址都从这个起点开始“编号”,方便按总线归类管理外设。
1.7 外设基地址
原文核心:每条总线上的外设都有自己的地址范围。
个人理解:如果总线是“高速公路”,外设就是路上的“服务区”,每个服务区有自己的“管辖范围”(地址范围)。比如,GPIOA挂在APB2总线上,其基地址是0x40010800,地址范围从0x40010800到下一个外设的基地址前,这样CPU就能通过地址范围准确区分不同外设(如GPIOA和GPIOB)。
1.8 外设寄存器地址
原文核心:外设地址范围内有多个寄存器,每个32位(4字节),对应特定功能(以GPIO为例)。
个人理解:一个外设(如GPIOA)就像一个“工具箱”,里面的每个“工具”(寄存器)对应不同功能:比如“GPIOA_CRL”控制低8位引脚模式,“GPIOA_ODR”控制输出电平。每个寄存器占4字节(32位),是因为STM32是32位处理器,一次可处理32位数据,这种设计能高效读写寄存器。
1.9 如何操作寄存器(以GPIOA引脚置1为例)
原文核心:步骤为“确定目标寄存器→计算地址→写入值”,并给出了具体示例。
个人理解:操作寄存器的本质是“地址+值”的精准控制。比如要让GPIOA所有引脚输出高电平,需找到“输出数据寄存器(ODR)”,计算其地址(GPIOA基地址+偏移量0x0C=0x4001080C),再写入0xFFFF(低16位置1)。这个过程像“按地址找到开关,再拨动开关”,需要对硬件地址和寄存器功能有清晰了解。
1. 通过绝对地址访问内存单元
原文示例:*(unsigned int*)(0x4001080C) = 0xFFFF;
个人理解:这种方式直接通过地址操作,优点是“直达底层”,缺点是可读性差——如果不查手册,没人知道0x4001080C是什么。适合对硬件极度熟悉的场景,或调试时快速验证功能。
2. 通过别名访问内存单元
原文给出两种方式:定义地址指针(#define GPIOA_ODR (unsigned int*)(0x4001080C)
)或直接定义解引用地址(#define GPIOA_ODR (*(unsigned int*)(0x4001080C))
)。
个人理解:这是实际开发中更推荐的方式。通过“GPIOA_ODR”这样的别名,代码可读性大幅提升,且修改时只需改宏定义,无需逐个修改地址。就像用“家门钥匙”代替“钥匙的物理编号”,更符合人类的记忆习惯。
2. 库函数介绍
2.1 为什么要使用库函数
原文核心:寄存器数量多,手动定义和配置耗时,库函数由官方开发,可直接移植,降低门槛、提高效率。
个人理解:STM32的外设和寄存器极其庞大(仅GPIO就有近10个寄存器,全芯片有上百个外设),如果每个寄存器都手动定义地址、查手册配置值,开发效率会极低。库函数就像“封装好的快捷指令”,比如要配置GPIO输出,直接调用GPIO_Init()
即可,无需关心底层寄存器的地址和具体值,适合快速开发。
2.2 库函数简单介绍
原文核心:库函数基于寄存器操作封装,在stm32f10x_gpio.h
等文件中实现,提供简洁接口。
个人理解:库函数不是“魔法”,其底层依然是操作寄存器。比如GPIO_SetBits(GPIOA, GPIO_Pin_All)
函数,内部其实是对GPIOA_ODR
寄存器写入0xFFFF。它的价值在于“抽象”——把复杂的寄存器配置逻辑(如时钟使能、模式设置、输出控制)打包成函数,让开发者只需关注“要做什么”,而非“怎么做”。
3. 寄存器和库函数的区别
原文从原理、代码量、开发效率、资源占用等方面对比了两者。
个人感悟:
- 寄存器是“手动挡”:直接操作硬件,代码精简、执行快,但需要深入了解硬件细节,开发门槛高,适合底层驱动开发或资源受限场景(如内存极小的设备)。
- 库函数是“自动挡”:屏蔽底层细节,开发效率高、易上手,但代码量较大(可能有冗余),执行速度略慢,适合应用层开发或快速原型验证。
实际开发中,两者并非对立:简单功能可用库函数快速实现,对性能要求高的部分(如中断处理)可结合寄存器操作优化。初学者建议先通过库函数入门,熟悉后再深入寄存器,理解底层原理。
学习产出:
- 了解并学习STMSTM32F103C8T6开发板的寄存器和库函数介绍
- 技术文章的1篇