占着坑,这周末把它写了,下周直接拿去水数字系统课作业((
前言
简单来说动机是这样的:这学期我们有一门数字系统设计的课:每个人发了FPGA实验板子(Ego1 ,芯片是Aritx7系列的),结合实验一起讲Verilog语言。不幸的,我们班80多人,只有我一个没有领到板子。那我就有理由好好用一把自己吃灰已久的XUPV5了不是(x
此前做过一些FPGA开发,也一直停留在做超小系统的级别(比如那个至今没时间填完坑的HP35复刻)。我意识到自己的问题不是说不会用还是怎么的,主要是懒和过度担心,有些时候有些问题明明是安静写那么一段时间就能写出来的,我却经常选择回避或者单纯的kill time,不干正事。现在我感觉有必要给自己找些小模块,花几个小时硬逼自己一个一个一口气不中断地写完,作为练习。
这次我们留了一个作业,基本还是在教同学们怎么用case写查找表,最终来驱动一组段码LED。我的开发板上并没有段码LED,而且我觉得如果我也做一个段码LED那就太无聊了,不如设计一个比较精致的HP字符LED屏控制器,然后再画一个PCB,做成XUPV5上那个LCD模块的形状,将它替代掉?
HDSP2000模块
HDSP-2000系列是HP在1975年推出的字符点阵LED显示器,陶瓷DIP12封装,以4个字符为一个模块。这种东西在HP自家的仪器中见得很多,比如HP在1980年推出的HP8662A极低相噪射频信号源上就用了类似的显示模块

这个模块的工艺非常精良,整体是玻璃+陶瓷,这对于见多了塑料屏幕的我而言无疑是很有诱惑力的,它看上去就算不是军用产品也至少是高可靠的工业产品。全新的HDSP模块售价从来不低,我在逛淘宝的时候看到有人卖一个叫“HP5616085”的元件,看上去和HDSP2000完全一样,但是我却查不到任何关于这个编号的资料,而且他所销售的这些模块只有这一个标记,我想一定是某种奇怪的内部编号。HP的显示模块都一直保持引脚兼容的优秀传统,我希望它能和HDSP2000一样用,就低价买了一大把回来。

这个显示器的结构相比于后来的几代微点阵字符屏产品显得非常的简陋,我此前用过的一些驱动简单的点阵屏比如HCMS2904,内置了完整的像素缓冲区、振荡器、同步、亮度控制、电流控制,单片机只需把像素数据往里扔一次就可以高枕无忧了。HDSP2000甚至不能保存完整的一帧——这个显示模块内部只能同时存储一竖列的数据,为了显示完整的字符,需要不停刷新。


- 其内部有一个28bit的移位寄存器,对应当前被选中的那一组竖列的像素数据(每一个字符每一列是7bit,总共4个字符)
- 五个Column Drive Input直接和五竖列LED的阳极分别相连,这五个引脚的电流非常大:可以达到500mA。
- LED的阴极接在一组受移位寄存器控制的电流源上,这些电流源还受一个Blanking引脚(vb)控制,将其拉低可以让屏幕熄灭。


硬件方面的事做起来算是比较轻松了,用某节水课的时间搞定了这么一个小设计,里面用到了一个开关电源来产生驱动LED的3.3V轨(因为它功耗巨大,电流最大可以达到3A,LDO不现实),用几个PNP管做大电流高侧驱动,再加一个74138来解决引脚不够用的问题。
但是测试的时候坑爹的事情就出现了,任凭我怎么弄这个屏幕的引脚就是无比安静,没有任何信号通过去。
XUPV5这个板子的部分外设与FPGA本身之间用了一个CPLD做“胶水逻辑与电平转换”,内置了一个程序,用户也可以选择自己给它编程。按照官方的意思这个CPLD自带的程序应该没有包含什么逻辑:是直通的;但是我实际测试觉得里面一定是做了一些逻辑,使得只有LCD的控制信号才能够通过去,因为之前写的LCD程序在板子上运行并没有什么问题,现在居然一个时钟信号都无法通过。我感到奇怪,但是很可惜,虽然十几年前Xilinx推出这个开发板时附送了那颗CPLD内程序的源码,用wayback machine也无法找回那份文件,我就暂且放弃了,换成了其他的3.3V IO口。虽然一切顺利,还是为没法把这个安装在LCD模块的位置上而感到可惜……

Verilog程序

引脚安排成了如图所示的样子,ABC经过74138译码后,通过一组三极管成为列驱动信号;D1D0分别对应两排屏幕的数据输入端,同一排内屏幕的Data out和Data in是串接的,因为该屏幕的时钟频率可以达到惊人(以1975年逻辑电路的普遍水准而言)的3MHz,这个移位寄存器可以接的非常长也不会影响刷新。
最终决定将系统设计成这样:
主时钟:600kHz
移位寄存器时钟:300kHz
移位寄存器信号:300kHz,相移90度以满足时序要求
工作步骤:
第一步:打出84个时钟,将字符数据发送至行移位寄存器
第二步:等待916个周期,时钟输出线静止
第三步:列计数器+1,准备发送下一列数据,回到第一步
每个刷新周期有1000个时钟,刷一列需要3.3ms,刷满一屏(5列)需要16.7ms,即60fps
这样可以保证较长时间是静止状态,肉眼看上去不会闪烁;对Blanking引脚发一个300kHz的PWM信号,就可以比较容易地调节亮度,而且因为静止的时间比较长,这个PWM信号是比较准的(即:PWM设置为0000就对应没有能量,1000就对应一半的能量,不会有显著的直流偏移。但是如果将刷新周期里的静止时间切分为小块,PWM频率又没有加快的话,会存在一个明显的直流偏移)
先从简单的开始,PWM产生,因为频率比较低,用的就是传统的累加-比较法
////////////////////////////////////////////////////////////////////////////
// PWM DIMMING
////////////////////////////////////////////////////////////////////////////
wire clk_pwm = clkdiv_300kHz_r; // 18.75kHz 4bit PWM
wire pwm_output;
wire [3:0] pwm_setup = GPIO_DIP_SW[3:0]; // for testing
reg [3:0] pwm_cnt_r;
assign pwm_output = (pwm_cnt_r > pwm_setup);
assign HDSP_VB = t_still & pwm_output;
always @ (posedge clk_pwm or negedge FPGA_CPU_RESET_B) begin
if(!FPGA_CPU_RESET_B) pwm_cnt_r <= 4'b0;
else pwm_cnt_r <= pwm_cnt_r + 4'b1;
end
然后是主要的状态机,用来产生所有和时间有关的信号,其实这里的各个计数器都可以和后文的合并,进一步减少资源占用
// goal: Frame refresh rate: 60fps, 16.67ms
// PWM dimming: 4bit 16level
// @300kHz, row refresh timing: 280us
// @300kHz, total row timing: 3.33us
// v
// _/^\_/^\_/^\_/^\_/^\_/^\_/^\_/^\_/^\_/^\_...
// _/^^^\___/^^^\___/^^^\___/^^^\___/^^^\___...
// _________/^^^^^^^\_______/^^^^^^^\_______...
// _____________/^^^v^^^\_______/^^^v^^^\___...
// ^
// Main Clock ^^^\___/^^^\___/^^^ @300kHz
// clk 0~83 prepare next data, phase out
// clk 84~999 keep still
// clk 999 set c, advance hdsp_c_r
reg [9:0] r_cycle_cnt_r; // Row refresh cycle counter
reg [2:0] hdsp_c_r; // Column counter
assign {HDSP_C, HDSP_B, HDSP_A} = hdsp_c_r;
// Timing Signals
wire t_sr_active = (r_cycle_cnt_r >= 10'd0)
& (r_cycle_cnt_r <= 10'd83);
wire t_sr_active_dly = (r_cycle_cnt_r >= 10'd1)
& (r_cycle_cnt_r <= 10'd84);
wire t_still = !t_sr_active_dly;
wire t_r_cycle_done = (r_cycle_cnt_r == 10'd999);
// Use 90deg phase shifted clk for better data timing
// Pause the clk when sr is not active
assign HDSP_CLK = (clk_600kHz ^ clk_300kHz) & t_sr_active_dly;
always @ (posedge clk_300kHz or negedge FPGA_CPU_RESET_B) begin
if(!FPGA_CPU_RESET_B) begin
r_cycle_cnt_r <= 10'b0;
hdsp_c_r <= 3'b0;
end else begin
if(t_r_cycle_done) begin
r_cycle_cnt_r <= 10'b0;
if(hdsp_c_r == 3'd4) hdsp_c_r <= 3'b0;
else hdsp_c_r <= hdsp_c_r + 3'b1;
end else begin
r_cycle_cnt_r <= r_cycle_cnt_r + 10'b1;
end
end
end
这里是字符产生,有一个12位DEC SIXBIT编码(ASCII前身)的字符寄存器,按照行计数器,以时间顺序读取这12个字符,用一个字符查找表加上列偏移量找到它的像素数据,再用行计数器取出其中一个bit,直接给到D[1:0],文中只给了一个。
这里用了generate for语法来初始化这个字符寄存器(因为懒得写输入输出了),虽然里面有很多乘法之类的,因为i在综合时被视作一个常数,这部分都会被自动优化掉。其实你看到我在这里写了这么一大堆东西,目的就是为了能够在程序里写下一句漂亮的字符串常量(orz),不然的话一个初始化没必要搞得这么麻烦。但是在取像素数据时不可避免会遇到x6和x5这两个乘法(除非将字符ROM以2为底对齐,那样的话又觉得有些浪费(话说回来,反正出来是bram,没有什么浪费的说法吧草)),我将其变为位移和加法的组合,避免了使用乘法器。
字符集的下载点这里,这是我以前为了另一个HP显示屏设计的字符集,形状上都比较方正。这个文件里面是DEC SIXBIT(将7bit ASCII编码减去0x20即可得到)编码顺序排列的64个字符,每个字符分为五竖列,按从左到右排列;每一列内的LSB是字符的上侧。
////////////////////////////////////////////////////////////////////////////
// TEXT GENERATION
////////////////////////////////////////////////////////////////////////////
// 12-digits in total
// Data format: DEC SIXBIT
// LED Font: 7*5 Lithias Font, LSB top
// LIF_DEC_SIXBIT.hex / coe
// wire [7:0] lbuf_ascii_input[11:0];
wire [95:0] lbuf_ascii_in;
assign lbuf_ascii_in = "HELLO,WORLD!"; // 8bit ASCII constant
reg [71:0] lbuf_ascii_1_r;
// TODO: merge the following two registers into r_cycle_cnt_r
reg [2:0] r_portion_cnt_r; // count from 0 to 6
reg [3:0] cur_digit_cnt_r; // count from 0 to 11
wire [8:0] font_rom_addr;
wire [6:0] font_rom_out;
wire data_out_1;
// offset6 is a 0~72 offset times 6
wire [6:0] lbuf_offset6 = {1'b0, cur_digit_cnt_r, 2'b0}
+ {2'b0, cur_digit_cnt_r, 1'b0};
wire [5:0] lbuf_sel = lbuf_ascii_1_r[offset6 +: 6];
assign font_rom_addr = {1'b0, lbuf_sel, 2'b0} + {3'b0, lbuf_sel} + hdsp_c_r;
assign data_out_1 = font_rom_out[r_portion_cnt_r];
assign HDSP_D = {data_out_1, 1'b0};
// For testing only: converting 8bit ASCII to 6bit DEC SIXBIT
genvar i;
generate
for(i=0; i<12; i=i+1) begin : generate_lbuf_loader
always @ (posedge t_sr_active or negedge FPGA_CPU_RESET_B) begin
if(!FPGA_CPU_RESET_B) lbuf_ascii_1_r[6*(12-i)-1:6*(12-i)-6]
<= 6'b0;
else lbuf_ascii_1_r[6*(12-i)-1:6*(12-i)-6]
<= {lbuf_ascii_in[i*8+7:i*8]}-8'b00100000;
end
end
endgenerate
always @ (posedge clk_300kHz or negedge FPGA_CPU_RESET_B) begin
if(!FPGA_CPU_RESET_B) begin
r_portion_cnt_r <= 3'b0;
cur_digit_cnt_r <= 4'b0;
end else begin
if(t_still) begin
r_portion_cnt_r <= 3'b0;
cur_digit_cnt_r <= 4'd0;
end else if(r_portion_cnt_r == 3'd6) begin
r_portion_cnt_r <= 3'b0;
cur_digit_cnt_r <= cur_digit_cnt_r + 4'b1;
end else begin
r_portion_cnt_r <= r_portion_cnt_r + 3'b1;
cur_digit_cnt_r <= cur_digit_cnt_r;
end
end
end
font_rom font_rom_inst(
.a(font_rom_addr),
.spo(font_rom_out));
最后再加上PLL,弄出所需要的600kHz时钟,就算是完成了。我的开发板上有一个100MHz的时钟源,我用PLL将其降到6MHz后再进一步用锁存器分频得到所需的时钟。
呼……这就是一个下午的成果,感觉对于项目的复杂度而言花的时间还是太多了。最终将这个程序通过JTAG写进去,就得到了一句名言

