一、什么是寄存器?(最通俗的解释)

简单来说:

寄存器是CPU 内部的高速存储单元,用来临时存放数据、地址或控制信息,供CPU快速访问和使用。你可以把它理解为CPU 自带的“超快小盒子”(速度极快),用来放当前正在用的数据或指令,比内存(RAM)快得多但数量很少。但是我可以很负责任的跟你们说,寄存器就是存放相关工具的小盒子。为了解释这个概念。我给你们看一下由MCU 提供的技术文档

  • TOP1:
      名称:CAN 主控制寄存器
      作用:用于配置和控制 CAN(Controller Area Network,控制器局域网)外设核心工作模式与关键行为的重要寄存器

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • TOP2:
      名称:ADC 状态寄存器(ADC_SR)
      作用:ADC 状态寄存器(ADC_SR)用于反映 ADC 转换状态与监测模拟看门狗情况,具体作用如下

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • TOP3:
      名称:GPIOx_CRL
      作用:具体作用如下用于配置 GPIO 端口的低 8 位引脚 (Pin0 ~ Pin7)的工作模式和输出类型。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

关键就是MCU一直所说的寄存器!
寄存器内封装了相关对应的功能。这些功能对应了相关的功能。比如说下面这些例子,都是单片机/STM32 里最常见的“寄存器 - 功能”对应,你可以对应着理解“寄存器是如何封装硬件能力的”

二、STM32 寄存器实例解析

以下内容速览一下就可以,因为现阶段你可能看不懂。看完文章后可以自己再来看看

1. GPIO 输入/输出 → 对应 GPIOx_CRL / GPIOx_CRH / GPIOx_IDR /

  - GPIOx_CRL / GPIOx_CRH (端口配置寄存器)
  就像你给引脚“选职业”(参照玩王者荣耀)——让 Pin0 是当“输入引脚”还是“输出引脚”,输出时速度多快,输入时是浮空/上拉/下拉……写这些寄存器的值,就是在给引脚“分配工作”。
  - GPIOx_IDR (输入数据寄存器)
  像“看引脚现在干啥呢”——读取 IDR 寄存器,就能知道外部电路给这个引脚灌了高电平还是低电平(比如按键有没有被按下)。
  - GPIOx_ODR (输出数据寄存器)
  像“指挥引脚干啥呢”——往 ODR 写 0 或 1,引脚就会输出低电平或高电平(比如让 LED 亮/灭)。

2. 定时器(Timer)→ 对应 TIMx_ARR / TIMx_PSC / TIMx_CNT 等

  定时器就像“芯片里的电子秒表”,用来计时、产生周期性中断、输出 PWM波……靠的就是一系列寄存器配合:
  - TIMx_ARR (自动重装载寄存器)
  相当于“定闹钟的时间”——写这个寄存器的值,就是设置定时器“数到多少后触发事件”(比如数到 1000 就溢出)。
  - TIMx_PSC (预分频器寄存器)
  相当于“给秒表调快慢”——写这个值,能改变定时器计数的“基准频率”(比如把 72MHz 主频分成 7200 分频,计数速度就变慢,计时更久)。
  - TIMx_CNT (计数器寄存器)
  相当于“秒表现在走到多少了”——读这个寄存器,就能知道定时器当前计了多少个数;写这个寄存器,还能“重置秒表”(比如清零重新开始计时)。

3. 串口通信(UART)→ 对应 USARTx_SR / USARTx_DR /

  串口用来和外设/电脑收发数据,全靠寄存器控制“啥时候发、啥时候收、发多快、收没收到”:
  - USARTx_SR (状态寄存器)
  相当于“串口的「状态指示灯」”——读这个寄存器,能知道“有没有收到数据?发送缓冲区空没空?有没有出错?”(比如 RXNE=1 表示“收到新数据啦,可以读了”)。
  - USARTx_DR (数据寄存器)
  相当于“串口的「收发缓冲区」”——往 DR 写数据,串口就会自动把这个数据发出去;读 DR,就能拿到串口刚收到的数据。
  - USARTx_BRR (波特率寄存器)
  相当于“串口的「说话速度调节器」”——写这个寄存器的值,就能设置串口通信的波特率(比如 9600、115200),决定每秒发多少个 bit。

