STM32F4系列Reference manual学习:STM32F4xx USB OTG_HS
第一章:从零认识 USB OTG
1.1 USB 是什么?
USB(通用串行总线)就像一条"公路",让设备之间可以互相传输数据。我们日常用的 U 盘、鼠标、键盘,都是通过 USB 和电脑连接的。
电脑 (主机/Host) <---USB线---> U盘 (外设/Device)
1.2 OTG 是什么?
普通 USB 有固定角色:电脑永远是"主人"(Host),U盘永远是"仆人"(Device)。
OTG(On-The-Go)打破了这个限制,同一个设备可以根据需要动态切换角色:
手机 <---OTG线---> U盘
^
|
手机此时变成"主人"(Host),可以读取U盘
1.3 STM32F4 的 OTG_HS 控制器
STM32F4xx 内部有一个 USB OTG 高速(High-Speed)控制器,简称 OTG_HS。
它的速度等级:
| 速度名称 | 缩写 | 速度 |
|---|---|---|
| 高速 | HS | 480 Mbits/s |
| 全速 | FS | 12 Mbits/s |
| 低速 | LS | 1.5 Mbits/s |
第二章:OTG_HS 的整体结构
2.1 硬件框图理解
+--------------------------------------------------+
| STM32F4xx 芯片内部 |
| |
| +----------+ AHB总线 +------------------+ |
| | CPU |<------------>| OTG_HS核心 | |
| +----------+ | | |
| | +------------+ | |
| +----------+ | | FIFO RAM | | |
| | 内存 |<------------>| | (4KB数据区) | | |
| +----------+ DMA传输 | +------------+ | |
| +--------+---------+ |
+------------------------------------ | ----------+
|
+-----------------+------------------+
| |
+-------+------+ +---------+------+
| 内置FS PHY | | ULPI接口 |
| (全速收发器) | | (外接HS PHY) |
+-------+------+ +---------+------+
| |
USB FS接口 USB HS接口
(D+/D-) (D+/D-)
2.2 两种 PHY 接口
PHY(Physical Layer,物理层)就是负责实际电信号收发的电路。
OTG_HS 支持两种 PHY:
方式一:内置 FS PHY(全速)
- 直接在芯片内部,不需要额外元件
- 最高只能达到全速(12 Mbits/s)
- 使用引脚:OTG_HS_DP、OTG_HS_DM
方式二:外置 ULPI PHY(高速) - 需要外接一个高速 PHY 芯片(比如 USB3300)
- 可达到高速(480 Mbits/s)
- 使用 12 根 ULPI 接口引脚
ULPI接口引脚说明:
OTG_HS_ULPI_CK - 时钟信号
OTG_HS_ULPI_DIR - 方向控制(谁在说话)
OTG_HS_ULPI_STP - 停止信号
OTG_HS_ULPI_NXT - 下一个数据请求
OTG_HS_ULPI_D0~7 - 8位双向数据总线
第三章:关键概念解释
3.1 Host(主机)和 Device(设备)
想象一个餐厅:
- Host(主机) = 顾客,主动点菜(发起请求)
- Device(设备) = 厨师,按顾客要求做菜(响应请求)
USB 通信永远是 Host 主动发起的,Device 只能被动响应。
3.2 Endpoint(端点)
端点就像是设备上的"邮箱",每个端点有固定的传输类型:
设备内部:
+--------------------+
| 端点0(EP0) | <- 控制端点,双向,用于配置通信
| 端点1 IN | <- 从设备发数据给主机
| 端点1 OUT | <- 主机发数据给设备
| 端点2 IN |
| ... |
+--------------------+
OTG_HS 在设备模式下的端点配置:
| 端点类型 | 数量 | 说明 |
|---|---|---|
| 控制端点 EP0 | 1个(双向) | 必有,用于枚举和配置 |
| IN端点 | 5个 | 设备→主机方向 |
| OUT端点 | 5个 | 主机→设备方向 |
3.3 四种传输类型
| 类型 | 特点 | 使用场景 |
|---|---|---|
| Control(控制) | 可靠,有应答,低速 | USB 枚举、配置命令 |
| Bulk(批量) | 可靠,有应答,尽力而为 | U盘数据传输 |
| Interrupt(中断) | 定时传输,有保证 | 鼠标、键盘 |
| Isochronous(同步) | 实时,不保证正确性 | 音频、视频流 |
3.4 HNP 和 SRP
这两个协议是 OTG 特有的:
SRP(Session Request Protocol,会话请求协议)
- 作用:省电。当总线空闲时,Host 可以关掉 VBUS(5V电源)
- B设备需要通信时,通过 SRP 告诉 A设备"请打开电源"
SRP 流程:
B设备: "我要通信!"(数据线脉冲 + VBUS脉冲)
A设备: "好的,打开VBUS"
B设备: 连接成功
HNP(Host Negotiation Protocol,主机协商协议)
- 作用:动态交换 Host/Device 角色
- 比如:手机(B设备)和另一台手机(A设备)可以协商谁当主机
3.5 FIFO(先进先出缓冲区)
FIFO 就像一个排队系统:
数据写入 --> [数据1][数据2][数据3][数据4] --> 数据读出
最早写入的最先读出
OTG_HS 有 4KB 的 FIFO RAM,分配给不同的接收和发送缓冲:
4KB FIFO RAM 分配示意:
+------------------+
| RX FIFO | <- 接收所有端点数据(共用)
| (接收缓冲区) |
+------------------+
| TX FIFO 0 | <- EP0 发送缓冲
+------------------+
| TX FIFO 1 | <- EP1 发送缓冲
+------------------+
| TX FIFO 2 | <- EP2 发送缓冲
+------------------+
| ... |
+------------------+
第四章:寄存器详解
寄存器就是芯片内部的"控制面板",CPU 通过读写这些寄存器来控制 OTG_HS 的行为。
4.1 寄存器地址空间布局
基地址 + 偏移:
0x0000 ~ 0x03FF : 全局核心寄存器(1KB)
0x0400 ~ 0x07FF : 主机模式寄存器(1KB)
0x0800 ~ 0x0DFF : 设备模式寄存器(1.5KB)
0x0E00 ~ 0x0FFF : 电源和时钟门控寄存器
0x1000 ~ 0x1FFF : EP0/CH0 的 FIFO 访问区(4KB)
0x2000 ~ 0x2FFF : EP1/CH1 的 FIFO 访问区(4KB)
...
0x20000~0x3FFFF : 直接访问 FIFO RAM(调试用,128KB)
4.2 最重要的全局寄存器
OTG_HS_GAHBCFG(AHB配置寄存器,偏移0x008)
AHB 是芯片内部的"高速公路",这个寄存器配置 CPU 和 OTG_HS 之间的数据传输方式。
| 位 | 名称 | 说明 |
|---|---|---|
| 0 | GINT | 全局中断使能(1=允许产生中断) |
| 4:1 | HBSTLEN | AHB 突发传输长度 |
| 5 | DMAEN | DMA 使能(1=DMA模式,0=CPU轮询模式) |
| 7 | TXFELVL | TX FIFO 空中断触发时机 |
| 8 | PTXFELVL | 周期性TX FIFO 空中断触发时机 |
OTG_HS_GUSBCFG(USB配置寄存器,偏移0x00C)
| 位 | 名称 | 说明 |
|---|---|---|
| 6 | PHYSEL | PHY选择:0=外置ULPI高速PHY,1=内置全速PHY |
| 8 | SRPCAP | SRP能力使能 |
| 9 | HNPCAP | HNP能力使能 |
| 13:10 | TRDT | USB周转时间(根据AHB频率设置) |
| 29 | FHMOD | 强制主机模式 |
| 30 | FDMOD | 强制设备模式 |
TRDT 的计算:
- AHB频率在30MHz以上时,TRDT最小值为 0x90x90x9
- 这个值越大,给设备准备数据的时间越充裕
OTG_HS_GINTSTS(核心中断状态寄存器,偏移0x014)
这个寄存器是"中断报告板",某位为1表示发生了对应事件:
| 位 | 名称 | 说明 | 模式 |
|---|---|---|---|
| 0 | CMOD | 当前模式:0=设备模式,1=主机模式 | 两者 |
| 1 | MMIS | 模式不匹配中断 | 两者 |
| 3 | SOF | 帧起始信号 | 两者 |
| 4 | RXFLVL | RX FIFO 非空(有数据可读) | 两者 |
| 12 | USBRST | 检测到USB复位 | 设备 |
| 13 | ENUMDNE | 枚举完成 | 设备 |
| 18 | IEPINT | IN端点有中断 | 设备 |
| 19 | OEPINT | OUT端点有中断 | 设备 |
| 24 | HPRTINT | 主机端口中断 | 主机 |
| 25 | HCINT | 主机通道中断 | 主机 |
| 29 | DISCINT | 检测到设备断开 | 主机 |
| 31 | WKUINT | 检测到唤醒/远程唤醒 | 两者 |
第五章:主机模式详解
5.1 主机模式的工作流程
5.2 Host Channel(主机通道)
主机模式下,每次和某个端点通信都要用一个"通道"(Channel)。
OTG_HS 有 12个主机通道(CH0~CH11),但同时最多只能有 8个请求 在等待队列里。
通道就像电话线:
主机
|--- 通道0 ---> 设备EP1(传U盘数据)
|--- 通道1 ---> 设备EP2(传音频)
|--- 通道2 ---> 设备EP0(控制命令)
...
5.3 主机通道相关寄存器
每个通道 x(x=0~11)都有一组寄存器:
OTG_HS_HCCHARx(通道特性寄存器),地址 = 0x500 + 0x20*x
| 位 | 名称 | 说明 |
|---|---|---|
| 10:0 | MPSIZ | 最大包大小(字节) |
| 14:11 | EPNUM | 目标端点号 |
| 15 | EPDIR | 方向:0=OUT,1=IN |
| 17 | LSDEV | 低速设备标志 |
| 19:18 | EPTYP | 端点类型:00=控制,01=同步,10=批量,11=中断 |
| 21:20 | MC | 每微帧传输包数 |
| 28:22 | DAD | 目标设备地址 |
| 29 | ODDFRM | 奇数帧传输(同步/中断用) |
| 30 | CHDIS | 禁用通道 |
| 31 | CHENA | 使能通道(开始传输) |
OTG_HS_HCTSIZx(通道传输大小寄存器),地址 = 0x510 + 0x20*x
| 位 | 名称 | 说明 |
|---|---|---|
| 18:0 | XFRSIZ | 总传输字节数 |
| 28:19 | PKTCNT | 包数量 |
| 30:29 | DPID | 数据PID(DATA0/DATA1/DATA2/MDATA) |
| 31 | DOPING | 发送PING协议(高速OUT用) |
包数量的计算:
PKTCNT=⌈XFRSIZMPSIZ⌉PKTCNT = \lceil \frac{XFRSIZ}{MPSIZ} \rceilPKTCNT=⌈MPSIZXFRSIZ⌉
如果传输大小不是最大包大小的整数倍,最后一个包就是短包(Short Packet)。
第六章:设备模式详解
6.1 设备状态机
6.2 设备枚举过程
"枚举"就是主机和设备相互认识的过程:
第1步:主机发送USB复位(SE0信号持续10ms)
第2步:设备复位,进入Default状态
第3步:主机读取设备描述符(Device Descriptor)
第4步:主机分配地址(SET_ADDRESS命令)
第5步:主机读取完整配置描述符
第6步:主机发送SET_CONFIGURATION命令
第7步:枚举完成,设备可以正常工作
6.3 设备模式重要寄存器
OTG_HS_DCFG(设备配置寄存器,偏移0x800)
| 位 | 名称 | 说明 |
|---|---|---|
| 1:0 | DSPD | 设备速度:00=高速,11=全速(内置PHY) |
| 2 | NZLSOHSK | 非零长度状态OUT包握手处理方式 |
| 10:4 | DAD | 设备地址(SET_ADDRESS后写入) |
| 12:11 | PFIVL | 周期帧结束中断时机(80%/85%/90%/95%) |
OTG_HS_DCTL(设备控制寄存器,偏移0x804)
| 位 | 名称 | 说明 |
|---|---|---|
| 0 | RWUSIG | 远程唤醒信号(设备主动唤醒主机) |
| 1 | SDIS | 软断开(模拟拔掉USB线) |
| 7 | SGINAK | 设置全局IN NAK |
| 8 | CGINAK | 清除全局IN NAK |
| 9 | SGONAK | 设置全局OUT NAK |
| 10 | CGONAK | 清除全局OUT NAK |
软断开(Soft Disconnect)的作用:
- 设备可以在不实际拔线的情况下,让主机认为设备已断开
- 设置 SDIS=1 后,主机会看到设备断开
- 清除 SDIS=0 后,主机会重新枚举设备
第七章:中断系统详解
7.1 中断层级结构
OTG_HS 的中断是分级的,就像公司的组织架构:
NVIC (中断控制器)
|
+-- OTG_HS全局中断 (GINTSTS)
| |
| +-- IEPINT (有IN端点中断)
| | |
| | +-- DAINT (具体哪个端点)
| | |
| | +-- DIEPINTx (具体什么事件)
| |
| +-- OEPINT (有OUT端点中断)
| | |
| | +-- DAINT (具体哪个端点)
| | |
| | +-- DOEPINTx (具体什么事件)
| |
| +-- HCINT (有通道中断,主机模式)
| |
| +-- HAINT (具体哪个通道)
| |
| +-- HCINTx (具体什么事件)
|
+-- OTG_HS_EP1_IN 专用中断
+-- OTG_HS_EP1_OUT 专用中断
+-- OTG_HS_WKUP 唤醒中断
处理中断的步骤:
1. CPU响应中断
2. 读 GINTSTS,找出是哪类中断
3. 如果是端点中断:
a. 读 DAINT,找出是哪个端点
b. 读 DIEPINTx 或 DOEPINTx,找出具体原因
c. 处理后,写1清除对应位
4. 如果是通道中断(主机模式):
a. 读 HAINT,找出是哪个通道
b. 读 HCINTx,找出具体原因
c. 处理后,写1清除对应位
第八章:FIFO 内存分配
8.1 设备模式下的 FIFO 分配
FIFO RAM 总大小为 4KB(1024个32位字),需要手动分配给各个缓冲区:
接收FIFO(RX FIFO)- 所有OUT端点共用
最小大小计算:
RxFIFOmin=10+1+最大包大小4+1+OUT端点数量RxFIFO_{min} = 10 + 1 + \frac{最大包大小}{4} + 1 + OUT端点数量RxFIFOmin=10+1+4最大包大小+1+OUT端点数量
其中:
- 101010 = 控制端点 SETUP 包预留空间(10个字)
- 111 = 全局OUT NAK预留
- 最大包大小4\frac{最大包大小}{4}4最大包大小 = 接收一个最大包所需空间(除以4因为单位是32位字)
- +1+1+1 = 包状态信息
发送FIFO(TX FIFO)- 每个IN端点独立
TxFIFOmin[n]=最大包大小EPn4TxFIFO_{min}[n] = \frac{最大包大小_{EP_n}}{4}TxFIFOmin[n]=4最大包大小EPn
8.2 主机模式下的 FIFO 分配
| FIFO | 最小大小 | 建议大小 |
|---|---|---|
| RX FIFO | 最大包4+1\frac{最大包}{4}+14最大包+1 | 2×(最大包4+1)2 \times (\frac{最大包}{4}+1)2×(4最大包+1) |
| 非周期性TX FIFO | 最大包4\frac{最大包}{4}4最大包 | 2×最大包42 \times \frac{最大包}{4}2×4最大包 |
| 周期性TX FIFO | 最大包4\frac{最大包}{4}4最大包 | 根据通道数决定 |
第九章:低功耗模式
9.1 低功耗等级
| 模式 | 描述 | USB 兼容性 |
|---|---|---|
| Run | 正常运行 | 必须,USB激活时 |
| Sleep | CPU暂停 | 兼容,USB挂起时可进入 |
| Stop | 深度睡眠 | 兼容,USB挂起时可进入 |
| Standby | 断电 | 不兼容,退出后需重新初始化 |
9.2 省电寄存器 OTG_HS_PCGCCTL(偏移0xE00)
| 位 | 名称 | 说明 |
|---|---|---|
| 0 | STPPCLK | 停止PHY时钟(USB挂起时使用) |
| 1 | GATEHCLK | 门控AHB时钟(省电) |
| 4 | PHYSUSP | PHY已挂起标志(只读) |
第十章:完整初始化代码示例
下面是一个完整的 STM32F4 OTG_HS 设备模式初始化代码,带详细注释:
#include <cstdint>
// ============================================================
// OTG_HS 寄存器基地址(STM32F4xx 通常是 0x40040000)
// ============================================================
#define OTG_HS_BASE (0x40040000UL)
// 全局寄存器
#define OTG_HS_GOTGCTL (*((volatile uint32_t *)(OTG_HS_BASE + 0x000)))
#define OTG_HS_GOTGINT (*((volatile uint32_t *)(OTG_HS_BASE + 0x004)))
#define OTG_HS_GAHBCFG (*((volatile uint32_t *)(OTG_HS_BASE + 0x008)))
#define OTG_HS_GUSBCFG (*((volatile uint32_t *)(OTG_HS_BASE + 0x00C)))
#define OTG_HS_GRSTCTL (*((volatile uint32_t *)(OTG_HS_BASE + 0x010)))
#define OTG_HS_GINTSTS (*((volatile uint32_t *)(OTG_HS_BASE + 0x014)))
#define OTG_HS_GINTMSK (*((volatile uint32_t *)(OTG_HS_BASE + 0x018)))
#define OTG_HS_GRXSTSP (*((volatile uint32_t *)(OTG_HS_BASE + 0x020)))
#define OTG_HS_GRXFSIZ (*((volatile uint32_t *)(OTG_HS_BASE + 0x024)))
#define OTG_HS_GNPTXFSIZ (*((volatile uint32_t *)(OTG_HS_BASE + 0x028)))
#define OTG_HS_GCCFG (*((volatile uint32_t *)(OTG_HS_BASE + 0x038)))
// 设备模式寄存器
#define OTG_HS_DCFG (*((volatile uint32_t *)(OTG_HS_BASE + 0x800)))
#define OTG_HS_DCTL (*((volatile uint32_t *)(OTG_HS_BASE + 0x804)))
#define OTG_HS_DSTS (*((volatile uint32_t *)(OTG_HS_BASE + 0x808)))
#define OTG_HS_DIEPMSK (*((volatile uint32_t *)(OTG_HS_BASE + 0x810)))
#define OTG_HS_DOEPMSK (*((volatile uint32_t *)(OTG_HS_BASE + 0x814)))
#define OTG_HS_DAINT (*((volatile uint32_t *)(OTG_HS_BASE + 0x818)))
#define OTG_HS_DAINTMSK (*((volatile uint32_t *)(OTG_HS_BASE + 0x81C)))
// 电源和时钟门控
#define OTG_HS_PCGCCTL (*((volatile uint32_t *)(OTG_HS_BASE + 0xE00)))
// EP0 控制寄存器(IN方向)
#define OTG_HS_DIEPCTL0 (*((volatile uint32_t *)(OTG_HS_BASE + 0x900)))
#define OTG_HS_DIEPTSIZ0 (*((volatile uint32_t *)(OTG_HS_BASE + 0x910)))
#define OTG_HS_DIEPINT0 (*((volatile uint32_t *)(OTG_HS_BASE + 0x908)))
// EP0 控制寄存器(OUT方向)
#define OTG_HS_DOEPCTL0 (*((volatile uint32_t *)(OTG_HS_BASE + 0xB00)))
#define OTG_HS_DOEPTSIZ0 (*((volatile uint32_t *)(OTG_HS_BASE + 0xB10)))
#define OTG_HS_DOEPINT0 (*((volatile uint32_t *)(OTG_HS_BASE + 0xB08)))
// ============================================================
// 位定义
// ============================================================
// GAHBCFG 位定义
#define GAHBCFG_GINT (1U << 0) // 全局中断使能
#define GAHBCFG_DMAEN (1U << 5) // DMA使能
#define GAHBCFG_TXFELVL (1U << 7) // TX FIFO空中断:0=半空,1=全空
// GUSBCFG 位定义
#define GUSBCFG_PHYSEL (1U << 6) // PHY选择:1=内置FS PHY
#define GUSBCFG_SRPCAP (1U << 8) // SRP能力
#define GUSBCFG_HNPCAP (1U << 9) // HNP能力
#define GUSBCFG_TRDT_SHIFT 10 // TRDT字段位移
#define GUSBCFG_FDMOD (1U << 30) // 强制设备模式
// GRSTCTL 位定义
#define GRSTCTL_CSRST (1U << 0) // 核心软复位
#define GRSTCTL_RXFFLSH (1U << 4) // 清空RX FIFO
#define GRSTCTL_TXFFLSH (1U << 5) // 清空TX FIFO
#define GRSTCTL_TXFNUM_ALL (0x10U << 6)// 清空所有TX FIFO
#define GRSTCTL_AHBIDL (1U << 31) // AHB主机空闲
// GINTSTS 位定义
#define GINTSTS_USBRST (1U << 12) // USB复位
#define GINTSTS_ENUMDNE (1U << 13) // 枚举完成
#define GINTSTS_RXFLVL (1U << 4) // RX FIFO非空
#define GINTSTS_IEPINT (1U << 18) // IN端点中断
#define GINTSTS_OEPINT (1U << 19) // OUT端点中断
#define GINTSTS_USBSUSP (1U << 11) // USB挂起
#define GINTSTS_ESUSP (1U << 10) // 早期挂起
#define GINTSTS_WKUPINT (1U << 31) // 唤醒中断
#define GINTSTS_SOF (1U << 3) // 帧起始
// GCCFG 位定义
#define GCCFG_PWRDWN (1U << 16) // 断电控制:1=激活收发器
#define GCCFG_VBUSBSEN (1U << 19) // B设备VBUS检测使能
#define GCCFG_VBUSASEN (1U << 18) // A设备VBUS检测使能
// DCFG 位定义
#define DCFG_DSPD_HS 0 // 高速
#define DCFG_DSPD_FS 3 // 全速(内置PHY)
#define DCFG_DSPD_SHIFT 0 // 速度字段位移
#define DCFG_DAD_SHIFT 4 // 设备地址字段位移
// DCTL 位定义
#define DCTL_RWUSIG (1U << 0) // 远程唤醒信号
#define DCTL_SDIS (1U << 1) // 软断开
// DOEPMSK 位定义
#define DOEPMSK_XFRCM (1U << 0) // 传输完成中断使能
#define DOEPMSK_STUPM (1U << 3) // SETUP阶段完成中断使能
// DIEPMSK 位定义
#define DIEPMSK_XFRCM (1U << 0) // 传输完成中断使能
#define DIEPMSK_TOM (1U << 3) // 超时条件中断使能
// DOEPTSIZ0 位定义
#define DOEPTSIZ0_STUPCNT_3 (3U << 29) // 接收3个SETUP包
// ============================================================
// 辅助延时函数(实际项目需要使用精确定时器)
// ============================================================
static void delay_ms(uint32_t ms)
{
// 简单的忙等待,实际应用中用 SysTick 或硬件定时器
volatile uint32_t count = ms * 8000; // 假设 CPU 频率 168MHz
while (count--) {}
}
// ============================================================
// 等待 AHB 主机进入空闲状态
// 必须在 flush FIFO 之前调用
// ============================================================
static void wait_ahb_idle(void)
{
uint32_t timeout = 200000;
// GRSTCTL[31] = AHBIDL,为1时表示AHB主机空闲
while (!(OTG_HS_GRSTCTL & GRSTCTL_AHBIDL))
{
if (--timeout == 0) break; // 防止死循环
}
}
// ============================================================
// 步骤1:核心软复位
// 复位所有状态机,清空所有中断,清空所有FIFO
// ============================================================
static void otg_core_soft_reset(void)
{
wait_ahb_idle();
// 写1启动复位
OTG_HS_GRSTCTL |= GRSTCTL_CSRST;
// 等待复位完成(硬件自动清零该位)
uint32_t timeout = 200000;
while (OTG_HS_GRSTCTL & GRSTCTL_CSRST)
{
if (--timeout == 0) break;
}
// 复位后等待至少3个PHY时钟周期,确保同步
delay_ms(1);
}
// ============================================================
// 步骤2:清空 RX FIFO 和所有 TX FIFO
// ============================================================
static void otg_flush_fifos(void)
{
// 等待AHB空闲
wait_ahb_idle();
// 清空 RX FIFO
OTG_HS_GRSTCTL |= GRSTCTL_RXFFLSH;
uint32_t timeout = 200000;
while (OTG_HS_GRSTCTL & GRSTCTL_RXFFLSH)
{
if (--timeout == 0) break;
}
// 清空所有 TX FIFO(TXFNUM=0x10 表示清空全部)
OTG_HS_GRSTCTL = GRSTCTL_TXFFLSH | GRSTCTL_TXFNUM_ALL;
timeout = 200000;
while (OTG_HS_GRSTCTL & GRSTCTL_TXFFLSH)
{
if (--timeout == 0) break;
}
}
// ============================================================
// 步骤3:核心全局初始化
// 配置PHY、FIFO大小、AHB参数
// ============================================================
static void otg_core_init(void)
{
// -------------------------------------------------------
// 3.1 选择 PHY
// 使用内置全速 PHY(不需要外接 USB3300 等芯片)
// -------------------------------------------------------
OTG_HS_GUSBCFG |= GUSBCFG_PHYSEL; // 选择内置FS PHY
// -------------------------------------------------------
// 3.2 核心软复位(更换PHY后必须复位)
// -------------------------------------------------------
otg_core_soft_reset();
// -------------------------------------------------------
// 3.3 激活 PHY 收发器
// PWRDWN=1 表示"取消断电"(激活)
// -------------------------------------------------------
OTG_HS_GCCFG = GCCFG_PWRDWN;
// 等待 PHY 时钟稳定
delay_ms(20);
// -------------------------------------------------------
// 3.4 强制设备模式(不依赖ID引脚)
// FDMOD=1:无论ID引脚电平如何,始终作为Device工作
// -------------------------------------------------------
OTG_HS_GUSBCFG |= GUSBCFG_FDMOD;
delay_ms(50); // 等待模式切换生效(至少25ms)
// -------------------------------------------------------
// 3.5 配置 AHB 参数
// - 使用 CPU 轮询模式(不使用DMA)
// - 使能全局中断
// - TX FIFO 中断触发点:FIFO 全空时触发
// -------------------------------------------------------
OTG_HS_GAHBCFG = GAHBCFG_GINT // 使能全局中断
| GAHBCFG_TXFELVL; // TX FIFO全空时产生中断
// -------------------------------------------------------
// 3.6 配置 USB 参数
// TRDT=9:AHB频率30MHz以上时的最小值
// 不使能 SRP 和 HNP(简单设备不需要)
// -------------------------------------------------------
OTG_HS_GUSBCFG = (OTG_HS_GUSBCFG & ~(0xFU << GUSBCFG_TRDT_SHIFT))
| (9U << GUSBCFG_TRDT_SHIFT) // TRDT=9
| GUSBCFG_PHYSEL // 保持内置PHY选择
| GUSBCFG_FDMOD; // 保持强制设备模式
// -------------------------------------------------------
// 3.7 配置 RX FIFO 大小
// 用于接收所有OUT端点数据
// 这里分配 128 个 32位字 = 512 字节
// -------------------------------------------------------
OTG_HS_GRXFSIZ = 128; // 单位:32位字
// -------------------------------------------------------
// 3.8 配置 EP0 的 TX FIFO(用于 IN 方向传输)
// 高16位:FIFO深度(32个字=128字节)
// 低16位:FIFO起始地址(紧接RX FIFO之后,即从128开始)
// -------------------------------------------------------
OTG_HS_GNPTXFSIZ = (32U << 16) // TX0 FIFO 深度:32个字
| (128U << 0); // TX0 FIFO 起始地址:128(紧跟RX FIFO)
}
// ============================================================
// 步骤4:设备模式初始化
// ============================================================
static void otg_device_init(void)
{
// -------------------------------------------------------
// 4.1 使能 B 设备 VBUS 检测
// 检测 VBUS 是否有效(5V),有效后才能连接
// -------------------------------------------------------
OTG_HS_GCCFG |= GCCFG_VBUSBSEN;
// -------------------------------------------------------
// 4.2 配置设备速度
// DSPD=3:全速,使用内置 PHY
// -------------------------------------------------------
OTG_HS_DCFG = (DCFG_DSPD_FS << DCFG_DSPD_SHIFT);
// -------------------------------------------------------
// 4.3 清空 FIFO
// -------------------------------------------------------
otg_flush_fifos();
// -------------------------------------------------------
// 4.4 清除所有挂起的中断
// 写1清除 rc_w1 类型的中断标志
// -------------------------------------------------------
OTG_HS_GINTSTS = 0xFFFFFFFF;
// -------------------------------------------------------
// 4.5 使能需要的中断
// USBRST:USB复位检测(必须,用于枚举)
// ENUMDNE:枚举完成(必须,配置端点用)
// RXFLVL:接收FIFO非空(接收数据用)
// IEPINT:IN端点中断(发送完成)
// OEPINT:OUT端点中断(接收完成)
// ESUSP/USBSUSP:挂起检测(省电用)
// -------------------------------------------------------
OTG_HS_GINTMSK = (1U << 12) // USBRST
| (1U << 13) // ENUMDNE
| (1U << 4) // RXFLVL
| (1U << 18) // IEPINT
| (1U << 19) // OEPINT
| (1U << 10) // ESUSP
| (1U << 11); // USBSUSP
}
// ============================================================
// 步骤5:USB复位中断处理
// 当主机发送复位信号(SE0,持续10ms)后,硬件触发此中断
// ============================================================
static void otg_handle_usb_reset(void)
{
// -------------------------------------------------------
// 5.1 为所有 OUT 端点设置 NAK
// NAK = 拒绝接收,防止在配置好之前就收到数据
// 这里只处理 EP0(简化示例)
// -------------------------------------------------------
OTG_HS_DOEPCTL0 |= (1U << 27); // SNAK=1
// -------------------------------------------------------
// 5.2 清除设备地址(复位后地址归零)
// -------------------------------------------------------
OTG_HS_DCFG &= ~(0x7FU << DCFG_DAD_SHIFT);
// -------------------------------------------------------
// 5.3 使能端点 0 的中断
// INEP0=1 (bit 0) 使能 IN EP0
// OUTEP0=1 (bit 16) 使能 OUT EP0
// -------------------------------------------------------
OTG_HS_DAINTMSK = (1U << 0) // IN EP0 中断
| (1U << 16); // OUT EP0 中断
// -------------------------------------------------------
// 5.4 配置 OUT 端点公共中断
// -------------------------------------------------------
OTG_HS_DOEPMSK = DOEPMSK_XFRCM // 传输完成
| DOEPMSK_STUPM; // SETUP阶段完成
// -------------------------------------------------------
// 5.5 配置 IN 端点公共中断
// -------------------------------------------------------
OTG_HS_DIEPMSK = DIEPMSK_XFRCM // 传输完成
| DIEPMSK_TOM; // 超时(控制IN端点用)
// -------------------------------------------------------
// 5.6 配置 EP0 OUT,准备接收 SETUP 包
// STUPCNT=3:最多接收3个连续SETUP包
// -------------------------------------------------------
OTG_HS_DOEPTSIZ0 = DOEPTSIZ0_STUPCNT_3;
// -------------------------------------------------------
// 5.7 使能 EP0 OUT,等待SETUP包到来
// -------------------------------------------------------
OTG_HS_DOEPCTL0 |= (1U << 31) // EPENA=1 使能端点
| (1U << 26); // CNAK=1 清除NAK,允许接收
}
// ============================================================
// 步骤6:枚举完成中断处理
// 主机完成速度检测后触发此中断
// ============================================================
static void otg_handle_enum_done(void)
{
// -------------------------------------------------------
// 6.1 读取枚举速度
// DSTS[2:1] = ENUMSPD
// 00=高速,11=全速
// -------------------------------------------------------
uint32_t speed = (OTG_HS_DSTS >> 1) & 0x3;
(void)speed; // 实际应根据速度配置最大包大小
// -------------------------------------------------------
// 6.2 配置 EP0 最大包大小
// 全速下控制端点最大包大小为64字节
// OTG_HS_DIEPCTL0 的 MPSIZ[1:0]:
// 00=64字节,01=32字节,10=16字节,11=8字节
// -------------------------------------------------------
OTG_HS_DIEPCTL0 &= ~(0x3U); // 清除MPSIZ位
OTG_HS_DIEPCTL0 |= 0; // 设置为64字节
// -------------------------------------------------------
// 6.3 在DMA模式下:使能EP0 OUT以接收下一个SETUP包
// 这里是CPU轮询模式,跳过DMA配置
// -------------------------------------------------------
// -------------------------------------------------------
// 6.4 激活 EP0 IN 端点的收发器
// -------------------------------------------------------
OTG_HS_DIEPCTL0 |= (1U << 31); // EPENA
// 此时设备已经准备好接收 SOF 和控制传输
}
// ============================================================
// 主中断处理函数
// 所有 OTG_HS 中断都由此函数分发处理
// ============================================================
void OTG_HS_IRQHandler(void)
{
// 读取当前激活的中断(只看已使能的)
uint32_t gintsts = OTG_HS_GINTSTS & OTG_HS_GINTMSK;
// -------------------------------------------------------
// 处理 USB 复位中断
// -------------------------------------------------------
if (gintsts & GINTSTS_USBRST)
{
otg_handle_usb_reset();
OTG_HS_GINTSTS = GINTSTS_USBRST; // 写1清除中断
}
// -------------------------------------------------------
// 处理枚举完成中断
// -------------------------------------------------------
if (gintsts & GINTSTS_ENUMDNE)
{
otg_handle_enum_done();
OTG_HS_GINTSTS = GINTSTS_ENUMDNE; // 写1清除中断
}
// -------------------------------------------------------
// 处理 RX FIFO 非空中断(有数据从主机发来)
// -------------------------------------------------------
if (gintsts & GINTSTS_RXFLVL)
{
// 必须先屏蔽 RXFLVL 中断,再读取数据
OTG_HS_GINTMSK &= ~(1U << 4); // 屏蔽 RXFLVL
// 读取包状态(弹出FIFO顶部状态字)
uint32_t grxstsp = OTG_HS_GRXSTSP;
uint32_t pktsts = (grxstsp >> 17) & 0xF; // 包状态
uint32_t bcnt = (grxstsp >> 4) & 0x7FF; // 字节数
uint32_t epnum = grxstsp & 0xF; // 端点号
(void)bcnt;
(void)epnum;
// 只处理实际的 OUT 数据包(pktsts=0b0010=2)
if (pktsts == 0x2)
{
// TODO: 从 FIFO 读取数据到用户缓冲区
// 每次读32位(4字节),共需读 ceil(bcnt/4) 次
}
// 恢复 RXFLVL 中断
OTG_HS_GINTMSK |= (1U << 4);
}
}
// ============================================================
// 主函数:初始化 OTG_HS 设备
// ============================================================
int main(void)
{
// 步骤1:使能 OTG_HS 时钟(需要操作 RCC 寄存器,这里省略)
// RCC->AHB1ENR |= RCC_AHB1ENR_OTGHSEN;
// RCC->AHB1ENR |= RCC_AHB1ENR_OTGHSULPIEN; // 如使用ULPI PHY
// 步骤2:配置 GPIO 引脚为 OTG 功能(省略)
// PA11 -> OTG_HS_DM
// PA12 -> OTG_HS_DP
// 步骤3:核心初始化
otg_core_init();
// 步骤4:设备模式初始化
otg_device_init();
// 步骤5:配置 NVIC 使能 OTG_HS 中断(省略,需要CMSIS支持)
// NVIC_SetPriority(OTG_HS_IRQn, 6);
// NVIC_EnableIRQ(OTG_HS_IRQn);
// 步骤6:主循环(等待中断处理所有USB事务)
while (1)
{
// 空循环,USB事件全部由中断处理
}
return 0;
}
第十一章:SOF(帧起始信号)
11.1 什么是 SOF
SOF(Start of Frame)是 USB 主机每帧开始时发送的特殊包:
- 全速模式:每 1ms 发一次 SOF
- 高速模式:每 125μs 发一次 micro-SOF
时间轴:
|<-- 1ms -->|<-- 1ms -->|<-- 1ms -->|
SOF SOF SOF SOF
^ ^ ^
| | |
中断 中断 中断
11.2 SOF 与 TIM2 的连接
OTG_HS 的 SOF 脉冲可以内部连接到定时器2(TIM2)的输入触发,实现精确的 USB 时钟同步,这对音频设备特别有用:
OTG_HS 核心
|
| SOF 脉冲(每帧一次)
|
+-------> SOF引脚(对外输出)
|
+-------> TIM2 ITR1(内部连接)
|
+-> 输入捕获:精确测量帧周期
+-> 输出比较:在帧内精确定时
配置方法:设置 TIM2_OR 寄存器中的 ITR1_RMP 位,启用此连接。
第十二章:TRDT 值的理解
TRDT(USB Turnaround Time)是主机发送 IN 令牌后,设备准备数据的最大时间,单位是 PHY 时钟周期。
为什么需要 TRDT?
主机发送 IN 令牌
|
v
设备 MAC 接收到 IN 令牌
|
v (需要时间:PHY同步 + AHB读FIFO)
|
设备 PHY 开始发送数据
这个"需要时间"就是 TRDT,它和 AHB 总线频率有关:
TRDTmin={0x9AHB≥30MHzTRDT_{min} = \begin{cases} 0x9 & AHB \geq 30\text{MHz} \end{cases}TRDTmin={0x9AHB≥30MHz
值越大越安全,但会降低 IN 传输性能。
第十三章:完整流程总结
附录:常用寄存器速查
| 寄存器 | 偏移 | 主要用途 |
|---|---|---|
| GOTGCTL | 0x000 | OTG控制和状态 |
| GAHBCFG | 0x008 | AHB配置(DMA、中断) |
| GUSBCFG | 0x00C | USB配置(PHY、速度) |
| GRSTCTL | 0x010 | 复位控制 |
| GINTSTS | 0x014 | 中断状态(写1清除) |
| GINTMSK | 0x018 | 中断使能掩码 |
| GRXFSIZ | 0x024 | RX FIFO大小 |
| GNPTXFSIZ | 0x028 | EP0/非周期TX FIFO大小 |
| GCCFG | 0x038 | 通用核心配置(PHY供电,VBUS) |
| DCFG | 0x800 | 设备配置 |
| DCTL | 0x804 | 设备控制 |
| DSTS | 0x808 | 设备状态(枚举速度) |
| DIEPMSK | 0x810 | IN端点公共中断掩码 |
| DOEPMSK | 0x814 | OUT端点公共中断掩码 |
| DAINT | 0x818 | 所有端点中断状态 |
| DAINTMSK | 0x81C | 所有端点中断掩码 |
| PCGCCTL | 0xE00 | 电源和时钟门控 |
STM32F4xx FSMC 灵活静态存储控制器深度解析
第一章:从零认识 FSMC
1.1 FSMC 是什么?
FSMC(Flexible Static Memory Controller,灵活静态存储控制器)是 STM32F40x/41x 芯片内部的一个硬件模块,它的作用就像一个"翻译官":
CPU (说 AHB 语言)
|
| AHB 总线
|
FSMC <--- "翻译官"
|
| 各种外部存储器协议
|
+----+----+----+
| | | |
NOR SRAM NAND PSRAM
Flash Flash
为什么需要 FSMC?
CPU 内部用的是 AHB 总线协议(高速、同步),但外部存储芯片各有各的接口协议(有的异步、有的同步、有的需要地址锁存)。FSMC 帮助 CPU 把 AHB 事务转换成外部存储器能理解的时序信号。
1.2 FSMC 能连接哪些存储器?
| 存储器类型 | 典型用途 | Bank 编号 |
|---|---|---|
| SRAM / ROM | 静态存储,高速读写 | Bank 1 |
| NOR Flash | 程序存储、大容量只读 | Bank 1 |
| PSRAM (CRAM) | 伪静态随机存储 | Bank 1 |
| NAND Flash | 大容量数据存储(如 U 盘) | Bank 2/3 |
| PC Card / CompactFlash | 存储卡接口 | Bank 4 |
第二章:FSMC 的整体结构
2.1 硬件框图
+--------------------------------------------------+
| STM32F4xx 芯片内部 |
| |
| +----------+ AHB总线 +-----------------+ |
| | CPU |<------------->| FSMC 核心 | |
| +----------+ | | |
| | +-------------+ | |
| | | 配置寄存器 | | |
| | +-------------+ | |
| | +-------------+ | |
| | | NOR/PSRAM | | |
| | | 控制器 | | |
| | +-------------+ | |
| | +-------------+ | |
| | | NAND/PCCard | | |
| | | 控制器 | | |
| | +-------------+ | |
+-----------------------------.------------------+-+
|
+------------------+------------------+
| |
NOR/PSRAM 接口信号 NAND/PCCard 接口信号
FSMC_A[25:0] 地址总线 FSMC_NCE[3:2] 片选
FSMC_D[15:0] 数据总线 FSMC_INT[3:2] 中断
FSMC_NE[4:1] 片选 FSMC_NCE4_1/2 PCCard片选
FSMC_NOE 输出使能 FSMC_NIORD/NIOWR IO读写
FSMC_NWE 写使能 FSMC_NREG 寄存器选择
FSMC_CLK 时钟(同步用) FSMC_CD 卡检测
FSMC_NWAIT 等待输入
2.2 四个存储体(Bank)的地址分配
FSMC 把 CPU 的地址空间分成 4 个固定的 256MB 区域:
地址空间:
0x6000 0000 +------------------+
| Bank 1 | NOR Flash / PSRAM / SRAM
| 4 x 64MB | (4个子Bank,各64MB)
0x6FFF FFFF +------------------+
0x7000 0000 +------------------+
| Bank 2 | NAND Flash
| 4 x 64MB |
0x7FFF FFFF +------------------+
0x8000 0000 +------------------+
| Bank 3 | NAND Flash
| 4 x 64MB |
0x8FFF FFFF +------------------+
0x9000 0000 +------------------+
| Bank 4 | PC Card / CompactFlash
| 4 x 64MB |
0x9FFF FFFF +------------------+
Bank 1 内部的 4 个子 Bank 由地址线 HADDR[27:26] 选择:
| HADDR[27:26] | 选中的子 Bank |
|---|---|
| 00 | Bank1-NOR/PSRAM 1(地址 0x6000 0000) |
| 01 | Bank1-NOR/PSRAM 2(地址 0x6400 0000) |
| 10 | Bank1-NOR/PSRAM 3(地址 0x6800 0000) |
| 11 | Bank1-NOR/PSRAM 4(地址 0x6C00 0000) |
第三章:外部地址的换算规则
3.1 为什么地址要换算?
CPU 内部使用字节地址(每个地址对应1字节),但外部存储器可能是 8位或 16位宽度。
16位宽度存储器的地址换算:
外部存储器地址=HADDR[25:0]2外部存储器地址 = \frac{HADDR[25:0]}{2}外部存储器地址=2HADDR[25:0]
即 FSMC 内部使用 HADDR[25:1] 右移1位作为外部地址 A[24:0]。
为什么这样做?
CPU 想访问字节地址 0x00000002(第2字节)
对应 16位存储器的第1个字(包含字节0和字节1)
外部地址 = 2 / 2 = 1 → A[0]=1
CPU 想访问字节地址 0x00000004(第4字节)
对应 16位存储器的第2个字
外部地址 = 4 / 2 = 2 → A[1]=1
重要规则: 无论存储器是 8 位还是 16 位宽,FSMC_A[0] 都要连接到存储器的 A[0]。
3.2 总线事务宽度不匹配的处理
| CPU 请求大小 | 存储器宽度 | FSMC 处理方式 |
|---|---|---|
| 等于存储器宽度 | - | 直接访问,无问题 |
| 大于存储器宽度 | - | 自动拆分成多次小访问 |
| 小于存储器宽度 | 带字节选择(SRAM) | 用 NBL[1:0] 选择字节 |
| 小于存储器宽度 | 无字节选择(NOR Flash 16位) | 读:允许(丢弃多余字节);写:不允许 |
第四章:NOR Flash / PSRAM 控制器详解
4.1 异步访问时序图解
所有异步访问分为以下几个阶段:
时间轴:
|<--ADDSET-->|<--------DATAST-------->|<-BUSTURN->|
^ ^ ^
| | |
地址建立阶段 数据有效阶段 总线转向阶段
(地址输出到片选) (NOE/NWE 有效期间) (总线释放缓冲)
读操作时序(Mode1,以 SRAM 为例):
HCLK: ___/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\___
| | | | | | | | |
A[]: |<-ADDSET->|<-------DATAST------->|
| valid | valid |
NEx: ‾|__________|______________________|‾‾‾
(低电平,片选有效)
NOE: ‾|__________|______________________|‾‾‾
(低电平,输出使能有效)
NWE: ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
(保持高,读操作不写)
D[]: --------XXXX<===数据由存储器驱动===>
写操作时序(Mode1):
HCLK: ___/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_
| | | | | | | |
A[]: |<-ADDSET->|<--DATAST+1-->|1clk|
NEx: ‾|__________|______________|‾‾‾‾
NOE: ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
NWE: ‾|__________|______________|‾‾‾‾
D[]: ----------<==FSMC 驱动数据==>|
^
最后1个HCLK
保证数据保持时间
注意:写操作 DATAST 实际有效时间是 DATAST+1 个 HCLK,这个"+1"是为了保证数据保持时间(Data Hold Time)满足要求。
4.2 五种访问模式对比
| 模式 | 适用存储器 | NOE 行为 | NWE 行为 | NADV 信号 | 独立读写时序 |
|---|---|---|---|---|---|
| Mode 1 | SRAM/PSRAM | 整个访问期间有效 | 整个访问期间有效 | 无 | 否(共用BTR) |
| Mode A | SRAM/PSRAM | 每次访问切换(OE toggling) | 整个访问期间有效 | 无 | 是(BTR+BWTR) |
| Mode 2 | NOR Flash | 整个访问期间有效 | 整个访问期间有效 | 有(地址锁存) | 否 |
| Mode B | NOR Flash | 整个访问期间有效 | 每次访问切换 | 有 | 是 |
| Mode C | NOR Flash | 每次访问切换 | 整个访问期间有效 | 有 | 是 |
| Mode D | 扩展地址 | 切换,NADV后继续 | 切换 | 有(含ADDHLD) | 是 |
Mode A 和 Mode 1 的关键区别:
Mode A 在读操作时 NOE 会"切换"(先高后低),这让存储器有机会在下一次读之前释放数据总线,适合需要 OE 切换的 SRAM。
4.3 同步访问(Burst Mode)
同步访问时,FSMC 向存储器输出时钟信号 FSMC_CLK,存储器按时钟节拍批量输出数据(突发传输):
FSMC_CLK周期=(CLKDIV+1)×HCLK周期FSMC\_CLK 周期 = (CLKDIV + 1) \times HCLK 周期FSMC_CLK周期=(CLKDIV+1)×HCLK周期
其中 CLKDIV 最小为1,所以:
fCLK_min=fHCLK2f_{CLK\_min} = \frac{f_{HCLK}}{2}fCLK_min=2fHCLK
数据延迟(DATLAT)的含义:
在发出突发读命令后,存储器需要若干时钟周期才能准备好第一个数据:
等待时钟数=DATLAT+2 个 CLK 周期等待时钟数 = DATLAT + 2 \text{ 个 CLK 周期}等待时钟数=DATLAT+2 个 CLK 周期
不同 NOR Flash 厂商的定义略有差异:
- 有些 NOR Flash: 延迟=DATLAT+2延迟 = DATLAT + 2延迟=DATLAT+2 个 CLK
- 有些 NOR Flash: 延迟=DATLAT+3延迟 = DATLAT + 3延迟=DATLAT+3 个 CLK
4.4 WAIT 信号(等待管理)
当存储器需要更多时间准备数据时,它会拉低 NWAIT 引脚,FSMC 就会延长当前访问阶段。
DATAST 的最小值计算:
对于使用 WAIT 信号的异步访问,DATAST 必须足够大,使 NWAIT 能在数据阶段结束前 4 个 HCLK 被检测到:
DATAST≥4+max_wait_assertion_timeHCLKDATAST \geq 4 + max\_wait\_assertion\_time_{HCLK}DATAST≥4+max_wait_assertion_timeHCLK
即:存储器最慢的等待时间(以 HCLK 计)加上4个时钟的检测裕量。
第五章:关键寄存器详解
5.1 寄存器地址规律
每个 Bank 有一对控制/时序寄存器:
Bank x (x=1~4):
FSMC_BCRx: 基地址 0xA0000000 + 8*(x-1) 控制寄存器
FSMC_BTRx: 基地址 0xA0000000 + 0x04 + 8*(x-1) 读时序寄存器
FSMC_BWTRx: 基地址 0xA0000000 + 0x104 + 8*(x-1) 写时序寄存器(扩展模式)
5.2 FSMC_BCRx 控制寄存器详解
这个寄存器决定"用什么类型的存储器"以及"用什么方式访问":
| 位 | 名称 | 说明 |
|---|---|---|
| 0 | MBKEN | Bank使能:1=使能,0=禁止(访问禁止的Bank会产生AHB错误) |
| 1 | MUXEN | 地址/数据复用:1=复用(节省引脚),0=独立 |
| 3:2 | MTYP | 存储器类型:00=SRAM,01=PSRAM,10=NOR Flash |
| 5:4 | MWID | 数据总线宽度:00=8位,01=16位 |
| 6 | FACCEN | NOR Flash访问使能:必须为1才能访问NOR |
| 8 | BURSTEN | 突发模式使能(读操作) |
| 9 | WAITPOL | NWAIT极性:0=低有效,1=高有效 |
| 11 | WAITCFG | 等待时序配置:0=NWAIT在等待态前一周期有效,1=在等待态期间有效 |
| 12 | WREN | 写使能:1=允许写,0=禁止写 |
| 13 | WAITEN | 同步模式等待使能 |
| 14 | EXTMOD | 扩展模式:1=读写使用不同时序寄存器(BTR/BWTR) |
| 15 | ASYNCWAIT | 异步等待:1=异步模式也检测NWAIT |
| 18:16 | CPSIZE | CRAM页大小(Cellular RAM 1.5用) |
| 19 | CBURSTRW | 突发写使能(PSRAM写操作用) |
5.3 FSMC_BTRx 时序寄存器详解
这个寄存器决定"每个访问阶段持续多少个时钟":
| 位 | 名称 | 单位 | 说明 |
|---|---|---|---|
| 3:0 | ADDSET | HCLK | 地址建立时间:0~15个周期 |
| 7:4 | ADDHLD | HCLK | 地址保持时间(Mode D和复用模式用) |
| 15:8 | DATAST | HCLK | 数据建立时间:1~255个周期 |
| 19:16 | BUSTURN | HCLK | 总线转向时间:0~15个周期 |
| 23:20 | CLKDIV | HCLK | CLK分频比:CLK=HCLK/(CLKDIV+1) |
| 27:24 | DATLAT | CLK | 同步数据延迟(延迟+2个CLK) |
| 29:28 | ACCMOD | - | 访问模式:00=A,01=B,10=C,11=D |
实际访问时间计算(异步读,Mode1):
tread=(ADDSET+DATAST+BUSTURN)×tHCLKt_{read} = (ADDSET + DATAST + BUSTURN) \times t_{HCLK}tread=(ADDSET+DATAST+BUSTURN)×tHCLK
实际访问时间计算(异步写,Mode1):
twrite=(ADDSET+DATAST+1+1)×tHCLKt_{write} = (ADDSET + DATAST + 1 + 1) \times t_{HCLK}twrite=(ADDSET+DATAST+1+1)×tHCLK
其中"+1"是写操作末尾自动添加的保持周期,再"+1"是总线转向。
第六章:NAND Flash 控制器详解
6.1 NAND Flash 的特殊性
NAND Flash 的接口和 NOR Flash 完全不同:
- 没有独立地址总线,命令、地址、数据都通过同一个数据总线传输
- 用 CLE(命令锁存使能)区分"命令"
- 用 ALE(地址锁存使能)区分"地址"
- 用 R/NB(Ready/Busy)信号表示是否准备好
FSMC 用地址线来控制 CLE 和 ALE:
FSMC_A[17] -> NAND Flash 的 ALE(地址锁存使能)
FSMC_A[16] -> NAND Flash 的 CLE(命令锁存使能)
6.2 NAND Flash 的地址空间划分
每个 NAND Bank 内部按地址分三个区域,CPU 通过写入不同地址段来发送命令/地址/数据:
| HADDR[17:16] | 区域名称 | 地址范围 | 用途 |
|---|---|---|---|
| 00 | 数据区 | 0x000000~0x0FFFFF | 读写实际数据 |
| 01 | 命令区 | 0x010000~0x01FFFF | 写入读/写/擦除命令 |
| 1X | 地址区 | 0x020000~0x03FFFF | 写入目标地址 |
6.3 NAND Flash 典型读页操作
6.4 NAND Flash 访问时序参数
NAND 控制器使用 FSMC_PMEMx 寄存器配置时序,有四个参数:
| 参数 | 对应位 | 说明 |
|---|---|---|
| MEMSETx | [7:0] | 命令建立时间:地址有效到命令有效的延迟 |
| MEMWAITx | [15:8] | 命令有效时间:NWE/NOE 保持低电平的时间 |
| MEMHOLDx | [23:16] | 数据保持时间:NWE/NOE 释放后地址/数据的保持时间 |
| MEMHIZx | [31:24] | 数据高阻时间:写操作开始后数据总线保持高阻的时间 |
6.5 ECC 硬件校验
FSMC 内置了 ECC(Error Correction Code,错误校验码)硬件,基于 Hamming 算法,能对 NAND Flash 数据做:
- 1位错误自动纠正
- 2位错误检测
支持的页大小:256、512、1024、2048、4096、8192 字节。
ECC 使用流程:
写数据时:
1. 使能 ECCEN(FSMC_PCRx bit6 = 1)
2. 写数据到 NAND
3. 读取 FSMC_ECCRx 中的 ECC 值
4. 将 ECC 值存入 NAND 的 Spare Area(备用区)
读数据并校验时:
5. 清除并重新使能 ECCEN
6. 读数据回来
7. 读取新的 FSMC_ECCRx 值
8. 与备用区中存储的 ECC 比对
- 相同 → 数据正确
- 不同 → 有错误,软件根据差异计算出错位置并纠正
第七章:PC Card 控制器
7.1 PC Card 的三个地址空间
PC Card(PCMCIA/CompactFlash)有三个独立的访问空间:
| 空间 | 访问信号 | 用途 |
|---|---|---|
| Common Memory(通用存储) | NOE/NWE + nREG=1 | 数据存储(读写文件) |
| Attribute Memory(属性存储) | NOE/NWE + nREG=0 | 配置信息(CIS卡信息结构) |
| I/O Space(IO空间) | nIORD/nIOWR + nREG=0 | 设备IO寄存器访问 |
NREG 引脚决定访问哪个空间:
- NREG=1:通用存储空间
- NREG=0:属性存储空间或IO空间
第八章:Write FIFO 机制
FSMC 有一个写缓冲 FIFO:
- STM32F40x/41x:2个字(2×32位)
- STM32F42x/43x:16个字(16×32位)
FIFO 的作用:
没有FIFO时:
CPU 写 -> FSMC等待慢速存储器 -> CPU被阻塞等待 -> 效率低
有FIFO时:
CPU 写 -> 写入FIFO(快) -> CPU立即释放继续工作
|
FSMC 从FIFO取数据 -> 慢慢写入存储器
重要限制: FIFO 只缓存 AHB 突发写,且每次只缓存一个突发。如果有新的突发写请求,FIFO 会先等待当前突发完成(FIFO 排空)。
对于 ECC 计算,必须等 FIFO 排空才能读取正确的 ECC 值,可以通过检测 FSMC_SR 的 FEMPT 位(FIFO 空标志)来确认。
第九章:完整初始化代码示例
下面的代码演示如何用 FSMC 连接一块 16 位异步 SRAM(Mode A 模式):
#include <cstdint>
// ============================================================
// FSMC 寄存器基地址(STM32F4xx)
// ============================================================
#define FSMC_BASE (0xA0000000UL)
// Bank 1 控制寄存器(NOR/PSRAM/SRAM)
// 地址 = 基地址 + 8*(x-1),x=1~4
#define FSMC_BCR1 (*((volatile uint32_t *)(FSMC_BASE + 0x0000)))
#define FSMC_BTR1 (*((volatile uint32_t *)(FSMC_BASE + 0x0004)))
#define FSMC_BWTR1 (*((volatile uint32_t *)(FSMC_BASE + 0x0104)))
#define FSMC_BCR2 (*((volatile uint32_t *)(FSMC_BASE + 0x0008)))
#define FSMC_BTR2 (*((volatile uint32_t *)(FSMC_BASE + 0x000C)))
// NAND Flash 控制寄存器(Bank 2)
// 地址 = 基地址 + 0x40 + 0x20*(x-1),x=2~4
#define FSMC_PCR2 (*((volatile uint32_t *)(FSMC_BASE + 0x0060)))
#define FSMC_SR2 (*((volatile uint32_t *)(FSMC_BASE + 0x0064)))
#define FSMC_PMEM2 (*((volatile uint32_t *)(FSMC_BASE + 0x0068)))
#define FSMC_PATT2 (*((volatile uint32_t *)(FSMC_BASE + 0x006C)))
#define FSMC_ECCR2 (*((volatile uint32_t *)(FSMC_BASE + 0x0074)))
// ============================================================
// FSMC_BCRx 位定义
// ============================================================
#define BCR_MBKEN (1U << 0) // Bank 使能
#define BCR_MUXEN (1U << 1) // 地址数据复用使能
// MTYP[3:2]: 存储器类型
#define BCR_MTYP_SRAM (0U << 2) // SRAM/ROM
#define BCR_MTYP_PSRAM (1U << 2) // PSRAM (CRAM)
#define BCR_MTYP_NOR (2U << 2) // NOR Flash
// MWID[5:4]: 数据总线宽度
#define BCR_MWID_8BIT (0U << 4) // 8位数据总线
#define BCR_MWID_16BIT (1U << 4) // 16位数据总线
#define BCR_FACCEN (1U << 6) // Flash访问使能(NOR Flash必须置1)
#define BCR_BURSTEN (1U << 8) // 同步突发读使能
#define BCR_WAITPOL (1U << 9) // NWAIT极性:0=低有效,1=高有效
#define BCR_WAITCFG (1U << 11) // 等待信号配置
#define BCR_WREN (1U << 12) // 写使能
#define BCR_WAITEN (1U << 13) // 同步等待使能
#define BCR_EXTMOD (1U << 14) // 扩展模式(独立读写时序)
#define BCR_ASYNCWAIT (1U << 15) // 异步等待使能
#define BCR_CBURSTRW (1U << 19) // 同步突发写使能(PSRAM用)
// ============================================================
// FSMC_PCRx 位定义(NAND/PC Card控制)
// ============================================================
#define PCR_PWAITEN (1U << 1) // 等待特性使能
#define PCR_PBKEN (1U << 2) // Bank使能
#define PCR_PTYP_NAND (1U << 3) // 类型:1=NAND,0=PC Card
#define PCR_PWID_8BIT (0U << 4) // 数据总线:8位
#define PCR_PWID_16BIT (1U << 4) // 数据总线:16位
#define PCR_ECCEN (1U << 6) // ECC使能
// TAR[16:13]: ALE到RE的延迟
#define PCR_TAR_SHIFT 13
// TCLR[12:9]: CLE到RE的延迟
#define PCR_TCLR_SHIFT 9
// ECCPS[19:17]: ECC页大小
#define PCR_ECCPS_SHIFT 17
#define PCR_ECCPS_512 (1U << PCR_ECCPS_SHIFT) // 512字节
// ============================================================
// FSMC_SR 状态寄存器位定义
// ============================================================
#define SR_FEMPT (1U << 6) // FIFO空标志(只读)
#define SR_IREN (1U << 3) // 上升沿中断使能
#define SR_ILEN (1U << 4) // 高电平中断使能
#define SR_IFEN (1U << 5) // 下降沿中断使能
// ============================================================
// Bank 1 SRAM 的访问基地址
// ============================================================
#define SRAM_BANK1_BASE (0x60000000UL) // Bank1-NOR/PSRAM1 起始地址
// ============================================================
// Bank 2 NAND Flash 的三个地址空间基地址
// ============================================================
#define NAND_BANK2_BASE (0x70000000UL)
// 数据区:HADDR[17:16]=00
#define NAND_DATA_ADDR (NAND_BANK2_BASE | 0x00000000UL)
// 命令区:HADDR[17:16]=01,即地址位17=0,位16=1
#define NAND_CMD_ADDR (NAND_BANK2_BASE | 0x00010000UL)
// 地址区:HADDR[17:16]=1X,即地址位17=1
#define NAND_ADDR_ADDR (NAND_BANK2_BASE | 0x00020000UL)
// ============================================================
// 简单延时(实际项目用定时器)
// ============================================================
static void simple_delay(uint32_t n)
{
while (n--) { __asm("nop"); }
}
// ============================================================
// 函数:初始化 Bank1 连接 16位异步 SRAM(Mode A 模式)
//
// Mode A 特点:
// - NOE 在每次读操作中切换(OE toggling)
// - 可以独立配置读写时序(EXTMOD=1)
// - 适合大多数现代 SRAM
//
// 参数假设(以 IS62WV51216 为例,55ns 访问时间,HCLK=168MHz):
// HCLK 周期 = 1/168MHz ≈ 5.95ns
// 地址建立时间:ADDSET=0(0个HCLK)
// 数据建立时间:DATAST=9(9个HCLK ≈ 53.6ns > 55ns,满足要求)
// 总线转向时间:BUSTURN=0
// ============================================================
static void fsmc_sram_init(void)
{
// -------------------------------------------------------
// 第一步:配置 GPIO 为 FSMC 功能(实际需要配置多个引脚)
// 这里省略 GPIO 配置代码,实际需要操作 RCC 和 GPIO 寄存器
// 典型引脚:PD0~PD1(D2~D3), PD4(NOE), PD5(NWE), PD7(NE1)
// PD8~PD15(D13~D15,D0~D7)
// PE0~PE1(NBL0,NBL1), PE7~PE15(D4~D12)
// PF0~PF5, PF12~PF15(A0~A5, A6~A9)
// PG0~PG5(A10~A15)等
// -------------------------------------------------------
// RCC->AHB3ENR |= RCC_AHB3ENR_FSMCEN; // 使能FSMC时钟
// ... GPIO配置 ...
// -------------------------------------------------------
// 第二步:配置 FSMC_BCR1(Bank1 控制寄存器)
//
// 选项解释:
// BCR_MBKEN = Bank使能(必须)
// BCR_MTYP_SRAM= SRAM类型
// BCR_MWID_16BIT = 16位数据总线
// BCR_WREN = 允许写操作
// BCR_EXTMOD = 使能扩展模式(独立的读写时序)
// -------------------------------------------------------
FSMC_BCR1 = BCR_MTYP_SRAM // 存储器类型:SRAM
| BCR_MWID_16BIT // 数据宽度:16位
| BCR_WREN // 允许写
| BCR_EXTMOD // 使用独立读写时序(Mode A)
| (1U << 7) // bit7 必须为1(手册要求保持reset值)
| BCR_MBKEN; // 最后使能Bank(必须在配置完成后)
// -------------------------------------------------------
// 第三步:配置 FSMC_BTR1(Bank1 读时序寄存器)
//
// Mode A 读时序参数:
// ADDSET = 0 → 地址建立时间 = 0 个 HCLK(地址立即有效)
// ADDHLD = 0 → 地址保持时间(Mode A 读操作不使用)
// DATAST = 9 → 数据建立时间 = 9 个 HCLK ≈ 53.6ns
// BUSTURN= 0 → 总线转向时间 = 0 个 HCLK
// ACCMOD = 0 → 访问模式 A(00)
//
// 实际读周期 = ADDSET + DATAST + 1(切换) = 0 + 9 + 1 = 10 个 HCLK
// -------------------------------------------------------
FSMC_BTR1 = (0U << 0) // ADDSET = 0
| (0U << 4) // ADDHLD = 0(Mode A 读操作不用)
| (9U << 8) // DATAST = 9
| (0U << 16) // BUSTURN = 0
| (0U << 20) // CLKDIV = 0(异步不用)
| (0U << 24) // DATLAT = 0(异步不用)
| (0U << 28); // ACCMOD = 0b00(模式A)
// -------------------------------------------------------
// 第四步:配置 FSMC_BWTR1(Bank1 写时序寄存器)
//
// 只在 EXTMOD=1 时有效(独立写时序)
// Mode A 写时序参数:
// ADDSET = 0 → 地址建立时间 = 0 个 HCLK
// DATAST = 4 → 数据建立时间 = 4+1 = 5 个 HCLK(写操作自动+1)
// BUSTURN= 0
// ACCMOD = 0 → 访问模式 A
//
// 实际写周期 = ADDSET + (DATAST+1) + 1 = 0 + 5 + 1 = 6 个 HCLK
// -------------------------------------------------------
FSMC_BWTR1 = (0U << 0) // ADDSET = 0
| (0U << 4) // ADDHLD = 0
| (4U << 8) // DATAST = 4(实际写有效时间=5HCLK)
| (0U << 16) // BUSTURN = 0
| (0U << 28); // ACCMOD = 0b00(模式A)
}
// ============================================================
// 函数:通过 FSMC 读写 SRAM
// ============================================================
// 写16位数据到 SRAM
// addr: 相对于 SRAM 起始地址的偏移(字节偏移)
// data: 要写入的16位数据
static void sram_write16(uint32_t addr, uint16_t data)
{
// 计算实际访问地址:SRAM基地址 + 字节偏移
volatile uint16_t *p = (volatile uint16_t *)(SRAM_BANK1_BASE + addr);
*p = data; // 直接写,FSMC 自动生成时序
}
// 从 SRAM 读16位数据
static uint16_t sram_read16(uint32_t addr)
{
volatile uint16_t *p = (volatile uint16_t *)(SRAM_BANK1_BASE + addr);
return *p; // 直接读,FSMC 自动生成时序
}
// 写32位数据(FSMC 自动拆分成两次16位访问)
static void sram_write32(uint32_t addr, uint32_t data)
{
volatile uint32_t *p = (volatile uint32_t *)(SRAM_BANK1_BASE + addr);
*p = data; // FSMC 自动处理32位→两次16位
}
// ============================================================
// 函数:初始化 Bank2 连接 8位 NAND Flash
// ============================================================
static void fsmc_nand_init(void)
{
// -------------------------------------------------------
// 配置 FSMC_PCR2(NAND Bank2 控制寄存器)
//
// PTYP = 1:NAND Flash 类型
// PBKEN = 1:使能 Bank
// PWID = 00:8位数据总线
// ECCEN = 0:暂时不使能ECC(在读写数据时单独使能)
// ECCPS = 001:每512字节计算一次ECC
// TAR = 1:ALE到RE的延迟 = (1+1+2)*HCLK = 4 HCLK
// TCLR = 1:CLE到RE的延迟 = (1+1+2)*HCLK = 4 HCLK
// -------------------------------------------------------
FSMC_PCR2 = PCR_PTYP_NAND // NAND类型
| PCR_PWID_8BIT // 8位数据总线
| PCR_ECCPS_512 // ECC页大小512字节
| (1U << PCR_TAR_SHIFT) // TAR=1
| (1U << PCR_TCLR_SHIFT) // TCLR=1
| PCR_PBKEN; // 使能Bank
// -------------------------------------------------------
// 配置 FSMC_PMEM2(通用存储空间时序寄存器)
//
// 以下参数适用于常见 NAND Flash(如 Samsung K9F1G08):
// MEMSET = 1 → 地址建立时间 = 1+1 = 2 HCLK ≈ 12ns(tCS min)
// MEMWAIT = 3 → 命令有效时间 = 3+1 = 4 HCLK ≈ 24ns(tWP min)
// MEMHOLD = 2 → 数据保持时间 = 2 HCLK(写)或4HCLK(读)
// MEMHIZ = 2 → 高阻时间 = 2 HCLK
// -------------------------------------------------------
FSMC_PMEM2 = (1U << 0) // MEMSET = 1
| (3U << 8) // MEMWAIT = 3
| (2U << 16) // MEMHOLD = 2
| (2U << 24); // MEMHIZ = 2
// -------------------------------------------------------
// 配置 FSMC_PATT2(属性存储空间时序寄存器)
// 用于最后一个地址字节写入(prewait 功能)
// ATTHOLD 需满足 tWB 时序要求
// -------------------------------------------------------
FSMC_PATT2 = (1U << 0) // ATTSET = 1
| (3U << 8) // ATTWAIT = 3
| (7U << 16) // ATTHOLD = 7(满足 tWB >= (7+1)*HCLK ≈ 48ns)
| (2U << 24); // ATTHIZ = 2
}
// ============================================================
// 函数:NAND Flash 发送命令
// ============================================================
static void nand_send_cmd(uint8_t cmd)
{
// 写到命令区(A[16]=1,CLE信号有效)
// Flash 看到 CLE=1 时,会把数据解释为命令
volatile uint8_t *cmd_addr = (volatile uint8_t *)(NAND_CMD_ADDR);
*cmd_addr = cmd;
}
// ============================================================
// 函数:NAND Flash 发送地址字节
// ============================================================
static void nand_send_addr(uint8_t addr_byte)
{
// 写到地址区(A[17]=1,ALE信号有效)
// Flash 看到 ALE=1 时,会把数据解释为地址
volatile uint8_t *addr_reg = (volatile uint8_t *)(NAND_ADDR_ADDR);
*addr_reg = addr_byte;
}
// ============================================================
// 函数:NAND Flash 读取一页数据(简化版,不含错误处理)
// page_addr: 页地址(对于 1Gb x8 NAND,地址分5个字节)
// buf: 数据缓冲区
// size: 要读取的字节数(最多一页大小,如2048字节)
// ============================================================
static void nand_read_page(uint32_t page_addr, uint8_t *buf, uint32_t size)
{
volatile uint8_t *data_reg = (volatile uint8_t *)(NAND_DATA_ADDR);
// 第1步:发送"读页"命令(Samsung NAND 使用 0x00)
nand_send_cmd(0x00);
// 第2步:发送列地址(列地址=页内偏移,这里从头读,发0)
// 列地址 byte1:偏移低8位
nand_send_addr(0x00);
// 列地址 byte2:偏移高4位(2048字节页只需低4位)
nand_send_addr(0x00);
// 第3步:发送行地址(页号)
// 行地址 byte1:页号低8位
nand_send_addr((uint8_t)(page_addr & 0xFF));
// 行地址 byte2:页号[15:8]
nand_send_addr((uint8_t)((page_addr >> 8) & 0xFF));
// 最后一个地址字节写到"属性存储空间"(使用PATT时序,满足tWB)
// 对于某些 NAND,这可以保证 NCE 在 R/NB 下降前保持低电平
// 行地址 byte3:页号[17:16](写到属性区)
volatile uint8_t *attr_reg = (volatile uint8_t *)(NAND_BANK2_BASE | 0x00020000 | 0x0040000);
*attr_reg = (uint8_t)((page_addr >> 16) & 0x03);
// 第4步:发送"读开始"命令(Samsung NAND 使用 0x30)
nand_send_cmd(0x30);
// 第5步:等待 NAND Flash 准备好(等待 R/NB 为高)
// 实际项目中应等待 NWAIT/INT 引脚变高,或轮询GPIO
simple_delay(10000); // 简化处理,实际需要检测R/NB
// 第6步:逐字节读取数据
for (uint32_t i = 0; i < size; i++)
{
buf[i] = *data_reg; // NAND内部地址自动递增,无需改变读取地址
}
}
// ============================================================
// 函数:使用 ECC 写入 NAND 一页数据
// ============================================================
static uint32_t nand_write_page_with_ecc(uint32_t page_addr,
const uint8_t *buf,
uint32_t size)
{
volatile uint8_t *data_reg = (volatile uint8_t *)(NAND_DATA_ADDR);
// 第1步:使能 ECC 计算
FSMC_PCR2 |= PCR_ECCEN;
// 第2步:发送"页写入"命令
nand_send_cmd(0x80);
// 第3步:发送地址
nand_send_addr(0x00);
nand_send_addr(0x00);
nand_send_addr((uint8_t)(page_addr & 0xFF));
nand_send_addr((uint8_t)((page_addr >> 8) & 0xFF));
nand_send_addr((uint8_t)((page_addr >> 16) & 0x03));
// 第4步:写入数据(ECC 硬件自动计算)
for (uint32_t i = 0; i < size; i++)
{
*data_reg = buf[i];
}
// 第5步:等待 FIFO 排空,确保 ECC 计算完整
// FEMPT=1 表示 FIFO 已空
while (!(FSMC_SR2 & SR_FEMPT)) {}
// 第6步:读取 ECC 值
uint32_t ecc_value = FSMC_ECCR2;
// 第7步:禁用 ECC(清零后才能开始下一次计算)
FSMC_PCR2 &= ~PCR_ECCEN;
// 第8步:发送"页写入确认"命令
nand_send_cmd(0x10);
// 等待写操作完成
simple_delay(10000);
// 返回计算得到的 ECC 值(调用者需要将其写入 Spare Area)
return ecc_value;
}
// ============================================================
// 主函数:演示 FSMC 的使用
// ============================================================
int main(void)
{
// -------------------------------------------------------
// 初始化 SRAM(Bank1)
// -------------------------------------------------------
fsmc_sram_init();
// -------------------------------------------------------
// 测试 SRAM 读写
// -------------------------------------------------------
// 写入测试数据
sram_write16(0x0000, 0xABCD); // 写16位数据到偏移0
sram_write16(0x0002, 0x1234); // 写16位数据到偏移2
sram_write32(0x0004, 0xDEADBEEF); // 写32位数据到偏移4(自动拆分)
// 读回验证
uint16_t val1 = sram_read16(0x0000); // 应读到 0xABCD
uint16_t val2 = sram_read16(0x0002); // 应读到 0x1234
// 简单验证(实际项目可加断言)
(void)val1;
(void)val2;
// -------------------------------------------------------
// 初始化 NAND Flash(Bank2)
// -------------------------------------------------------
fsmc_nand_init();
// -------------------------------------------------------
// 读取 NAND 第0页数据
// -------------------------------------------------------
uint8_t page_buf[2048];
nand_read_page(0, page_buf, 2048);
// -------------------------------------------------------
// 主循环
// -------------------------------------------------------
while (1)
{
// 业务逻辑
}
return 0;
}
第十章:时序参数计算实例
10.1 SRAM 时序计算(IS62WV51216BLL-55,HCLK=168MHz)
已知条件:
- tHCLK=1168MHz≈5.95nst_{HCLK} = \frac{1}{168\text{MHz}} \approx 5.95\text{ns}tHCLK=168MHz1≈5.95ns
- 地址建立时间 tACS=0nst_{ACS} = 0\text{ns}tACS=0ns(无需等待)
- 读访问时间 tRC=55nst_{RC} = 55\text{ns}tRC=55ns(芯片手册)
- 写保持时间 tWH=5nst_{WH} = 5\text{ns}tWH=5ns
计算 DATAST(读):
DATASTread≥tRC−ADDSET×tHCLKtHCLK=55−05.95≈9.24DATAST_{read} \geq \frac{t_{RC} - ADDSET \times t_{HCLK}}{t_{HCLK}} = \frac{55 - 0}{5.95} \approx 9.24DATASTread≥tHCLKtRC−ADDSET×tHCLK=5.9555−0≈9.24
取整为 DATASTread=10DATAST_{read} = 10DATASTread=10,实际读访问时间 = 10×5.95=59.5ns>55ns10 \times 5.95 = 59.5\text{ns} > 55\text{ns}10×5.95=59.5ns>55ns,满足。
计算 DATAST(写):
写操作实际持续时间 = (DATAST+1)×tHCLK(DATAST+1) \times t_{HCLK}(DATAST+1)×tHCLK,需要 ≥tWH\geq t_{WH}≥tWH:
DATASTwrite≥tWHtHCLK−1=55.95−1≈−0.16DATAST_{write} \geq \frac{t_{WH}}{t_{HCLK}} - 1 = \frac{5}{5.95} - 1 \approx -0.16DATASTwrite≥tHCLKtWH−1=5.955−1≈−0.16
取 DATASTwrite=4DATAST_{write} = 4DATASTwrite=4(写操作 = 5个 HCLK ≈ 29.75ns,满足要求)。
10.2 NAND Flash 时序计算(HCLK=168MHz)
对于 NAND Flash tWP(写脉冲宽度)最小 12ns:
MEMWAIT≥tWPtHCLK−1=125.95−1≈1.02MEMWAIT \geq \frac{t_{WP}}{t_{HCLK}} - 1 = \frac{12}{5.95} - 1 \approx 1.02MEMWAIT≥tHCLKtWP−1=5.9512−1≈1.02
取 MEMWAIT=2MEMWAIT = 2MEMWAIT=2,实际写脉冲 = (2+1)×5.95=17.85ns>12ns(2+1) \times 5.95 = 17.85\text{ns} > 12\text{ns}(2+1)×5.95=17.85ns>12ns,满足。
第十一章:完整流程总结
附录:寄存器速查表
| 寄存器 | 地址偏移(Bank x) | 用途 |
|---|---|---|
| FSMC_BCR1~4 | 0x0000, 0x0008, 0x0010, 0x0018 | NOR/SRAM/PSRAM 控制 |
| FSMC_BTR1~4 | 0x0004, 0x000C, 0x0014, 0x001C | NOR/SRAM/PSRAM 读时序 |
| FSMC_BWTR1~4 | 0x0104, 0x010C, 0x0114, 0x011C | NOR/SRAM/PSRAM 写时序(扩展模式) |
| FSMC_PCR2~4 | 0x0060, 0x0080, 0x00A0 | NAND/PC Card 控制 |
| FSMC_SR2~4 | 0x0064, 0x0084, 0x00A4 | FIFO状态和中断 |
| FSMC_PMEM2~4 | 0x0068, 0x0088, 0x00A8 | 通用存储空间时序 |
| FSMC_PATT2~4 | 0x006C, 0x008C, 0x00AC | 属性存储空间时序 |
| FSMC_PIO4 | 0x00B0 | PC Card IO空间时序 |
| FSMC_ECCR2~3 | 0x0074, 0x0094 | ECC 计算结果 |
STM32 FMC(灵活存储控制器)从零详解
适用芯片:STM32F42xxx / STM32F43xxx
参考手册:RM0090
一、FMC 是什么?为什么需要它?
想象一下,STM32 的 CPU 只认识自己内部的 SRAM 和 Flash,但实际项目里往往需要更大的存储空间——比如外接一块 SDRAM 芯片来跑图形界面,或者外接 NOR Flash 来存放大量数据。
FMC(Flexible Memory Controller,灵活存储控制器) 就是 STM32 和这些外部存储芯片之间的"翻译官"。CPU 只需要像访问普通内存一样读写某个地址,FMC 自动把这个操作翻译成对应芯片能听懂的时序信号。
CPU (AHB总线)
|
| 普通的读/写请求(地址 + 数据)
v
[ FMC ] <--- 你要配置的控制器
|
| 翻译成各种芯片专用时序
v
外部存储芯片(SRAM / NOR Flash / NAND Flash / SDRAM)
二、FMC 内部结构
FMC 由五个主要模块组成:
+--------------------------------------------------+
| FMC 内部 |
| |
| +------------+ AHB 接口(配置寄存器) |
| | AHB 接口 | <-- CPU 通过这里访问 FMC |
| +------------+ |
| | |
| +----+----+----------+-----------+ |
| | | | | |
| v v v v |
| NOR/PSRAM NAND/PC SDRAM 外部设备 |
| 控制器 Card控制器 控制器 接口引脚 |
+--------------------------------------------------+
三、FMC 支持的存储器类型
FMC 支持三大类存储器,每类有对应的控制器:
| 存储器类型 | 控制器 | 典型用途 |
|---|---|---|
| SRAM / NOR Flash / PSRAM | NOR/PSRAM 控制器 | 代码存储、大容量数据缓存 |
| NAND Flash / PC Card | NAND/PC Card 控制器 | 文件系统存储(类似 U 盘) |
| SDRAM / LPSDR SDRAM | SDRAM 控制器 | 大容量高速内存(跑 GUI 等) |
四、外部存储器地址映射——FMC 的"地图"
FMC 把 STM32 的地址空间划分成 6 个固定大小的"银行"(Bank),每个银行 256MB,分别对应不同类型的存储器。
地址空间布局(简化)
0x6000_0000 +-----------------------+
| Bank 1 |
| NOR/PSRAM/SRAM |
| 4 个子银行,各 64MB |
| Bank1-NOR1 (NE1) |
| Bank1-NOR2 (NE2) |
| Bank1-NOR3 (NE3) |
| Bank1-NOR4 (NE4) |
0x6FFF_FFFF +-----------------------+
0x7000_0000 +-----------------------+
| Bank 2 |
| NAND Flash |
0x7FFF_FFFF +-----------------------+
0x8000_0000 +-----------------------+
| Bank 3 |
| NAND Flash |
0x8FFF_FFFF +-----------------------+
0x9000_0000 +-----------------------+
| Bank 4 |
| PC Card |
0x9FFF_FFFF +-----------------------+
| (保留区域) |
0xC000_0000 +-----------------------+
| SDRAM Bank 1 |
| 256MB |
0xCFFF_FFFF +-----------------------+
0xD000_0000 +-----------------------+
| SDRAM Bank 2 |
| 256MB |
0xDFFF_FFFF +-----------------------+
Bank 1 内部的子银行选择
Bank 1 又细分为 4 个子银行,由地址线 HADDR[27:26] 决定选哪个:
| HADDR[27:26] | 选中的子银行 |
|---|---|
| 00 | Bank1-NOR/PSRAM 1(NE1 引脚拉低) |
| 01 | Bank1-NOR/PSRAM 2(NE2 引脚拉低) |
| 10 | Bank1-NOR/PSRAM 3(NE3 引脚拉低) |
| 11 | Bank1-NOR/PSRAM 4(NE4 引脚拉低) |
五、NOR Flash / PSRAM / SRAM 控制器详解
5.1 地址翻译规则
CPU 发出的地址(HADDR)是字节地址,但外部存储器可能是 8 位、16 位或 32 位宽度,需要换算:
| 存储器位宽 | FMC 发给芯片的地址 | 最大容量 |
|---|---|---|
| 8 位 | HADDR[25:0](直接用) | 64MB × 8 = 512Mbit |
| 16 位 | HADDR[25:1] 右移1位 | 64MB/2 × 16 = 512Mbit |
| 32 位 | HADDR[25:2] 右移2位 | 64MB/4 × 32 = 512Mbit |
理解这个换算: 16 位存储器每个地址存 2 字节,所以同样的字节地址 / 2 才是芯片的字地址。
5.2 异步访问模式
异步模式下,FMC 通过 HCLK(AHB 时钟)计数来控制时序,不需要给存储器发时钟信号。一共有 6 种模式:
模式速查
Mode 1 --> SRAM/PSRAM 默认模式(无 NOE 切换)
Mode 2 --> NOR Flash 默认模式(有 NADV 信号)
Mode A --> SRAM/PSRAM + NOE 切换(独立读写时序)
Mode B --> NOR Flash + NWE 切换(独立读写时序)
Mode C --> NOR Flash + NOE 切换(独立读写时序)
Mode D --> 带地址保持阶段(ADDHLD)的扩展模式
Mode 1 时序图解析(SRAM 读操作)
时间轴 ->
HCLK ____|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_
NEx ‾‾‾‾‾|_________________________|‾‾‾
(NEx 拉低,选中芯片)
A[] ----<====地址稳定输出============>----
| ADDSET | DATAST |
| (地址建立)| (数据建立) |
NOE ‾‾‾‾‾‾‾‾‾‾‾|_________________|‾‾‾
(NOE 拉低,允许芯片输出数据)
D[] --------------------<==数据==>--------
(存储芯片把数据放到总线上)
各阶段含义:
- ADDSET(地址建立时间):地址信号稳定到 NOE/NWE 有效之间的等待周期数,单位 HCLK
- DATAST(数据建立时间):NOE/NWE 有效到数据采样之间的周期数,单位 HCLK
- BUSTURN(总线周转时间):NEx 拉高后到下次 NEx 拉低之间的间隔,防止总线冲突
Mode 1 写操作
时间轴 ->
NEx ‾‾‾|_____________________________________|‾‾‾
A[] ---<=====地址=======>---
ADDSET
NWE ‾‾‾‾‾‾‾‾‾‾‾|________________|‾‾‾|‾‾‾‾‾
(DATAST+1) 1HCLK
(保持时间)
D[] --------<============数据============>-------
(CPU 把数据放到总线)
注意写操作的数据建立时间是 DATAST+1 个 HCLK,比读多 1 个,这 1 个额外周期保证地址和数据在 NWE 上升沿后仍然保持稳定。因此 DATAST 必须大于 0。
5.3 关键寄存器 FMC_BCRx(控制寄存器)
每个子银行对应一个 BCR 寄存器(BCR1~BCR4),控制该银行的工作模式。
// 地址偏移:8 × (x-1),x=1~4
// 例如 Bank1 的 BCR 寄存器地址 = FMC基地址 + 0x00
// Bank2 的 BCR 寄存器地址 = FMC基地址 + 0x08
// FMC 基地址(STM32F4)
#define FMC_BASE 0xA0000000UL
#define FMC_BCR1 (*(volatile uint32_t *)(FMC_BASE + 0x00))
#define FMC_BCR2 (*(volatile uint32_t *)(FMC_BASE + 0x08))
主要位域说明:
| 位 | 名称 | 含义 |
|---|---|---|
| [0] | MBKEN | 1=使能该 Bank,0=禁用(访问禁用 Bank 会触发 AHB 错误) |
| [1] | MUXEN | 1=地址/数据复用模式,0=非复用 |
| [3:2] | MTYP | 00=SRAM,01=PSRAM,10=NOR Flash |
| [5:4] | MWID | 00=8位,01=16位,10=32位 |
| [6] | FACCEN | 1=允许 NOR Flash 访问 |
| [8] | BURSTEN | 1=开启同步突发读模式 |
| [11] | WAITCFG | NWAIT 信号时序配置 |
| [12] | WREN | 1=允许写操作 |
| [14] | EXTMOD | 1=使用独立的写时序寄存器 FMC_BWTRx |
| [15] | ASYNCWAIT | 1=异步模式下也响应 NWAIT 信号 |
| [20] | CCLKEN | 1=连续输出时钟(即使没有访问也持续输出) |
5.4 关键寄存器 FMC_BTRx(读时序寄存器)
// 地址偏移:0x04 + 8*(x-1)
// Bank1 对应 0x04,Bank2 对应 0x0C,以此类推
#define FMC_BTR1 (*(volatile uint32_t *)(FMC_BASE + 0x04))
| 位 | 名称 | 含义 |
|---|---|---|
| [3:0] | ADDSET | 地址建立阶段持续 ADDSET 个 HCLK |
| [7:4] | ADDHLD | 地址保持阶段(仅 Mode D/复用模式有效) |
| [15:8] | DATAST | 数据建立阶段持续 DATAST 个 HCLK(读)/ DATAST+1(写) |
| [19:16] | BUSTURN | 总线周转时间(0~15 个 HCLK) |
| [23:20] | CLKDIV | 同步时钟分频:FMC_CLK = HCLK / (CLKDIV+1) |
| [27:24] | DATLAT | 同步模式数据延迟(单位:FMC_CLK 周期) |
| [29:28] | ACCMOD | 访问模式选择:00=A,01=B,10=C,11=D |
5.5 同步访问(Burst 模式)
同步模式下,FMC 会给存储器发送时钟信号 FMC_CLK,存储器在时钟边沿输出连续数据(突发传输),速度更快。
FMC_CLK 频率计算公式(一般情况):
FMC_CLK=HCLKCLKDIV+1FMC\_CLK = \frac{HCLK}{CLKDIV + 1}FMC_CLK=CLKDIV+1HCLK
当存储器位宽为 32 位且 AHB 数据宽度小于 32 位时,公式变为:
FMC_CLK分频比=max(CLKDIV+1, MWIDAHB数据宽度)FMC\_CLK 分频比 = \max\left(CLKDIV + 1,\ \frac{MWID}{AHB数据宽度}\right)FMC_CLK分频比=max(CLKDIV+1, AHB数据宽度MWID)
举例:
- CLKDIV=1,MWID=32位,AHB数据=8位:FMC_CLK=HCLK/4FMC\_CLK = HCLK/4FMC_CLK=HCLK/4
- CLKDIV=1,MWID=16位,AHB数据=8位:FMC_CLK=HCLK/2FMC\_CLK = HCLK/2FMC_CLK=HCLK/2
六、NAND Flash / PC Card 控制器详解
6.1 NAND Flash 的特殊访问方式
NAND Flash 不像 SRAM 那样直接用地址线访问,而是通过命令/地址/数据三种操作来控制:
NAND Flash 访问地址空间(以 Bank2 为例,起始 0x7000_0000)
0x7000_0000 +----------------------+
| 数据区(Data) | HADDR[17:16] = 00
| 0x0000 ~ 0x0FFFF | 向这里读写 = 传数据
0x7001_0000 +----------------------+
| 命令区(Command) | HADDR[17:16] = 01
| 0x10000 ~ 0x1FFFF | 向这里写 = 发命令
0x7002_0000 +----------------------+
| 地址区(Address) | HADDR[17:16] = 1X
| 0x20000 ~ 0x3FFFF | 向这里写 = 发地址
0x7004_0000 +----------------------+
一次典型的 NAND Flash 读页操作流程:
6.2 ECC 错误纠正码
NAND Flash 存储时间长了容易出现位翻转错误,FMC 内置了 ECC 硬件模块自动计算校验码。
算法: Hamming 码(可以纠正 1 位错误,检测 2 位错误)
使用步骤:
1. 置位 ECCEN(FMC_PCRx[6])开始计算
|
2. 向 NAND Flash 写数据
(硬件自动在后台计算 ECC)
|
3. 读取 FMC_ECCRx 寄存器,保存 ECC 值
|
4. 清除 ECCEN,再置位 ECCEN
|
5. 从 NAND Flash 读回同一段数据
(硬件自动计算读取数据的 ECC)
|
6. 读取新的 FMC_ECCRx 值
|
7. 比较两次 ECC 值:
相同 -> 数据正确
不同 -> 有错误,软件进行纠正
ECC 支持的页大小(由 ECCPS[2:0] 配置):
| ECCPS[2:0] | 页大小 | ECC 有效位 |
|---|---|---|
| 000 | 256 字节 | ECC[21:0],22位 |
| 001 | 512 字节 | ECC[23:0],24位 |
| 010 | 1024 字节 | ECC[25:0],26位 |
| 011 | 2048 字节 | ECC[27:0],28位 |
| 100 | 4096 字节 | ECC[29:0],30位 |
| 101 | 8192 字节 | ECC[31:0],32位 |
6.3 NAND Flash 时序参数
每次访问分三个阶段,通过 FMC_PMEMx 寄存器配置:
时间轴 ->
NCEx ‾‾‾|___________________________________|‾‾‾
A[] |<-- MEMSET+1 -->|
(地址建立阶段)
NWE/NOE |<---- MEMWAIT+1 ---->|
(命令有效阶段)
数据/地址 |<-- MEMHOLD -->|
(数据保持阶段)
写操作还有 MEMHIZ:在 NWE 有效前,数据总线保持高阻态的时间
七、SDRAM 控制器详解
7.1 SDRAM 是什么
SDRAM(同步动态随机存取存储器)是最常见的外部高速内存,比 SRAM 便宜得多,容量大,但需要定期刷新(否则数据会丢失),访问时序也比 SRAM 复杂。
7.2 SDRAM 地址映射
HADDR[28] 决定访问哪个 SDRAM Bank:
| HADDR[28] | 选中的银行 | 控制寄存器 |
|---|---|---|
| 0 | SDRAM Bank 1 | FMC_SDCR1 / FMC_SDTR1 |
| 1 | SDRAM Bank 2 | FMC_SDCR2 / FMC_SDTR2 |
SDRAM 地址由三部分组成(以 16 位宽、13 行、11 列为例):
HADDR 各位的含义(16位数据总线,13行,11列):
Bit: 27 26 25 24 23 ... 13 12 ... 2 1 0
| | | | | | | | |
Res BA1 BA0 Row[12]...Row[0] Col[10]...Col[0] BM0
BA[1:0] = HADDR[26:25] -> 内部 Bank 选择(4个内部Bank用2位)
Row[12:0]= HADDR[24:12] -> 行地址(13位)
Col[10:0]= HADDR[11:1] -> 列地址(11位)
BM0 = HADDR[0] -> 字节掩码(16位模式)
7.3 SDRAM 初始化流程
SDRAM 上电后必须按照严格顺序初始化,不能直接读写:
7.4 SDRAM 时序参数详解
SDRAM 的时序参数在 FMC_SDTRx 中配置,单位均为 SDRAM 时钟周期:
SDRAM 访问涉及的主要命令序列:
1. Active(激活行):选中一个 Bank 中的一行
|
| TRCD 周期(行到列延迟)
v
2. Read/Write(读/写列):从激活的行中读写数据
|
| TWR 周期(写恢复时间,仅写操作)
v
3. Precharge(预充电):关闭当前行
|
| TRP 周期(预充电等待时间)
v
4. Active(可以激活下一行了)
各时序参数含义:
| 参数 | 寄存器位 | 含义 |
|---|---|---|
| TMRD | [3:0] | Load Mode Register 到 Active 的延迟 |
| TXSR | [7:4] | 退出 Self-Refresh 到 Active 的延迟 |
| TRAS | [11:8] | Self-Refresh 最短时间(行激活到预充电的最小间隔) |
| TRC | [15:12] | 两次 Refresh 命令之间的间隔(也是 Refresh 到 Active 的延迟) |
| TWR | [19:16] | 写操作后到 Precharge 之间的恢复延迟 |
| TRP | [23:20] | Precharge 到下一个命令的延迟 |
| TRCD | [27:24] | Active(行激活)到 Read/Write 之间的延迟 |
TWR 的计算约束(必须满足以下两个条件):
TWR≥TRAS−TRCDTWR \geq TRAS - TRCDTWR≥TRAS−TRCD
TWR≥TRC−TRCD−TRPTWR \geq TRC - TRCD - TRPTWR≥TRC−TRCD−TRP
7.5 CAS 延迟与读 FIFO
SDRAM 读操作的特殊之处:发出读命令后,数据不是立即出现,而是经过 CAS 延迟(1~3 个时钟)才出现。
FMC 内置了一个 6 行 × 32 位的缓存 FIFO,在 CAS 延迟期间预读数据,减少等待。
预读数据量计算公式:
预读数据数=⌊CAS延迟−1+RPIPE延迟2⌋预读数据数 = \left\lfloor \frac{CAS延迟 - 1 + RPIPE延迟}{2} \right\rfloor预读数据数=⌊2CAS延迟−1+RPIPE延迟⌋
举例:
- CAS=3,RPIPE=0:⌊(3−1+0)/2⌋=1\lfloor(3-1+0)/2\rfloor = 1⌊(3−1+0)/2⌋=1,存 4 个数据在 FIFO(CAS-1+1=3,再加RPIPE=0,实为 CAS latency-1+RPIPE+1 个)
- CAS=3,RPIPE=2:预读 5 个数据
7.6 自动刷新频率计算
SDRAM 必须定期刷新(一般每 64ms 刷新所有行一遍),否则数据丢失。
刷新计数器值的计算公式:
COUNT=(刷新速率×SDRAM时钟频率)−20COUNT = (刷新速率 \times SDRAM时钟频率) - 20COUNT=(刷新速率×SDRAM时钟频率)−20
其中:
刷新速率=刷新周期行数刷新速率 = \frac{刷新周期}{行数}刷新速率=行数刷新周期
实际举例(64ms 刷新,8192 行,60MHz SDRAM 时钟):
刷新速率=64 ms8192≈7.81 μs刷新速率 = \frac{64\ ms}{8192} \approx 7.81\ \mu s刷新速率=819264 ms≈7.81 μs
COUNT=7.81×10−6×60×106−20=468.6−20≈448COUNT = 7.81 \times 10^{-6} \times 60 \times 10^6 - 20 = 468.6 - 20 \approx 448COUNT=7.81×10−6×60×106−20=468.6−20≈448
注意:减去 20 是为了留出安全余量,防止刷新请求和访问请求同时发生时超时。COUNT 最小值为 41。
7.7 低功耗模式
SDRAM 支持两种低功耗模式:
两种低功耗模式对比
Self-Refresh 模式(MODE=101):
- SDRAM 自己做刷新,不需要外部时钟
- SDCLK 停止
- 数据完全保持,功耗最低
- 进入前 FMC 自动发 PALL 命令
Power-Down 模式(MODE=110):
- SDCLK 继续运行
- SDRAM 输入/输出缓冲关闭
- FMC 代替 SDRAM 执行刷新
- 适合短暂休眠
八、WAIT 信号机制
部分存储器速度较慢,当它还没准备好数据时,会拉低 NWAIT 引脚告诉 FMC:“等一下!”
异步 WAIT: 在数据建立阶段(DATAST)结束前 4 个 HCLK 必须能检测到 NWAIT,因此:
DATAST≥4+最大WAIT声明时间(单位:HCLK)DATAST \geq 4 + 最大WAIT声明时间(单位:HCLK)DATAST≥4+最大WAIT声明时间(单位:HCLK)
同步 WAIT(NOR Flash): 在 DATLAT+2 个 CLK 周期后检测,支持两种 WAIT 配置(WAITCFG=0 表示 NWAIT 在等待状态前一个周期有效,WAITCFG=1 表示在等待状态期间有效)。
九、完整 C++ 配置示例
下面是一个完整可运行的 STM32F4 FMC 配置示例,演示如何配置 Bank1 连接 16 位 SRAM(Mode 1,异步):
/*
* 文件:fmc_sram_example.cpp
* 功能:配置 STM32F4 FMC,使用 Mode 1 异步模式连接 16 位 SRAM
*
* 硬件假设:
* - SRAM 连接到 FMC Bank1-NOR/PSRAM1(使用 NE1 片选)
* - 数据总线:16 位
* - 系统时钟 HCLK = 168MHz
* - SRAM 访问时间 = 55ns(常见芯片如 IS61WV102416BLL)
* - 地址建立时间 tAS = 0ns,数据建立时间 tRC = 55ns
*
* 时序计算(HCLK=168MHz,每个周期约 5.95ns):
* ADDSET = ceil(tAS / T_HCLK) = ceil(0/5.95) = 0,最小值取 0
* DATAST = ceil(tRC / T_HCLK) = ceil(55/5.95) = ceil(9.24) = 10
* 但 DATAST 必须 > 0,取 10
*/
#include <cstdint>
// ---------- 寄存器地址定义 ----------
// FMC 寄存器基地址(STM32F42xxx/F43xxx)
static constexpr uint32_t FMC_BASE_ADDR = 0xA0000000UL;
// RCC 寄存器(用于使能 FMC 时钟)
static constexpr uint32_t RCC_BASE_ADDR = 0x40023800UL;
static constexpr uint32_t RCC_AHB3ENR = 0x38UL; // AHB3 外设时钟使能寄存器偏移
// GPIO 寄存器(用于配置 FMC 引脚为复用功能)
// 以 GPIOD 为例(FMC_D2, D3, CLK, NOE, NWE, NE1, D13-D15 等在 GPIOD)
static constexpr uint32_t GPIOD_BASE = 0x40020C00UL;
static constexpr uint32_t GPIOE_BASE = 0x40021000UL;
static constexpr uint32_t GPIOF_BASE = 0x40021400UL;
static constexpr uint32_t GPIOG_BASE = 0x40021800UL;
// GPIO 寄存器偏移
static constexpr uint32_t GPIO_MODER = 0x00UL; // 模式寄存器
static constexpr uint32_t GPIO_OSPEEDR = 0x08UL; // 速度寄存器
static constexpr uint32_t GPIO_PUPDR = 0x0CUL; // 上下拉寄存器
static constexpr uint32_t GPIO_AFRL = 0x20UL; // 复用功能低位寄存器(Pin0-7)
static constexpr uint32_t GPIO_AFRH = 0x24UL; // 复用功能高位寄存器(Pin8-15)
// FMC Bank1 寄存器偏移
static constexpr uint32_t FMC_BCR1_OFFSET = 0x00UL; // Bank1 控制寄存器
static constexpr uint32_t FMC_BTR1_OFFSET = 0x04UL; // Bank1 时序寄存器(读)
// ---------- 辅助函数:寄存器读写 ----------
/**
* @brief 读取 32 位寄存器
* @param base 外设基地址
* @param offset 寄存器偏移
*/
static inline uint32_t reg_read(uint32_t base, uint32_t offset)
{
return *(volatile uint32_t *)(base + offset);
}
/**
* @brief 写 32 位寄存器
* @param base 外设基地址
* @param offset 寄存器偏移
* @param value 写入值
*/
static inline void reg_write(uint32_t base, uint32_t offset, uint32_t value)
{
*(volatile uint32_t *)(base + offset) = value;
}
/**
* @brief 对寄存器进行位域修改(读-改-写)
* @param base 外设基地址
* @param offset 寄存器偏移
* @param mask 要修改的位掩码
* @param value 新值(已移位到正确位置)
*/
static inline void reg_modify(uint32_t base, uint32_t offset, uint32_t mask, uint32_t value)
{
uint32_t tmp = reg_read(base, offset);
tmp &= ~mask; // 清除目标位
tmp |= value; // 写入新值
reg_write(base, offset, tmp);
}
// ---------- 步骤一:使能 FMC 时钟 ----------
/**
* @brief 使能 FMC 外设时钟
*
* FMC 挂载在 AHB3 总线上,需要在 RCC_AHB3ENR 中置位第 0 位
*/
static void fmc_clock_enable()
{
// RCC_AHB3ENR bit0 = FMCEN(FMC 时钟使能)
reg_modify(RCC_BASE_ADDR, RCC_AHB3ENR, (1UL << 0), (1UL << 0));
}
// ---------- 步骤二:配置 GPIO 为 FMC 复用功能 ----------
/**
* @brief 配置单个 GPIO 引脚为复用功能模式
* @param gpio_base GPIO 端口基地址(GPIOD_BASE 等)
* @param pin 引脚编号(0~15)
* @param af_num 复用功能编号(FMC 对应 AF12)
*/
static void gpio_set_af(uint32_t gpio_base, uint8_t pin, uint8_t af_num)
{
// 1. 设置为复用功能模式(MODER[2*pin+1:2*pin] = 10)
uint32_t moder_mask = (0x3UL << (pin * 2));
uint32_t moder_val = (0x2UL << (pin * 2)); // 10 = 复用功能
reg_modify(gpio_base, GPIO_MODER, moder_mask, moder_val);
// 2. 设置为高速输出(OSPEEDR[2*pin+1:2*pin] = 11)
uint32_t speed_mask = (0x3UL << (pin * 2));
uint32_t speed_val = (0x3UL << (pin * 2)); // 11 = 最高速
reg_modify(gpio_base, GPIO_OSPEEDR, speed_mask, speed_val);
// 3. 无上下拉(PUPDR[2*pin+1:2*pin] = 00)
uint32_t pupd_mask = (0x3UL << (pin * 2));
reg_modify(gpio_base, GPIO_PUPDR, pupd_mask, 0UL);
// 4. 设置复用功能编号(AFR[H/L])
if (pin < 8)
{
// AFRL:管理 Pin0~Pin7
uint32_t afrl_mask = (0xFUL << (pin * 4));
uint32_t afrl_val = ((uint32_t)af_num << (pin * 4));
reg_modify(gpio_base, GPIO_AFRL, afrl_mask, afrl_val);
}
else
{
// AFRH:管理 Pin8~Pin15
uint8_t pin_h = pin - 8;
uint32_t afrh_mask = (0xFUL << (pin_h * 4));
uint32_t afrh_val = ((uint32_t)af_num << (pin_h * 4));
reg_modify(gpio_base, GPIO_AFRH, afrh_mask, afrh_val);
}
}
/**
* @brief 配置所有 FMC 引脚为 AF12(FMC 复用功能)
*
* 注意:实际项目中需要根据原理图确定用到了哪些引脚
* 这里只是演示最常用的 16 位 SRAM Bank1 配置
*
* GPIOD: D2(PD0), D3(PD1), CLK(PD3), NOE(PD4), NWE(PD5),
* NE1(PD7), D13(PD8), D14(PD9), D15(PD10), D0(PD14), D1(PD15)
* GPIOE: NBL0(PE0), NBL1(PE1), D4(PE7), D5(PE8), D6(PE9),
* D7(PE10), D8(PE11), D9(PE12), D10(PE13), D11(PE14), D12(PE15)
* GPIOF: A0-A5(PF0-PF5), A6-A9(PF12-PF15)
* GPIOG: A10(PG0), A11(PG1), A12(PG2), A13(PG3), A14(PG4), A15(PG5)
*/
static void fmc_gpio_init()
{
constexpr uint8_t FMC_AF = 12; // FMC 对应 AF12
// --- GPIOD 引脚 ---
const uint8_t gpiod_pins[] = {0, 1, 3, 4, 5, 7, 8, 9, 10, 14, 15};
for (uint8_t pin : gpiod_pins)
gpio_set_af(GPIOD_BASE, pin, FMC_AF);
// --- GPIOE 引脚 ---
const uint8_t gpioe_pins[] = {0, 1, 7, 8, 9, 10, 11, 12, 13, 14, 15};
for (uint8_t pin : gpioe_pins)
gpio_set_af(GPIOE_BASE, pin, FMC_AF);
// --- GPIOF 引脚(地址线 A0~A9)---
const uint8_t gpiof_pins[] = {0, 1, 2, 3, 4, 5, 12, 13, 14, 15};
for (uint8_t pin : gpiof_pins)
gpio_set_af(GPIOF_BASE, pin, FMC_AF);
// --- GPIOG 引脚(地址线 A10~A15)---
const uint8_t gpiog_pins[] = {0, 1, 2, 3, 4, 5};
for (uint8_t pin : gpiog_pins)
gpio_set_af(GPIOG_BASE, pin, FMC_AF);
}
// ---------- 步骤三:配置 FMC BCR1 和 BTR1 ----------
/**
* @brief 配置 FMC Bank1 为 16 位异步 SRAM(Mode 1)
*
* 时序参数说明(HCLK = 168MHz,周期 ≈ 5.95ns):
* 目标 SRAM:访问时间 55ns
* ADDSET = 0(地址建立 0 × 5.95 = 0ns,满足 tAS=0ns 要求)
* DATAST = 10(读数据建立 10 × 5.95 = 59.5ns > 55ns,满足要求)
* 写操作实际 = DATAST+1 = 11 × 5.95 = 65.5ns
* BUSTURN = 1(总线周转 1 × 5.95 = 5.95ns,避免连续访问总线冲突)
*/
static void fmc_bank1_sram_init()
{
// ===== 配置 FMC_BCR1 =====
// 各位含义参见本文第 5.3 节
uint32_t bcr1 = 0;
bcr1 |= (1UL << 0); // MBKEN=1:使能 Bank1
bcr1 |= (0UL << 1); // MUXEN=0:地址/数据非复用
bcr1 |= (0x0UL << 2); // MTYP=00:SRAM 类型
bcr1 |= (0x1UL << 4); // MWID=01:16 位数据总线
bcr1 |= (0UL << 6); // FACCEN=0:不访问 NOR Flash(SRAM 不需要)
bcr1 |= (0UL << 7); // 保留位必须为 1(实际复位值为1,此处显式设置)
bcr1 |= (1UL << 7); // 保留位=1(按复位值要求)
bcr1 |= (0UL << 8); // BURSTEN=0:关闭同步突发读(使用异步模式)
bcr1 |= (0UL << 9); // WAITPOL=0:NWAIT 低电平有效(本例不用 WAIT)
bcr1 |= (0UL << 11); // WAITCFG:不关心(WAITEN=0 时无效)
bcr1 |= (1UL << 12); // WREN=1:允许写操作
bcr1 |= (0UL << 13); // WAITEN=0:关闭同步等待功能
bcr1 |= (0UL << 14); // EXTMOD=0:不使用独立写时序寄存器
bcr1 |= (0UL << 15); // ASYNCWAIT=0:异步模式不响应 NWAIT
reg_write(FMC_BASE_ADDR, FMC_BCR1_OFFSET, bcr1);
// ===== 配置 FMC_BTR1(读时序)=====
uint32_t btr1 = 0;
btr1 |= (0UL << 0); // ADDSET=0:地址建立 0 个 HCLK
btr1 |= (0UL << 4); // ADDHLD:不关心(Mode 1 不用)
btr1 |= (10UL << 8); // DATAST=10:数据建立 10 个 HCLK(读)
btr1 |= (1UL << 16); // BUSTURN=1:总线周转 1 个 HCLK
btr1 |= (0xFUL << 20); // CLKDIV:不关心(异步模式)
btr1 |= (0xFUL << 24); // DATLAT:不关心(异步模式)
btr1 |= (0x0UL << 28); // ACCMOD=00:访问模式 A(但 EXTMOD=0,此处不关心)
reg_write(FMC_BASE_ADDR, FMC_BTR1_OFFSET, btr1);
}
// ---------- 主函数 ----------
int main()
{
// 1. 使能 FMC 时钟
fmc_clock_enable();
// 2. 配置 GPIO 引脚为 FMC 复用功能
fmc_gpio_init();
// 3. 配置 FMC Bank1 为 SRAM 模式
fmc_bank1_sram_init();
// 4. 现在可以像普通内存一样访问外部 SRAM
// Bank1-NOR1 的基地址为 0x60000000
volatile uint16_t *sram = (volatile uint16_t *)0x60000000UL;
// 写入测试数据
sram[0] = 0x1234;
sram[1] = 0x5678;
// 读回验证
uint16_t val0 = sram[0]; // 应该是 0x1234
uint16_t val1 = sram[1]; // 应该是 0x5678
// 验证结果(在调试器中查看 val0, val1)
if (val0 == 0x1234 && val1 == 0x5678)
{
// SRAM 访问成功
while (1) { /* 成功,停在这里 */ }
}
else
{
// SRAM 访问失败,检查时序或硬件连接
while (1) { /* 失败,停在这里 */ }
}
return 0;
}
十、FMC 寄存器总览
FMC 寄存器地址速查(基地址 0xA000_0000)
偏移 寄存器名 用途
------ ---------- ----
0x000 FMC_BCR1 Bank1 控制
0x004 FMC_BTR1 Bank1 读时序
0x008 FMC_BCR2 Bank2 控制
0x00C FMC_BTR2 Bank2 读时序
0x010 FMC_BCR3 Bank3 控制
0x014 FMC_BTR3 Bank3 读时序
0x018 FMC_BCR4 Bank4 控制
0x01C FMC_BTR4 Bank4 读时序
0x060 FMC_PCR2 NAND/PCCard Bank2 控制
0x064 FMC_SR2 Bank2 状态/中断
0x068 FMC_PMEM2 Bank2 通用存储器时序
0x06C FMC_PATT2 Bank2 属性存储器时序
0x074 FMC_ECCR2 Bank2 ECC 结果
0x080 FMC_PCR3 NAND/PCCard Bank3 控制
...(同 Bank2 偏移 +0x20)
0x0A0 FMC_PCR4 PC Card Bank4 控制
...(同 Bank2 偏移 +0x40)
0x0B0 FMC_PIO4 Bank4 I/O 空间时序
0x104 FMC_BWTR1 Bank1 写时序(EXTMOD=1 时有效)
0x10C FMC_BWTR2 Bank2 写时序
0x114 FMC_BWTR3 Bank3 写时序
0x11C FMC_BWTR4 Bank4 写时序
0x140 FMC_SDCR1 SDRAM Bank1 控制
0x144 FMC_SDCR2 SDRAM Bank2 控制
0x148 FMC_SDTR1 SDRAM Bank1 时序
0x14C FMC_SDTR2 SDRAM Bank2 时序
0x150 FMC_SDCMR SDRAM 命令模式寄存器
0x154 FMC_SDRTR SDRAM 刷新定时器
0x158 FMC_SDSR SDRAM 状态寄存器
十一、常见问题排查
问题1:访问外部存储器后 CPU 发生 HardFault
原因:访问了未使能的 Bank(MBKEN=0)
解决:确认 FMC_BCRx 中 MBKEN 已置1
问题2:读到的数据全是 0xFF 或乱码
原因:时序参数设置过小,存储器来不及响应
解决:增大 DATAST 值,重新计算
问题3:SDRAM 初始化后读写数据不对
原因:①刷新速率太低,数据丢失
②CAS 延迟设置与芯片不匹配
③初始化命令顺序错误
解决:对照芯片手册检查每个时序参数
问题4:NAND Flash 读取数据出现 ECC 错误
原因:①NAND 老化,位翻转
②ECC 未正确启用
解决:确认 ECCEN 位操作流程,实现软件纠错
问题5:FMC 访问 NOR Flash 报 AHB 错误
原因:FACCEN 位(FMC_BCRx[6])未置1
解决:设置 FACCEN=1
本文基于 STM32 RM0090 参考手册第 37 章整理,适用于 STM32F42xxx/F43xxx 系列。
STM32 DBG(调试支持)从零详解
适用芯片:STM32F4xx 全系列
参考手册:RM0090 第 38 章
一、调试支持是什么?为什么需要它?
写程序难免有 Bug。STM32 内置了一套专门的硬件电路,让调试器(比如 ST-Link、J-Link)可以:
- 暂停 CPU(在某行代码处停下来看变量)
- 单步执行(一行一行跑)
- 设置断点(到某个地址自动暂停)
- 监视内存(某个变量被改动时报警)
- 输出 trace 日志(不停机地把运行信息发给电脑)
这套硬件调试体系叫做 CoreSight,是 ARM 公司设计的标准调试架构,STM32F4 完整实现了它。
二、整体架构一览
调试主机(电脑上的 Keil/IAR/GDB)
|
| 通过调试线(JTAG 5根线 或 SWD 2根线)
v
+------------------------------------------+
| STM32F4xx 芯片 |
| |
| +---------+ +-----+ +-----+ |
| | SWJ-DP | | ITM | | ETM | |
| | 调试端口 | |仪表 | |指令 | |
| +---------+ |跟踪 | |跟踪 | |
| | +--+--+ +--+--+ |
| v | | |
| +---------+ v v |
| | AHB-AP | +---------------------+ |
| | 访问端口 | | TPIU(跟踪输出) | |
| +---------+ +---------------------+ |
| | | |
| v v |
| +----------+ TRACESWO / TRACED[] |
| | 总线矩阵 | (跟踪引脚输出到外部) |
| +----------+ |
| | |
| +---------+ +-----+ +-----+ |
| | Cortex | | FPB | | DWT | |
| | -M4核心 | |断点 | |监视点| |
| +---------+ +-----+ +-----+ |
| |
| DBGMCU(调试MCU组件) |
| 控制低功耗模式/外设冻结/跟踪引脚分配 |
+------------------------------------------+
三、两种调试接口:JTAG 和 SWD
3.1 JTAG 调试接口(5 根线)
JTAG 是工业标准,历史悠久,需要 5 根信号线:
调试器(J-Link等) STM32F4xx
JTMS <--------------> PA13 (测试模式选择)
JTCK <--------------> PA14 (测试时钟)
JTDI --------------> PA15 (测试数据输入)
JTDO <-------------- PB3 (测试数据输出)
NJTRST ------------> PB4 (测试复位,低有效)
3.2 SWD 调试接口(2 根线,推荐使用)
SWD(Serial Wire Debug)是 ARM 专为嵌入式设计的简化调试接口,只需 2 根线,功能与 JTAG 相当:
调试器(ST-Link等) STM32F4xx
SWDIO <--------------> PA13 (数据线,双向)
SWCLK <--------------> PA14 (时钟线)
SWD 比 JTAG 的优势:
- 只占 2 个引脚,节省 IO
- 支持异步 TRACE 输出(TRACESWO 复用 PB3/JTDO)
- 嵌入式项目首选
3.3 从 JTAG 切换到 SWD 的握手序列
默认上电是 JTAG 模式。调试器要切换到 SWD 模式,必须在 TMS/SWDIO 上发送特定序列:
切换步骤:
步骤1:发送 50 个以上的 TCK 脉冲,同时 TMS=1(全高)
1111111111111111111111111111111111111111111111111...(50个以上)
步骤2:发送 16 位魔法序列(MSB 先发)
0111100111100111
(这个特定序列告诉芯片:切换到 SWD 模式)
步骤3:再发送 50 个以上的 TCK 脉冲,同时 TMS=1(全高)
1111111111111111111111111111111111111111111111111...(50个以上)
完成后 SWD 模式激活,JTAG 引脚(PA15, PB3, PB4)可以释放给 GPIO 使用
四、SWJ-DP 引脚的灵活分配
根据需要,可以逐步释放调试引脚给普通 GPIO 使用:
| 模式 | PA13(SWDIO) | PA14(SWCLK) | PA15(JTDI) | PB3(JTDO) | PB4(NJTRST) |
|---|---|---|---|---|---|
| 完整 JTAG+SWD(复位默认) | 调试 | 调试 | 调试 | 调试 | 调试 |
| JTAG+SWD 但无 NJTRST | 调试 | 调试 | 调试 | 调试 | 释放为GPIO |
| 只用 SWD(禁用 JTAG) | 调试 | 调试 | 释放为GPIO | 释放为GPIO | 释放为GPIO |
| 全部禁用 | 释放为GPIO | 释放为GPIO | 释放为GPIO | 释放为GPIO | 释放为GPIO |
内置上下拉电阻(防止引脚悬空)
JTAG 引脚直接连到内部触发器,悬空会导致调试异常。STM32 内置了上下拉:
| 引脚 | 内置电阻 | 原因 |
|---|---|---|
| NJTRST | 上拉 | 低有效复位,默认保持无效 |
| JTDI | 上拉 | 数据默认为高(BYPASS 指令) |
| JTMS/SWDIO | 上拉 | 默认处于安全状态 |
| TCK/SWCLK | 下拉 | 防止时钟引脚误触发 |
五、JTAG TAP 链结构
STM32F4 内部有两个 JTAG TAP(测试访问端口)串联:
JTDI --> [边界扫描 TAP] --> [Cortex-M4 TAP] --> JTDO
IR: 5位 IR: 4位
ID: 0x06419041 ID: 0x4BA00477
(STM32F42/43) (ARM CM4 r0p1)
访问 Cortex-M4 TAP 时,需要先把边界扫描 TAP 置入 BYPASS 模式(发送全 1 指令),然后每次数据移位都会多出 1 个额外的 bypass 位。
切换到 SWD 模式后,边界扫描 TAP 自动禁用(JTMS 被强制拉高)。
六、ID 代码系统
设备 ID(用于调试器识别芯片型号)
地址 0xE004_2000,只读:
| 位 | 名称 | 含义 |
|---|---|---|
| [31:16] | REV_ID | 芯片版本号(见下表) |
| [15:12] | 保留 | - |
| [11:0] | DEV_ID | 设备型号:STM32F42/43 = 0x419 |
STM32F42xxx/F43xxx 版本号对照:
| REV_ID 值 | 版本 |
|---|---|
| 0x1000 | Revision A |
| 0x1003 | Revision Y |
| 0x1007 | Revision 1 |
| 0x2001 | Revision 3 |
| 0x2003 | Revision 4, 5 and B |
七、SWD 协议详解
SWD 是一个同步串行协议,每次通信分三个阶段:
7.1 包请求(8 位,主机发送)
位 7 位 6 位 5 位 4:3 位 2 位 1 位 0
Park Stop Parity A[3:2] RnW APnDP Start
(必须=1) (必须=0)(奇偶校)(地址)(1读/0写)(1=AP/0=DP)(必须=1)
举例:读 DP IDCODE 寄存器:
- Start=1, APnDP=0(DP访问), RnW=1(读), A[3:2]=00(地址0x0=IDCODE)
- Parity = 0 XOR 1 XOR 0 XOR 0 = 1
- Stop=0, Park=1
- 完整字节(LSB 先发)= 1010 0101 = 0xA5
7.2 ACK 响应(3 位,目标发送)
| ACK 值 | 含义 |
|---|---|
| 100 | OK,操作成功 |
| 010 | WAIT,目标忙,主机需重试 |
| 001 | FAULT,发生错误 |
7.3 数据传输(33 位)
- 位 [31:0]:实际数据(LSB 先传)
- 位 [32]:奇偶校验位(数据 32 位的奇偶)
7.4 SWD 重要细节
写操作需要额外 2 个空闲时钟周期
由于 SWCLK 和 HCLK 是异步时钟域,写完数据后必须给 2 个 SWCLK 周期(保持 SWDIO=低/IDLE 状态),写入才真正生效。
写操作时序:
SWCLK _____|‾|_|‾|_|‾|_|‾|_|‾|...(数据位)...|‾|_|‾|_|
SWDIO ===数据帧(包请求+ACK+数据+奇偶)=====|低|低|
^ ^
这2个周期必须保持低
写入才真正有效!
如果忘记这 2 个周期,在写 CTRL/STAT 请求上电之后立即发下一条命令会失败!
八、AHB-AP(AHB 访问端口)
AHB-AP 是调试器访问芯片内存的桥梁。调试器通过 SWD/JTAG -> AHB-AP -> 总线矩阵,可以直接读写芯片的任意内存地址(包括外设寄存器),而不需要 CPU 参与。
调试器
|
| SWD / JTAG
v
SWJ-DP / JTAG-DP
|
| DP SELECT 寄存器选择 AP 编号
v
AHB-AP(AHB 访问端口)
|
| 作为 AHB 主设备
v
总线矩阵(Bus Matrix)
|
+--- DCode Bus(访问 Flash)
+--- System Bus(访问 SRAM、外设)
+--- PPB(访问调试组件本身)
AHB-AP 的 9 个主要寄存器:
| 地址偏移 | 寄存器 | 作用 |
|---|---|---|
| 0x00 | CSW(控制状态字) | 配置传输大小(8/16/32位)、地址自增、保护属性 |
| 0x04 | TAR(传输地址) | 要读写的目标内存地址 |
| 0x0C | DRW(数据读写) | 向 TAR 地址读/写数据 |
| 0x10~0x1C | BD0~BD3(分块数据) | 不改变 TAR 直接访问 4 个对齐字 |
| 0xF8 | ROM 基地址 | 调试组件的 ROM 表基地址 |
| 0xFC | ID 寄存器 | AHB-AP 的标识码 |
九、核心调试寄存器
通过 AHB-AP 访问 CPU 内核的调试控制寄存器,可以暂停/单步/读写寄存器:
| 寄存器 | 全称 | 作用 |
|---|---|---|
| DHCSR | 调试停止控制和状态 | 使能调试、暂停 CPU、单步执行、查看 CPU 状态 |
| DCRSR | 调试核心寄存器选择 | 选择要读写哪个 CPU 寄存器(R0~R15、PC、SP等) |
| DCRDR | 调试核心寄存器数据 | 读出/写入被 DCRSR 选中的寄存器值 |
| DEMCR | 调试异常和监控控制 | 设置向量捕获、使能 TRACE(TRCENA 位) |
在复位时停住 CPU 的方法:
1. 设置 DEMCR 寄存器的 bit0(VC_CORERESET = 1)
-> 表示:遇到复位向量时触发调试停止
2. 设置 DHCSR 寄存器的 bit0(C_DEBUGEN = 1)
-> 表示:使能调试暂停功能
完成后,下次芯片复位,CPU 会在执行第一条指令前暂停,等待调试器接管
注意:这 4 个寄存器不会被系统复位清除,只有上电复位(POR)才会清除。
十、FPB(Flash 补丁断点单元)
FPB 提供两类功能:
FPB 的两种用途
用途一:硬件断点
在某个代码地址设断点 -> CPU 取指到该地址时自动停下
共 6 个指令比较器
用途二:代码补丁(Flash Patch)
把代码空间某地址的指令重定向到 SRAM 中的新指令
用于修复已烧录 Flash 中的 Bug,不用重新烧录
共 6 个指令比较器 + 2 个常量比较器
注意:硬件断点和软件补丁是互斥的,同一个比较器只能用于其中一种。
十一、DWT(数据监视点触发单元)
DWT 有 4 个比较器,每个可以配置为以下用途之一:
DWT 比较器用途(4选1)
1. 硬件监视点(Watchpoint)
- 监视某个内存地址的读/写/访问
- 满足条件时暂停 CPU
2. ETM 触发
- 满足条件时触发 ETM 开始/停止跟踪
3. PC 采样
- 定期采样 PC 寄存器值(用于性能分析)
4. 数据地址采样
- 采样访问数据的地址
DWT 还提供性能分析计数器:
| 计数器 | 统计内容 |
|---|---|
| 时钟周期计数 | 总运行时钟数 |
| 折叠指令 | 零周期执行的指令数 |
| LSU 操作 | 加载/存储操作次数 |
| 睡眠周期 | CPU 处于睡眠的时钟数 |
| CPI(每指令时钟数) | 平均每条指令消耗的时钟 |
| 中断开销 | 中断进入/退出消耗的周期 |
十二、ITM(仪表跟踪宏单元)——printf 调试神器
ITM 是最实用的调试工具之一,可以让程序在运行中把数据"打印"出来,发到电脑显示,而不需要停止 CPU。
工作原理
程序写入 ITM 激励端口寄存器
|
v
ITM 把数据打包成跟踪数据包
|
软件跟踪 <-- ITM 生成
硬件跟踪 <-- DWT 生成(经 ITM 中转)
时间戳包 <-- ITM 内置 21 位计数器
|
v
发送给 TPIU
|
v
TRACESWO 引脚 --> 调试器 --> 电脑显示
ITM 配置步骤
1. 设置 DEMCR 寄存器的 TRCENA 位 = 1(使能 TRACE 时钟)
2. 向 ITM Lock Access 寄存器(0xE000_0FB0)写入 0xC5ACCE55
(解锁 ITM 寄存器的写保护)
3. 向 ITM Trace Control 寄存器(0xE000_0E80)写入 0x00010005
含义:
- bit 0 (ITMENA) = 1:全局使能 ITM
- bit 2 (SYNCENA) = 1:使能同步触发
- bit 22:16 (ATB ID) = 1:设置跟踪源 ID(必须非 0)
4. 向 ITM Trace Enable 寄存器(0xE000_0E00)写入 0x1
(使能激励端口 0)
5. 向 ITM Trace Privilege 寄存器(0xE000_0E40)写入 0x1
(解除端口 7:0 的权限限制)
6. 向激励端口 0(0xE000_0000)写入要发送的数据
例如写入 'A' 的 ASCII 码 0x41,调试器就会显示 "A"
主要 ITM 寄存器
| 地址 | 寄存器 | 用途 |
|---|---|---|
| 0xE000_0FB0 | Lock Access | 写 0xC5ACCE55 解锁 |
| 0xE000_0E80 | Trace Control | 全局控制、ATB ID、时间戳 |
| 0xE000_0E40 | Trace Privilege | 端口权限掩码 |
| 0xE000_0E00 | Trace Enable | 每位对应一个激励端口 |
| 0xE000_0000~0xE000_007C | Stimulus Port 0~31 | 写入数据即可发送 |
十三、ETM(嵌入式跟踪宏单元)——指令跟踪
ETM 记录 CPU 执行的每一条指令,让调试器重建完整的执行路径。这是最强大但也最耗引脚的功能,只有大封装才有对应引脚。
ETM 数据通过 TPIU 输出,需要同步 TRACE 模式(多根引脚)。
十四、TPIU(跟踪端口接口单元)
TPIU 是所有跟踪数据的"出口",把来自 ITM 和 ETM 的数据格式化后输出到外部引脚。
两种输出模式
异步模式(SWO/TRACESWO):
- 只需 1 根引脚(PB3)
- 带宽有限
- 只能在 SWD 模式下使用(不能用 JTAG)
- 常用于 printf 风格调试
同步模式(TRACECK + TRACED[n]):
- 需要 2~5 根引脚(PE2 + PE3~PE6)
- 带宽更高,可跟踪指令流
- JTAG 和 SWD 均可使用
- 需要较大封装的芯片才有这些引脚
TRACE 引脚分配表
| TRACE_IOEN | TRACE_MODE | 模式 | PB3 | PE2 | PE3 | PE4 | PE5 | PE6 |
|---|---|---|---|---|---|---|---|---|
| 0 | XX | 无跟踪(默认) | 释放 | 释放 | 释放 | 释放 | 释放 | 释放 |
| 1 | 00 | 异步 SWO | SWO | 释放 | 释放 | 释放 | 释放 | 释放 |
| 1 | 01 | 同步 1位 | 释放 | CLK | D[0] | 释放 | 释放 | 释放 |
| 1 | 10 | 同步 2位 | 释放 | CLK | D[0] | D[1] | 释放 | 释放 |
| 1 | 11 | 同步 4位 | 释放 | CLK | D[0] | D[1] | D[2] | D[3] |
同步模式时,TRACECK 的输出频率 = HCLK/2HCLK / 2HCLK/2(TPIU 在 HCLK 上升沿驱动,一个周期采一次)。
TPIU 数据帧格式(16字节一帧)
一个 TPIU 数据帧 = 16 字节
字节布局:
[0] : 混合字节(bit0=0表示数据字节,bit7:1=数据高7位 或 新源ID)
[1] : 纯数据字节
[2] : 混合字节
[3] : 纯数据字节
...(交替排列)
[14] : 混合字节
[15] : 辅助字节(每位对应一个混合字节,说明bit0是数据还是ID变更)
"混合字节"中:
bit 0 = 0 -> 该字节是数据,bit7:1 是数据的高7位
bit 0 = 1 -> 该字节是源ID变更通知,bit7:1 是新的 ATB ID
十五、DBGMCU(调试 MCU 组件)
DBGMCU 是专门为调试使用场景设计的辅助模块,解决调试时遇到的各种实际问题。
15.1 低功耗模式下的调试支持
芯片进入睡眠/停止/待机模式时,HCLK/FCLK 会关闭,调试连接会断开。通过 DBGMCU_CR 可以让调试器在低功耗模式下保持连接:
| 位 | 名称 | 作用 |
|---|---|---|
| bit 0 | DBG_SLEEP | 1=睡眠模式时 HCLK 保持运行(用系统时钟继续驱动) |
| bit 1 | DBG_STOP | 1=停止模式时 FCLK/HCLK 由内部 RC 振荡器驱动保持运行 |
| bit 2 | DBG_STANDBY | 1=待机模式时不断电,FCLK/HCLK 保持运行,退出待机等同于复位 |
调试低功耗模式的原理:
正常情况 调试情况(设置 DBG_STOP=1)
CPU 执行 WFI/WFE CPU 执行 WFI/WFE
| |
HCLK 关闭 HCLK 不关闭(RC 振荡器顶替)
FCLK 关闭 FCLK 不关闭
调试连接断开 调试连接保持
...等待中断... ...调试器可以操作...
HCLK 恢复 一切正常
15.2 断点时外设的冻结控制
程序暂停在断点时,定时器、看门狗等外设默认继续运行,可能产生问题:
- 看门狗:调试断点处超时,触发复位,芯片重启,调试失败
- 定时器:控制电机的 PWM 定时器继续跑,电机可能失控
通过 DBGMCU_APB1_FZ 和 DBGMCU_APB2_FZ 寄存器,可以让这些外设在核心暂停时一起"冻结":
DBGMCU_APB1_FZ 寄存器(地址 0xE004_2008)控制的外设:
TIM2~TIM7, TIM12~TIM14 -> DBG_TIMx_STOP=1:定时器冻结
IWDG -> DBG_IWDG_STOP=1:独立看门狗冻结(调试必设!)
WWDG -> DBG_WWDG_STOP=1:窗口看门狗冻结
RTC -> DBG_RTC_STOP=1:RTC 停止计数
I2C1/2/3 -> DBG_I2Cx_SMBUS_TIMEOUT=1:SMBUS 超时冻结
CAN1/2 -> DBG_CANx_STOP=1:CAN 接收寄存器冻结
DBGMCU_APB2_FZ 寄存器(地址 0xE004_200C)控制的外设:
TIM1, TIM8 -> DBG_TIMx_STOP=1:定时器冻结
TIM9~TIM11 -> DBG_TIMx_STOP=1:定时器冻结
实用提示: 调试时建议总是设置 DBG_IWDG_STOP=1 和 DBG_WWDG_STOP=1,否则调试过程中看门狗超时会让芯片一直复位。
15.3 DBGMCU_CR 寄存器(地址 0xE004_2004)
31 8 7 5 4:3 2 1 0
| Reserved |TRACE|Res. |DBG_ |DBG_STOP |DBG_SLEEP |
| |IOEN | |STAND | | |
| |MODE | |BY | | |
TRACE 引脚分配控制(bits 7:5):
TRACE_IOEN=0 -> TRACE 引脚不分配(默认)
TRACE_IOEN=1, TRACE_MODE=00 -> 异步 TRACE(只用 TRACESWO)
TRACE_IOEN=1, TRACE_MODE=01 -> 同步 1位
TRACE_IOEN=1, TRACE_MODE=10 -> 同步 2位
TRACE_IOEN=1, TRACE_MODE=11 -> 同步 4位
十六、完整 C++ 代码示例
下面是一个完整可运行的调试辅助代码,包含:
- 调试模式下冻结看门狗
- 使用 ITM 输出调试信息(替代 UART printf)
/*
* 文件:dbg_support_example.cpp
* 功能:STM32F4 调试支持配置示例
* 1. 调试时冻结看门狗和定时器
* 2. 通过 ITM Stimulus Port 输出调试字符(SWO printf)
*
* 使用前提:
* - 调试器连接 SWD 接口(PA13/PA14)
* - 调试器开启 SWO 跟踪功能(Keil: Debug -> Trace, 勾选 Enable)
* - 系统时钟已初始化(本例假设 HCLK = 168MHz)
*/
#include <cstdint>
#include <cstddef>
// ============================================================
// 寄存器地址定义
// ============================================================
// DBGMCU 寄存器(挂载在外部 PPB 总线,地址固定)
static constexpr uint32_t DBGMCU_BASE = 0xE0042000UL;
static constexpr uint32_t DBGMCU_IDCODE_OFF = 0x00UL; // 设备 ID 代码(只读)
static constexpr uint32_t DBGMCU_CR_OFF = 0x04UL; // 调试控制寄存器
static constexpr uint32_t DBGMCU_APB1FZ_OFF = 0x08UL; // APB1 外设冻结控制
static constexpr uint32_t DBGMCU_APB2FZ_OFF = 0x0CUL; // APB2 外设冻结控制
// ITM 寄存器(Cortex-M4 内部,地址固定)
static constexpr uint32_t ITM_BASE = 0xE0000000UL;
static constexpr uint32_t ITM_STIM0_OFF = 0x0000UL; // 激励端口 0(写数据到此处)
static constexpr uint32_t ITM_TER_OFF = 0x0E00UL; // Trace Enable Register
static constexpr uint32_t ITM_TPR_OFF = 0x0E40UL; // Trace Privilege Register
static constexpr uint32_t ITM_TCR_OFF = 0x0E80UL; // Trace Control Register
static constexpr uint32_t ITM_LAR_OFF = 0x0FB0UL; // Lock Access Register
// DEMCR:调试异常和监控控制寄存器(CoreDebug 区域)
static constexpr uint32_t CoreDebug_BASE = 0xE000EDF0UL;
static constexpr uint32_t DEMCR_OFF = 0x00CUL; // 相对 CoreDebug 基地址
// ============================================================
// 辅助函数
// ============================================================
static inline uint32_t reg_read(uint32_t base, uint32_t offset)
{
return *(volatile uint32_t *)(base + offset);
}
static inline void reg_write(uint32_t base, uint32_t offset, uint32_t value)
{
*(volatile uint32_t *)(base + offset) = value;
}
static inline void reg_set_bits(uint32_t base, uint32_t offset, uint32_t mask)
{
*(volatile uint32_t *)(base + offset) |= mask;
}
// ============================================================
// 功能一:读取设备 ID 代码
// ============================================================
/**
* @brief 读取 STM32 设备 ID 代码
* @return DEV_ID[11:0],STM32F42/43 应为 0x419
*/
uint32_t dbg_get_device_id()
{
// DBGMCU_IDCODE 寄存器:bits[11:0] = DEV_ID
return reg_read(DBGMCU_BASE, DBGMCU_IDCODE_OFF) & 0x00000FFFUL;
}
/**
* @brief 读取芯片版本号
* @return REV_ID[15:0],例如 0x2003 = Revision 4/5/B
*/
uint32_t dbg_get_revision_id()
{
// DBGMCU_IDCODE 寄存器:bits[31:16] = REV_ID
return (reg_read(DBGMCU_BASE, DBGMCU_IDCODE_OFF) >> 16) & 0x0000FFFFUL;
}
// ============================================================
// 功能二:配置低功耗模式下的调试支持
// ============================================================
/**
* @brief 使能调试模式下的低功耗支持
*
* 调用后,CPU 进入 Sleep/Stop/Standby 时调试连接不断开
*/
void dbg_enable_low_power_debug()
{
uint32_t cr = 0;
// bit 0: DBG_SLEEP = 1
// 睡眠模式时 HCLK 继续运行(用系统时钟驱动)
cr |= (1UL << 0);
// bit 1: DBG_STOP = 1
// 停止模式时 FCLK/HCLK 由内部 RC 振荡器驱动继续运行
cr |= (1UL << 1);
// bit 2: DBG_STANDBY = 1
// 待机模式时芯片不断电,FCLK/HCLK 继续运行
cr |= (1UL << 2);
// 注意:bits 7:5 (TRACE_IOEN/TRACE_MODE) 暂不修改
// 如需 SWO 输出,下面的 itm_init() 会单独处理
reg_write(DBGMCU_BASE, DBGMCU_CR_OFF, cr);
}
// ============================================================
// 功能三:调试断点时冻结外设
// ============================================================
/**
* @brief 配置调试时外设冻结
*
* 调用后,当调试器在断点处暂停 CPU 时:
* - 看门狗计数停止(避免调试中途复位)
* - 定时器计数停止
* - I2C SMBUS 超时停止
* - CAN 接收寄存器不更新
*/
void dbg_freeze_peripherals_on_halt()
{
// ===== APB1 外设冻结 =====
uint32_t apb1_freeze = 0;
// bit 0: DBG_TIM2_STOP
apb1_freeze |= (1UL << 0);
// bit 1: DBG_TIM3_STOP
apb1_freeze |= (1UL << 1);
// bit 2: DBG_TIM4_STOP
apb1_freeze |= (1UL << 2);
// bit 3: DBG_TIM5_STOP
apb1_freeze |= (1UL << 3);
// bit 4: DBG_TIM6_STOP
apb1_freeze |= (1UL << 4);
// bit 5: DBG_TIM7_STOP
apb1_freeze |= (1UL << 5);
// bit 6: DBG_TIM12_STOP
apb1_freeze |= (1UL << 6);
// bit 7: DBG_TIM13_STOP
apb1_freeze |= (1UL << 7);
// bit 8: DBG_TIM14_STOP
apb1_freeze |= (1UL << 8);
// bit 10: DBG_RTC_STOP(RTC 停止计数)
apb1_freeze |= (1UL << 10);
// bit 11: DBG_WWDG_STOP(窗口看门狗冻结)
// 调试时必须设置,否则 WWDG 超时会让芯片复位!
apb1_freeze |= (1UL << 11);
// bit 12: DBG_IWDG_STOP(独立看门狗冻结)
// 调试时必须设置,否则 IWDG 超时会让芯片复位!
apb1_freeze |= (1UL << 12);
// bit 21: DBG_I2C1_SMBUS_TIMEOUT(I2C1 SMBUS 超时冻结)
apb1_freeze |= (1UL << 21);
// bit 22: DBG_I2C2_SMBUS_TIMEOUT
apb1_freeze |= (1UL << 22);
// bit 23: DBG_I2C3_SMBUS_TIMEOUT
apb1_freeze |= (1UL << 23);
// bit 25: DBG_CAN1_STOP(CAN1 接收寄存器冻结)
apb1_freeze |= (1UL << 25);
// bit 26: DBG_CAN2_STOP
apb1_freeze |= (1UL << 26);
reg_write(DBGMCU_BASE, DBGMCU_APB1FZ_OFF, apb1_freeze);
// ===== APB2 外设冻结 =====
uint32_t apb2_freeze = 0;
// bit 0: DBG_TIM1_STOP
apb2_freeze |= (1UL << 0);
// bit 1: DBG_TIM8_STOP
apb2_freeze |= (1UL << 1);
// bit 16: DBG_TIM9_STOP
apb2_freeze |= (1UL << 16);
// bit 17: DBG_TIM10_STOP
apb2_freeze |= (1UL << 17);
// bit 18: DBG_TIM11_STOP
apb2_freeze |= (1UL << 18);
reg_write(DBGMCU_BASE, DBGMCU_APB2FZ_OFF, apb2_freeze);
}
// ============================================================
// 功能四:ITM 初始化和字符输出(SWO printf)
// ============================================================
/**
* @brief 初始化 ITM,用于通过 SWO 引脚输出调试信息
*
* 初始化完成后,调用 itm_send_char() 即可在调试器的
* "SWO 查看器" 窗口中看到输出的字符
*/
void itm_init()
{
// 步骤 1:设置 DEMCR 寄存器的 TRCENA 位(bit 24)
// 这个位是所有跟踪功能的"总开关",必须先开
reg_set_bits(CoreDebug_BASE, DEMCR_OFF, (1UL << 24)); // TRCENA = 1
// 步骤 2:解锁 ITM 寄存器的写保护
// 必须先写入这个"魔法值"才能修改其他 ITM 寄存器
reg_write(ITM_BASE, ITM_LAR_OFF, 0xC5ACCE55UL);
// 步骤 3:配置 ITM Trace Control 寄存器
// 0x00010005 含义:
// bit 0 (ITMENA) = 1 -> 全局使能 ITM
// bit 2 (SYNCENA) = 1 -> 使能同步触发(让 DWT 产生同步包)
// bit 22:16 (ATB ID) = 1 -> 跟踪源 ID = 1(不能为 0)
reg_write(ITM_BASE, ITM_TCR_OFF, 0x00010005UL);
// 步骤 4:使能激励端口 0
// ITM_TER 每一位对应一个激励端口(0~31),置 1 = 使能
reg_write(ITM_BASE, ITM_TER_OFF, 0x00000001UL); // 使能端口 0
// 步骤 5:设置端口权限(允许非特权代码访问端口 7:0)
// ITM_TPR 每一位对应 8 个端口的权限块
reg_write(ITM_BASE, ITM_TPR_OFF, 0x00000001UL); // bit 0 = 端口 7:0 可访问
}
/**
* @brief 通过 ITM 激励端口 0 发送一个字节
* @param ch 要发送的字符
*
* 调试器会在 SWO 查看器中显示这个字符
* 这个函数可以作为 printf 的底层输出函数
*/
void itm_send_char(char ch)
{
// 检查 ITM 是否已使能(ITMENA bit)
if ((reg_read(ITM_BASE, ITM_TCR_OFF) & 0x00000001UL) == 0)
return; // ITM 未使能,直接返回
// 检查端口 0 是否已使能
if ((reg_read(ITM_BASE, ITM_TER_OFF) & 0x00000001UL) == 0)
return; // 端口 0 未使能,直接返回
// 等待激励端口 0 的 FIFO 有空位
// 当端口寄存器读回非 0 时,表示可以写入
volatile uint32_t *stim0 = (volatile uint32_t *)(ITM_BASE + ITM_STIM0_OFF);
while (*stim0 == 0)
{
// 自旋等待(实际项目中可以加超时保护)
}
// 写入字符(8 位写入)
// 写入后 ITM 硬件自动打包成跟踪数据包,通过 TPIU 发出
*(volatile uint8_t *)stim0 = (uint8_t)ch;
}
/**
* @brief 通过 ITM 发送字符串
* @param str 要发送的字符串
*/
void itm_send_string(const char *str)
{
if (str == nullptr)
return;
while (*str != '\0')
{
itm_send_char(*str);
++str;
}
}
/**
* @brief 通过 ITM 发送 32 位十六进制数(调试用)
* @param value 要显示的数值
*/
void itm_send_hex32(uint32_t value)
{
const char hex_chars[] = "0123456789ABCDEF";
itm_send_string("0x");
// 从高位到低位逐个输出 16 进制字符
for (int i = 28; i >= 0; i -= 4)
{
itm_send_char(hex_chars[(value >> i) & 0xFUL]);
}
itm_send_char('\n');
}
// ============================================================
// 主函数:综合演示
// ============================================================
int main()
{
// 1. 初始化低功耗调试支持
// 调试时进入 Sleep/Stop 也不会断开连接
dbg_enable_low_power_debug();
// 2. 初始化外设冻结
// 调试断点时看门狗、定时器自动暂停
dbg_freeze_peripherals_on_halt();
// 3. 初始化 ITM(SWO printf 功能)
itm_init();
// 4. 读取并输出设备 ID
uint32_t dev_id = dbg_get_device_id();
uint32_t rev_id = dbg_get_revision_id();
itm_send_string("STM32 DBG Init OK\n");
itm_send_string("Device ID: ");
itm_send_hex32(dev_id); // STM32F42/43 应输出 0x00000419
itm_send_string("Revision: ");
itm_send_hex32(rev_id);
// 5. 主循环:每隔一段时间发送心跳信息
uint32_t count = 0;
while (1)
{
// 简单延时(实际项目中用 SysTick 或定时器)
for (volatile uint32_t i = 0; i < 1000000UL; ++i) {}
itm_send_string("Heartbeat count: ");
itm_send_hex32(count);
++count;
}
return 0;
}
十七、调试流程速查
十八、常见问题速查
问题1:调试器无法连接,提示 "No target found"
原因:①引脚被用户代码改为 GPIO
②时钟未使能
③引脚悬空(SWCLK 需要下拉)
解决:在复位状态下连接,上电后立即连接
检查 PA13/PA14 是否被误用为普通 GPIO
问题2:调试过程中芯片一直复位
原因:看门狗(IWDG/WWDG)超时触发复位
解决:设置 DBGMCU_APB1_FZ 中的
DBG_IWDG_STOP=1 和 DBG_WWDG_STOP=1
问题3:SWO printf 没有输出
原因:①TRCENA 未开(DEMCR bit24)
②ITM 未解锁(未写 0xC5ACCE55 到 LAR)
③ATB ID = 0(无效)
④调试器 SWO 频率设置与实际 HCLK 不符
解决:按本文第十二章的步骤逐一检查
确认调试器中配置的 SWO 速率 = HCLK 频率
问题4:ETM 指令跟踪不工作
原因:引脚未分配(TRACE_IOEN 未设置)
或封装太小没有 TRACED 引脚
解决:在 DBGMCU_CR 设置 TRACE_IOEN=1
TRACE_MODE 根据引脚数量选择
问题5:进入 Stop 模式后调试器断开
原因:DBG_STOP 未置 1
解决:调用前先设置 DBGMCU_CR bit1 = 1
更多推荐


所有评论(0)