0. 引言
在第 3 节中,我们连接了一个旋钮,可供人与开发板交互。那么现在问题来了,既然人拧旋钮能控制开发板上的东西,那我们能不能让开发板把人输入的数据发送给其他设备呢?显然,是可以的。
目录
1. 设计思路
想把数据发送给其他设备有很多种方法可以实现,我们可以根据需求自己设计编码和传输方式,也可以选用一种较为通行的通信协议。
UART(Universal Asynchronous Receiver / Transmitter,一般俗称“串口”)是一种常用的通信方式,它最早出现于 1959 年发布的 DEC PDP-1 计算机,1970s 被广泛用于连接计算机主机与用户终端,现代常用于嵌入式设备。它异步,不是总线,数据结构还非常直白,所以实现较为容易。而且,在 YADAN Board 上还有专门的一片 CH340 芯片,可以将 UART 信号转换成 USB 信号,这样就可以直接通过 USB 端口把数据传给电脑了。所以,我们将基于第 3 节的代码,尝试增加一个 UART 发送器,将 ADC 的采样结果(可以旋转旋钮改变)发送给电脑。
UART 的一帧数据的结构通常如下:
……空闲 | 起始位 | 数据位 | 校验位 | 停止位 | 空闲…… |
…… | 1 位 | 一般 5 - 9 位 | 0 或 1 位 | 一般 1 - 2 位 | …… |
固定 H | 固定 L | 可高位优先、也可低位优先 | 视校验方式 | 固定 H | 固定 H |
我们可以先实现最简单且最常见的方式,即一帧包含 8 位低位优先的数据,停止位为 1 位,无校验位:
……空闲 | 起始位 | 数据位 | 停止位 | 空闲…… |
…… | 1 位 | 一般 8 位 | 1 位 | …… |
固定 H | 固定 L | 低位优先 | 固定 H | 固定 H |
此种设定下,假如要发送一字节的二进制数 00110010
,只要把信号线上的电平拉成这样:
▔▔▔▔▔▁▁▔▁▁▔▔▁▁▔▔▔▔▔▔ (波形)
0 1 0 0 1 1 0 0 (数据:因为是低位优先,所以和上边文字里写的是倒着的)
空空空空空起数数数数数数数数停空空空空空 (注释:空-空闲;起-起始位;数-数据位;停-停止位)
考虑到这一流程,我们可以编写如下图所示的一个模块 UART_TX
。它有三个输入端口,若收到控制开始传输的信号,就会从 TX 端口开始发送传入的一字节数据。
将 ADC_drv
输出的 8 位数据也传入 UART_TX
,再写一个 UART_TX_ticker
模块每隔一段时间给 UART_TX
发送控制开始传输的信号,最终可搭好如下图所示的整个系统。
这个系统不仅能继续实现第 3 节的代码中原有的旋旋钮调 LED 亮度的功能,还会将反应旋钮角度的数据通过 UART 发送出去。
2. 实现
在写 UART_TX
的代码时,需要想想,两个码元 (symbol) 的时间间隔应该为多少呢?几种常用的 UART 波特率(Baud,每秒传输的码元数,1 Baud = 1 symbol/s)为 9600、57600、115200 等,我们可以以 115200 波特率来设计 UART_TX
模块。
翻看 YADAN Board 的介绍 可知,P34 号引脚连接的时钟频率是 24 MHz,那么要多少分频才能让它变为 115200 Hz 呢?208.33 分频吗?
不,带小数的分频系数是没法实现的,我们直接 208 分频即可。虽然这会使实际的传输波特率为大约 115384.61 Baud,但是 UART 是一种异步的通信方式,波特率存在允许范围内的误差是不影响传输的。
最终,UART_TX
模块的代码如下:
module UART_TX (input CLK_24MHz,
input [7: 0] data_in,
input start, // 开始发送
output reg TX = 1'b1);
// 功能:用 UART 发送一字节数据(波特率: 约 115384.61,数据位: 8,奇偶校验: 无)
reg [6: 0] counter = 'd0; // 用于分频(208 分频可实现约 115384.61 的波特率)
reg clk_uart = 1'b0; // 分频后的时钟
reg [3: 0] state = 'd0; // 指示发送状态(0: 闲置,1: 发送起始位,2 ~ 9: 发送数据,10: 发送停止位)
reg [7: 0] data_buffer = 8'd0; // start 触发时存储 data_in 端口输入的数据
always @(posedge CLK_24MHz) begin // 208 分频
if (counter < 'd103) begin
counter = counter + 1;
end
else begin
counter = 'd0;
clk_uart = ~clk_uart;
end
end
always @(posedge clk_uart) begin
if (state == 'd0 && start == 1'b1) begin
state = 1;
data_buffer = data_in;
end
else if ('d0 < state && state < 'd9) begin
state = state + 1;
end
else begin
state = 0;
end
case (state)
'd0: TX = 1'b1;
'd1: TX = 1'b0;
'd2: TX = data_buffer[0];
'd3: TX = data_buffer[1];
'd4: TX = data_buffer[2];
'd5: TX = data_buffer[3];
'd6: TX = data_buffer[4];
'd7: TX = data_buffer[5];
'd8: TX = data_buffer[6];
'd9: TX = data_buffer[7];
default: begin
TX = 1'b1;
state = 0;
end
endcase
end
endmodule
在 这个页面 获取完整的项目文件,综合它、生成比特流文件、烧录进开发板,把开发板上的 Micro-USB 端口与电脑相连,打开串口监视器,即可看到开发板发来的数据。旋转旋钮,即可让数据变化。
3. 用 Python 版 OpenCV 写一个简单的数据显示软件
用普通的文字版串口监视器不够直观,我们可以干脆自己写一个软件来可视化接收到的数据🤠。
我在项目目录下还放了个 GUI_by_Python
文件夹,里边有个 ADC_plot.py
文件,如果你配置好了所需的 Python 环境,运行它就可以看到类似下图的效果。折线图能反应旋钮所处角度的变化情况,且画面右上角以人类易读的十进制形式展示出了接收到的数据。
(其实 SerialPlot 也可以画折线图,不过第 5 个实验我们将处理格式较复杂的数据,所以可以自己画图先打打基础)
4. 一些调试方法
在自己编写通信模块的时候,不一定能一次就写对,而且光看代码或者跑仿真也不一定能直观地找到哪里有问题,所以我们也可以尝试另外两种运行时的调试方法。
第一种方法是使用 TD 工具自带的 ChipWatcher,我们可以设置触发条件,在 FPGA 运行时抓取所需的信号来观察。它的使用方法可以在 TD 工具的软件手册中查看。
第二种方法是使用逻辑分析仪,它可以采样并记录数字信号,供开发者观察。逻辑分析仪的配套软件一般还能根据一些常见的通信协议尝试解析数据,比如我还没写对 UART_TX
模块的时候,它能够如下图所示标记出不正常的码元。各大购物网站上均可以找到逻辑分析仪,对于不复杂的场景,几十元的款式即能满足需求。
5. 留个作业
在这个实验里,我们只实现了 UART_TX
模块来发送出去数据,那怎样才能实现 UART_RX
模块来接收数据呢?这个问题留给读者作为作业,比如可以尝试让电脑通过 UART 发指令控制开发板上 LED 的开 / 关。欢迎将你的实现方法或代码分享在此帖下方或者论坛中。