4. ADC 模数转换 → 对应 ADCx_CR1 / ADCx_CR2 / ADCx_SQR1 等

  ADC 是把“模拟电压(比如传感器输出的 0~3.3V)”转成“数字量(芯片能处理的 0~4095)”的模块,靠寄存器控制“采哪个通道、多快速度采、采多少次平均”:
  - ADCx_CR1 (控制寄存器 1)
  相当于“ADC 的「总开关+模式选择」”——写这个寄存器,能打开/关闭 ADC,选择是单次转换还是连续转换,选择扫描模式(一次采多个通道)等。
  - ADCx_CR2 (控制寄存器 2)
  相当于“ADC 的「触发方式+对齐方式」”——写这个寄存器,能设置 ADC是软件触发还是外部引脚触发,设置转换结果左对齐还是右对齐(方便读数)。
  - ADCx_SQR1~SQR4 (序列寄存器)
  相当于“ADC 的「通道队列」”——写这些寄存器,能设置“先采哪个通道、再采哪个通道、一共采多少个通道”,让 ADC 按顺序自动扫描多个模拟输入。
  - ADCx_DR (数据寄存器)
  相当于“ADC 的「转换结果缓存区」”——ADC 完成一次转换后,数字量结果会自动存在 DR 里,软件读 DR 就能拿到这次采样的电压对应的数字值。

5. PWM 输出(脉宽调制)→ 对应 TIMx_CCR1 / TIMx_ARR

  PWM 是用来输出“高低电平占空比可调”的脉冲信号(比如控制电机转速、LED 亮度),核心也是寄存器配合:
  - TIMx_ARR (自动重装载寄存器)
  相当于“PWM 的「周期长度」”——写这个值,决定 PWM 一个周期有多长(比如 ARR=1000,就是 1000 个计数周期为一个 PWM 周期)。
  - TIMx_CCR1 (捕获/比较寄存器 1)
  相当于“PWM 的「占空比调节」”——写这个值,决定在一个 ARR 周期内,高电平持续多久(比如 CCR1=500,ARR=1000,那占空比就是 50%)。
  - TIMx_CCER (捕获/比较使能寄存器)
  相当于“PWM 的「通道开关」”——写这个寄存器的位,能打开/关闭某个 PWM 通道(比如只让 CH1 输出 PWM,CH2 不输出)。

6. 复位与时钟 → 对应 RCC_CR / RCC_CFGR 等

  芯片要正常工作,得先“上电复位”“配置时钟频率”,这些都靠 RCC(Reset and Clock Control)寄存器:
  - RCC_CR (时钟控制寄存器)
  相当于“芯片的「电源开关+时钟源选择」”——写这个寄存器,能启动/关闭 HSE(外部高速晶振)、HSI(内部高速 RC 振荡器)等时钟源,还能看时钟有没有稳定(比如 PLLRDY=1 表示锁相环已经稳定,可以用 PLL 时钟)。
  - RCC_CFGR (时钟配置寄存器)
  相当于“芯片的「时钟分频器+外设总线选择」”——写这个寄存器,能把高速时钟(比如 72MHz)分频后,分配给 APB1、APB2 等不同外设总线(比如让 GPIO 总线跑 36MHz,让定时器总线跑 72MHz),还能选择系统时钟是用 HSE 还是 PLL 倍频后的时钟。

三、从软件层面理解地址与封装

  这里仅仅只是我们从硬件层面上理解一块芯片如何让一堆元器件完成对应的功能的,接着为了解开这个疑惑。我打开了官方提供的头文件哪些资料。终于恍然大悟先看三张图(图可能不对但是原理是一样的。工作时间找不到 对应的头文件)

地址值!(这个很关键)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 这里的地址值与官方提供的数据手册 一一对应。
宏定义 地址值 说明
CAN_MCR_INRQ ((uint16_t)0x0001) 初始化请求
CAN_MCR_SLEEP ((uint16_t)0x0002) 睡眠模式请求
CAN_MCR_TXFP ((uint16_t)0x0004) 发送 FIFO 优先级
CAN_MCR_RFLM ((uint16_t)0x0008) 接收 FIFO 锁定模式
CAN_MCR_NART ((uint16_t)0x0010) 禁止自动重传
CAN_MCR_AWUM ((uint16_t)0x0020) 自动唤醒模式
CAN_MCR_ABOM ((uint16_t)0x0040) 自动总线关闭管理
CAN_MCR_TTCM ((uint16_t)0x0080) 时间触发通信模式
CAN_MCR_RESET ((uint16_t)0x8000) CAN 软件主复位

