毕业设计救星:手把手教你用Verilog点亮0.96寸OLED屏(附完整代码)
毕业设计救星手把手教你用Verilog点亮0.96寸OLED屏附完整代码第一次接触FPGA和OLED屏时看着开发板和那块小小的显示屏我完全不知道从何下手。SPI协议、初始化命令、字库数据这些名词让我一头雾水。直到找到一份可用的Verilog驱动代码才真正理解了整个流程。本文将分享如何快速验证代码、修改显示内容并深入解析关键状态机的工作原理。1. 硬件准备与工程搭建在开始编码之前我们需要准备好硬件环境。0.96寸OLED屏通常采用SSD1306驱动芯片通过SPI接口与FPGA通信。以下是所需材料清单FPGA开发板如Xilinx Artix-7或Altera Cyclone IV0.96寸OLED显示屏128x64分辨率杜邦线若干微USB数据线硬件连接时需要注意引脚对应关系OLED引脚FPGA引脚功能说明GNDGND地线VCC3.3V电源D0(SCK)任意IO时钟线D1(SDA)任意IO数据线RES任意IO复位DC任意IO数据/命令选择CS任意IO片选提示不同厂家的OLED屏引脚排列可能不同务必查阅具体型号的规格书在Quartus或Vivado中新建工程时建议选择与开发板匹配的FPGA型号。创建顶层模块时需要声明以下端口module oled_driver ( input clk, // 系统时钟如50MHz input reset_n, // 低电平复位 output oled_sclk, // SPI时钟 output oled_sdin, // SPI数据 output oled_dc, // 数据/命令选择 output oled_res, // 复位信号 output oled_cs // 片选信号 );2. SPI通信协议实现SSD1306驱动的OLED屏采用SPI通信协议我们需要在Verilog中实现SPI主设备。SPI的时序要求严格必须确保时钟和数据信号的同步。SPI模式配置要点时钟极性(CPOL)0空闲时低电平时钟相位(CPHA)0第一个边沿采样数据位顺序MSB优先时钟频率建议不超过10MHz以下是SPI发送单字节数据的Verilog实现reg [2:0] spi_state; reg [3:0] bit_cnt; reg [7:0] spi_data; always (posedge clk or negedge reset_n) begin if (!reset_n) begin spi_state 0; oled_sclk 0; oled_cs 1; end else begin case (spi_state) 0: begin // 空闲状态 oled_cs 1; if (spi_start) begin spi_state 1; oled_cs 0; bit_cnt 0; end end 1: begin // 准备数据 oled_sdin spi_data[7-bit_cnt]; spi_state 2; end 2: begin // 上升沿 oled_sclk 1; spi_state 3; end 3: begin // 下降沿 oled_sclk 0; if (bit_cnt 7) begin spi_state 0; end else begin bit_cnt bit_cnt 1; spi_state 1; end end endcase end end3. OLED初始化与驱动状态机SSD1306需要一系列初始化命令才能正常工作。这些命令包括设置显示时钟分频、对比度控制、显示偏移等。我们可以将这些命令存储在ROM中// 初始化命令序列 parameter [7:0] INIT_CMD [0:25] { 8hAE, 8hD5, 8h80, 8hA8, 8h3F, 8hD3, 8h00, 8h40, 8h8D, 8h14, 8h20, 8h00, 8hA1, 8hC8, 8hDA, 8h12, 8h81, 8hCF, 8hD9, 8hF1, 8hDB, 8h40, 8hA4, 8hA6, 8h2E, 8hAF };驱动OLED的核心是一个状态机它负责协调初始化、清屏、设置坐标和显示数据等操作。状态机设计如下localparam [2:0] IDLE 0, INIT 1, CLEAR 2, SET_POS 3, WRITE 4, DELAY 5; reg [2:0] state; reg [7:0] cmd_index; reg [15:0] delay_cnt; always (posedge clk or negedge reset_n) begin if (!reset_n) begin state INIT; cmd_index 0; end else begin case (state) INIT: begin if (cmd_index 26) begin send_cmd(INIT_CMD[cmd_index]); cmd_index cmd_index 1; end else begin state CLEAR; end end CLEAR: begin send_cmd(8h21); // 设置列地址 send_cmd(0); // 起始列 send_cmd(127); // 结束列 send_cmd(8h22); // 设置页地址 send_cmd(0); // 起始页 send_cmd(7); // 结束页 state WRITE; end // 其他状态处理... endcase end end4. 自定义显示内容实现毕业设计中经常需要显示个人信息或特定图案。我们可以通过修改字库数据来实现自定义显示。SSD1306的显存分为8页每页128列每列8位。4.1 ASCII字符显示首先准备ASCII字符的点阵数据。以下是大写字母A的6x8点阵定义parameter [47:0] FONT_A { 6h00, 6h7C, 6h12, 6h11, 6h12, 6h7C };显示字符串的函数实现task display_string; input [7:0] page; input [7:0] column; input [8*20-1:0] str; integer i; begin set_position(page, column); for (i 0; i 20; i i 1) begin if (str[8*i : 8] ! 0) begin send_data(get_font_data(str[8*i : 8] - 32)); end end end endtask4.2 显示学号或姓名要显示学号20230001可以这样调用display_string(2, 20, 20230001);如果需要显示中文需要准备16x16的点阵字库。以下是电字的部分点阵数据parameter [127:0] CN_CHAR_DIAN { 8h00,8h00,8h00,8h00,8h00,8h00,8h00,8h00, 8h00,8h00,8h00,8h00,8h00,8h00,8h00,8h00 // 实际项目中需要完整的16x16点阵数据 };4.3 图形显示除了文字还可以显示简单的图形。例如显示一个128x64的边框task draw_border; integer i; begin // 上边框 set_position(0, 0); for (i 0; i 128; i i 1) send_data(8hFF); // 下边框 set_position(7, 0); for (i 0; i 128; i i 1) send_data(8hFF); // 左右边框 for (i 0; i 8; i i 1) begin set_position(i, 0); send_data(8h01); set_position(i, 127); send_data(8h80); end end endtask5. 常见问题排查在实际调试过程中可能会遇到各种问题。以下是几个常见问题及解决方法屏幕无任何显示检查电源连接确保OLED屏获得3.3V供电确认复位信号时序正确低电平至少3μs用逻辑分析仪检查SPI信号是否正常显示内容错乱确认SPI时钟频率不超过10MHz检查DC信号是否正确命令为低数据为高验证初始化命令序列是否完整发送部分区域显示异常检查显存更新范围是否正确确认字符点阵数据与显示模式匹配垂直或水平寻址编译错误确保所有寄存器都有初始值检查模块端口与实例化是否匹配确认使用的Verilog语法与工具链兼容注意如果使用Xilinx FPGA可能需要添加IO约束文件指定引脚的电平标准和驱动能力调试时可以添加一些辅助信号方便观察内部状态reg [7:0] debug_state; assign leds {reset_n, oled_cs, oled_dc, debug_state[4:0]};6. 完整代码结构解析提供的驱动代码主要包含以下几个部分时钟分频模块将系统时钟分频为适合SPI通信的频率产生时钟的上升沿和下降沿检测信号SPI发送状态机控制CS、DC信号的时序实现8位数据的串行发送OLED控制状态机管理初始化流程处理显存更新请求协调命令和数据发送字库存储器存储ASCII字符点阵数据可选的中文字库存储显存管理维护显示缓冲区实现局部更新优化关键状态机状态定义localparam STATE_IDLE 3d0, STATE_SHIFT 3d1, STATE_CLEAR 3d2, STATE_SETXY 3d3, STATE_DISPLAY 3d4, STATE_DELAY 3d5;状态转移条件always (posedge clk or negedge reset_n) begin if (!reset_n) begin state STATE_IDLE; end else begin case (state) STATE_IDLE: if (init_start) state STATE_INIT; else if (update_req) state STATE_SETXY; STATE_SHIFT: if (shift_done) state next_state; // 其他状态转移... endcase end end7. 性能优化技巧当显示内容复杂或更新频繁时可以考虑以下优化措施双缓冲技术维护两个显示缓冲区后台更新完成后再切换显示reg [1023:0] buffer[0:1]; // 128x64位显存 reg buffer_sel; always (posedge vsync) begin buffer_sel ~buffer_sel; update_buffer(buffer_sel); end局部更新只更新发生变化的内容区域减少SPI通信量硬件加速使用FPGA的DSP单元实现图形运算通过DMA传输显示数据时钟优化根据需求动态调整SPI时钟频率空闲时降低系统时钟频率表格不同优化技术的效果对比优化技术内存占用执行速度实现复杂度双缓冲高快中局部更新低中低硬件加速高很快高时钟动态调整低可变中实际项目中我的经验是从最简单的实现开始逐步添加优化。先确保功能正确再考虑性能提升。在毕业设计场景下通常不需要太复杂的优化基本的驱动实现就足够满足要求。