资讯动态

野火霸天虎 STM32F407初步认识

发布时间:2026/9/24 7:46:34 来源:尧图企业网站定制
刚开始学习单片机时我使用的是野火霸天虎 STM32F407 开发板做的第一个实验是通过寄存器操作点亮 LED。代码并不长但真正动手后我发现自己对很多概念还没有理解清楚芯片和 CPU 是什么关系往一个地址写入数据为什么就能改变引脚电平开漏输出“写 1”为什么不一定得到高电平I²C 主机发完数据后从机又是怎样在同一根线上回复应答的这篇笔记就从这些问题出发把硬件结构、寄存器操作和通信时序串起来最后再回到一个经常被忽略的问题单片机上电后是怎样一步步运行到main()的本文以 STM32F407ZGT6 为例。LED 部分使用开发板上连接 PF6 的红灯低电平点亮启动流程以文中使用的 Keil/Arm 启动文件为例。1. 先分清芯片、CPU 和程序学习 STM32 时我曾把“芯片”“CPU”和“程序”混在一起理解。实际上它们分别对应不同的概念。STM32F407 微控制器 ├─ Cortex-M4 内核执行指令 ├─ Flash通常保存程序和常量 ├─ SRAM保存运行时数据 ├─ 总线与总线矩阵连接内核、存储器和外设 └─ GPIO、USART、I²C、SPI、定时器、ADC 等片内外设STM32F407 是整颗微控制器芯片Cortex-M4 是其中执行指令的 CPU 内核固件则是让这些硬件按要求工作的程序。开发板出厂时用户 Flash 中可能已经烧录了演示程序。芯片的系统存储器中还保存着 ST 预置的引导程序用于支持特定的程序下载方式。这些都是程序不能与 CPU 内核混为一谈。以点灯为例我编写的main.c只是源代码。工具链需要对它进行编译、链接生成机器指令和相关数据再通过下载工具写入片内 Flash。CPU 执行这些机器指令才会完成时钟使能、GPIO 配置和引脚电平控制。这里还要区分两种“寄存器”内核寄存器例如R0供 CPU 执行指令、保存中间数据等使用。外设寄存器例如 GPIO 的MODER、ODR用于配置外设或读取外设状态通常通过映射到的地址访问。程序中的一次计算可以只在内核寄存器中完成。只有当程序访问相应外设寄存器时才可能进一步影响 GPIO 等硬件。并不是每条指令的执行结果都会输出到芯片引脚。2. CPU 怎样访问 Flash、RAM 和外设2.1 地址决定访问对象总线负责传递访问请求CPU 执行程序时需要读取指令也需要读写数据。访问 Flash、SRAM 或 GPIO 等外设时它会通过相应接口和总线发起访问。在 Cortex-M4 的结构图中经常能看到 I-Code、D-Code 和 System 接口。可以先这样理解接口典型用途I-Code从代码区域读取指令例如执行 Flash 中的程序D-Code访问代码区域中的数据例如读取 Flash 中的常量System访问 SRAM、外设等区域也可以从普通 SRAM 取指这不是“指令只走 I、数据只走 D”的绝对分工。具体使用哪个接口与访问类型和地址区域有关。程序通常只需要给出访问地址无须手动选择走哪条总线。2.2 地址空间不等于实际存储容量32 位地址可以表示 4 GB 的字节地址空间但这不意味着 STM32F407 内部有 4 GB 存储器。芯片只在其中一部分地址上实现了存储器和外设。因此教材中的“512 MB Block”表示地址分区不能直接理解为芯片实际配备了 512 MB 内存。STM32F407 的 SRAM 也不是一整块连续、用途完全相同的存储器存储区域容量起始地址SRAM1112 KB0x20000000SRAM216 KB0x2001C000CCM RAM64 KB0x10000000这三部分合计 192 KB此外还有独立的 4 KB 备份 SRAM。普通 SRAM1、SRAM2 与 CCM RAM 的访问能力有所不同DMA 不能访问这里的 CCM RAM因此 DMA 缓冲区应放在 DMA 可访问的普通 SRAM 中。具体结构可查阅 ST 的 RM0090 参考手册。2.3 地址映射一个地址对应哪块硬件地址映射可以理解为芯片规定某个地址范围对应哪一块存储器或外设。以启动时的0x00000000为例STM32F407 可以根据启动配置把不同存储器映射到这个区域BOOT0BOOT1启动存储器0任意主 Flash10系统存储器11嵌入式 SRAM从主 Flash 启动时同一份 Flash 内容既可以通过0x08000000开始的地址访问也可以通过0x00000000开始的启动映射区域访问。这里发生的是地址映射变化不是把 Flash 中的程序复制了一份到另一个存储器里。3. 寄存器地址、指针和位操作有什么联系3.1 找到地址才能访问寄存器STM32 的外设采用内存映射方式。对程序来说可以通过指定地址访问外设寄存器。一个寄存器的地址通常可以这样计算寄存器地址 外设基地址 寄存器偏移 GPIOF_BSRR 地址 0x40021400 0x18 0x40021418用 C 语言访问它时可以把这个地址转换成指针再解引用#include stdint.h #define GPIOF_BSRR (*(volatile uint32_t *)0x40021418U) // 前提已开启 GPIOF 时钟并将 PF6 配置为推挽输出。 GPIOF_BSRR 1U 22; // 将 PF6 对应的输出位清零拉低 PF6这条宏定义可以拆开理解0x40021418U寄存器地址。(volatile uint32_t *)把地址转换为指向 32 位对象的指针并告知编译器这里的访问具有硬件意义。最外层的*访问这个地址对应的寄存器。GPIOF_BSRR给这段表达式起一个便于阅读的名字。宏不会创建寄存器寄存器本来就存在于硬件中。volatile用于保留必要的硬件读写但不意味着读写操作自动具备原子性。设备头文件则把这些地址和结构预先定义好让我们可以用更易读的名字访问寄存器。例如某些版本的头文件使用GPIOF-BSRR较旧的标准库头文件可能拆成BSRRL和BSRRH。写代码时应以实际使用的头文件为准。3.2 位操作修改寄存器中的指定位置寄存器中的不同位通常负责不同功能因此不能随意覆盖整个寄存器。最常见的两种操作是reg | mask; // 将 mask 对应的位设置为 1 reg ~mask; // 将 mask 对应的位清为 0对于占用多位的配置字段通常需要先清除旧值再写入新值。例如MODER中每个引脚占两位。PF6 对应第 13、12 位配置为普通输出时需要将这两位写成01// 前提已开启 GPIOF 时钟且设备头文件已正确包含。 GPIOF-MODER ~(3U 12); // 清除第 13、12 位 GPIOF-MODER | (1U 12); // 写入 01普通输出模式位编号从 0 开始所以 PF6 的配置字段从第6 × 2 12位开始。3.3 为什么 BSRR 写第 22 位却能控制 PF6BSRR是按位设置、复位寄存器向低 16 位中的某一位写 1会将ODR对应位置 1。向高 16 位中的某一位写 1会将ODR对应位清 0。写 0 的位置不执行相应操作。因此PF6 对应的设置位是第 6 位复位位是第6 16 22位GPIOF_BSRR 1U 6; // ODR 第 6 位置 1 GPIOF_BSRR 1U 22; // ODR 第 6 位清 0BSRR 中写入的 1 表示“执行对应操作”不代表一定输出高电平。这里应直接赋值不需要使用|。BSRR 可以在一次寄存器写入中修改指定输出位避免为了修改 ODR 而执行“先读、再改、再写”。4. GPIO 的四种模式分别用来做什么GPIO 配置涉及多个寄存器可以先按功能记住它们寄存器主要作用MODER选择模式00输入、01输出、10复用、11模拟OTYPER选择输出类型0推挽、1开漏PUPDR选择内部电阻00无上下拉、01上拉、10下拉OSPEEDR配置输出速度等级影响输出边沿不是设置自动翻转频率IDR在数字输入通路有效时读取引脚实际逻辑电平ODR保存 GPIO 输出数据BSRR按位设置或清除 ODRAFR在复用模式下选择引脚对应的外设功能4.1 输入模式读取外部电平例如按键一端接引脚另一端接地引脚配置为输入上拉松开按键时上拉使引脚保持高电平读到 1。按下按键时引脚接地读到 0。“浮空输入”表示内部上拉和下拉都关闭。如果外部电路也没有确定电平引脚读数就可能不稳定。4.2 输出模式由程序控制引脚普通输出模式下程序通过ODR或BSRR控制输出状态再由输出电路作用到引脚上。点亮 LED 就是典型用途。需要注意输出模式和输出类型是两个不同的配置项。选择普通输出后还需要根据用途选择推挽或开漏。4.3 复用模式由片内外设控制引脚一个引脚可能既能作为普通 GPIO也能作为 USART、SPI、I²C 等外设的信号引脚。以串口发送为例配置好复用功能并初始化 USART 后发送波形由 USART 外设控制。CPU 通常只需要按驱动流程提交数据无须逐位翻转 GPIO。4.4 模拟模式连接模拟功能测量电压时需要选择支持 ADC 的引脚将其配置为模拟模式并配置 ADC 外设。IDR读取的是数字逻辑状态不能直接给出“当前电压为多少伏”。模拟模式下相应的数字输入通路会被关闭不能套用普通数字输入的读取方式。另外上拉和下拉都是弱电阻不等于主动驱动引脚输出高、低电平。这一区别在理解开漏输出时尤其关键。5. 推挽与开漏写入的值怎样变成引脚电平为了看懂输出电路先约定图中的两个名称IN输出电路收到的内部控制信号。在普通 GPIO 输出模式下可以对应理解为 ODR 中的输出位。OUT芯片外部的 GPIO 引脚例如 PF6。IN 不是另一根外部引脚。下面的图只是输出级功能示意省略了保护电路和输入检测电路箭头指向 MOS 管时表示控制关系。5.1 推挽能主动拉高也能主动拉低VDD例如 3.3 V │ ┌──────► [P-MOS] │ │ IN ──► 驱动控制 ─┤ ●──── OUT │ │ └──────► [N-MOS] │ GND在正常稳定状态下两个输出管一个接通另一个断开IN上方 P-MOS下方 N-MOSOUT1接通断开主动拉高0断开接通主动拉低例如外接 LED 按下面的方式连接PA5 → 限流电阻 → LED → GND 阳极朝向 PA5PA5 输出高电平时电流经过上方输出管、限流电阻和 LED 流向地LED 点亮PA5 输出低电平时LED 熄灭。这是一个高电平点亮的接法与本文开发板上的 PF6 红灯接法不同。5.2 开漏只能主动拉低写 1 表示放开外部上拉电源例如 3.3 V │ 上拉电阻 │ ●──── OUT │ IN ──► 驱动控制 ───────────► [N-MOS] │ GND开漏输出不使用上方 P-MOS 主动拉高引脚INN-MOSOUT0接通主动拉低1断开放开引脚由上拉和外部电路决定电平这里的“放开”只表示自己的下拉管断开。引脚仍然接在外部电路上数字输入通路有效时也仍然可以读取电平。因此开漏输出写 1 后可能出现三种情况有上拉而且没有设备拉低引脚为高电平。有其他设备拉低引脚仍为低电平。没有上拉也没有其他电路确定电平引脚可能悬空。这就是为什么开漏模式下ODR 为 1IDR 却可能读到 0。5.3 灯亮不亮还要看接线开漏同样可以用来点灯。例如3.3 V → 限流电阻 → LED → OUT 阳极朝向电源OUT 拉低时电流流向地LED 点亮OUT 放开时LED 熄灭。所以“写 1 灯亮”或“写 0 灯亮”并不是 GPIO 的固定规律LED 的点亮电平由实际接线决定。本文开发板的 PF6 红灯是低电平点亮接线依据见野火的 LED 实验说明。6. 把前面的知识用起来通过寄存器点亮红灯点亮 PF6 红灯需要完成三个基本步骤使能 GPIOF 时钟 ↓ 将 PF6 配置为推挽输出 ↓ 将 PF6 拉低点亮红灯下面给出直接访问寄存器地址的main.c示例。它需要放进已经配置好 STM32F407 启动文件、系统文件和链接设置的工程中地址宏使用自定义名称便于与设备头文件区分。#include stdint.h #define REG32(addr) (*(volatile uint32_t *)(addr)) #define LED_RCC_AHB1ENR REG32(0x40023830U) #define LED_GPIOF_MODER REG32(0x40021400U) #define LED_GPIOF_OTYPER REG32(0x40021404U) #define LED_GPIOF_OSPEEDR REG32(0x40021408U) #define LED_GPIOF_PUPDR REG32(0x4002140CU) #define LED_GPIOF_BSRR REG32(0x40021418U) int main(void) { // 1. 使能 GPIOF 时钟AHB1ENR 的第 5 位为 GPIOFEN。 LED_RCC_AHB1ENR | (1U 5); (void)LED_RCC_AHB1ENR; // 回读留出时钟使能后的必要间隔 // 2. 先预置输出数据为高切换到输出模式时红灯先保持熄灭。 LED_GPIOF_BSRR (1U 6); // 3. 配置 PF6推挽、低速、无上下拉。 LED_GPIOF_OTYPER ~(1U 6); LED_GPIOF_OSPEEDR ~(3U 12); LED_GPIOF_PUPDR ~(3U 12); // 4. PF6 的 MODER 字段写入 01普通输出模式。 LED_GPIOF_MODER ~(3U 12); LED_GPIOF_MODER | (1U 12); // 5. 拉低 PF6点亮红灯。 LED_GPIOF_BSRR (1U 22); while (1) { } }把最后的控制语句改成LED_GPIOF_BSRR (1U 6);就会将 PF6 拉高关闭红灯。这段代码把几层关系串在了一起C 语句通过地址访问寄存器寄存器中的位控制 GPIO 电路GPIO 电路改变引脚电平外部 LED 因电流变化而亮灭。7. I²C主机发完 8 位从机怎样回复 ACK理解了开漏输出再看 I²C就容易回答一个问题多个设备接在同一组信号线上为什么还能轮流发送数据和应答电气上各设备可以拉低总线也可以放开总线协议上地址和通信时序决定哪个设备在什么时候发送数据。开漏是共享总线的电气基础设备寻址和通信规则同样不可缺少。I²C 的基本规则可参考 NXP 的 UM10204 总线规范。下面以STM32F407 为主机、传感器为从机主机发送0xA5、从机回复 ACK 为例。为便于理解先作几个约定只有一个主机从机不延长时钟起始信号、从机地址、写方向和地址应答都已经完成。这里展开的是后续一个数据字节的传输。7.1 每个设备控制自己的输出管每个设备的 SDA 引脚都有自己的开漏输出管和输入检测电路。下面用“开关”指代下拉 N-MOS开关接通把 SDA 拉低。开关断开不再拉低 SDA但仍可以读取总线电平。3.3 V │ 上拉电阻 │ 共享 SDA ─────●──────────●──────────●───── │ │ 主机 N-MOS 从机 N-MOS │ │ GND GND 主机控制自己的 N-MOS 从机控制自己的 N-MOS 主机读取共享 SDA 从机读取共享 SDA两个设备需要共地。SCL 是另一根独立信号线同样需要按接口要求配置上拉不能与 SDA 合并成一根线。主机不会通过一根额外的“控制线”去打开从机的 MOS 管。从机的 I²C 接口电路观察 SDA、SCL根据地址、数据和当前通信阶段决定何时接收、何时应答。对于常规开漏连接只要有一个设备拉低 SDA总线就是低电平所有设备都放开时总线才会被上拉电阻拉高。7.2 发送一位时SDA 和 SCL 怎样配合不要把“开关接通”误认为“发送 1”发送的数据主机 SDA 开关总线状态0接通主机将 SDA 拉低1断开接收方也放开 SDA由上拉产生高电平一个数据位的传输过程可以概括为阶段主机操作 SCL主机操作 SDA从机行为准备数据拉低 SCL根据本位数据拉低或放开 SDA放开 SDA准备接收接收数据放开 SCL使其升高保持本位数据稳定在有效时序内读取 SDA结束本位再次拉低 SCL满足保持时间后可准备下一位保留接收结果普通数据传输期间SCL 低时准备数据SCL 高时保持 SDA 稳定。START 和 STOP 是例外它们利用 SCL 高电平期间 SDA 的变化表示开始或结束。这些只是动作顺序。实际硬件或模拟程序还需要满足建立时间、保持时间、上升时间和时钟周期等要求。7.3 发送 0xA5逐位看看谁在拉低 SDAI²C 先发送最高位0xA5 1010 0101 发送顺序1 → 0 → 1 → 0 → 0 → 1 → 0 → 1下表描述各个时钟高电平期间的状态开关切换应在前面的低电平阶段按时序要求准备好SCL 时钟内容主机 SDA 开关从机 SDA 开关SDA 电平接收结果第 1 个bit7 1断开断开高从机读到 1第 2 个bit6 0接通断开低从机读到 0第 3 个bit5 1断开断开高从机读到 1第 4 个bit4 0接通断开低从机读到 0第 5 个bit3 0接通断开低从机读到 0第 6 个bit2 1断开断开高从机读到 1第 7 个bit1 0接通断开低从机读到 0第 8 个bit0 1断开断开高从机读到 1第 9 个ACK断开接通低主机读到 ACK前 8 位中从机一直放开自己的 SDA 输出管但输入检测电路仍在工作所以它能够接收数据。其他未参与本次通信的从机也应放开 SDA。7.4 第 8 位结束后怎样交接 SDA交接发生在第 8 个时钟结束后、第 9 个时钟升高前的低电平阶段。主机拉低 SCL结束第 8 位。此时从机已经收到了10100101。主机放开 SDA。本例最后一位是 1主机的输出管原本就断开保持断开即可若最后一位为 0就需要在应答阶段放开。从机准备 ACK拉低 SDA。从机保持这个低电平覆盖第 9 个时钟的高电平阶段。主机产生第 9 个时钟并读取 SDA。主机读到低电平就识别为 ACK。应答电平由从机给出但第 9 个时钟仍由主机产生。主机再次拉低 SCL结束应答位。从机按时序要求释放 SDA随后进入下一字节传输或结束操作。其中主机释放 SDA 和从机准备 ACK 都需要在应答时钟到来前完成不应把上面的分步描述理解成双方还存在额外的逐步握手信号。若第 9 个时钟期间从机没有拉低 SDA双方都放开总线被上拉为高主机就读到NACK。只有应答时隙里的低电平才表示 ACK数据位中的 0 不是 ACK。这里讨论的是“主机写、从机收”。如果主机从传感器读取数据则前 8 位由从机发送第 9 位由主机决定 ACK 或 NACK主机通常通过对最后一个读取字节发送 NACK表示不再继续接收。所以更通用的理解是发送方在应答位释放 SDA由该字节的接收方决定 ACK/NACK。7.5 硬件 I²C 和模拟 I²C代码各自负责什么实现方式主机侧的逐位动作由谁完成传感器侧由谁完成硬件 I²CCPU 配置并操作 I²C 外设由外设按通信状态控制引脚传感器内部的 I²C 接口电路GPIO 模拟 I²CCPU 执行程序逐位控制 GPIO 并安排时序仍由传感器内部的 I²C 接口电路完成使用硬件 I²C 时可以这样理解分工程序 / CPU └─ 配置引脚复用开漏、初始化 I²C、按驱动流程提交地址和数据 └─ STM32 片内 I²C 外设 ├─ 控制自己的 SDA 输出管 ├─ 产生 SCL 时钟 └─ 检测 ACK/NACK更新状态 传感器内部 I²C 接口 └─ 识别地址、接收数据、根据状态决定是否应答 └─ 控制传感器自己的 SDA 输出管“外设”在这里可能指 STM32 片内的 I²C 模块也可能指板外的传感器阅读资料时需要分清。如果使用 GPIO 模拟 I²C主机侧的逐位操作就需要由程序实现。下面仅展示已配置为开漏的 SDA 引脚如何拉低、放开和读取并不是完整的通信代码。示例假定设备头文件提供 32 位BSRR成员// SDA_PIN 是引脚编号GPIOx 是对应 GPIO 端口。 GPIOx-BSRR 1U (SDA_PIN 16U); // 拉低 SDA GPIOx-BSRR 1U SDA_PIN; // 放开 SDA bit (GPIOx-IDR SDA_PIN) 1U; // 读取实际总线电平接收 ACK 前模拟 I²C 主机必须先放开 SDA再配合第 9 个时钟读取。如果主机自己一直拉低 SDA即使从机没有应答也可能误读成 ACK。硬件 I²C 的复用模式下引脚输出由 I²C 外设接管不能把上面的 GPIO 操作片段当成硬件 I²C 的发送方法。8. 最后回到起点上电后程序怎样运行到 main()前面一直在讨论main()中的代码但 CPU 上电后并不是直接从main()的第一行开始执行。以从用户 Flash 启动、使用本文 Keil/Arm 启动文件的工程为例上电或复位 ↓ 硬件读取向量表中的初始 MSP 和复位入口 ↓ Reset_Handler ↓ SystemInit() ↓ Arm C 运行库入口 __main() ↓ 用户编写的 main() ↓ 使能 GPIO 时钟、配置引脚、点亮红灯8.1 最开始的两个值由硬件读取复位后Cortex-M4 从启动映射区域中的向量表读取初始主栈指针 MSP建立初始栈位置。复位入口地址确定从哪里开始执行复位处理代码。读取这两个值是硬件完成的不需要先写一段 C 代码来实现。8.2 Reset_Handler、SystemInit 和 __main 各自做什么Reset_Handler是启动文件提供的复位处理入口。本文使用的启动文件包含如下调用关系LDR R0, SystemInit BLX R0 LDR R0, __main BX R0前两条指令把SystemInit的地址放入 R0再调用它。函数返回后后两条指令跳转到__main。SystemInit()负责工程所需的底层系统初始化具体设置取决于系统文件的实现。__main()则是这里所用 Arm 工具链的运行库入口负责准备 C 程序运行环境随后调用用户的main()。例如int count 3; int state;如果这两个变量定义在文件作用域那么在正常进入main()前count应具有初始值 3state应被初始化为 0。这些工作由启动流程和运行库根据链接结果完成。所以__main和main不是同一个函数也不是同一个名字的两种写法。8.3 启动文件中几个容易混淆的关键字写法含义IMPORT引用其他模块提供的符号EXPORT导出符号使其他模块能够引用[WEAK]表示弱定义可被同名的强定义替换上述关键字和__main入口属于本文采用的工具链写法。换用其他工具链时启动代码形式和符号名称可能不同需要结合实际工程阅读。9. 这次点灯实验我真正想弄清楚的事对我来说点亮 LED 的价值不仅在于看到灯亮而在于理解一条 C 语句如何与硬件发生联系源代码 → 编译链接 → 下载到 Flash → CPU 执行 ↓ 通过总线访问外设寄存器 ↓ GPIO 输出电路变化 ↓ 引脚电平变化 ↓ LED 亮灭沿着这条关系继续往下看推挽和开漏就不再只是两个配置选项I²C 的 ACK 也不再只是时序图上的一个低电平它们都可以对应到具体的电路状态和控制动作。以后再学习新的外设时我也会先问自己哪个寄存器控制它哪个模块负责产生信号引脚上的电平最终由谁决定

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价