FPGA驱动SSD1306 OLED:SPI时序、显存映射与Verilog实现

发布时间:2026/9/25 1:01:32
FPGA驱动SSD1306 OLED:SPI时序、显存映射与Verilog实现 1. 为什么选择FPGA驱动SSD1306从需求到方案1.1 这个项目到底在做什么SSD1306 是一颗很常见的 OLED 驱动芯片0.96 寸 128x64 分辨率的模块基本都是它在扛。你在某宝上花十几块钱买到的四针 I2C 模块、七针 SPI 模块拆开看大概率就是 SSD1306 或者它的兼容型号。它便宜、好买、资料多但真正用 FPGA 去驱动它的时候很多人会卡在几个地方时序对不上、初始化命令写错、显存映射搞混、屏幕点不亮或者花屏。这个项目的核心目标很明确用 Verilog 写一套纯 RTL 的驱动逻辑通过 SPI 协议把图像数据送到 SSD1306让 128x64 的 OLED 屏幕正常显示内容。不依赖任何软核处理器不跑 MicroBlaze 或 Nios就是纯硬件状态机加显存控制。适合刚接触 FPGA、想找一个“看得见摸得着”的入门项目练手的人也适合已经会点 Verilog 但没做过完整外设驱动的朋友。我选这个题目不是因为它简单而是因为它麻雀虽小五脏俱全。一个完整的 OLED 驱动项目里你会碰到时钟分频、状态机设计、SPI 时序生成、显存管理、复位同步、跨时钟域处理这些 FPGA 开发的核心技能点。把它吃透比你看十遍语法书都管用。1.2 为什么用 SPI 而不是 I2CSSD1306 支持 I2C 和 SPI 两种接口模块上通常通过电阻跳线或者背面焊盘来选择。I2C 只要两根线省引脚但速率受限标准模式 100kHz快速模式 400kHz对于 128x64 的屏幕来说刷一整屏 1024 字节的数据400kHz 下大概要 20ms 以上做动画会明显卡顿。SPI 就不一样了。SSD1306 的 SPI 接口最高能跑到 10MHz 左右手册里写的是 10MHz 的时钟周期最小 100ns实际我跑 8MHz 非常稳。8MHz 下刷一屏 1024 字节加上命令开销大概 1ms 出头做简单动画完全够用。而且 SPI 的时序比 I2C 简单得多没有 ACK 应答、没有起始停止条件就是时钟沿打数据对 FPGA 新手来说调试难度低很多。还有一个实际原因很多 FPGA 开发板上引出的扩展口SPI 的引脚排列比 I2C 更常见接线方便。你拿杜邦线一插就能跑不用额外焊上拉电阻。1.3 整体方案框图和模块划分我的设计思路是把整个驱动拆成四个模块每个模块职责单一方便单独仿真和调试时钟分频模块把板载 50MHz 时钟分频出 SPI 需要的 SCLK以及状态机用的低速使能信号。SPI 发送模块负责把一字节数据按 SPI 模式 0 的时序串行移出同时控制 DC 引脚区分命令和数据。初始化控制模块上电后按固定顺序发送 SSD1306 的初始化命令序列包括显示关闭、时钟设置、复用率、对比度、寻址模式等。显存与刷新模块维护一块 128x64 bit 的显存对应 1024 字节按页地址模式或水平地址模式把数据推送到屏幕。模块之间的连接关系很简单初始化模块和刷新模块共用 SPI 发送模块通过一个仲裁或者分时复用的方式切换。我实际做的时候用了一个简单的优先级仲裁——初始化没完成之前刷新模块不工作初始化完成后刷新模块独占 SPI 通道。注意SSD1306 的 SPI 不是标准四线 SPI它没有 MISO因为屏幕只接收不发送。所以你的 SPI 主机只需要 MOSI、SCLK、CS、DC 四根线加上复位线一共五根。别被“SPI”这个名字误导去接 MISO接错了反而可能干扰。2. SSD1306 核心细节解析与 Verilog 实操要点2.1 SSD1306 的显存结构和寻址模式SSD1306 内部有一块 128x64 bit 的 GDDRAM也就是显存。它的组织方式是按页Page划分的一共 8 页每页 8 行每页有 128 列。每个字节的数据对应一页里某一列的 8 个像素LSB 在上MSB 在下。这里有个容易搞混的地方屏幕的物理行和显存的页不是一一对应的。Page0 对应物理行 0 到 7Page1 对应行 8 到 15以此类推。你在写显存数据的时候一个字节控制的是垂直方向的 8 个像素而不是水平方向的 8 个像素。这个“竖着存”的方式和很多人的直觉相反我第一次做的时候就在这里翻了车显示出来的字全是横着的条纹。SSD1306 支持三种寻址模式寻址模式命令值特点适用场景页寻址模式0x02手动指定页和列写完一页数据后列地址自动回绕局部刷新适合小区域更新水平寻址模式0x00列地址自动递增跨页自动换行全屏刷新最常用垂直寻址模式0x01页地址自动递增跨页自动换列特殊排列数据我推荐新手直接用水平寻址模式。设置好起始页和结束页、起始列和结束列之后你只管往显存里灌数据地址会自动走不用手动干预。初始化的时候发这几个命令// 水平寻址模式设置命令序列 8h20, 8h00, // 设置寻址模式为水平寻址 8h21, 8h00, 8h7F, // 列地址范围 0~127 8h22, 8h00, 8h07 // 页地址范围 0~7这三条命令发完后面连续写 1024 字节数据就会自动填满整个屏幕。2.2 SPI 模式 0 的时序要求和 Verilog 实现SSD1306 的 SPI 接口工作在模式 0也就是 CPOL0、CPHA0。具体来说时钟空闲时为低电平数据在时钟上升沿被采样所以你的数据要在时钟下降沿或者上升沿之前就准备好。我实际用的方案是在时钟下降沿改变 MOSI上升沿时 SSD1306 采样这样建立时间和保持时间都很充裕。Verilog 里用一个简单的移位寄存器就能实现// SPI 发送模块核心逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin sclk_r 1b0; mosi_r 1b0; bit_cnt 4d0; end else if (spi_start) begin if (bit_cnt 8) begin sclk_r ~sclk_r; if (sclk_r 1b0) begin mosi_r data_in[7 - bit_cnt]; bit_cnt bit_cnt 1b1; end end else begin sclk_r 1b0; bit_cnt 4d0; end end end这段代码的逻辑是每来一个使能周期翻转一次 SCLK。在 SCLK 从高变低的时候把数据的下一位放到 MOSI 上。这样当 SCLK 从低变高时数据已经稳定了SSD1306 可以正确采样。实操心得SCLK 的频率不要一上来就拉满。我建议先用 1MHz 跑通确认屏幕能正常显示之后再逐步提高到 8MHz 甚至 10MHz。很多“点不亮”的问题都是因为时序太快建立时间不够导致的。降速能解决一大半调试问题。2.3 DC 引脚和 CS 引脚的控制逻辑DC 引脚是 SSD1306 用来区分命令和数据的。DC 为低时SPI 上传输的字节被当作命令DC 为高时被当作显存数据。这个引脚的状态必须在 CS 有效之前就设置好并且在整字节传输期间保持不变。CS 是片选低电平有效。SSD1306 在 CS 为高时会忽略 SCLK 和 MOSI 上的任何变化。我一般在一字节传输开始前拉低 CS传输结束后拉高。如果你板上只有这一颗 SPI 从设备也可以把 CS 直接接地省一根控制线。但我不推荐这么做因为调试的时候你可能会想用逻辑分析仪抓波形有 CS 做触发会方便很多。// DC 和 CS 控制逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin dc_r 1b0; cs_r 1b1; end else if (send_cmd) begin dc_r 1b0; // 命令模式 cs_r 1b0; end else if (send_data) begin dc_r 1b1; // 数据模式 cs_r 1b0; end else begin cs_r 1b1; end end这里有个细节DC 要在 CS 拉低之前就稳定。我见过有人把 DC 和 CS 同时变化结果第一个字节偶尔会被当成错误的类型。稳妥的做法是 DC 提前半个 SCLK 周期设置好。2.4 初始化命令序列的完整解读SSD1306 上电后必须经过正确的初始化才能正常工作。手册里给的命令很多但实际用到的核心命令就那么十几条。我整理了一份最小可用的初始化序列每条命令我都标注了作用// SSD1306 初始化命令序列128x64SPI 模式 8hAE, // 关闭显示 8hD5, 8h80, // 设置显示时钟分频和振荡频率 8hA8, 8h3F, // 设置复用率为 64 8hD3, 8h00, // 设置显示偏移为 0 8h40, // 设置显示起始行为 0 8h8D, 8h14, // 使能电荷泵 8h20, 8h00, // 设置内存寻址模式为水平寻址 8hA1, // 段重映射左右翻转 8hC8, // 扫描方向重映射上下翻转 8hDA, 8h12, // 设置 COM 引脚硬件配置 8h81, 8hCF, // 设置对比度 8hD9, 8hF1, // 设置预充电周期 8hDB, 8h40, // 设置 VCOMH 电压 8hA4, // 使能整个显示 8hA6, // 设置正常显示非反色 8hAF // 开启显示这里面有几条命令特别关键漏了或者写错了屏幕就不会亮0x8D, 0x14使能电荷泵。SSD1306 需要内部升压来驱动 OLED不使能电荷泵屏幕完全不亮。这是新手最容易漏的一条。0xA8, 0x3F设置复用率。128x64 的屏幕必须设成 0x3F63设错了显示区域会不对。0x20, 0x00水平寻址模式。不设这个后面写显存数据地址不会自动递增。0xAF开启显示。前面所有命令发完最后发这条屏幕才会亮。注意初始化命令之间需要留足够的延时。SSD1306 处理命令需要时间尤其是电荷泵启动需要几毫秒。我一般每条命令之间等 1ms电荷泵使能后等 10ms。延时不够会导致初始化失败而且这种失败是偶发的很难查。3. 完整实操过程与核心环节实现3.1 硬件连接和引脚分配先说你手头需要的东西一块 FPGA 开发板Altera、Xilinx、Lattice 都行我用的是 Cyclone IV 和 Artix-7 各试了一遍、一个 0.96 寸 SPI 接口的 SSD1306 OLED 模块、几根杜邦线。接线很简单七针模块上通常标着 GND、VCC、D0、D1、RES、DC、CS。SPI 模式下只用其中六根OLED 引脚连接到 FPGA说明GNDGND共地VCC3.3V供电别接 5VD0SCLKSPI 时钟D1MOSISPI 数据RES复位引脚低电平复位DC命令/数据选择低命令高数据CS片选低有效D0 和 D1 的命名有点迷惑在 I2C 模式下它们是 SCL 和 SDA在 SPI 模式下就是 SCLK 和 MOSI。模块背面一般有跳线电阻选择模式买的时候确认是 SPI 模式或者自己改焊。实操心得VCC 一定要接 3.3V。虽然有些模块标称支持 3.3V 到 5V但 SSD1306 的 IO 电平是 3.3V 的接 5V 长期运行可能损坏芯片。FPGA 的 IO 电平也要确认是 3.3V 标准别用 1.8V 的 bank。3.2 时钟分频和复位同步处理板载时钟一般是 50MHz直接拿来跑状态机太快了而且 SPI 也不需要这么高的频率。我的做法是分两级一级分频出 1MHz 的使能信号给 SPI 模块用另一级分频出 1kHz 的使能信号给延时计数器用。// 时钟分频50MHz - 1MHz 使能 reg [5:0] cnt_1m; reg en_1m; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt_1m 6d0; en_1m 1b0; end else if (cnt_1m 6d24) begin cnt_1m 6d0; en_1m 1b1; end else begin cnt_1m cnt_1m 1b1; en_1m 1b0; end end50MHz 除以 50 得到 1MHz计数到 24 翻转一次实际 SCLK 频率是 1MHz。这个使能信号只持续一个时钟周期SPI 模块用它来推进状态机。复位信号我用了异步复位、同步释放的方式。异步复位能保证上电时寄存器立即回到已知状态同步释放能避免复位撤除时的亚稳态问题。这是 FPGA 设计里的标准做法尤其是跨时钟域的时候。// 复位同步释放 reg rst_sync1, rst_sync2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin rst_sync1 1b0; rst_sync2 1b0; end else begin rst_sync1 1b1; rst_sync2 rst_sync1; end end wire rst_synced_n rst_sync2;3.3 初始化状态机的实现初始化模块我用了一个状态机按顺序把命令序列发出去。状态机分几个阶段上电等待、发送命令、命令间延时、初始化完成。// 初始化状态机简化版 localparam IDLE 3d0; localparam POWER_UP 3d1; localparam SEND_CMD 3d2; localparam CMD_DELAY 3d3; localparam DONE 3d4; reg [2:0] init_state; reg [7:0] cmd_index; reg [15:0] delay_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin init_state IDLE; cmd_index 8d0; end else begin case (init_state) IDLE: begin init_state POWER_UP; delay_cnt 16d50000; // 上电等待 1ms end POWER_UP: begin if (delay_cnt 0) init_state SEND_CMD; else delay_cnt delay_cnt - 1b1; end SEND_CMD: begin if (spi_done) begin init_state CMD_DELAY; delay_cnt 16d50000; end end CMD_DELAY: begin if (delay_cnt 0) begin if (cmd_index CMD_TOTAL - 1) init_state DONE; else begin cmd_index cmd_index 1b1; init_state SEND_CMD; end end else delay_cnt delay_cnt - 1b1; end DONE: init_state DONE; endcase end end命令序列存在一个 ROM 里用 cmd_index 去查表。综合工具会自动把它映射成分布式 RAM 或者块 RAM不占额外逻辑资源。3.4 显存写入和屏幕刷新初始化完成后就可以往屏幕写数据了。我用了一个 1024 字节的显存数组综合工具会把它推断成块 RAM。写显存和读显存刷屏是两个独立的过程通过一个简单的双口 RAM 或者时分复用单口 RAM 来实现。// 显存定义 reg [7:0] gram [0:1023]; // 刷新状态机把显存数据依次送到 SPI always (posedge clk or negedge rst_n) begin if (!rst_n) begin refresh_addr 10d0; refresh_state R_IDLE; end else begin case (refresh_state) R_IDLE: begin if (init_done) begin refresh_state R_SEND; refresh_addr 10d0; end end R_SEND: begin if (spi_done) begin if (refresh_addr 10d1023) begin refresh_state R_IDLE; end else begin refresh_addr refresh_addr 1b1; end end end endcase end end刷屏的时候DC 要拉高因为传的是显存数据。每传完一字节地址加一直到 1024 字节全部传完。水平寻址模式下SSD1306 会自动把数据填到对应的页和列你不用手动切换页地址。实操心得如果你要做动画不要每次都全屏刷新。SSD1306 的显存是可以局部更新的只改变化的区域传输的数据量能减少很多。比如做一个秒表只需要刷新数字所在的那几页其他区域不动。这样帧率能提高好几倍。3.5 显示测试图案的生成为了验证驱动是否正常我一般先显示一个测试图案而不是直接上复杂内容。最简单的测试是隔行点亮或者棋盘格这样能一眼看出显存映射对不对。// 棋盘格测试图案生成 integer i; always (*) begin for (i 0; i 1024; i i 1) begin // 每页交替 0xAA 和 0x55 if ((i / 128) % 2 0) gram[i] 8hAA; else gram[i] 8h55; end end0xAA 是 101010100x55 是 01010101交替出现就是竖条纹。如果显示出来是横条纹说明你的页和列搞反了。如果完全不显示先查电荷泵和 0xAF 命令。如果显示反色检查 0xA6 和 0xA7 命令。4. 常见问题与排查技巧实录4.1 屏幕完全不亮怎么办这是最常见的问题我按排查优先级列一下排查项检查方法可能原因供电万用表量 VCC 和 GND没接电、电压不对复位逻辑分析仪看 RES 波形复位没释放、复位时间不够电荷泵确认发了 0x8D, 0x14漏发、发错顺序显示开启确认最后发了 0xAF漏发SPI 时序抓 SCLK 和 MOSI时钟太快、相位不对DC 引脚抓 DC 波形命令和数据搞反我遇到最多的情况是电荷泵没使能。很多人照着网上的代码抄但不同模块的初始化序列略有差异有些代码里没有 0x8D 这条命令屏幕就不亮。另外复位时间要够RES 至少拉低 10us 再拉高我一般给 1ms。4.2 显示花屏或者错位花屏通常是显存映射或者寻址模式的问题。先确认你用的是水平寻址模式命令 0x20, 0x00 发了没有。然后确认列地址范围是 0 到 127页地址范围是 0 到 7。如果显示内容整体偏移检查 0xD3 显示偏移命令应该是 0x00。如果左右镜像了检查 0xA1 和 0xA00xA1 是正常0xA0 是左右翻转。如果上下颠倒了检查 0xC8 和 0xC0。还有一种花屏是数据位序搞反了。SSD1306 的一个字节里LSB 对应上面的像素MSB 对应下面的像素。如果你把位序搞反了显示出来就是上下颠倒的图案。4.3 SPI 通信不稳定的排查SPI 不稳定的表现是偶尔显示正常、偶尔花屏或者初始化偶尔失败。这种问题最难查因为它是偶发的。首先降速。把 SCLK 从 8MHz 降到 1MHz如果问题消失说明是时序余量不够。检查你的建立时间和保持时间SSD1306 要求数据在 SCLK 上升沿前至少 10ns 稳定在上升沿后至少 10ns 保持。1MHz 下这些余量非常充裕8MHz 下就要仔细算了。其次检查 CS 和 DC 的时序。CS 必须在 SCLK 之前拉低在最后一个 SCLK 之后拉高。DC 要在 CS 拉低之前稳定。我见过有人把 CS 和 SCLK 同时变化结果第一个 bit 丢失。注意如果你用逻辑分析仪抓波形采样率至少要是 SCLK 的 10 倍以上。8MHz 的 SCLK 需要 80MHz 以上的采样率才能看清细节。采样率不够的话你看到的波形是混叠的会误导排查方向。4.4 显存数据写入不生效有时候初始化成功了屏幕也亮了但写显存数据没反应。先确认 DC 引脚在写数据时是高电平。然后确认 CS 在传输期间是低电平。如果这两点都对检查你的 SPI 发送模块是不是真的把数据发出去了。一个常见的 bug 是状态机在发送完一字节后没有正确回到空闲状态导致后续字节被吞掉。用 SignalTap 或者 ILA 抓一下 spi_done 信号看它是不是每字节都正确产生了一次脉冲。还有一个坑是显存数组被综合工具优化掉了。如果你定义了 gram 数组但只写不读或者读的时候地址是常量综合工具可能会把它优化成常量。加一个(* ramstyle block *)或者(* keep 1 *)属性可以防止被优化。4.5 常见问题速查表现象最可能原因快速验证方法完全不亮电荷泵未使能检查 0x8D 命令亮但无内容0xAF 未发或显存全 0发 0xAF写测试图案花屏寻址模式错误检查 0x20 命令左右镜像段重映射错误切换 0xA0/0xA1上下颠倒扫描方向错误切换 0xC0/0xC8偶发花屏SPI 时序余量不足降低 SCLK 频率局部不显示页地址范围错误检查 0x22 命令参数对比度太低对比度设置过小调整 0x81 参数5. 进阶优化与项目扩展方向5.1 提高刷新率的几个手段全屏刷新 1024 字节在 8MHz SPI 下大概 1ms 出头理论上能到 900fps 以上但实际受限于状态机开销和命令间隔我实测大概能到 200fps 左右。如果你要做流畅动画可以进一步优化。第一个手段是提高 SPI 时钟。SSD1306 手册标称最高 10MHz我实测 12MHz 也能跑但不保证所有批次都稳定。建议不要超过 10MHz。第二个手段是局部刷新。只更新变化的页不动的页跳过。比如做一个指针式时钟只有指针扫过的区域需要更新数据量能减少 80% 以上。第三个手段是双缓冲。用两块显存一块用于显示一块用于写入写完之后切换。这样刷新和写入不冲突能充分利用 SPI 带宽。不过 1024 字节的显存对大多数 FPGA 来说不算什么块 RAM 资源足够。5.2 显示中文字符和自定义图案SSD1306 本身不带字库所有字符都要你自己生成点阵数据。ASCII 字符用 8x16 或者 6x8 的点阵中文字符用 16x16 的点阵。取模软件比如 PCtoLCD2002可以生成对应的十六进制数组。在 Verilog 里你可以把字库存在 ROM 里用字符编码作为地址去查表。显示的时候把查到的点阵数据按页写入显存。注意中文字符是 16x16 的跨两页写入的时候要处理好页边界。// 字库 ROM 示例8x16 ASCII case (char_code) 8h41: begin // A // 上半部分 8 字节下半部分 8 字节 font_data {8h00, 8h00, 8h00, 8h00, ...}; end // ... endcase5.3 用 FPGA 做简单图像处理OLED 只有 128x64 的分辨率能显示的内容有限但用来做图像处理的验证平台刚刚好。你可以把摄像头采集的数据降采样到 128x64然后显示在 OLED 上。或者做二值化、边缘检测这些简单算法结果直接显示出来。我试过用 OV7670 摄像头加 SSD1306 做一个简易的“电子取景器”虽然分辨率很低但整个数据流是通的摄像头采集、FPGA 处理、OLED 显示。这个项目做下来对 FPGA 图像处理的理解会深很多。5.4 从 SSD1306 迁移到其他显示屏学会了 SSD1306 的驱动迁移到其他 SPI 接口的显示屏会容易很多。比如 ST7789 的 TFT 屏也是 SPI 接口只是初始化命令和显存组织方式不同。核心的 SPI 发送模块、状态机框架、时钟分频这些都可以复用。关键差异在于ST7789 是彩色屏每个像素 16 位RGB565数据量是 SSD1306 的 16 倍对 SPI 带宽要求更高。你可能需要把 SCLK 提到 20MHz 以上或者用并口模式。但驱动框架是一样的改改命令序列和显存位宽就行。实操心得做 FPGA 外设驱动最值钱的不是某一块屏幕的驱动代码而是你调试时序、看波形、排查问题的能力。SSD1306 是个很好的练手对象因为它简单、便宜、坏了不心疼。把这个项目吃透后面做更复杂的外设会顺手很多。我个人在实际操作中的体会是FPGA 驱动 OLED 这件事难点不在 Verilog 语法而在对时序的理解和对细节的耐心。初始化命令少发一条、延时少等一毫秒、DC 引脚早变了半个周期屏幕就是不亮。但正是这种“差一点都不行”的特性逼着你去认真读手册、仔细抓波形、反复验证。这个过程走下来你对数字系统设计的理解会上一个台阶。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询