ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Lattice FPGA实战:LCMXO2数字时钟的分频与Nokia5110驱动

Lattice FPGA实战:LCMXO2数字时钟的分频与Nokia5110驱动 简介LATTICE小脚丫LCMXO2开发板搭配Nokia5110液晶屏的数字时钟设计资源面向FPGA入门学习者与数字系统实践者可帮助正在熟悉Lattice Diamond流程或Verilog HDL的读者快速掌握LCD驱动、按键控制与分频计时等常用逻辑。资源内含完整Verilog源码、PDF原理图及文档资料时钟以25MHz输入为基准通过时钟分频、按键消抖与Nokia5110驱动等模块实现时分秒显示和按键调时功能功能划分清晰便于参考其模块化设计方法与工程组织方式。压缩包共71个文件以.v源码、.pdf原理图、.doc说明文档为主同时包含lpf约束文件、jed配置文件以及综合仿真生成的工程辅助文件整体仅1.32MB轻量易查阅适合直接对照学习或在此基础上二次开发。已有383人学习浏览这套资料可作为基于小脚丫开发板与Nokia5110液晶屏实现数字时钟的完整配套参考能够节省整理外设驱动与底层时序的时间。1. 没接晶振也能被“25MHz”坑到小脚丫 LCMXO2 与 Nokia5110 数字时钟一块几十克的小脚丫 LCMXO2 开发板配上拆机 Nokia5110 屏就能组成带按键校时的数字时钟。可这个工程里最容易翻车的不是 Verilog 逻辑本身而是开发板上那颗 25MHz 晶振的引脚约束和 Lattice Diamond 的时序设置。很多人把源码下载后直接综合结果秒走得像跑表或者屏幕白屏问题大多出在分频参数和约束文件上。这套资料包含顶层Digital_clock.v、Clock_div.v、clock_cnt.v、key_board.v、LCD_nokia5110.v还有fpga_board.pdf原理图和 JED 烧录文件适合正在学习 Lattice FPGA、或准备从其他厂商转到 Lattice 的开发者。下面按分频计数、Nokia5110 驱动、Diamond 下载、参数化移植这几个环节拆开讲拿到手能直接复现。2. 分频器与时钟计数从 25MHz 到 1Hz 再到“时分秒”2.1 为什么建议生成 1Hz 使能而不是 1Hz 时钟很多刚开始写 FPGA 的人习惯把 25MHz 计数到 25000000 后取反得到一个 1Hz 时钟去驱动秒计数。这个写法在面包板上能跑但在 Lattice Diamond 里做时序收敛时容易被约束卡住内部多出一个低频时钟域综合器和布局布线器要额外处理时钟树仿真也容易遇到posedge clk_1Hz和主时钟边沿不对齐的问题。这个工程的clk_in是 25MHzClock_div.v里更稳妥的做法是每 25000000 拍输出一个高电平脉冲clk_1Hz_en秒计数模块只在脉冲为 1 时更新一次寄存器其余时间所有寄存器继续由 25MHz 主时钟驱动。module Clock_div( input clk_in, // 25MHz master clock input rst_n_in, // active low reset output reg clk_1Hz_en // 1Hz clock enable pulse ); localparam integer CNT_MAX 25_000_000 - 1; reg [24:0] cnt; always (posedge clk_in or negedge rst_n_in) begin if (!rst_n_in) begin cnt 0; clk_1Hz_en 0; end else if (cnt CNT_MAX) begin cnt 0; clk_1Hz_en 1; // 只保持一个主时钟周期 end else begin cnt cnt 1b1; clk_1Hz_en 0; end end endmodule这里把CNT_MAX定义为25000000-1cnt用 25 位无符号数最大计到 24999999。关键点是clk_1Hz_en只在cntCNT_MAX的那一拍拉高下一拍立即拉低所以它不是低频时钟而是一个脉冲。后面的clock_cnt.v里判断if (clk_1Hz_en) ...就能让秒寄存器每秒加一同时避免产生第二个时钟源。2.2 分频参数表与移植时的边界目标频率输入频率CNT_MAX 十进制CNT_MAX 十六进制cnt 位宽1Hz25MHz249999990x17D783F251Hz50MHz499999990x2FAF07F261Hz100MHz999999990x5F5E0FF27如果把这套代码用到其他晶振频率的 LCMXO2 板子上要根据上表修改CNT_MAX。更推荐的做法是用parameter integer CLK_MHZ 25配合localparam integer CNT_MAX CLK_MHZ * 1_000_000 - 1;这样顶层例化时只需要传一个参数不用手算十六进制和位宽。$clog2(CNT_MAX1)可以自动算出计数器的位宽Lattice LSE 支持这个系统函数但旧版本偶尔会报错遇到时再手动改回固定位宽。注意如果只改CNT_MAX不改cnt位宽在 50MHz 晶振下计数器会溢出回绕秒信号会变成伪随机脉冲实际观察到的现象就是走字忽快忽慢。2.3 clock_cnt.v 的 BCD 计数与 23:59:59 回滚有了 1Hz 使能脉冲后clock_cnt.v的任务是把“秒、分、时”拆成 6 个 BCD 位。不要把所有进位条件写在一个always块里硬推秒个位和秒十位是两级独立进位秒个位到 9 时使能有效时归零并输出sec_carry秒十位收到进位后加一到 5 时归零并输出min_carry。分钟同理小时十位到 2 且个位到 3 时整个小时回零。reg [3:0] sec_unit; reg sec_carry; always (posedge clk_in or negedge rst_n_in) begin if (!rst_n_in) begin sec_unit 4d0; sec_carry 1b0; end else if (clk_1Hz_en) begin if (sec_unit 4d9) begin sec_unit 4d0; sec_carry 1b1; end else begin sec_unit sec_unit 1b1; sec_carry 1b0; end end else begin sec_carry 1b0; end end这段代码的关键是sec_carry不能长期保持高电平否则秒十位会在同一拍内被连续加多次。所以无论有没有使能都先把进位清零只有秒个位从 9 变 0 的那一拍才拉高。实际工程中clock_cnt.v会继续把秒十位、分个位、分十位、时个位和时十位全部按这种方式连接最后得到一组[23:0]或 6 个 4 位 BCD 输出供显示模块直接使用。3. Nokia5110 驱动与按键校时PCD8544 写字节、字库和三段式状态机3.1 Nokia5110 和 LCMXO2 的接口信号Nokia5110 的控制器是 PCD8544SPI 接口需要五根控制线RST、CE、DC、DIN、SCLK另外还有背光控制 BL。顶层Digital_clock.v里列出了lcd_rst_n_out、lcd_ce_n_out、lcd_dc_out、lcd_bl_out实际上LCD_nokia5110.v模块内部还需要sclk和din两个输出只不过在顶层例化时未必暴露出来。对照fpga_board.pdf原理图时要把这 6 个信号全部找到。LCD 信号方向作用备注lcd_rst_n_outoutput复位低有效上电后拉低至少 1us 再释放lcd_ce_n_outoutput片选低有效每个字节传输期间保持低lcd_dc_outoutput1数据0命令须在 SPI 时钟前建立lcd_bl_outoutput背光开关可控制 PWM 亮度sclk / dinoutputSPI 时钟和数据顶层未列出LPF 里也要约束PCD8544 是 MSB first每传输一个字节后 CE 拉高。如果把 DC 信号和字节内容同时变化容易读到错误命令所以要先让 DC 稳定再启动 SPI 时钟。RST 初始化时拉低再拉高否则屏幕上会出现随机噪点或花屏。3.2 写一个字节的基础状态机下面的代码提供了一个 PCD8544 写字节状态机的骨架把 bit 计数、移位输出、CE 和 DC 放在同一个状态机里。实际模块会在上层增加命令/数据选择信号但核心时序就是这样module LCD_nokia5110( input clk_in, input rst_n_in, input start_write, input [7:0] tx_data, input dc_ctrl, output reg lcd_ce_n_out, output reg lcd_dc_out, output reg sclk, output reg din, output reg busy ); localparam IDLE 2d0; localparam SHIFT 2d1; localparam STOP 2d2; reg [3:0] bit_cnt; reg [1:0] state; always (posedge clk_in or negedge rst_n_in) begin if (!rst_n_in) begin state IDLE; bit_cnt 0; lcd_ce_n_out 1; lcd_dc_out 0; sclk 0; din 0; busy 0; end else begin case (state) IDLE: begin if (start_write) begin lcd_dc_out dc_ctrl; lcd_ce_n_out 1b0; bit_cnt 3d7; busy 1b1; state SHIFT; end end SHIFT: begin sclk 1b0; // 先在低电平放数据 din tx_data[bit_cnt]; bit_cnt bit_cnt - 1b1; if (bit_cnt 0) state STOP; end STOP: begin sclk 1b1; // 上升沿锁存数据 lcd_ce_n_out 1b1; busy 1b0; state IDLE; end endcase end end endmodule这个状态机在IDLE收到start_write后拉低片选进入SHIFT状态每个主时钟周期发送一个 bit。数据在sclk低电平时摆到din上随后在STOP状态拉高sclk对应 PCD8544 在时钟上升沿采样。bit_cnt从 7 减到 0正好发完 8 bit。代码里的busy信号可以让上层模块知道当前字节传输是否结束避免连续写入时覆盖数据。PCD8544 初始化序列一般是0x21, 0xC0, 0x06, 0x13, 0x20, 0x0C分别对应扩展命令、Vop 对比度、温度系数、偏置、基本命令、显示模式。如果对比度太低在实机上表现为“有内容但是看不清”可以调大0xC0的数值比如改成0xBF。显示位置用 X/Y 坐标命令设置每次写入前要先指定列地址。3.3 字库把“时分秒”转成 5×7 点阵Nokia5110 在 84×48 分辨率下显示一个字符通常占 5 字节每字节的 bit6..bit0 对应一列的 7 个点。这个工程的LCD_nokia5110.v里一般会有一组case把 ASCII 码映射成 5 字节点阵。例如数字0可以写成case (char) 8h30: lcd_rom {8h3E, 8h51, 8h49, 8h45, 8h3E}; // 字符 0 8h31: lcd_rom {8h00, 8h42, 8h7F, 8h40, 8h00}; // 字符 1 8h3A: lcd_rom {8h00, 8h12, 8h00, 8h12, 8h00}; // 冒号 : default: lcd_rom {8h00, 8h00, 8h00, 8h00, 8h00}; endcaseVerilog 里lcd_rom的位宽要声明为 40 bit正好对应 5×8 点阵。case每个分支都要返回 40 bit 数据否则综合时会有位宽不匹配警告。把clock_cnt.v的 BCD 位加上 8h30 转成 ASCII 后再逐字节写入 LCD 的显示缓存就可以看到数字跳动。冒号和分隔符也放进字库否则显示“12:34:56”时中间会空出来。显示刷新不建议在每秒变化时把整屏清掉重发可以先写一个 84×6 的显示 buffer每秒只更新时间字段所在的行。这样不会闪烁也能减少 SPI 写入占用留给按键校时逻辑更多余量。3.4 key_board.v 三段式消抖和边沿检测按键key1_set和key2_up是机械开关直接连到 FPGA 端口上会引入抖动。key_board.v里我一般用三段式状态机第一段把外部按键同步到主时钟域第二段用计数器确认按下超过 10ms 才有效第三段输出一个单周期的按键脉冲。去抖计数器在 25MHz 下大约是 250000 个周期。reg [17:0] debounce_cnt; always (posedge clk_in or negedge rst_n_in) begin if (!rst_n_in) debounce_cnt 18d0; else if (key_sync debounce_cnt ! 18d249999) debounce_cnt debounce_cnt 1b1; else if (!key_sync) debounce_cnt 18d0; end assign key_valid (debounce_cnt 18d249999);key_valid高电平只持续一个主时钟周期后面的clock_ctl.v用它控制clock_cnt.v的校时使能。切换模式时key1_set改变状态机状态调节时间时key2_up让当前模式的 BCD 位加一。这里最容易出错的是校时加一和秒进位同时发生所以进入校时模式后要屏蔽clk_1Hz_en只保留按键脉冲否则秒位会跳两下。4. Lattice Diamond 工程约束与烧录验证从 LPF 到 JED4.1 顶层模块端口和 LPF 引脚约束的对应关系拿到源码后先打开Digital_clock.ldf或Digital_clock.ccl。.ldf是 Lattice Diamond 工程文件.ccl是命令行编译脚本。综合实现后会生成Digital_clock_impl1.jed但真正决定能不能跑起来的是Digital_clock.lpf和fpga_board.pdf。fpga_board.pdf是开发板原理图要对照它找到 25MHz 晶振、按键、Nokia5110 排针对应的 FPGA 引脚然后在.lpf里写LOCATE。下面是一段最基础的 LPF 引脚约束# 不检查异步路径避免复位产生错误时序报告 BLOCK RESETPATHS; BLOCK ASYNCPATHS; # 25MHz 主时钟 LOCATE COMP clk_in SITE G2; FREQUENCY PORT clk_in 25.000 MHz; # 复位按键低有效 LOCATE COMP rst_n_in SITE M4; IOBUF PORT rst_n_in PULLMODEUP;LOCATE COMP把端口映射到具体 FPGA 引脚PULLMODEUP设置复位按键的上拉FREQUENCY PORT告诉布线器时钟频率让静态时序分析去检查路径。G2和M4只是示例不同批次的小脚丫开发板引脚不一定相同必须对照fpga_board.pdf修改。如果漏掉FREQUENCY PORTDiamond 不会报错但时序报告会显示时钟约束未定义Nokia5110 的 SPI 时序可能偶发乱码。4.2 从综合到生成 JEDEC 的完整流程Lattice MachXO2 的下载文件是.jed。工程目录里的Digital_clock_impl1_map.ncd、Digital_clock_impl1.par、Digital_clock_impl1.pad、Digital_clock_impl1.twr都是布局布线过程的中间产物可以配合 Diamond 的 Timing Analyzer 查看。文件作用.lpf引脚、频率、约束.ngd综合后的逻辑网表.ncd布局布线后网表.pad引脚分配报告.twr时序分析报告.jed最终写入芯片的文件Diamond 支持 TCL 批处理不用每次都在 GUI 里手动点。下面是在 Diamond Tcl Console 里执行的完整流程# 打开工程并重新跑综合、映射、布局布线、生成 JED prj_project open Digital_clock.ldf prj_run Synthesis -impl impl1 prj_run Map -impl impl1 prj_run PAR -impl impl1 prj_run Generate -impl impl1这段脚本先重新综合再用 MAP 把逻辑映射到 LCMXO2 的片内资源PAR 做布局布线最后 Generate 生成.jed。-impl impl1对应实现名如果工程里改成了别的名字这里也要同步改。使用 Synplify 作为综合器时prj_run Synthesis会自动调用它在后台执行。4.3 下载前检查JED、器件型号和 FTDI 驱动小脚丫 LCMXO2 开发板通常自带 USB 下载器在系统里识别为 FTDI 设备。打开 Diamond Programmer新建工程Device Family 选 MachXO2具体型号以板子上丝印为准然后指定Digital_clock_impl1.jed。下载时如果报Cannot identify device先检查开发板供电、USB 线是否为数据线再确认 Lattice USB 驱动已安装。写完后LCMXO2 是 SRAM 加 Flash 的结构。默认Direct Download只把 JED 写入 SRAM掉电后就丢失如果需要上电自动运行数字时钟必须在 Operation 里选择Flash Download把 JED 写入内部配置 Flash。这个工程源码目录里同时有.jed和 impl1 中间文件说明作者已经在 Diamond 里跑通过完整流程重新编译后基本不需要改动。注意直接下载和 Flash 下载的操作选项不同临时调试用 SRAM 模式就行做小样机或课程设计时一定要选择 Flash Download。4.4 常见报错未分配引脚、频率违例和引脚冲突第一次编译时最常见的是Unrouted constraint或Cant place component。这类报错要么是 LPF 里少了某个引脚要么是LOCATE的 SITE 名称不合法。建议先只分配clk_in和rst_n_in跑一遍 Map 看报告再逐步加入按键和 LCD 引脚这样能快速定位是哪个信号没约束。如果 PAR 后时序报告显示Setup time violation优先检查FREQUENCY PORT是否写了 25再检查Clock_div.v里的cnt是否被综合成过长的计数器。LCMXO2 的 LUT 资源不多Nokia5110 的 SPI 时钟不需要很高通常设 1MHz 左右即可。在 LPF 里给sclk额外加FREQUENCY PORT sclk 1.000 MHz;可以让 PAR 更合理地布线避免为了超高 SPI 频率而牺牲其他路径。5. 参数化移植到其他 LCMXO2 板只改一个参数不重写分频器5.1 用 parameter 代替硬编码的 25000000这套工程里Clock_div.v如果写死了25000000换到 50MHz 或 100MHz 晶振的 LCMXO2 板子就要重新算计数上限和位宽很容易漏改。更可靠的写法是把晶振频率作为参数传入module Clock_div #( parameter integer CLK_MHZ 25 )( input clk_in, input rst_n_in, output reg clk_1Hz_en ); localparam integer CNT_MAX CLK_MHZ * 1_000_000 - 1; reg [$clog2(CNT_MAX1)-1:0] cnt; always (posedge clk_in or negedge rst_n_in) begin if (!rst_n_in) begin cnt 0; clk_1Hz_en 0; end else if (cnt CNT_MAX) begin cnt 0; clk_1Hz_en 1; end else begin cnt cnt 1b1; clk_1Hz_en 0; end end endmodule顶层例化时只要改一个参数Clock_div #( .CLK_MHZ(50) ) u_div( .clk_in(clk_in), .rst_n_in(rst_n_in), .clk_1Hz_en(clk_1Hz_en) );CNT_MAX在 25MHz 下的值是 24999999在 50MHz 下自动变成 49999999位宽由$clog2(CNT_MAX1)自动计算。这样移植到不同频率的板子时不需要动状态机和显示模块。如果 Lattice LSE 不识别$clog2就按第 2 章的表手动声明位宽但要记得一起改。5.2 用仿真快速验证分频器而不是盯着秒表看手边没有示波器时可以临时把CNT_MAX改成 2499保留clk_1Hz_en输出在 ModelSim 或 EDA Playground 里跑 5000 个周期观察sec_unit是否在使能脉冲后加一。这样能隔离“分频器错”和“液晶显示错”。烧板前必须改回真正的25_000_000-1否则时钟走得飞快。最后别忘了同步修改 LPF 里的频率约束FREQUENCY PORT clk_in 50.000 MHz;如果实际晶振是 50MHz但 LPF 里仍写 25MHzPAR 会用更宽松的预算去布线时序收敛结果不代表真实情况。按键去抖计数也应该和 CLK_MHZ 绑定把DEBOUNCE_MS * CLK_MHZ * 1000做成可配置参数这样整份数字时钟工程才能在任意 LCMXO2 板子上无缝复用。本文还有配套的精品资源点击获取
返回列表