我们重点关注“地址值”这一列,再结合数据手册

很好!在这里我们知道这个是 CAN_MCR 寄存各个位的说明,那这和这个库函数的封装又有什么关系呢?接下来我为了来解决这个问题让我冥思苦想很多天,终于那天看到;那就结合“位”逻辑来分析这些问题,于是我就有了以下的分析;每个位是只有“0”和“1”两个状态我们把每个位从 31 位到 0 位编译成对应的二进制码(用“-”分开)对应的就是“0000-0000-0000-0000(B)”把对应的二进制码翻译成十六进制码就是“0000(ox)”那和地址值有什么关系呢?别急嘛!现在就来告诉你们!

四、地址值与寄存器位使能的关系

现在就说假设我们要把“INRQ”这一位启动该怎么办呢?很简单就是写入“0000-0000-0000-0001(B)”就行了,翻译成十六进制你就会有新的重大发现“0001(ox)”欸?那不就和库函数封装的是一样的吗?那对这一位写“1 是做什么的呢?”根据相应的介绍就是“初始化请求”对应的我想实现“禁止自动重传”这个功能怎么办?参照《STM32F1XX 中文参考手册》可知,我们只需要把“位 4”写“1”就行;对应二进制就是“0000-0000-0001-0000”,翻译成十六进制就是“0010(ox)”在这里就不惊讶的发现。他对应着就是库文件里的“CAN_MCR_NART”那请问屏幕前的你要是让你去手敲“0000-0000-0001-0000”这些,你作何感想。

五、地址值的封装

于是乎,芯片原厂就为了方便编译就把这些地址值封装成了我们对应的库,追根溯源我真的带你们去找了对应的封装语句
就是下面的

typedef struct
{
  uint16_t CAN_Prescaler;          // CN: 指定时间量子的长度 -- EN: Specifies the length of a time quantum
  uint8_t CAN_Mode;               // CN: CAN 操作模式 -- EN: CAN operating mode
  uint8_t CAN_SJW;               // CN: 同步跳转宽度 -- EN: Synchronization Jump Width
  uint8_t CAN_BS1;                // CN: 位段1的时间量子数 -- EN: Number of time quanta in Bit Segment 1
  uint8_t CAN_BS2;                // CN: 位段2的时间量子数 -- EN: Number of time quanta in Bit Segment 2
  FunctionalState CAN_TTCM;       // CN: 时间触发通信模式 -- EN: Time Triggered Communication Mode
  FunctionalState CAN_ABOM;       // CN: 自动总线关闭管理 -- EN: Automatic Bus-Off Management
  FunctionalState CAN_AWUM;       // CN: 自动唤醒模式 -- EN: Automatic Wake-up Mode
  FunctionalState CAN_NART;       // CN: 禁止自动重传模式 -- EN: No Automatic Retransmission
  FunctionalState CAN_RFLM;       // CN: 接收FIFO锁定模式 -- EN: Receive FIFO Locked Mode
  FunctionalState CAN_TXFP;       // CN: 发送FIFO优先级 -- EN: Transmit FIFO Priority
} CAN_InitTypeDef;

他们有把“1”和“0”封装成对应的“ENABLE” 和 “DISABLE”也就是封装成下面这样

大家也不难发现这些语句都是封装。那么这句“CAN_InitStruct->CAN_NART”这句话只不是就可直接理解为通过 C语言关键字“struct”定义一个结构体,然后为了方便引用,再用 C 语言关键字“typedef”将这个结构体重命名为“CAN_InitTypeDef”再接着定义一个名字为“CAN_Init”的函数,再接着在这个函数内定义一个结构体指针用来给寄存器直接赋值。因为是结构体指针封装我们就要用操作符“->”来进行解引用。也就是这个操作

六、CAN 初始化函数–“CAN_Init”的解析

再接着看这个“CAN_Init”中发现了什么?

1. 首先看这个语句

第 156 行定义的是定义初始化状态,跳转对应的宏定义得到下面的图片;这里定义了两个地址值 0X 000X 01 将其转化位二进制可以知道
0X 00 = 0000-0000(B)
0X 01 = 0000-0001(B)

