4、SDRAM扩展实战:W9825G6KH芯片介绍、SDRAM初始化流程

各位好,我是老周。今天咱们聊聊SDRAM扩展实战。说实话,在工业MCU项目里,内部RAM不够用是家常便饭。跑个复杂的算法,或者做个大点的显示缓冲区,内部那几十KB根本不够看。这时候,外挂一片SDRAM就是最实在的解决方案。

我选W9825G6KH这颗芯片来讲,原因很简单——它太经典了。华邦的这颗SDRAM,容量32MB,16位数据宽度,工业级温度范围,价格也亲民。我在好几个项目里都用过它,从来没出过幺蛾子。

W9825G6KH芯片速览

先看几个关键参数,心里有个底:

参数
容量 256Mb(32MB)
数据宽度 16位
Bank数量 4个
行地址 A0-A12(13位)
列地址 A0-A8(9位)
工作电压 3.3V ± 0.3V
封装 TSOP-54

嗯,这里有个小细节。它的行地址是13位,列地址是9位,4个Bank。算一下:2^13 × 2^9 × 4 × 16位 = 256Mb。没错,对得上。

个人经验: 我建议你在选型时,优先考虑16位宽度的SDRAM。8位的虽然便宜,但读写效率差一倍。32位的又太贵,而且MCU引脚也不一定够。16位是个甜点区。

SDRAM初始化流程

SDRAM的初始化,说白了就是让它从「刚上电的混沌状态」进入「可以正常读写的就绪状态」。这个过程有严格的时序要求,一步都不能错。

我画了个流程图,帮你理清思路:

1. 上电:VDD稳定到3.3V 2. 等待200μs(CKE保持高电平) 3. 预充电所有Bank(PRE命令) 4. 执行8个刷新周期(REF命令) 5. 模式寄存器配置(MRS命令) 初始化完成,可以读写

这个流程看着简单,但每个步骤都有坑。我一个一个说。

第一步:上电与稳定

上电后,VDD必须在200μs内稳定到3.3V。同时,CKE引脚必须保持高电平。为什么?因为CKE是时钟使能,如果它拉低了,SDRAM会进入掉电模式,初始化就卡住了。

我曾经在一个项目里,因为电源纹波太大,VDD稳定时间超过了200μs,结果SDRAM死活初始化不成功。后来加了颗10μF的电容才搞定。嗯,电源设计真的不能省。

第二步:预充电所有Bank

预充电,说白了就是把所有Bank的行都关掉,让SDRAM回到「待机状态」。这个命令通过控制CS、RAS、CAS、WE这几个引脚来实现。

命令编码是这样的:

// 预充电命令(PRE)
// CS=0, RAS=0, CAS=1, WE=0
// A10=1 表示预充电所有Bank
// A10=0 表示只预充电当前Bank

void SDRAM_PrechargeAll(void) {
    // 设置控制引脚
    SDRAM_CS = 0;
    SDRAM_RAS = 0;
    SDRAM_CAS = 1;
    SDRAM_WE = 0;
    // A10拉高,预充电所有Bank
    SDRAM_A10 = 1;
    // 保持至少tRP时间(20ns)
    delay_ns(20);
    // 恢复默认状态
    SDRAM_CS = 1;
}
注意: 预充电后必须等待tRP时间(行预充电时间)。W9825G6KH的tRP是20ns。如果你用100MHz的时钟,就是2个时钟周期。别偷懒,少一个周期都不行。

第三步:8个刷新周期

SDRAM是动态存储器,电容会漏电,所以必须定期刷新。初始化时,需要连续执行8个刷新周期,让内部电路稳定下来。

刷新命令和预充电很像,只是控制引脚不同:

// 刷新命令(REF)
// CS=0, RAS=0, CAS=0, WE=1

void SDRAM_Refresh(void) {
    SDRAM_CS = 0;
    SDRAM_RAS = 0;
    SDRAM_CAS = 0;
    SDRAM_WE = 1;
    // 保持至少tRFC时间(70ns)
    delay_ns(70);
    SDRAM_CS = 1;
}

// 连续执行8次
for(int i = 0; i < 8; i++) {
    SDRAM_Refresh();
}

你想想看,为什么是8次?不是4次也不是16次?其实这是芯片厂商规定的。W9825G6KH的数据手册里明确写了,初始化时需要至少8个刷新周期。我建议你严格按照手册来,别自作聪明。

第四步:模式寄存器配置

这一步最关键。模式寄存器(Mode Register)决定了SDRAM的工作方式,包括突发长度、突发类型、CAS延迟等。

W9825G6KH的模式寄存器有13位(A0-A12),各位的定义如下:

