第一章:从零认识 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。
它的速度等级:

速度名称缩写速度
高速HS480 Mbits/s
全速FS12 Mbits/s
低速LS1.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 在设备模式下的端点配置:

端点类型数量说明
控制端点 EP01个(双向)必有,用于枚举和配置
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 之间的数据传输方式。

名称说明
0GINT全局中断使能(1=允许产生中断)
4:1HBSTLENAHB 突发传输长度
5DMAENDMA 使能(1=DMA模式,0=CPU轮询模式)
7TXFELVLTX FIFO 空中断触发时机
8PTXFELVL周期性TX FIFO 空中断触发时机

OTG_HS_GUSBCFG(USB配置寄存器,偏移0x00C)

名称说明
6PHYSELPHY选择:0=外置ULPI高速PHY,1=内置全速PHY
8SRPCAPSRP能力使能
9HNPCAPHNP能力使能
13:10TRDTUSB周转时间(根据AHB频率设置)
29FHMOD强制主机模式
30FDMOD强制设备模式

TRDT 的计算:

  • AHB频率在30MHz以上时,TRDT最小值为 0x90x90x9
  • 这个值越大,给设备准备数据的时间越充裕
OTG_HS_GINTSTS(核心中断状态寄存器,偏移0x014)

这个寄存器是"中断报告板",某位为1表示发生了对应事件:

名称说明模式
0CMOD当前模式:0=设备模式,1=主机模式两者
1MMIS模式不匹配中断两者
3SOF帧起始信号两者
4RXFLVLRX FIFO 非空(有数据可读)两者
12USBRST检测到USB复位设备
13ENUMDNE枚举完成设备
18IEPINTIN端点有中断设备
19OEPINTOUT端点有中断设备
24HPRTINT主机端口中断主机
25HCINT主机通道中断主机
29DISCINT检测到设备断开主机
31WKUINT检测到唤醒/远程唤醒两者

第五章:主机模式详解

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:0MPSIZ最大包大小(字节)
14:11EPNUM目标端点号
15EPDIR方向:0=OUT,1=IN
17LSDEV低速设备标志
19:18EPTYP端点类型:00=控制,01=同步,10=批量,11=中断
21:20MC每微帧传输包数
28:22DAD目标设备地址
29ODDFRM奇数帧传输(同步/中断用)
30CHDIS禁用通道
31CHENA使能通道(开始传输)

OTG_HS_HCTSIZx(通道传输大小寄存器),地址 = 0x510 + 0x20*x

名称说明
18:0XFRSIZ总传输字节数
28:19PKTCNT包数量
30:29DPID数据PID(DATA0/DATA1/DATA2/MDATA)
31DOPING发送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:0DSPD设备速度:00=高速,11=全速(内置PHY)
2NZLSOHSK非零长度状态OUT包握手处理方式
10:4DAD设备地址(SET_ADDRESS后写入)
12:11PFIVL周期帧结束中断时机(80%/85%/90%/95%)

OTG_HS_DCTL(设备控制寄存器,偏移0x804)

