项目描述及需要实现的功能
-
旋转电位计可以产生0-3.3V的电压
-
利用板上的串行ADC对电压进行转换
-
将电压值在板上的OLED屏幕上显示出来
上面是官方给出的项目要求,为了利用OLED屏幕的剩余空间,本人额外增加了记录电压数值并绘制图形的功能。项目实现效果如下图所示,详细效果可以通过视频查看。
项目实现思路
项目设计框图如下所示,系统整体由两个模块组成:数模转换模块和显示屏驱动模块。一侧用于获取电压数值,一侧用于对屏幕进行刷新,并实现数据存储功能。
项目最终RTL视图如下:
项目解析
模数转换模块
训练板上自带ADS7868芯片,可以采集电位计产生的电压。该芯片使用SPI协议进行通讯,采样数据共有八位。可以根据芯片手册中的时序编写驱动,获取模拟电压对应数字量化数值。
驱动获取的数值实际上是输入电压对应参考电压的量化数值,取值范围是0-255,真正使用时需要根据参考电压3.3V进行数据转换才能得到真正的电平数值。
在实现这部分代码时,参考了电子森林提供的简易电压表设计教程,该教程中指出在进行二进制数值转BCD码时可以使用左移加三的方法,效理很高。
显示屏驱动模块
该模块在编写时参考了电子森林提供的OLED图形化显示教程,主要工作是读懂SSD1306芯片手册,了解数据更新的方式,之后编写控制程序。与该程序相比,我的显示程序最大的不同是调用了lattice diamond提供的ip核生成了单口RAM用于存储初始屏幕图形和常用字符的数字信息。
该模块的工作方式是:当程序启动后,模块首先从RAM中加载图片数据,对整个显示屏幕进行刷新。之后每隔一段时间更新电压数值并根据寄存器组中存储的数值重新绘制电压信号波形。
接下来将对其中的要点进行简要介绍:
1. RAM模块
上图是单口RAM的具体设置参数,地址深度为256,对应地址线宽度为8,数据宽度为32,并通过数据文件进行初始化。在调用IP核时,最好避免勾选”enable output register“选项,这样能够保证地址线更新的下一个时钟能够正确更新数据。
在上述设计下,RAM共有32×256位数据,显示屏的像素点数是128×32。因此可以将RAM的前半部分存放自定义的图片数据,后半部分存放常用字符。下面是我编写的可以将image2cpp网站生成的点阵数据数组转化为对应RAM宽度的.mem文件的python脚本,有需要的朋友可以直接取用。
# 将image2cpp生成的数组文件转化为.mem文件
s = """0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x09, 0x00, 0x00, 0x40, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x09, 0x00, 0x00, 0x40, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xcb, 0xdd, 0x8c, 0xf3, 0x18,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0x29, 0x12, 0x52, 0x44, 0xa0,
0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0x29, 0x12, 0x5e, 0x47, 0xa0,
0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x29, 0x12, 0x50, 0x44, 0x20,
0x3b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xc8, 0xd2, 0x4e, 0x33, 0xa0,
0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90,
0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90,
0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90,
0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa0,
0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40,
0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00"""
s = s.split('\n')
res = ["" for i in range(8)]
print(res)
with open('res.mem', 'w') as fh:
count = 0
# 转为二进制存储到数组
for i in range(len(s)) :
chars = s[i].split(',')
for char in chars:
char = char.strip()
if not char:
continue
index = i % 8
res[index] = ''.join([res[index], '{:08b}'.format(int(char, 16))])
# 纵向八位拆分为两个16进制数字写入
count = 0
for i in range(len(res[0])):
cur = ''
for j in range(7,-1,-1):
cur = cur + res[j][i]
num = '{:02x}'.format(int(cur, 2))
fh.write(num.upper())
count = count + 1
if (count) % 4 == 0:
fh.write('\n')
2. 寄存器组模块
为了实现电压波形显示功能,我实现了带有译码功能的寄存器组模块。该模块能够存储32个5位电平数值,并能根据当前地址线输出对应寄存器数据的32位译码结果,可直接用于屏幕刷新。
// 32*5寄存器组 + 5-32译码器
module register_and_decoder (
input data_clk,
input rst_n,
input [4:0] val,
input [4:0] address,
output [31:0] val_decode
);
reg [4:0] data [31:0]; // 数据存储
integer i;
// 时钟到来之后数据移位
always @(posedge data_clk or negedge rst_n) begin
if(!rst_n) begin
for(i = 0; i < 32; i = i +1) begin
data[i] <= 5'd0;
end
end
else begin
for(i = 32; i > 0; i = i - 1) begin
data[i] <= data[i-1];
end
data[0] <= val;
end
end
// 译码输出
wire [31:0] tmp = 32'b1 << data[address];
assign val_decode = {data_rvs(tmp[31-:8]), data_rvs(tmp[23-:8]), data_rvs(tmp[15-:8]), data_rvs(tmp[7-:8])}; // 为了顺利输出需要对每组数据的大小端进行颠倒
// 函数块 大小端转化
// https://www.runoob.com/w3cnote/verilog-function.html
function [7:0] data_rvs;
input [7:0] data_in;
integer k;
begin
for(k = 0; k < 8; k = k + 1) begin
data_rvs[8 - k - 1] = data_in[k];
end
end
endfunction
endmodule
3. 数据刷新方式
SSD1306芯片支持三种地址模式,在显示驱动模块中均有用到。下面是驱动模块主循环部分代码,U_SCREEN、U_NUM、U_WAVE三种状态是在原有代码基础上新定义的状态,用于对屏幕显示、电压数值、电压波形进行刷新。
MAIN:begin
case(cnt_main)
5'd0: begin cnt_main <= cnt_main + 1'b1; state <= INIT; end // 初始化硬件
5'd1: begin cnt_main <= cnt_main + 1'b1; state <= U_SCREEN; state_scan <= U_SCREEN; end // 写入基本屏幕数据
5'd2: begin cnt_main <= cnt_main + 1'b1; voltage_reg <= voltage_val; reg_clk <= HIGH; end // 暂存电压数值
5'd3: begin cnt_main <= cnt_main + 1'b1; state <= U_NUM; state_scan <= U_NUM; reg_clk <= LOW; end // 刷新显示数字
5'd4: begin cnt_main <= cnt_main + 1'b1; state <= U_WAVE; state_scan <= U_WAVE; end // 刷新电压波形
5'd5: begin
if (screen_delay >= 22'd1200000) begin
screen_delay <= 22'd0;
cnt_main <= cnt_main + 1'b1;
end
else begin
screen_delay <= screen_delay + 1'b1;
end
end
5'd6: begin cnt_main <= 5'd2; end
default: state <= IDLE;
endcase
end
在刷新屏幕时,因为需要对整个屏幕进行刷新,并且从RAM中不断加载数据,因此芯片被设置为水平地址模式;对电压数值显示进行更新时,由于只需要更新一小部分数据内容,采用了页地址模式;在绘制波形时,因为寄存器中每个数据需要对应屏幕的一列像素,采用垂直地址模式能省去不必要的逻辑。
有兴趣的朋友可以查看源码中不同地址模式的设置方式,但需要注意的是,垂直地址模式设置之后需要对显示区域进行重新设置,如果在之后又将地址模式设置为页地址模式,页地址的起始列将从上次设置的显示区间进行计算,为了避免不必要的麻烦,可以在垂直地址模式数据更新结束之后将显示区间重新设置。
总结与心得体会
感谢电子森林网站提供的诸多教程,通过对教程的学习和动手实践,我对FPGA的了解程度有了很大的提高。
本项目实现的功能与标准功能相比额外增加了一项波形显示功能,该项功能是看完项目三的要求后想到的。虽然实现了电压波形绘制功能,但其仍有一些问题:
-
手动调整电压数值时存在抖动现象,导致不能绘制出较为平滑的波形。
-
显示屏幕共有32行,能够显示0-3.1的电压,对于更高的电压不能较好展示。
另外,因为对数字逻辑掌握得不是很好,屏幕驱动模块的设计显得有些凌乱,并且通用性不强。或许可以对其重新设计,将其划分为不同的模块来实现,提高代码利用率。当本期活动结束后,我会参考其他朋友的项目对代码进行修改,并尝试实现项目三的功能,期待大家的高质量项目。
在准备提交项目文档时,看到了https://www.eetree.cn/wiki/scope_verilog 这篇文章。如果能够早点发现并且配合DDS的教程:https://www.eetree.cn/wiki/dds_verilog,或许能够完成项目三的要求,有些遗憾。