名称 说明
A0-A2 突发长度 000=1, 001=2, 010=4, 011=8
A3 突发类型 0=顺序, 1=交错
A4-A6 CAS延迟 010=2, 011=3
A7-A12 保留/测试模式 通常设为0

我个人习惯用CAS延迟=3,突发长度=4。为什么?因为大多数MCU的SDRAM控制器都支持这个配置,兼容性最好。

// 模式寄存器配置(MRS)
// CS=0, RAS=0, CAS=0, WE=0
// 地址线A0-A12写入配置值

void SDRAM_MRS(void) {
    // 配置值:CAS=3, 突发长度=4, 顺序突发
    // A4-A6 = 011 (CAS=3)
    // A0-A2 = 010 (突发长度=4)
    // A3 = 0 (顺序突发)
    uint16_t config = (3 << 4) | (2 << 0);
    
    SDRAM_CS = 0;
    SDRAM_RAS = 0;
    SDRAM_CAS = 0;
    SDRAM_WE = 0;
    
    // 将配置值放到地址线上
    SDRAM_ADDR = config;
    
    // 保持至少tMRD时间(2个时钟周期)
    delay_clk(2);
    SDRAM_CS = 1;
}
避坑指南: 我曾经把CAS延迟设成了2,结果SDRAM在高温下频繁出错。后来查手册才发现,W9825G6KH在100MHz以上时,CAS延迟必须≥3。所以,如果你跑高频,CAS=3是更稳妥的选择。

读写操作实战

初始化完成后,就可以正常读写SDRAM了。读写操作的核心是「行激活 → 读写命令 → 预充电」这个循环。

举个例子,我想往地址0x1000写入一个16位数据:

// 写操作示例
// 地址0x1000,数据0xABCD

// 1. 行激活(ACTIVE命令)
// 行地址 = 0x1000 >> 9 = 0x8
// Bank地址 = 0(假设Bank0)
SDRAM_CS = 0;
SDRAM_RAS = 0;
SDRAM_CAS = 1;
SDRAM_WE = 1;
SDRAM_ADDR = 0x8;  // 行地址
SDRAM_BA = 0;      // Bank0
delay_ns(20);      // 等待tRCD

// 2. 写命令(WRITE命令)
// 列地址 = 0x1000 & 0x1FF = 0x0
SDRAM_CS = 0;
SDRAM_RAS = 1;
SDRAM_CAS = 0;
SDRAM_WE = 0;
SDRAM_ADDR = 0x0;  // 列地址
SDRAM_DQ = 0xABCD; // 写入数据
// A10=0,不自动预充电
delay_ns(20);      // 等待tWR

// 3. 预充电(关闭当前行)
SDRAM_PrechargeAll();

读操作类似,只是WE引脚拉高:

// 读操作示例
// 读取地址0x1000的数据

// 1. 行激活(同上)
SDRAM_CS = 0;
SDRAM_RAS = 0;
SDRAM_CAS = 1;
SDRAM_WE = 1;
SDRAM_ADDR = 0x8;
SDRAM_BA = 0;
delay_ns(20);

// 2. 读命令(READ命令)
SDRAM_CS = 0;
SDRAM_RAS = 1;
SDRAM_CAS = 0;
SDRAM_WE = 1;  // 读操作
SDRAM_ADDR = 0x0;
// 等待CAS延迟(3个时钟周期)
delay_clk(3);
uint16_t data = SDRAM_DQ; // 读取数据

// 3. 预充电
SDRAM_PrechargeAll();
关键点: 读操作时,数据不是立即出现的。从发出读命令到数据出现在DQ引脚上,需要等待CAS延迟个时钟周期。W9825G6KH的CAS延迟可以是2或3,我建议用3,更稳定。

嗯,到这里,SDRAM的初始化流程和基本读写操作就讲完了。你可能会问,为什么每次读写完都要预充电?其实不一定。如果你连续读写同一行的数据,可以跳过预充电,直接发新的读写命令。这叫「页操作」,效率更高。但如果你要访问不同行,就必须先预充电,再激活新行。

我在一个图像采集项目里,就是利用页操作来提升SDRAM吞吐量的。一帧图像数据连续存储在SDRAM的一行里,读的时候只需要激活一次行,然后连续发读命令就行。速度比逐行激活快了将近一倍。

好了,今天就聊到这儿。记住,SDRAM的时序要求很严格,初始化流程一步都不能错。如果你在实际调试中遇到问题,先检查电源,再检查时序,最后检查配置。八成的问题都出在这三个地方。


公众号:蓝海资料掘金营,微信deep3321