名称说明
0RWUSIG远程唤醒信号(设备主动唤醒主机)
1SDIS软断开(模拟拔掉USB线)
7SGINAK设置全局IN NAK
8CGINAK清除全局IN NAK
9SGONAK设置全局OUT NAK
10CGONAK清除全局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最大包+12×(最大包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激活时
SleepCPU暂停兼容,USB挂起时可进入
Stop深度睡眠兼容,USB挂起时可进入
Standby断电不兼容,退出后需重新初始化

9.2 省电寄存器 OTG_HS_PCGCCTL(偏移0xE00)


名称说明
0STPPCLK停止PHY时钟(USB挂起时使用)
1GATEHCLK门控AHB时钟(省电)
4PHYSUSPPHY已挂起标志(只读)

第十章:完整初始化代码示例

下面是一个完整的 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端点完成

附录:常用寄存器速查


寄存器偏移主要用途
GOTGCTL0x000OTG控制和状态
GAHBCFG0x008AHB配置(DMA、中断)
GUSBCFG0x00CUSB配置(PHY、速度)
GRSTCTL0x010复位控制
GINTSTS0x014中断状态(写1清除)
GINTMSK0x018中断使能掩码
GRXFSIZ0x024RX FIFO大小
GNPTXFSIZ0x028EP0/非周期TX FIFO大小
GCCFG0x038通用核心配置(PHY供电,VBUS)
DCFG0x800设备配置
DCTL0x804设备控制
DSTS0x808设备状态(枚举速度)
DIEPMSK0x810IN端点公共中断掩码
DOEPMSK0x814OUT端点公共中断掩码
DAINT0x818所有端点中断状态
DAINTMSK0x81C所有端点中断掩码
PCGCCTL0xE00电源和时钟门控

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
00Bank1-NOR/PSRAM 1(地址 0x6000 0000)
01Bank1-NOR/PSRAM 2(地址 0x6400 0000)
10Bank1-NOR/PSRAM 3(地址 0x6800 0000)
11Bank1-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 1SRAM/PSRAM整个访问期间有效整个访问期间有效否(共用BTR)
Mode ASRAM/PSRAM每次访问切换(OE toggling)整个访问期间有效是(BTR+BWTR)
Mode 2NOR Flash整个访问期间有效整个访问期间有效有(地址锁存)
Mode BNOR Flash整个访问期间有效每次访问切换
Mode CNOR 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 控制寄存器详解

这个寄存器决定"用什么类型的存储器"以及"用什么方式访问":

名称说明
0MBKENBank使能:1=使能,0=禁止(访问禁止的Bank会产生AHB错误)
1MUXEN地址/数据复用:1=复用(节省引脚),0=独立
3:2MTYP存储器类型:00=SRAM,01=PSRAM,10=NOR Flash
5:4MWID数据总线宽度:00=8位,01=16位
6FACCENNOR Flash访问使能:必须为1才能访问NOR
8BURSTEN突发模式使能(读操作)
9WAITPOLNWAIT极性:0=低有效,1=高有效
11WAITCFG等待时序配置:0=NWAIT在等待态前一周期有效,1=在等待态期间有效
12WREN写使能:1=允许写,0=禁止写
13WAITEN同步模式等待使能
14EXTMOD扩展模式:1=读写使用不同时序寄存器(BTR/BWTR)
15ASYNCWAIT异步等待:1=异步模式也检测NWAIT
18:16CPSIZECRAM页大小(Cellular RAM 1.5用)
19CBURSTRW突发写使能(PSRAM写操作用)

5.3 FSMC_BTRx 时序寄存器详解

这个寄存器决定"每个访问阶段持续多少个时钟":

名称单位说明
3:0ADDSETHCLK地址建立时间:0~15个周期
7:4ADDHLDHCLK地址保持时间(Mode D和复用模式用)
15:8DATASTHCLK数据建立时间:1~255个周期
19:16BUSTURNHCLK总线转向时间:0~15个周期
23:20CLKDIVHCLKCLK分频比:CLK=HCLK/(CLKDIV+1)
27:24DATLATCLK同步数据延迟(延迟+2个CLK)
29:28ACCMOD-访问模式: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~40x0000, 0x0008, 0x0010, 0x0018NOR/SRAM/PSRAM 控制
FSMC_BTR1~40x0004, 0x000C, 0x0014, 0x001CNOR/SRAM/PSRAM 读时序
FSMC_BWTR1~40x0104, 0x010C, 0x0114, 0x011CNOR/SRAM/PSRAM 写时序(扩展模式)
FSMC_PCR2~40x0060, 0x0080, 0x00A0NAND/PC Card 控制
FSMC_SR2~40x0064, 0x0084, 0x00A4FIFO状态和中断
FSMC_PMEM2~40x0068, 0x0088, 0x00A8通用存储空间时序
FSMC_PATT2~40x006C, 0x008C, 0x00AC属性存储空间时序
FSMC_PIO40x00B0PC Card IO空间时序
FSMC_ECCR2~30x0074, 0x0094ECC 计算结果

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 / PSRAMNOR/PSRAM 控制器代码存储、大容量数据缓存
NAND Flash / PC CardNAND/PC Card 控制器文件系统存储(类似 U 盘)
SDRAM / LPSDR SDRAMSDRAM 控制器大容量高速内存(跑 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]选中的子银行
00Bank1-NOR/PSRAM 1(NE1 引脚拉低)
01Bank1-NOR/PSRAM 2(NE2 引脚拉低)
10Bank1-NOR/PSRAM 3(NE3 引脚拉低)
11Bank1-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]MBKEN1=使能该 Bank,0=禁用(访问禁用 Bank 会触发 AHB 错误)
[1]MUXEN1=地址/数据复用模式,0=非复用
[3:2]MTYP00=SRAM,01=PSRAM,10=NOR Flash
[5:4]MWID00=8位,01=16位,10=32位
[6]FACCEN1=允许 NOR Flash 访问
[8]BURSTEN1=开启同步突发读模式
[11]WAITCFGNWAIT 信号时序配置
[12]WREN1=允许写操作
[14]EXTMOD1=使用独立的写时序寄存器 FMC_BWTRx
[15]ASYNCWAIT1=异步模式下也响应 NWAIT 信号
[20]CCLKEN1=连续输出时钟(即使没有访问也持续输出)

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 有效位
000256 字节ECC[21:0],22位
001512 字节ECC[23:0],24位
0101024 字节ECC[25:0],26位
0112048 字节ECC[27:0],28位
1004096 字节ECC[29:0],30位
1018192 字节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]选中的银行控制寄存器
0SDRAM Bank 1FMC_SDCR1 / FMC_SDTR1
1SDRAM Bank 2FMC_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 值版本
0x1000Revision A
0x1003Revision Y
0x1007Revision 1
0x2001Revision 3
0x2003Revision 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 值含义
100OK,操作成功
010WAIT,目标忙,主机需重试
001FAULT,发生错误

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 个主要寄存器:

地址偏移寄存器作用
0x00CSW(控制状态字)配置传输大小(8/16/32位)、地址自增、保护属性
0x04TAR(传输地址)要读写的目标内存地址
0x0CDRW(数据读写)向 TAR 地址读/写数据
0x10~0x1CBD0~BD3(分块数据)不改变 TAR 直接访问 4 个对齐字
0xF8ROM 基地址调试组件的 ROM 表基地址
0xFCID 寄存器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_0FB0Lock Access写 0xC5ACCE55 解锁
0xE000_0E80Trace Control全局控制、ATB ID、时间戳
0xE000_0E40Trace Privilege端口权限掩码
0xE000_0E00Trace Enable每位对应一个激励端口
0xE000_0000~0xE000_007CStimulus 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_IOENTRACE_MODE模式PB3PE2PE3PE4PE5PE6
0XX无跟踪(默认)释放释放释放释放释放释放
100异步 SWOSWO释放释放释放释放释放
101同步 1位释放CLKD[0]释放释放释放
110同步 2位释放CLKD[0]D[1]释放释放
111同步 4位释放CLKD[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 0DBG_SLEEP1=睡眠模式时 HCLK 保持运行(用系统时钟继续驱动)
bit 1DBG_STOP1=停止模式时 FCLK/HCLK 由内部 RC 振荡器驱动保持运行
bit 2DBG_STANDBY1=待机模式时不断电,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 幸运抽奖

更多推荐