GUVA-S12SD 是一款紫外光电二极管传感器,探测波长 240~370 nm(正好覆盖太阳紫外线的 UVA + UVB 波段),输出一个和紫外强度成正比的模拟电压。
输出是模拟信号fpga读不了,所以必须在外面加一颗 ADC(模数转换器)把模拟电压变成数字量
本教程用 ADS1115 这颗 16 位 I2C ADC 来做这件事:GUVA-S12SD 的模拟输出接 ADS1115 的 AIN0,FPGA 通过 I2C 读回 16 位数字码值,换算成毫伏(mV)后从串口打印出来。
信号链路是「紫外强度 → 模拟电压 → ADC 采样 → FPGA 读数字 → 串口输出」,这也是所有模拟传感器接 FPGA 的标准套路。
1、模块来源
GUVA-S12SD 模块一般只有 3 个管脚:
VCC:电源,2.9~5.5 V(推荐 3.3 V)GND:地线,必须和 FPGA 共地SIG:模拟电压输出,0~1 V(和紫外强度成正比)
光有 GUVA-S12SD 还不够——FPGA 读不了模拟电压。我们还需要一颗 ADS1115 ADC 模块来做模数转换:
2、参数
GUVA-S12SD 传感器
工作电压:2.9~5.5 V(推荐 3.3 V)
输出信号:模拟电压,0~1 V(正比于 UV 强度)
光谱响应:240~370 nm(UVA + UVB)
响应时间:毫秒级(具体值以规格书为准)
管脚数量:3 Pin(VCC、GND、SIG)
ADS1115 ADC
工作电压:2.0~5.5 V
通讯接口:I2C
从机地址:7 位地址 0x48(ADDR 引脚接 GND 时)
分辨率:16 位(含符号位)
量程:±6.144 V(PGA 可配,本教程用这个)
LSB 精度:0.1875 mV
转换速率:128 SPS(一次转换约 7.8 ms)
管脚数量:4 Pin(VCC、GND、SCL、SDA),模块上一般还有 ADDR 和 ALERT
⚠️警告:
ADS1115 模块的 ADDR 引脚决定 I2C 地址:接 GND = 0x48,接 VDD = 0x49,悬空 = 0x4A。本教程假设 ADDR 接 GND。如果你的模块 ADDR 引脚没接,地址可能不是 0x48,读回来的数据就是乱的。接线前先确认 ADDR 接法!
3、实战任务
使用立创·逻辑派 FPGA-G1 开发板读取 GUVA-S12SD 紫外传感器的模拟电压(经过 ADS1115 采样),换算成毫伏后通过串口打印出来:
UV:0123mV采样一轮约 11 ms(10 ms 转换等待 + I2C 收发和串口发送),所以串口会连续不断地打印,大约每秒 90 行。数值为 AIN0 引脚上的电压(0~6143 mV)。拿手机闪光灯照一下传感器,数值应该明显上升;遮住传感器,数值应该降下来。
4、时序说明
4.1 ADS1115 读写时序
这里不再对时序赘述,有兴趣可去 ADS1115 那一章了解详细时序
4.2 数据换算
ADS1115 读回 16 位补码,量程 ±6.144 V 时 1 LSB = 0.1875 mV。换算公式:
代码里不用真的做除法(FPGA 做除法贵),乘 3 再右移 4 位就等于除以 16:
mv = (code × 3) >> 4GUVA-S12SD 输出是 0~1 V 的正电压,理论上码值范围 0~5333(1000mV / 0.1875)。如果读到负数(bit15=1),那是噪声毛刺,按 0 处理。
UV 强度和电压的关系请看规格书的响应曲线,不同模块电路不一样,这里不做换算。
5、程序编写
整个工程分四个模块:
- top.v(顶层):定时产生采样请求;把 ADC 码值换算成毫伏并拆成十进制位;拼 11 个字节的 ASCII 串喂给串口。
- ads1115_ctrl.v(ADS1115 驱动):步骤状态机,指挥 i2c_master 按顺序完成一次采样(写配置 → 等 10ms → 读结果)。
- i2c_master.v(I2C 主机):底层字节搬运,跟接什么芯片无关,写一次进 IP 库以后所有 I2C 芯片都复用。
- uart_tx.v(串口发送):和前面串口章节通用,原样沿用。
`timescale 1ns / 1ps
module top (
input sys_clk, // 系统时钟 50MHz
input rst_n, // 复位,低电平有效
output scl, // I2C 时钟线 → ADS1115 SCL
inout sda, // I2C 数据线 → ADS1115 SDA
output uart_tx // 串口发送 → 上位机
);
// ---------------------------------------------------------------------
// 内部连线
// ---------------------------------------------------------------------
// ads1115_ctrl ↔ i2c_master
wire i2c_start;
wire [ 1: 0] i2c_cmd;
wire [ 7: 0] i2c_tx;
wire i2c_nack;
wire [ 7: 0] i2c_rx;
wire i2c_done;
wire i2c_busy;
// ads1115_ctrl → top
wire signed [15:0] adc_data;
wire adc_valid;
// top → uart_tx
wire uart_tx_done;
wire uart_tx_busy;
reg uart_tx_req;
reg [ 7: 0] uart_tx_data;
// 采样启动脉冲:上电后第一次 + 每次串口发完后再来一次
reg measure_start;
reg first_done; // 上电后的第一次采样已经发过了
// ---------------------------------------------------------------------
// 码值 → 毫伏: mv = code * 3 / 16 (0.1875mV/LSB, 乘3右移4)
// 负码值(噪声毛刺)按 0 处理
// ---------------------------------------------------------------------
wire [16:0] mv_prod = adc_data[15] ? 17'd0 : {2'b00, adc_data[14:0]} * 2'd3;
wire [12:0] mv = mv_prod[16:4]; // 0~6143 mV
reg [12:0] mv_r; // 锁存本轮要打印的值
wire [ 3:0] d_q = mv_r / 1000; // 千位
wire [ 3:0] d_b = (mv_r % 1000) / 100; // 百位
wire [ 3:0] d_s = (mv_r % 100) / 10; // 十位
wire [ 3:0] d_g = mv_r % 10; // 个位
// ---------------------------------------------------------------------
// 打印行控制: "UV:XXXXmV\r\n" 共 11 字节
// adc_valid 来 → 锁存 mv, work_en 拉高开始发
// 发完第 11 字节 → work_en 落下, 同时触发下一轮采样
// ---------------------------------------------------------------------
reg work_en;
reg [ 3:0] byte_cnt;
always @(posedge sys_clk or negedge rst_n) begin
if (!rst_n) begin
measure_start <= 1'b0;
first_done <= 1'b0;
work_en <= 1'b0;
byte_cnt <= 4'd0;
mv_r <= 13'd0;
end else begin
measure_start <= 1'b0;
uart_tx_req <= 1'b0; // 默认拉低,只在发的那一拍拉高
// 上电第一次:复位后 i2c 空闲就启动
if (!first_done) begin
if (!i2c_busy) begin
measure_start <= 1'b1;
first_done <= 1'b1;
end
end
// 采样完成:锁存结果,开始打印
else if (adc_valid) begin
mv_r <= mv;
work_en <= 1'b1;
end
// 打印进行中
else if (work_en) begin
if (uart_tx_done) begin
if (byte_cnt == 4'd10) begin // 11 字节发完
work_en <= 1'b0;
byte_cnt <= 4'd0;
measure_start <= 1'b1; // 启动下一轮采样
end else begin
byte_cnt <= byte_cnt + 1'b1;
end
end
else if (!uart_tx_busy) begin
case (byte_cnt)
4'd0: uart_tx_data <= "U";
4'd1: uart_tx_data <= "V";
4'd2: uart_tx_data <= ":";
4'd3: uart_tx_data <= {4'h3, d_q}; // '0'+千位
4'd4: uart_tx_data <= {4'h3, d_b}; // '0'+百位
4'd5: uart_tx_data <= {4'h3, d_s}; // '0'+十位
4'd6: uart_tx_data <= {4'h3, d_g}; // '0'+个位
4'd7: uart_tx_data <= "m";
4'd8: uart_tx_data <= "V";
4'd9: uart_tx_data <= 8'h0D; // 回车
4'd10: uart_tx_data <= 8'h0A; // 换行
default: uart_tx_data <= 8'h20;
endcase
uart_tx_req <= 1'b1;
end
end
end
end
// ---------------------------------------------------------------------
// 例化:ADS1115 控制器(上层步骤机)
// ---------------------------------------------------------------------
ads1115_ctrl u_ads1115_ctrl (
.clk (sys_clk ),
.rst_n (rst_n ),
.start (measure_start ),
.adc_data (adc_data ),
.adc_valid (adc_valid ),
.i2c_start (i2c_start ),
.i2c_cmd (i2c_cmd ),
.i2c_tx (i2c_tx ),
.i2c_nack (i2c_nack ),
.i2c_rx (i2c_rx ),
.i2c_done (i2c_done ),
.i2c_busy (i2c_busy )
);
// ---------------------------------------------------------------------
// 例化:I2C 主机(底层搬字节)
// ---------------------------------------------------------------------
i2c_master u_i2c_master (
.clk (sys_clk ),
.rst_n (rst_n ),
.start (i2c_start ),
.cmd (i2c_cmd ),
.tx_data (i2c_tx ),
.nack (i2c_nack ),
.rx_data (i2c_rx ),
.ack_err ( ), // 这里先不接,调试时可以用 SignalTap 抓
.busy (i2c_busy ),
.done (i2c_done ),
.scl (scl ),
.sda (sda )
);
// ---------------------------------------------------------------------
// 例化:串口发送
// ---------------------------------------------------------------------
uart_tx u_uart_tx (
.clk (sys_clk ),
.rst_n (rst_n ),
.uart_tx_req (uart_tx_req ),
.tx_in_data (uart_tx_data ),
.uart_txd (uart_tx ),
.uart_tx_done (uart_tx_done ),
.uart_tx_busy (uart_tx_busy )
);
endmodule2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
`timescale 1ns / 1ps
module ads1115_ctrl (
input clk, // 系统时钟 50MHz
input rst_n, // 复位,低电平有效
// ---------- 对外接口 ----------
input start, // 脉冲:开始一次采样
output reg signed [15:0] adc_data, // 读回的 16 位补码结果
output reg adc_valid, // 单周期脉冲:adc_data 有效
// ---------- 连接 i2c_master 的命令接口 ----------
output reg i2c_start,
output reg [ 1: 0] i2c_cmd,
output reg [ 7: 0] i2c_tx,
output reg i2c_nack,
input [ 7: 0] i2c_rx,
input i2c_done,
input i2c_busy
);
// ---------------------------------------------------------------------
// 常量:跟 i2c_master 的命令表保持一致
// ---------------------------------------------------------------------
localparam CMD_START = 2'b00;
localparam CMD_WRITE = 2'b01;
localparam CMD_READ = 2'b10;
localparam CMD_STOP = 2'b11;
// ADS1115 要发的字节(ADDR 引脚接 GND,7 位地址 = 0x48)
localparam DEV_WRITE = 8'h90; // 0x48 左移 1 位,最低位 0 = 写
localparam DEV_READ = 8'h91; // 0x48 左移 1 位,最低位 1 = 读
localparam REG_CONFIG = 8'h01; // 指针:Config 寄存器
localparam REG_CONV = 8'h00; // 指针:Conversion 寄存器
localparam CFG_H = 8'hC1; // OS=1单次启动 + MUX=AIN0单端 + PGA=±6.144V + MODE=单次
localparam CFG_L = 8'h83; // DR=128SPS + 比较器关闭
// 等待时间:128SPS 转换一次要 1/128 ≈ 7.8ms,但芯片内部时钟有误差(可达±10%)
// 所以等 10ms 保险,确保转换真的结束了再读
// 50MHz × 10ms = 500000 个时钟周期
localparam WAIT_CNT = 20'd500000;
// ---------------------------------------------------------------------
// 步骤编码:一次采样拆成 16 步,一步一步走
// ---------------------------------------------------------------------
localparam STEP_IDLE = 4'd0;
localparam STEP_START = 4'd1; // 起始条件
localparam STEP_ADDR_W = 4'd2; // 发地址 + 写
localparam STEP_PTR_CFG = 4'd3; // 指针指向 Config
localparam STEP_CFG_H = 4'd4; // 配置高字节
localparam STEP_CFG_L = 4'd5; // 配置低字节,发完芯片开始转
localparam STEP_STOP1 = 4'd6; // 停止,释放总线
localparam STEP_WAIT = 4'd7; // 等 10ms
localparam STEP_START2 = 4'd8; // 起始条件
localparam STEP_ADDR_W2 = 4'd9; // 发地址 + 写
localparam STEP_PTR_CONV = 4'd10; // 指针指向 Conversion
localparam STEP_RSTART = 4'd11; // 重复起始(不发 STOP,直接再来一个 START)
localparam STEP_ADDR_R = 4'd12; // 发地址 + 读
localparam STEP_RX_H = 4'd13; // 读高字节,回 ACK
localparam STEP_RX_L = 4'd14; // 读低字节,回 NACK
localparam STEP_STOP2 = 4'd15; // 停止,一次采样结束
// ---------------------------------------------------------------------
// 内部寄存器
// ---------------------------------------------------------------------
reg [ 3: 0] step; // 当前步骤
reg [19: 0] wait_cnt; // 等待计数器
reg cmd_req; // 给 i2c_master 的启动脉冲(打一拍)
reg [ 7: 0] data_h; // 读回的高字节
reg busy; // 整个采样流程忙标志
// ---------------------------------------------------------------------
// 主流程:一步一步发命令,i2c_done 表示这一步的 I2C 动作做完了
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
step <= STEP_IDLE;
wait_cnt <= 20'd0;
cmd_req <= 1'b0;
data_h <= 8'd0;
adc_data <= 16'sd0;
adc_valid <= 1'b0;
busy <= 1'b0;
i2c_start <= 1'b0;
i2c_cmd <= 2'b00;
i2c_tx <= 8'd0;
i2c_nack <= 1'b0;
end else begin
cmd_req <= 1'b0; // 默认拉低,只在发命令那拍拉高
i2c_start <= 1'b0; // 默认拉低,只拉高一拍(i2c_master 靠这一拍启动)
adc_valid <= 1'b0; // 默认拉低,只在完成那拍拉高
case (step)
// =============================================================
STEP_IDLE: begin
busy <= 1'b0;
if (start) begin
busy <= 1'b1;
step <= STEP_START;
end
end
// ---- 阶段一:写 Config,启动转换 ----
STEP_START: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_START;
i2c_start <= 1'b1; // 打一拍启动
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_ADDR_W;
end
STEP_ADDR_W: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= DEV_WRITE;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_PTR_CFG;
end
STEP_PTR_CFG: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= REG_CONFIG;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_CFG_H;
end
STEP_CFG_H: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= CFG_H;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_CFG_L;
end
STEP_CFG_L: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= CFG_L;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
// 发完这个字节,ADS1115 立刻开始转换
if (i2c_done) step <= STEP_STOP1;
end
STEP_STOP1: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_STOP;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
step <= STEP_WAIT;
wait_cnt <= 20'd0;
end
end
// ---- 中间:纯等 10ms,啥也不发 ----
STEP_WAIT: begin
if (wait_cnt == WAIT_CNT)
step <= STEP_START2;
else
wait_cnt <= wait_cnt + 1'b1;
end
// ---- 阶段二:读结果 ----
STEP_START2: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_START;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_ADDR_W2;
end
STEP_ADDR_W2: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= DEV_WRITE;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_PTR_CONV;
end
STEP_PTR_CONV: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= REG_CONV;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
// 指针切好了,下一步不停顿,直接重复起始去读
if (i2c_done) step <= STEP_RSTART;
end
// ★ 重复起始:中间不发 STOP,再来一个 START(和普通 START 波形一样)
STEP_RSTART: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_START;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_ADDR_R;
end
STEP_ADDR_R: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= DEV_READ;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_RX_H;
end
STEP_RX_H: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_READ;
i2c_nack <= 1'b0; // 回 ACK:"还要下一个字节"
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
data_h <= i2c_rx;
step <= STEP_RX_L;
end
end
STEP_RX_L: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_READ;
i2c_nack <= 1'b1; // 回 NACK:"这是最后一个,别发了"
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
adc_data <= {data_h, i2c_rx}; // 高字节 + 低字节 = 16 位补码
step <= STEP_STOP2;
end
end
STEP_STOP2: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_STOP;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
adc_valid <= 1'b1; // ★ 通知顶层:数据好了
step <= STEP_IDLE;
end
end
default: step <= STEP_IDLE;
endcase
end
end
endmodule2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
`timescale 1ns / 1ps
module i2c_master (
input clk, // 系统时钟 50MHz
input rst_n, // 复位,低电平有效
// ---------- 命令接口(上层 ads1115_ctrl 调用) ----------
input start, // 脉冲:开始一次操作
input [ 1: 0] cmd, // 操作类型,见上面命令表
input [ 7: 0] tx_data, // 要发送的字节(cmd=01 时用)
input nack, // 读字节后:0=回 ACK(还要读),1=回 NACK(读完了)
output reg [ 7: 0] rx_data, // 收到的字节(cmd=10 时有效)
output reg ack_err, // 写字节时:1 = 从机没应答
output reg busy, // 1 = 正在干活,别发新命令
output reg done, // 一次操作完成,单周期脉冲
// ---------- I2C 物理引脚 ----------
output reg scl,
inout sda
);
// ---------------------------------------------------------------------
// 常量定义
// ---------------------------------------------------------------------
localparam CMD_START = 2'b00; // 起始条件
localparam CMD_WRITE = 2'b01; // 写 1 字节
localparam CMD_READ = 2'b10; // 读 1 字节
localparam CMD_STOP = 2'b11; // 停止条件
// 节拍宽度:SCL 半个周期 = TICK 个系统时钟
// SCL 频率 = 50MHz / (2*100) = 250kHz,远低于 400kHz,稳
localparam TICK = 100;
// 状态定义
localparam IDLE = 4'd0; // 空闲
localparam START = 4'd1; // 发起始条件
localparam WR_BIT = 4'd2; // 写 8 位数据
localparam WR_ACK = 4'd3; // 写完,收从机 ACK
localparam RD_BIT = 4'd4; // 读 8 位数据
localparam RD_ACK = 4'd5; // 读完,发 ACK / NACK
localparam STOP = 4'd6; // 发停止条件
// ---------------------------------------------------------------------
// 内部寄存器
// ---------------------------------------------------------------------
reg [ 3: 0] state; // 当前状态
reg [ 1: 0] phase; // 一个 bit / 一个动作里分 4 拍
reg [15: 0] tcnt; // 拍内计数器,数到 TICK-1
reg [ 2: 0] bit_cnt; // 位计数器 0~7
reg [ 7: 0] sh_tx; // 发送移位寄存器
reg [ 7: 0] sh_rx; // 接收移位寄存器
reg sda_oe; // 1 = 主机驱动 SDA,0 = 放开(高阻,靠上拉)
reg sda_out; // 主机想发到 SDA 上的电平
// SDA 三态门:sda_oe=1 时输出 sda_out,否则松手
assign sda = sda_oe ? sda_out : 1'bz;
// 拍内最后一个时钟(用来切换 phase)
wire tick_end = (tcnt == TICK - 1);
// ---------------------------------------------------------------------
// 拍计数器:每个 phase 走 TICK 个系统时钟
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tcnt <= 16'd0;
phase <= 2'd0;
end else if (state == IDLE) begin
tcnt <= 16'd0;
phase <= 2'd0;
end else if (tick_end) begin
tcnt <= 16'd0;
phase <= phase + 1'b1;
end else begin
tcnt <= tcnt + 1'b1;
end
end
// ---------------------------------------------------------------------
// 状态机:决定下一步干什么
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
busy <= 1'b0;
done <= 1'b0;
ack_err <= 1'b0;
bit_cnt <= 3'd0;
sh_tx <= 8'd0;
sh_rx <= 8'd0;
rx_data <= 8'd0;
end else begin
done <= 1'b0; // done 永远只拉高一拍
case (state)
// ---------------------------------------------------------
IDLE: begin
busy <= 1'b0;
bit_cnt <= 3'd0;
if (start) begin
busy <= 1'b1;
sh_tx <= tx_data; // 锁存要发的字节
case (cmd)
CMD_START: state <= START;
CMD_WRITE: state <= WR_BIT;
CMD_READ : state <= RD_BIT;
CMD_STOP : state <= STOP;
default : state <= IDLE;
endcase
end
end
// ---------------------------------------------------------
// 起始条件:SCL 高的时候 SDA 由高变低
// phase0: SCL=1 SDA=1 (准备好空闲电平)
// phase1: SCL=1 SDA=0 (★ 这里产生 START)
// phase2: SCL=0 (时钟拉低,准备发数据)
// phase3: 结束
// ---------------------------------------------------------
START: begin
if (tick_end && phase == 2'd3) begin
state <= IDLE;
done <= 1'b1;
end
end
// ---------------------------------------------------------
// 写 8 位(MSB 先发),每 bit 4 拍
// phase0: SCL=0,把这一位摆到 SDA 上
// phase1: SCL=1
// phase2: SCL=1 保持(从机在这中间采样)
// phase3: SCL=0,bit_cnt+1
// ---------------------------------------------------------
WR_BIT: begin
if (tick_end && phase == 2'd3) begin
if (bit_cnt == 3'd7) begin
bit_cnt <= 3'd0;
state <= WR_ACK;
end else begin
bit_cnt <= bit_cnt + 1'b1;
sh_tx <= {sh_tx[6:0], 1'b0}; // 左移,下一位到最高位
end
end
end
// ---------------------------------------------------------
// 写完收 ACK:主机松手,从机拉低 = ACK
// phase0: SCL=0,放开 SDA
// phase1: SCL=1
// phase2: SCL=1 保持,这里采样 ACK
// phase3: SCL=0,结束
// ---------------------------------------------------------
WR_ACK: begin
if (tick_end && phase == 2'd2) begin
if (sda) // 没拉低 = NACK,记错误
ack_err <= 1'b1;
end
if (tick_end && phase == 2'd3) begin
state <= IDLE;
done <= 1'b1;
end
end
// ---------------------------------------------------------
// 读 8 位(MSB 先收),每 bit 4 拍
// phase0: SCL=0,主机放开 SDA,让从机驱动
// phase1: SCL=1
// phase2: SCL=1 保持,这里采样数据
// phase3: SCL=0,bit_cnt+1
// ---------------------------------------------------------
RD_BIT: begin
if (tick_end && phase == 2'd2) begin
sh_rx <= {sh_rx[6:0], sda}; // 收一位,左移
end
if (tick_end && phase == 2'd3) begin
if (bit_cnt == 3'd7) begin
bit_cnt <= 3'd0;
state <= RD_ACK;
end else begin
bit_cnt <= bit_cnt + 1'b1;
end
end
end
// ---------------------------------------------------------
// 读完发 ACK / NACK:第 9 个时钟由主机说话
// nack=0 → 拉低 SDA 回 ACK("还要下一个")
// nack=1 → 放开 SDA 回 NACK("这是最后一个")
// ---------------------------------------------------------
RD_ACK: begin
if (tick_end && phase == 2'd3) begin
rx_data <= sh_rx; // 锁存这一字节
state <= IDLE;
done <= 1'b1;
end
end
// ---------------------------------------------------------
// 停止条件:SCL 高的时候 SDA 由低变高
// phase0: SCL=0 SDA=0 (准备好)
// phase1: SCL=1 SDA=0
// phase2: SCL=1 SDA=1 (★ 这里产生 STOP)
// phase3: 结束
// ---------------------------------------------------------
STOP: begin
if (tick_end && phase == 2'd3) begin
state <= IDLE;
done <= 1'b1;
end
end
default: state <= IDLE;
endcase
end
end
// ---------------------------------------------------------------------
// SCL 生成:按状态和拍序输出高低
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
scl <= 1'b1;
else begin
case (state)
// 起始条件:phase0/1 保持高,phase2 开始拉低
START: scl <= (phase <= 2'd1);
// 写/读数据、ACK:phase0 低,phase1/2 高,phase3 低
WR_BIT, WR_ACK, RD_BIT, RD_ACK:
scl <= (phase == 2'd1) || (phase == 2'd2);
// 停止条件:phase0 低,phase1 开始拉高
STOP: scl <= (phase >= 2'd1);
// 空闲:保持高
default: scl <= 1'b1;
endcase
end
end
// ---------------------------------------------------------------------
// SDA 输出控制
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sda_oe <= 1'b1;
sda_out <= 1'b1;
end else begin
case (state)
// ---- 起始条件 ----
START: begin
sda_oe <= 1'b1;
sda_out <= (phase == 2'd0); // phase0 高,phase1 起变低
end
// ---- 写数据:SCL 低的时候摆数据 ----
WR_BIT: begin
sda_oe <= 1'b1;
sda_out <= sh_tx[7]; // 每次取最高位(MSB 先发)
end
// ---- 写完收 ACK:必须放手,让从机拉 ----
WR_ACK: begin
sda_oe <= 1'b0;
sda_out <= 1'b1;
end
// ---- 读数据:放手,从机驱动 ----
RD_BIT: begin
sda_oe <= 1'b0;
sda_out <= 1'b1;
end
// ---- 读完发 ACK/NACK:主机自己说话 ----
RD_ACK: begin
sda_oe <= 1'b1;
sda_out <= nack; // 0=ACK,1=NACK
end
// ---- 停止条件 ----
STOP: begin
sda_oe <= 1'b1;
sda_out <= (phase >= 2'd2); // phase2 起变高
end
// ---- 空闲:SDA 放高 ----
default: begin
sda_oe <= 1'b1;
sda_out <= 1'b1;
end
endcase
end
end
endmodule2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
`timescale 1ns / 1ps
module uart_tx (
input clk, // 系统时钟 50MHz
input rst_n, // 复位,低电平有效
input uart_tx_req, // 发送请求,单周期脉冲
input [ 7: 0] tx_in_data, // 要发送的字节
output reg uart_txd, // 串口发送引脚
output reg uart_tx_done, // 单周期脉冲:一个字节发完了
output reg uart_tx_busy // 1 = 正在发,别塞新数据
);
// ---------------------------------------------------------------------
// 参数
// ---------------------------------------------------------------------
parameter CLK_FREQ = 26'd50_000_000; // 系统时钟 50MHz
parameter UART_BPS = 17'd115200; // 波特率 115200
// 一位数据持续多少个系统时钟:50M / 115200 ≈ 434
localparam BAUD_CNT_MAX = CLK_FREQ / UART_BPS;
// ---------------------------------------------------------------------
// 内部寄存器
// ---------------------------------------------------------------------
reg [ 8: 0] baud_cnt; // 波特率计数器 0~433
reg [ 3: 0] bit_cnt; // 位计数器 0~9(1 起始 + 8 数据 + 1 停止)
reg [ 7: 0] data_reg; // 锁存要发的数据
// ---------------------------------------------------------------------
// 忙标志:收到请求就忙,发完 10 位才清
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
uart_tx_busy <= 1'b0;
else if (uart_tx_req)
uart_tx_busy <= 1'b1;
else if (bit_cnt == 4'd10 && baud_cnt == BAUD_CNT_MAX - 1)
uart_tx_busy <= 1'b0; // 10 位全部发完
end
// ---------------------------------------------------------------------
// 波特率计数器:每 BAUD_CNT_MAX 个时钟走 1 位
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
baud_cnt <= 9'd0;
else if (!uart_tx_busy)
baud_cnt <= 9'd0;
else if (baud_cnt == BAUD_CNT_MAX - 1)
baud_cnt <= 9'd0;
else
baud_cnt <= baud_cnt + 1'b1;
end
// ---------------------------------------------------------------------
// 位计数器:数到 10 清零
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
bit_cnt <= 4'd0;
else if (!uart_tx_busy)
bit_cnt <= 4'd0;
else if (baud_cnt == BAUD_CNT_MAX - 1)
bit_cnt <= bit_cnt + 1'b1;
end
// ---------------------------------------------------------------------
// 数据锁存:请求来的那一拍把数据存住
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
data_reg <= 8'd0;
else if (uart_tx_req)
data_reg <= tx_in_data;
end
// ---------------------------------------------------------------------
// 发送波形:一位一位往外打
// bit_cnt=0 起始位(低)
// bit_cnt=1~8 数据,从最低位开始
// bit_cnt=9 停止位(高)
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
uart_txd <= 1'b1; // 空闲时为高电平
// ★ 不忙的时候必须把线拉高。原来没有这一句: 空闲时 baud_cnt 和 bit_cnt 都是 0,
// 下面那个条件一直成立、一直走 bit_cnt=0(起始位)分支, 串口线在空闲时一直输出低电平,
// 接收端从第一字节起就错位, 收到的全是乱码。
else if (!uart_tx_busy)
uart_txd <= 1'b1;
else if (baud_cnt == 9'd0) begin // 每一位的开头切换电平
case (bit_cnt)
4'd0: uart_txd <= 1'b0; // 起始位
4'd1: uart_txd <= data_reg[0];
4'd2: uart_txd <= data_reg[1];
4'd3: uart_txd <= data_reg[2];
4'd4: uart_txd <= data_reg[3];
4'd5: uart_txd <= data_reg[4];
4'd6: uart_txd <= data_reg[5];
4'd7: uart_txd <= data_reg[6];
4'd8: uart_txd <= data_reg[7]; // 数据最高位
4'd9: uart_txd <= 1'b1; // 停止位
default: uart_txd <= 1'b1;
endcase
end
end
// ---------------------------------------------------------------------
// 完成脉冲:停止位发完的那一刻(bit_cnt 刚到 9 的最后一个时钟)
// 这一拍就代表"这一个字节发完了"
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
uart_tx_done <= 1'b0;
else if (bit_cnt == 4'd9 && baud_cnt == BAUD_CNT_MAX - 1)
uart_tx_done <= 1'b1; // 停止位结束 → 发送完成
else
uart_tx_done <= 1'b0;
end
endmodule2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
6、I/O 绑定
顶层模块 top 的端口与约束文件 guva_s12sd.cst 一一对应:
| 端口 | 方向 | FPGA 引脚 | 接线 |
|---|---|---|---|
sys_clk | input | T7 | 板载 50 MHz 系统时钟 |
rst_n | input | F10 | 复位按键(按下为低,异步复位) |
scl | output | R14 | 接 J5 排针空闲脚 → ADS1115 模块 SCL |
sda | inout | T15 | 接 J5 排针空闲脚 → ADS1115 模块 SDA;双向,靠 sda_oe 切换 |
uart_tx | output | F12 | 接 USB 转串口 RX,115200-8-N-1 |
对应的约束文件内容如下:
// 引脚约束(立创 LJPI-G1 开发板)
IO_LOC "sys_clk" T7;
IO_PORT "sys_clk" IO_TYPE=LVCMOS33 PULL_MODE=UP BANK_VCCIO=3.3;
IO_LOC "rst_n" F10;
IO_PORT "rst_n" IO_TYPE=LVCMOS33 PULL_MODE=UP BANK_VCCIO=3.3;
// ---- ADS1115 的 I2C
IO_LOC "scl" R14;
IO_PORT "scl" IO_TYPE=LVCMOS33 PULL_MODE=UP DRIVE=8 BANK_VCCIO=3.3;
IO_LOC "sda" T15;
IO_PORT "sda" IO_TYPE=LVCMOS33 PULL_MODE=UP DRIVE=8 BANK_VCCIO=3.3;
// ---- UART 发送引脚
IO_LOC "uart_tx" F12;
IO_PORT "uart_tx" IO_TYPE=LVCMOS33 PULL_MODE=UP DRIVE=8 BANK_VCCIO=3.3;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
🖼️ 图片占位|Gowin 引脚绑定界面 待补图:Gowin FPGA Designer 里 FloorPlanner 的引脚绑定截图,与上表一致
接线分两步:
第一步:GUVA-S12SD → ADS1115(模拟信号)
| GUVA-S12SD | ADS1115 模块 |
|---|---|
| VCC | VDD(3.3 V) |
| GND | GND |
| SIG | AIN0 |
第二步:ADS1115 → 开发板(I2C 数字信号)
| ADS1115 模块 | 开发板 |
|---|---|
| VCC | 3.3 V |
| GND | GND(和 FPGA 共地) |
| SCL | R14(J5 排针空闲脚) |
| SDA | T15(J5 排针空闲脚) |
| ADDR | GND(地址 = 0x48) |
⚠️警告:
ADS1115 的 ADDR 引脚必须接 GND,否则 I2C 地址不是 0x48,FPGA 喊话芯片不理,读回来全是 0 或者乱码。另外 GUVA-S12SD 和 ADS1115 要用同一个 3.3 V 电源,共地。
7、程序下载
- 用 Gowin_V1.9.10.02 打开工程
ziwai.gprj,综合、布局布线后下载到开发板(器件 GW2A-LV18PG256C8/I7)。 - 按接线表接好 GUVA-S12SD、ADS1115 和开发板(确认 ADDR 接 GND),给开发板上电。
- 打开串口助手,连上
F12对应的串口,就能看到连续打印(约 11 ms 一行):
UV:0123mV
UV:0126mV
UV:0124mV2
3
拿手机闪光灯或者紫外灯照一下传感器,数值应该明显上升;用手遮住传感器,数值应该降下来。如果一直是 UV:0000mV,先检查 ADS1115 的 ADDR 是不是接了 GND、SCL/SDA 有没有接反。
串口助手设置
波特率 115200、数据位 8、停止位 1、无校验。输出是纯 ASCII(UV:xxxxmV),编码选 UTF-8 或 ASCII 都行。
🪜常见问题排查
一直输出 UV:0000mV:检查 ADS1115 ADDR 是否接 GND;SCL/SDA 是否接反;用万用表量 AIN0 有没有电压(照传感器时应该有 0.x V)。
输出乱码:检查串口波特率是否 115200;GND 是否和上位机共地。
数值不动:传感器可能被遮住了,或者 SIG 线没接好。拿万用表量 ADS1115 的 AIN0 引脚,照传感器时应该有 0~1 V 变化。