#define CAN_InitStatus_Failed  ((uint8_t)0x00) /*!< CAN初始化失败 -- EN: CAN initialization failed */
#define CAN_InitStatus_Success ((uint8_t)0x01) /*!< CAN初始化成功 -- EN: CAN initialization OK   */

在对应寄存器可知(如下)
这里是对“INRQ”进行了置 “0”操作也就是初始化,但他写的却是“CANinitialization failed”让人很疑惑!(这是官方的事情我们不必深究!)
翻开对应说明得到了印证。

2. 接着就看这一句

这是用来检查语法错误,翻阅相关资料得知这里用到了“assert_param()”

/* 检查参数 -- EN: Check the parameters */
assert_param(IS_CAN_ALL_PERIPH(CANx));
assert_param(IS_FUNCTIONAL_STATE(CAN_InitStruct->CAN_TTCM));
assert_param(IS_FUNCTIONAL_STATE(CAN_InitStruct->CAN_ABOM));
assert_param(IS_FUNCTIONAL_STATE(CAN_InitStruct->CAN_AWUM));
assert_param(IS_FUNCTIONAL_STATE(CAN_InitStruct->CAN_NART));
assert_param(IS_FUNCTIONAL_STATE(CAN_InitStruct->CAN_RFLM));
assert_param(IS_FUNCTIONAL_STATE(CAN_InitStruct->CAN_TXFP));
assert_param(IS_CAN_MODE(CAN_InitStruct->CAN_Mode));
assert_param(IS_CAN_SJW(CAN_InitStruct->CAN_SJW));
assert_param(IS_CAN_BS1(CAN_InitStruct->CAN_BS1));
assert_param(IS_CAN_BS2(CAN_InitStruct->CAN_BS2));
assert_param(IS_CAN_PRESCALER(CAN_InitStruct->CAN_Prescaler));

这个函数(具体怎么实现的不必深究,只需要知道这个实在检查语法问题!)

3. 再接着

这里说的是退出睡眠模式和请求初始化模式,那么分析一下其背后的逻辑

/* 退出睡眠模式 -- EN: Exit from sleep mode */
CANx->MCR &= (~(uint32_t)CAN_MCR_SLEEP);

/* 请求初始化 -- EN: Request initialisation */
CANx->MCR |= CAN_MCR_INRQ;

3.1 先查看“CAN_MCR_SLEEP”的宏定义

#define CAN_MCR_SLEEP    ((uint16_t)0x0002) /*!< 睡眠模式请求 -- EN: Sleep Mode Request */

依旧把 0X0002 翻译成对应的二进制(0000-0000-0000-0010(B))
对应寄存器为对“SLEEP”进行置“1”操作
那对“SLEEP”进行置“1”会发生什么呢?
我们看手册对应的介绍
答案:就是进入睡眠操作

那怎么解除呢?
很简单就是对这一位置“0”就可以了。
那怎么置零呢?
就要看这一个语句了!

3.2 解除睡眠模式的方法

  • 原始语句
      CAN_MCR_SLEEP = ((uint16_t) 0x0002) = (0000-0000-0000-0010)
  • 取反(~)为
      ~(0000-0000-0000-0010) = (1111-1111-1111-1101)
  • 简写
      CANx->MCR &= 1111-1111-1111-1101
  • 完全等于
      CANx->MCR = CANx->MCR & 1111-1111-1111-1101
  • 完全等于
      CANx->MCR = 0000-0000-0000-0010 & 1111-1111-1111-1101
  • 等于
      CANx->MCR = 0000-0000-0000-0000

就完成了对“SLEEP”进行置“0”的操作

3.3 初始化的方法

接下来再看,这里说的是请求初始化

再对应相应的宏定义
依旧把 0X 0001 翻译成二进制(0000-0000-0000-0001(B)),就是对寄存器第“0”位进行写“1”操作,参照寄存器说明可知
这正是初始化请求

计算步骤

  • 原始语句
      CANx->MCR |= 0000-0000-0000-0001
  • 完全等于
      CANx->MCR = CANx->MCR | 0000-0000-0000-0001
  • 完全等于
      CANx->MCR = 0000-0000-0000-0001 | 0000-0000-0000-0001
  • 完全等于
      CANx->MCR = 0000-0000-0000-0001

对应寄存器位第 0 位写 1,也就是“INRQ”写“1”
参照表格就是初始化(CAN)

