第一章:从零认识 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 主机模式的工作流程

PCDET中断

XFRC中断

错误

芯片上电

核心初始化

主机初始化

打开VBUS电源

等待设备连接?

发送USB复位信号
至少10ms

等待PENCHNG中断

读取设备速度 PSPD

配置FIFO大小

初始化通道 Channel

开始数据传输

传输完成?

释放通道

错误处理

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 设备状态机

检测到VBUS有效

收到USB复位

收到SET_ADDRESS命令

收到SET_CONFIGURATION命令

总线空闲3ms

检测到恢复信号

VBUS断开

Powered

DefaultState

Address

Configured

Suspended

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={0x9AHB30MHz
值越大越安全,但会降低 IN 传输性能。

第十三章:完整流程总结

USBRST

ENUMDNE

RXFLVL

IEPINT

OEPINT

上电

RCC 使能 OTG_HS 时钟

GPIO 配置为 USB 功能

选择PHY: 内置FS 或 外置ULPI

核心软复位 CSRST

激活PHY: PWRDWN=1

强制设备模式 FDMOD=1

配置AHB参数
GAHBCFG

配置USB参数
GUSBCFG TRDT=9

配置RX FIFO大小
GRXFSIZ

配置TX FIFO大小
GNPTXFSIZ

使能VBUS检测
GCCFG VBUSBSEN

配置设备速度
DCFG DSPD

清空FIFO

清除所有中断

使能所需中断
GINTMSK

使能NVIC中断

等待中断

复位处理
配置EP0

枚举完成
配置最大包大小

读取接收FIFO
处理数据

IN端点完成

OUT端点完成

附录:常用寄存器速查


寄存器 偏移 主要用途
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}DATAST4+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 典型读页操作

开始

配置 FSMC_PCRx
设置Bank类型为NAND
PTYP=1 PBKEN=1

写命令到命令区
CPU写 0x00 到 0x7001_0000
CLE有效,Flash识别为命令

写地址到地址区
分4次写入页地址字节
ALE有效,Flash识别为地址

等待 R/NB 信号
Flash内部处理页数据
FSMC 保持NCE有效

从数据区读取数据
逐字节读 0x7000_0000
NAND自动递增内部地址

读取完成

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=168MHz15.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.24DATASTreadtHCLKtRCADDSET×tHCLK=5.955509.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.16DATASTwritetHCLKtWH1=5.95510.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.02MEMWAITtHCLKtWP1=5.951211.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,满足。

第十一章:完整流程总结

SRAM/NOR/PSRAM

NAND Flash

PC Card

需要

不需要

系统上电

使能FSMC时钟
RCC AHB3ENR

配置GPIO为FSMC复用功能
地址线 数据线 控制线

选择存储器类型?

配置FSMC_BCRx
存储器类型 总线宽度
写使能 扩展模式

配置FSMC_PCRx
NAND类型 总线宽度
ECC页大小 延迟参数

配置FSMC_PCRx
PC Card类型
等待特性使能

配置FSMC_BTRx
读时序参数
ADDSET DATAST BUSTURN

需要独立写时序?
EXTMOD=1?

配置FSMC_BWTRx
写时序参数
与读时序不同

读写共用BTRx时序

配置FSMC_PMEMx
通用空间时序

配置FSMC_PATTx
属性空间时序
满足tWB要求

使能Bank
MBKEN=1 or PBKEN=1

直接通过指针访问存储器
FSMC自动生成时序信号

NAND ECC?

写前使能ECCEN
写数据
等FIFO空FEMPT=1
读FSMC_ECCRx
禁用ECCEN

正常读写操作

结束

附录:寄存器速查表


寄存器 地址偏移(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 读页操作流程:

配置 FMC_PCRx 和 FMC_PMEMx 寄存器

CPU 写命令区:发送读命令 0x00

CPU 写地址区:发送起始地址
多次写入,每次一个字节

等待 R/NB 信号变高
表示 NAND 准备好了

CPU 从数据区逐字节读取数据

还有更多页?

结束

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 上电后必须按照严格顺序初始化,不能直接读写:

配置 FMC_SDCR1/2:时钟分频、位宽、行列数等

配置 FMC_SDTR1/2:各种时序参数

发送命令:Clock Enable
MODE=001,CTB1/CTB2置位
SDCKE 开始输出高电平

等待至少 100μs
等待 SDRAM 内部稳定

发送命令:Precharge All
MODE=010
关闭所有行

发送命令:Auto-Refresh
MODE=011,重复 8 次
刷新所有 Bank

发送命令:Load Mode Register
MODE=100
配置 CAS 延迟、突发长度等

配置刷新定时器 FMC_SDRTR
设置自动刷新周期

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 - TRCDTWRTRASTRCD
TWR≥TRC−TRCD−TRPTWR \geq TRC - TRCD - TRPTWRTRCTRCDTRP

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⌊(31+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 ms7.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×106×60×10620=468.620448

注意:减去 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)DATAST4+最大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 是一个同步串行协议,每次通信分三个阶段:

100=OK

010=WAIT

001=FAULT

主机发送
包请求 8位

目标回应
ACK 3位

ACK 结果

数据传输
33位

主机重试

错误处理

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=1DBG_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++ 代码示例

下面是一个完整可运行的调试辅助代码,包含:

  1. 调试模式下冻结看门狗
  2. 使用 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;
}

十七、调试流程速查

SWD

JTAG

调试器连接 SWD/JTAG

系统复位保持

选择调试接口

发送 JTAG->SWD 切换序列
TMS=1 50次 + 0111100111100111 + TMS=1 50次

直接使用 5 根线

读取 SW-DP ID Code
0x2BA01477

先 BYPASS 边界扫描 TAP
再访问 CM4 TAP

上电请求:写 CTRL/STAT
等待 2 个 SWCLK 空闲周期

访问 AHB-AP
读写任意内存地址

设置 DHCSR.C_DEBUGEN=1
DEMCR.VC_CORERESET=1

释放系统复位
CPU 在复位向量处停住

设置断点/监视点
FPB / DWT 配置

继续运行
CPU 正常执行

触发断点?

CPU 暂停
调试器读写寄存器/内存

十八、常见问题速查

问题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
Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