接下来看这句话!

/* 等待应答 -- EN: Wait the acknowledge */
while ((((CANx->MSR & CAN_MSR_INAK) != CAN_MSR_INAK) && (wait_ack != INAK_TIMEOUT)))
{
    wait_ack++;
}

这句话的核心是什么?
访问了一个状态寄存器,通俗的说也就是一个能说明一个当前处于什么状态的寄存器。也就是说这个有没有初始化成功,不成功就一直在循环里等待并进入循环让“wait_ack”进行“++”操作,这个数字的目的就是为了以防止一直循环下去而设置的一个超时机制
具体是怎么知道在这个状态这里简述的就是这里有个状态寄存器,而且访问了对应的位

4. 判断是否初始化是否成功

接下来再看这一句话

/* 检查应答 -- EN: Check acknowledge */
if ((CANx->MSR & CAN_MSR_INAK) != CAN_MSR_INAK)
{
    InitStatus = CAN_InitStatus_Failed;
}

这里说的是,如果最终硬件没有置位 “INAK”(初始化模式确认位),说明进入初始化模式失败,初始化状态设为失败。而且将“InitStatus”进行置零操作。反之如果成功就进入循环体内,也就是这样

else
{
    /* 设置时间触发通信模式 -- EN: Set the time triggered communication mode */
    if (CAN_InitStruct->CAN_TTCM == ENABLE)
    {
        CANx->MCR |= CAN_MCR_TTCM;
    }
    else
    {
        CANx->MCR &= ~(uint32_t)CAN_MCR_TTCM;
    }

    /* 设置自动总线关闭管理 -- EN: Set the automatic bus-off management */
    if (CAN_InitStruct->CAN_ABOM == ENABLE)
    {
        CANx->MCR |= CAN_MCR_ABOM;
    }
    else
    {
        CANx->MCR &= ~(uint32_t)CAN_MCR_ABOM;
    }

    /* 设置自动唤醒模式 -- EN: Set the automatic wake-up mode */
    if (CAN_InitStruct->CAN_AWUM == ENABLE)
    {
        CANx->MCR |= CAN_MCR_AWUM;
    }
    else
    {
        CANx->MCR &= ~(uint32_t)CAN_MCR_AWUM;
    }

    /* 设置禁止自动重传 -- EN: Set the no automatic retransmission */
    if (CAN_InitStruct->CAN_NART == ENABLE)
    {
        CANx->MCR |= CAN_MCR_NART;
    }
    else
    {
        CANx->MCR &= ~(uint32_t)CAN_MCR_NART;
    }
}

那我随便拿出进行分析吧
这里说的是用 if 判断结构体“CAN_InitStruct”下的“CAN_NART”位是否写“1”(“1”被宏定义封装成“ENABLE”)如果是我们就把他赋值为 0x0010 对应二进制码为(0000-0000-0001-0000)就是第 4位写 1对应寄存器就是 “NART” 位写“1”

#define CAN_MCR_NART    ((uint16_t)0x0010) /*!< 禁止自动重传 -- EN: No Automatic Retransmission */

再参考《STM32F1XX 中文参考手册》其对应的就是
使能“禁止报文自动重传”功能

很好那要是失败了呢?就来到接下来的话了
依旧参照宏定义进行逻辑分析

  • 原式子
      CANx->MCR &= ~(uint32_t)CAN_MCR_AWUM = ~(0X 0020) = ~(0000-0000-0010-0000)
  • 进行取反操作
      CANx->MCR &= ~(uint32_t)CAN_MCR_AWUM = (1111-1111-1101-0000)
  • 然后
      CANx->MCR &= (1111-1111-1101-0000)
      CANx->MCR = CANx->MCR & (1111-1111-1101-0000) = (0000-0000-0010-0000) & (1111-1111-1101-0000)
      = (0000-0000-0000-0000)

就完成了对“禁止报文自动重传”完成了失能操作。

所以懂了吗?
就此我们已经完全理解了我们这些代码具体做了什么,简言之就是把他翻译成对应的地址值。
为了验证我这个猜想我想了很多办法。
终于再看到嵌入式讲师的杨 的那一刻时,有了很多启发。

七、对“.hex”文件的解析

于是乎我终于打开了这个文件,那一刻我欣喜若狂。我的答案得到了应证

:020000040800F2
:10000000D804002049010008E5050008FF030008A6
:10001000E105000891010008330B00080000000012
...
:10015000FEE7FEE7FEE7FEE7FEE7FEE7FEE7FEE777

很好!就是这样。
现在我直接给出
结论:“.hex”文件就是一个储存各个寄存器地址值的文件。这里大概的格式应该为

  • 包括了程序代码(函数)、只读数据(如常量字符串)、初始化的全局变量等;
  • 每一行都包含:地址信息、数据内容、校验和等元信息。

大概翻译为
Intel HEX 文件的每一行都以冒号(:)开头,后面跟着一系列的十六进制数字,代表不同的字段。每一行的结构如下:

  • 起始码(1 字节) :总是冒号(:)。
  • 字节数(1 字节) :表示该行数据字段的字节数。
  • 地址(2 字节) :表示该行数据在内存中的起始地址。
  • 记录类型(1 字节) :表示该行的类型,常见的有:
      - 00:数据记录。
      - 01:文件结束记录。
      - 02:扩展段地址记录。
      - 04:扩展线性地址记录。
  • 数据(可变长度) :实际的数据内容。
  • 校验和(1 字节) :用于验证该行数据的完整性。

八、浅谈寄存器寻址的方式

接下来我们就来浅浅谈一下再这些寄存器背后具体是怎么寻址的呢?这里大概就是一个树状结构机型寻址比如现在给定一串二进制码

0010-0100-0100-0010 这是一个两字节数据
由于是高位先发则
0010-0100-0100-0010 则应该是选择“时钟线 2”
接着再继续 0010-0100-0100-0010 则应该是选择“NVIC”
接着再继续 0010-0100-0100-0010 则应该是选择“NVIC_1”
接着再继续 0010-0100-0100-0010 则应该是选择“检测位!”

所以 0010-0100-0100-0010 的作用是开启挂在总线下的“时钟线 2”下的“NVIC_2”的检测功能。

九、寄存器方式启动 GPIOA 总线下的 Pin_0

很好!既然如此我们现在魔改杨 的这串代码!

1. 源代码

点亮一颗 LED

#include "stm32f10x.h"
#include "Delay.h"

int main(void)
{
    // CN: 开启GPIOA的时钟 -- EN: Enable GPIOA clock
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    // CN: GPIO初始化 -- EN: GPIO initialization
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;           // CN: GPIO引脚,赋值为第0号引脚 -- EN: GPIO Pin, assigned to Pin 0
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;    // CN: GPIO速度,赋值为50MHz -- EN: GPIO Speed, assigned to 50MHz
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;     // CN: GPIO模式,赋值为推挽输出模式 -- EN: GPIO Mode, assigned to Push-Pull output

    // CN: 将赋值后的结构体变量传递给GPIO_Init函数 -- EN: Pass the assigned structure variable to GPIO_Init function
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // CN: 方法1:GPIO_ResetBits设置低电平,GPIO_SetBits设置高电平 -- EN: Method 1: GPIO_ResetBits to set low, GPIO_SetBits to set high
    // CN: 将PA0引脚设置为低电平 -- EN: Set PA0 pin to low level
    GPIO_ResetBits(GPIOA, GPIO_Pin_0);
}

2. 源代码寄存器启动的代码

魔改成直接用寄存器配置!

#include "stm32f10x.h"

int main(void)
{
    // CN: 使能复位和时钟控制(RCC) -- EN: Enable Reset and Clock Control (RCC)
    // CN: 查手册基地址为0x40021000,APB2寄存器使能寄存器偏移地址为0x18,故得到APB2寄存器使能寄存器基地址为0x40021018 -- EN: Base address from manual is 0x40021000, APB2 peripheral clock enable register offset is 0x18, so the address is 0x40021018
    volatile unsigned int *a = (unsigned int *)0x40021018;

    // CN: 使能GPIO端口A。查手册基地址为0x40010800。端口配置低寄存器查询对应偏移地址为0x00,故得到GPIO端口A寄存器端口配置低寄存器基地址为0x40010800 -- EN: Enable GPIO Port A. Base address is 0x40010800. Port configuration low register offset is 0x00, so the address is 0x40010800
    volatile unsigned int *b = (unsigned int *)0x40010800;

    // CN: 使能GPIO端口A,查手册基地址为0x40010800,端口设置/清除寄存器偏移地址为0x10,故得到端口位设置/清除寄存器基地址为0x40010810 -- EN: Enable GPIO Port A. Base address is 0x40010800, port bit set/reset register offset is 0x10, so the address is 0x40010810
    volatile unsigned int *c = (unsigned int *)0x40010810;

    // CN: 使能复位和时钟控制(APB2) -- EN: Enable Reset and Clock Control (APB2)
    *a |= (1 << 2);           // CN: 位2是GPIOA时钟使能 -- EN: Bit 2 is GPIOA clock enable

    // CN: 配置A0引脚为推挽输出,速度50MHz -- EN: Configure A0 pin as push-pull output, speed 50MHz
    *b &= ~(0xF << 0);        // CN: 清零CRL寄存器前4位 -- EN: Clear the first 4 bits of CRL register
    *b |= (0x3 << 0);         // CN: 设置为推挽输出模式,速度50MHz -- EN: Set as push-pull output mode, speed 50MHz

    while(1)
    {
        // CN: 使能端口为电平低,点亮LED -- EN: Set port low to light up LED
        *c = (1 << 16);       // CN: 复位位 -- EN: Reset bit
    }
}

3. 现象如下

(现象图片描述:LED被成功点亮)

既然疯狂!!!那就彻底疯狂!!!手撕总线地址!!!

十、总线方式启动 GPIOA 总线下的 Pin_0

代码如下

  • 步骤 1:查询《STM32F1XX 中文参考手册》 RCC 总线基地址

现在我们已经找到对应的寄存器,又知道单片机内部是靠时钟协同的,那我们就要启动时钟。那么我们该怎么使能这个寄存器呢?让时钟跑起来呢?

那我们得大概了解一下芯片内部结构吧!就比如你要去火车站得知道这个城市大概的分布啥的。
比如:奶茶店在哪里啊?哪里可以看妹子啊?哪里可以打车啥的… 那我们就查找《STM32F103XX 中文参考手册》(因为英文的我看不懂)
(还怪尴尬的…)

那换句话说,你找妹子出去玩都知道看地图看导航。比方说吃的大概怎么走,喝的在哪里,玩的在哪里!
那你搞开发咋就不知道去了解一下大概的架构嘞,搞开发和妹子出去玩一样。我们先选定一个引脚(比如说:我一眼就相中了 PA0),那我,就知道他是挂载在GPIOA 总线下的第 0 个位上(就像你和妹子要去的那个奶茶店,在那个街道上一样)。

那我查找《<STM32F103XX 中文参考手册>–储存器和总线架构》这一章知道了
GPIOA 是挂在 APB2 这条时钟线上的呀,然后查询这个时钟总线的地址,查找《STM32F103XX 中文参考手册》可知

基地址为 0x4002 1000,再找到总线上对应的寄存器查询《STM32F1XX 中文参考手册》得偏移地址为 0x18
相加为:0x 4002 1000 + 0x18 = 0x 4002 1018

接着我就犯了一个错误

  • 步骤 2:直接把这个地址写进“main”里

程序崩了(嘿嘿!这还挺尴尬的…)
于是乎我用杨 教我查询报错的方法

  • 步骤 3:查询报错

这里说的是这个表示在 main.c 文件的第 68 行,存在一个表达式,但该表达式没有任何实际效果。所以我就查找自己的知识库。
到这里请问:C 语言能操作地址的工具是什么?
答案:指针

很好!就用指针!

于是就有了下面的操作

  • 步骤 4:定义指针
volatile unsigned int *a = (unsigned int *)0x40021018;
  • 步骤 5:使能 APB2 时钟线

查找手册可知要想使能 APB2 就需要对寄存器某一位进行写“1”操作
先找到对应的寄存器–即 APB2 外设时钟使能寄存器的介绍
也就是也就是说我们只需要对寄存器,这位第二位写“1”就可了
换句话就是说把这个“1”移到位 2 或者说左移 2 位就可以了(从 0 位开始)甚至直接写一个 01000x4 都可以

  • 最后得到
      *a |= (1 << 2);
      或
      *a |= 0x4;

为什么在硬件寄存器访问中需要 volatile
volatile unsigned int *a = (unsigned int *)0x40021018;

这里使用 volatile 的原因包括:

寄存器值可能被硬件改变:某些寄存器位可能由硬件外设自动修改,

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