TCS34725 是艾迈斯欧司朗(ams)推出的一款 RGB 颜色传感器,I2C 接口,内部把光电二极管阵列、模数转换器(ADC)和数字接口做在一起,能同时测出红(R)、绿(G)、蓝(B)和透明(Clear)四个通道的光照强度。供电范围 2.7 - 3.6 V,只要 SCL、SDA 两根线就能读回四个 16 位码值。
它的用法和前面学过的寄存器式 I2C 器件(比如 ADS1115)很像:先写寄存器配置,再读数据。新知识点有三个:寄存器地址要带命令位(实际发 0x80 | 寄存器编号)、数据是小端存放(低字节在前)、初始化要先上电再开 ADC 并等第一轮积分转完才有数。拿不同颜色的物体放在传感器上方,对应通道的数值会明显变大,很适合做颜色识别的入门练手。
🪜注意:
TCS34725 的 INT 中断引脚本例不使用;ADDR 引脚不同接法对应不同的器件地址(悬空或接 GND 时是 0x29),请以你手上模块的规格书为准。若你手里模块的地址与此不同,改 tcs34725_driver.v 里的 DEV_ADDR 即可。
1、模块来源

VCC:电源,芯片工作电压 2.7 - 3.6 V(推荐 3.3 V;有的模块标VIN,请以模块原理图为准)GND:地线,必须和 FPGA 共地SCL:I2C 时钟线,需上拉(若模块板上没有自带上拉,需外接)SDA:I2C 数据线,双向,同样需上拉
2、参数
工作电压:2.7 - 3.6 V(芯片供电范围,推荐 3.3 V)
通讯接口:I2C
从机地址:7 位地址 0x29(ADDR 悬空或接 GND 时;写地址 = 0x29 左移一位 = 0x52,读地址 = 0x53)
器件 ID:读 0x12 寄存器,期望 0x44(TCS34725);读到 0x4D 是 TCS34721 / TCS34727
积分时间:由 ATIME 寄存器设定,(256 − ATIME)× 2.4 ms,本例用 0xEB = 50.4 ms(详见 4.2 节)
增益:由 CONTROL 寄存器设定,可选 1x / 4x / 16x / 60x,本例用 16x
测量输出:C(透明)、R、G、B 四个通道的 16 位原始码值(0 ~ 65535)
单次采集等待:第一轮积分约 50 ms,本例等 60 ms 再读(见第 5 节代码)
管脚数量:4 Pin(VCC、GND、SCL、SDA)
3、实战任务
使用立创·逻辑派 FPGA-G1 开发板编写 TCS34725 驱动代码,上电自动完成初始化,之后每隔 1 秒自动采集一次,按一下按键(key)也可以手动立刻补采一次,采完通过串口把四个通道的码值发给上位机显示:
R:12345 G:08765 B:04321 C:25432一行一共 33 个字节(含回车换行):R:、G:、B:、C: 后面各跟 5 位十进制数字,不足 5 位补零,取值范围 0 ~ 65535。判读方法:拿一个红色物体放在传感器正上方,R 的值应该明显大于 G、B;换绿色物体,G 应该最大,以此类推。
4、时序说明
4.1 读写时序
TCS34725 走的是标准 I2C 协议。I2C 时序我们这里不再进行赘述,有兴趣可去模块里的 IIC 屏幕里学习
我们在这里简单的过一下 TCS34725 的读写过程时序。
器件地址为 0x29,但是最后一位是读写位,需要空出一位给读写位,因此需要左移一位,即 0x29<<1 得到 0X52(读操作最低位填 1,得到 0x53)
TCS34725 是寄存器式器件,读写时寄存器地址必须带命令位:地址字节的 bit7 必须为 1,也就是实际发送 0x80 | 寄存器编号。比如 ID 寄存器编号是 0x12,发出去的是 0x92。
采集步骤:(写=0,读=1)
写寄存器:起始信号 -> 器件地址左移 1 位 + 写(0x52)-> 等待传感器应答 -> 发送寄存器地址(0x80 | 寄存器编号)-> 等待传感器应答 -> 发送数据字节 -> 等待传感器应答 -> 停止信号
读寄存器:起始信号 -> 器件地址左移 1 位 + 写(0x52)-> 等待传感器应答 -> 发送寄存器地址(0x80 | 0x14)-> 等待传感器应答 -> 重复起始信号 -> 器件地址左移 1 位 + 读(0x53)-> 等待传感器应答 -> 读第 1 个字节(主机回 ACK)-> 读第 2 个字节(主机回 ACK)-> …… -> 读第 8 个字节(主机回 NACK,表示「没有下一个了」)-> 停止信号
读多字节时寄存器地址自动递增,一次连读 8 个字节就拿到全部数据:C_L C_H R_L R_H G_L G_H B_L B_H。注意是小端:每个通道都是低字节在前、高字节在后。
上电后按下面的顺序配置一遍(首次采集前做一次就够,之后每次采集只读数据):
- 读 ID 验证:读
0x12寄存器,确认芯片在(期望0x44) - PON 上电:写
0x00(ENABLE)=0x01,只上电,ADC 还没开 - 等 3 ms:等上电稳定(规格书要求 2.4 ms,留裕量)
- 开 ADC:写
0x00(ENABLE)=0x03(PON + AEN),ADC 开始积分 - 设积分时间:写
0x01(ATIME)=0xEB - 设增益:写
0x0F(CONTROL)=0x02(16x) - 等第一轮积分转完:本例等 60 ms,不然读到的全是 0
4.2 数据换算
四个通道读回的都是 16 位无符号原始码值,先按小端把两个字节拼回 16 位:
通道码值(以透明通道为例,C_L 在前、C_H 在后):
其中 C_L、C_H 是读回的低、高字节(十进制 0 ~ 255)。红、绿、蓝三个通道完全一样,只是换一对字节。代入算例:读回 C_L = 0x34、C_H = 0x12,则 C = 0x12 × 256 + 0x34 = 4660。
码值大小只反映「这一轮积分里收到多少光」,和积分时间、增益设置都有关系,不是直接的物理单位。本教程直接把原始码值打印出来,用比较大小的方法判颜色。积分时间按公式换算:
其中 ATIME 是写进积分时间寄存器的值。本例写 0xEB(十进制 235),代入公式:(256 − 235)× 2.4 ms = 21 × 2.4 ms = 50.4 ms。
这个积分时间决定了传感器单次采集「曝光」多久,时间越长进光越多、越适合暗环境,但出数也越慢。想要别的积分时间,按公式反推 ATIME 即可,常用的几档:
ATIME | 十进制 | 积分时间 |
|---|---|---|
0xF6 | 246 | 24 ms |
0xEB | 235 | 50.4 ms(本例) |
0xD5 | 213 | 103.2 ms |
0xC0 | 192 | 153.6 ms |
注意积分时间越长,一轮采样的耗时就越长,顶层等待时间(见 5.4 节)要跟着加大。
5、程序编写
整个工程分四个模块:
- top.v(顶层):1 秒定时 / 按键消抖产生采集请求;把四个通道的码值拆成 5 位十进制;拼 33 个字节的打印行喂给串口。
- tcs34725_driver.v(TCS34725 驱动):单个 case 步骤机,步骤 0 ~ 43 共 44 步,按顺序指挥底层 I2C:首次采集先做初始化(验 ID → PON → 等 → AEN → ATIME → GAIN → 等积分),之后每次只读 8 个字节数据。
- i2c_master.v(I2C 主机):底层搬字节,只管发起始 / 写 1 字节 / 读 1 字节 / 发停止
- uart_tx.v(串口发送):和前面串口章节通用,原样沿用。
`timescale 1ns / 1ps
module top (
input sys_clk, // 系统时钟 50MHz
input rst_n, // 复位按键,按下为低,异步复位
input key, // 手动触发按键,按下为低
output scl, // I2C 时钟线 → TCS34725 SCL
inout sda, // I2C 数据线 → TCS34725 SDA
output uart_tx // 串口发送 → 上位机
);
// ---------------------------------------------------------------------
// 常量定义
// ---------------------------------------------------------------------
// 1 秒定时:50MHz × 1s = 50000000 个时钟周期
localparam SEC_CNT = 26'd50_000_000;
// 按键消抖:20ms,50MHz × 20ms = 1000000 个时钟周期
localparam KEY_CNT = 21'd1000000;
// ---------------------------------------------------------------------
// 内部连线
// ---------------------------------------------------------------------
// tcs34725_driver ↔ 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;
// tcs34725_driver → top
wire [15:0] clear_data;
wire [15:0] red_data;
wire [15:0] green_data;
wire [15:0] blue_data;
wire data_valid;
// top → uart_tx
wire uart_tx_done;
wire uart_tx_busy;
reg uart_tx_req;
reg [ 7: 0] uart_tx_data;
// 采样启动脉冲:上电后第一次 + 每 1 秒 / 按键按下时再来一拍
reg measure_start;
reg first_done; // 上电后的第一次采样已经发过了
// 1 秒定时
reg [25:0] sec_cnt;
reg sec_pulse; // 1 秒到,单周期脉冲
// 按键消抖(按下为低)
reg [ 1: 0] key_sync; // 两级同步,防亚稳态
reg key_deb; // 消抖后的稳定电平
reg key_deb_r; // 上一拍的消抖电平,用来找按下沿
reg [20:0] key_cnt;
reg key_pulse; // 按下沿,单周期脉冲
// ---------------------------------------------------------------------
// 打印用锁存值:data_valid 来的时候存住,打印过程用它拆位
// ---------------------------------------------------------------------
reg [15:0] r_r;
reg [15:0] g_r;
reg [15:0] b_r;
reg [15:0] c_r;
// 16 位原始码值 → 5 位十进制(万/千/百/十/个),'0'+数字就是字符
wire [ 3:0] r_d4 = r_r / 16'd10000; // 万位
wire [ 3:0] r_d3 = (r_r % 16'd10000) / 16'd1000; // 千位
wire [ 3:0] r_d2 = (r_r % 16'd1000) / 16'd100; // 百位
wire [ 3:0] r_d1 = (r_r % 16'd100) / 16'd10; // 十位
wire [ 3:0] r_d0 = r_r % 16'd10; // 个位
wire [ 3:0] g_d4 = g_r / 16'd10000;
wire [ 3:0] g_d3 = (g_r % 16'd10000) / 16'd1000;
wire [ 3:0] g_d2 = (g_r % 16'd1000) / 16'd100;
wire [ 3:0] g_d1 = (g_r % 16'd100) / 16'd10;
wire [ 3:0] g_d0 = g_r % 16'd10;
wire [ 3:0] b_d4 = b_r / 16'd10000;
wire [ 3:0] b_d3 = (b_r % 16'd10000) / 16'd1000;
wire [ 3:0] b_d2 = (b_r % 16'd1000) / 16'd100;
wire [ 3:0] b_d1 = (b_r % 16'd100) / 16'd10;
wire [ 3:0] b_d0 = b_r % 16'd10;
wire [ 3:0] c_d4 = c_r / 16'd10000;
wire [ 3:0] c_d3 = (c_r % 16'd10000) / 16'd1000;
wire [ 3:0] c_d2 = (c_r % 16'd1000) / 16'd100;
wire [ 3:0] c_d1 = (c_r % 16'd100) / 16'd10;
wire [ 3:0] c_d0 = c_r % 16'd10;
// ---------------------------------------------------------------------
// 1 秒定时:到点打一拍,和按键/上电触发并列
// ---------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if (!rst_n) begin
sec_cnt <= 26'd0;
sec_pulse <= 1'b0;
end else begin
sec_pulse <= 1'b0; // 默认拉低,只在到点那拍拉高
if (sec_cnt == SEC_CNT - 1'b1) begin
sec_cnt <= 26'd0;
sec_pulse <= 1'b1;
end else begin
sec_cnt <= sec_cnt + 1'b1;
end
end
end
// ---------------------------------------------------------------------
// 按键消抖:电平稳定保持 20ms 才认,然后找按下沿(按下为低)
// ---------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if (!rst_n) begin
key_sync <= 2'b11; // 松手 = 高电平
key_deb <= 1'b1;
key_deb_r <= 1'b1;
key_cnt <= 21'd0;
key_pulse <= 1'b0;
end else begin
key_sync <= {key_sync[0], key}; // 两级同步
key_deb_r <= key_deb;
key_pulse <= key_deb_r & ~key_deb; // 消抖后由高变低 = 按下,给一拍脉冲
// 电平变了就开始数,数满 20ms 还是新电平就认它
if (key_sync[1] != key_deb) begin
if (key_cnt == KEY_CNT) begin
key_deb <= key_sync[1];
key_cnt <= 21'd0;
end else begin
key_cnt <= key_cnt + 1'b1;
end
end else begin
key_cnt <= 21'd0; // 电平没变(或抖回去了),重新数
end
end
end
// ---------------------------------------------------------------------
// 打印行控制: "R:12345 G:12345 B:12345 C:12345\r\n" 共 33 字节
// data_valid 来 → 锁存 4 个值, work_en 拉高开始发
// 发完第 33 字节 → work_en 落下, 等下一次触发
// ---------------------------------------------------------------------
reg work_en;
reg [ 5: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 <= 6'd0;
r_r <= 16'd0;
g_r <= 16'd0;
b_r <= 16'd0;
c_r <= 16'd0;
uart_tx_req <= 1'b0;
uart_tx_data <= 8'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 (data_valid) begin
r_r <= red_data;
g_r <= green_data;
b_r <= blue_data;
c_r <= clear_data;
work_en <= 1'b1;
end
// 打印进行中
else if (work_en) begin
if (uart_tx_done) begin
if (byte_cnt == 6'd32) begin // 33 字节发完
work_en <= 1'b0;
byte_cnt <= 6'd0;
end else begin
byte_cnt <= byte_cnt + 1'b1;
end
end
else if (!uart_tx_busy) begin
case (byte_cnt)
6'd0: uart_tx_data <= "R";
6'd1: uart_tx_data <= ":";
6'd2: uart_tx_data <= {4'h3, r_d4}; // '0'+万位
6'd3: uart_tx_data <= {4'h3, r_d3};
6'd4: uart_tx_data <= {4'h3, r_d2};
6'd5: uart_tx_data <= {4'h3, r_d1};
6'd6: uart_tx_data <= {4'h3, r_d0}; // '0'+个位
6'd7: uart_tx_data <= " "; // 空格
6'd8: uart_tx_data <= "G";
6'd9: uart_tx_data <= ":";
6'd10: uart_tx_data <= {4'h3, g_d4};
6'd11: uart_tx_data <= {4'h3, g_d3};
6'd12: uart_tx_data <= {4'h3, g_d2};
6'd13: uart_tx_data <= {4'h3, g_d1};
6'd14: uart_tx_data <= {4'h3, g_d0};
6'd15: uart_tx_data <= " ";
6'd16: uart_tx_data <= "B";
6'd17: uart_tx_data <= ":";
6'd18: uart_tx_data <= {4'h3, b_d4};
6'd19: uart_tx_data <= {4'h3, b_d3};
6'd20: uart_tx_data <= {4'h3, b_d2};
6'd21: uart_tx_data <= {4'h3, b_d1};
6'd22: uart_tx_data <= {4'h3, b_d0};
6'd23: uart_tx_data <= " ";
6'd24: uart_tx_data <= "C";
6'd25: uart_tx_data <= ":";
6'd26: uart_tx_data <= {4'h3, c_d4};
6'd27: uart_tx_data <= {4'h3, c_d3};
6'd28: uart_tx_data <= {4'h3, c_d2};
6'd29: uart_tx_data <= {4'h3, c_d1};
6'd30: uart_tx_data <= {4'h3, c_d0};
6'd31: uart_tx_data <= 8'h0D; // 回车
6'd32: uart_tx_data <= 8'h0A; // 换行
default: uart_tx_data <= 8'h20;
endcase
uart_tx_req <= 1'b1;
end
end
// 没在打印:1 秒到 / 按键按下 → 触发下一轮采样
else if (sec_pulse || key_pulse) begin
measure_start <= 1'b1;
end
end
end
// ---------------------------------------------------------------------
// 例化:TCS34725 驱动(上层步骤机)
// ---------------------------------------------------------------------
tcs34725_driver u_tcs34725_driver (
.clk (sys_clk ),
.rst_n (rst_n ),
.start (measure_start ),
.clear_data (clear_data ),
.red_data (red_data ),
.green_data (green_data ),
.blue_data (blue_data ),
.data_valid (data_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
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
`timescale 1ns / 1ps
module tcs34725_driver (
input clk, // 系统时钟 50MHz
input rst_n, // 复位,低电平有效
// ---------- 对外接口 ----------
input start, // 脉冲:开始一次采集
output reg [15:0] clear_data, // 透明通道原始码值 0~65535
output reg [15:0] red_data, // 红色通道原始码值 0~65535
output reg [15:0] green_data, // 绿色通道原始码值 0~65535
output reg [15:0] blue_data, // 蓝色通道原始码值 0~65535
output reg data_valid, // 单周期脉冲:上面 4 个数据有效
// ---------- 连接 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; // 起始条件(重复起始 Sr 也用它)
localparam CMD_WRITE = 2'b01; // 写 1 字节
localparam CMD_READ = 2'b10; // 读 1 字节
localparam CMD_STOP = 2'b11; // 停止条件
// TCS34725 要发的字节(ADDR 悬空/接 GND,7 位地址 = 0x29)
localparam DEV_WRITE = 8'h52; // 0x29 左移 1 位,最低位 0 = 写
localparam DEV_READ = 8'h53; // 0x29 左移 1 位,最低位 1 = 读
// 寄存器地址:读写时必须带命令位 bit7=1,实际发 0x80 | 寄存器编号
localparam REG_ID = 8'h92; // 0x80|0x12 器件 ID(期望 0x44)
localparam REG_ENABLE = 8'h80; // 0x80|0x00 ENABLE 使能
localparam REG_ATIME = 8'h81; // 0x80|0x01 ATIME 积分时间
localparam REG_CTRL = 8'h8F; // 0x80|0x0F CONTROL 增益
localparam REG_DATA = 8'h94; // 0x80|0x14 数据寄存器起始
// 写进各寄存器的配置值
localparam CFG_PON = 8'h01; // ENABLE: bit0 PON 上电
localparam CFG_AEN = 8'h03; // ENABLE: bit0 PON + bit1 AEN 开 ADC
localparam CFG_ATIME = 8'hEB; // ATIME: 积分时间(见文件头【积分时间说明】)
localparam CFG_GAIN = 8'h02; // CONTROL: 16x 增益(0x00=1x 0x01=4x 0x02=16x 0x03=60x)
// ---------------------------------------------------------------------
// 等待时间
// ---------------------------------------------------------------------
// 上电稳定:规格书要求 2.4ms,留裕量等 3ms
// 50MHz × 3ms = 150000 个时钟周期
localparam WAIT_PON = 22'd150000;
// 第一轮积分:ATIME=0xEB 按公式 (256-235)×2.4ms = 50.4ms,等 60ms 保险
// 50MHz × 60ms = 3000000 个时钟周期
localparam WAIT_INT = 22'd3000000;
// ---------------------------------------------------------------------
// 步骤编码:一次流程拆成 44 步,每个总线动作一步,纯等待也占一步
// ---------------------------------------------------------------------
localparam STEP_IDLE = 6'd0; // 空闲,等 start
// ---- 阶段〇:读 ID 验证 ----
localparam STEP_ID_START = 6'd1; // 起始条件
localparam STEP_ID_ADDR_W = 6'd2; // 发地址 + 写
localparam STEP_ID_REG = 6'd3; // 命令字节指向 ID 寄存器
localparam STEP_ID_RSTART = 6'd4; // 重复起始(不发 STOP,直接再来一个 START)
localparam STEP_ID_ADDR_R = 6'd5; // 发地址 + 读
localparam STEP_ID_RX = 6'd6; // 读 ID 一个字节,回 NACK
localparam STEP_ID_STOP = 6'd7; // 停止
// ---- 阶段一:PON 上电 ----
localparam STEP_PON_START = 6'd8; // 起始条件
localparam STEP_PON_ADDR = 6'd9; // 发地址 + 写
localparam STEP_PON_REG = 6'd10; // 命令字节指向 ENABLE
localparam STEP_PON_DATA = 6'd11; // 写 0x01,只上电
localparam STEP_PON_STOP = 6'd12; // 停止
// ---- 中间:等上电稳定 ----
localparam STEP_WAIT_PON = 6'd13; // 等 3ms
// ---- 阶段二:PON + AEN,开 ADC ----
localparam STEP_AEN_START = 6'd14; // 起始条件
localparam STEP_AEN_ADDR = 6'd15; // 发地址 + 写
localparam STEP_AEN_REG = 6'd16; // 命令字节指向 ENABLE
localparam STEP_AEN_DATA = 6'd17; // 写 0x03,ADC 开始积分
localparam STEP_AEN_STOP = 6'd18; // 停止
// ---- 阶段三:写积分时间 ----
localparam STEP_ATM_START = 6'd19; // 起始条件
localparam STEP_ATM_ADDR = 6'd20; // 发地址 + 写
localparam STEP_ATM_REG = 6'd21; // 命令字节指向 ATIME
localparam STEP_ATM_DATA = 6'd22; // 写 0xEB
localparam STEP_ATM_STOP = 6'd23; // 停止
// ---- 阶段四:写增益 ----
localparam STEP_GAIN_START= 6'd24; // 起始条件
localparam STEP_GAIN_ADDR = 6'd25; // 发地址 + 写
localparam STEP_GAIN_REG = 6'd26; // 命令字节指向 CONTROL
localparam STEP_GAIN_DATA = 6'd27; // 写 0x02,16x 增益
localparam STEP_GAIN_STOP = 6'd28; // 停止
// ---- 中间:等第一轮积分转完 ----
localparam STEP_WAIT_INT = 6'd29; // 等 60ms
// ---- 阶段五:读 8 字节数据 ----
localparam STEP_RD_START = 6'd30; // 起始条件
localparam STEP_RD_ADDR_W = 6'd31; // 发地址 + 写
localparam STEP_RD_REG = 6'd32; // 命令字节指向数据起始 0x14
localparam STEP_RD_RSTART = 6'd33; // 重复起始
localparam STEP_RD_ADDR_R = 6'd34; // 发地址 + 读
localparam STEP_RX_C_L = 6'd35; // 读 C 低字节,回 ACK
localparam STEP_RX_C_H = 6'd36; // 读 C 高字节,回 ACK
localparam STEP_RX_R_L = 6'd37; // 读 R 低字节,回 ACK
localparam STEP_RX_R_H = 6'd38; // 读 R 高字节,回 ACK
localparam STEP_RX_G_L = 6'd39; // 读 G 低字节,回 ACK
localparam STEP_RX_G_H = 6'd40; // 读 G 高字节,回 ACK
localparam STEP_RX_B_L = 6'd41; // 读 B 低字节,回 ACK
localparam STEP_RX_B_H = 6'd42; // 读 B 高字节,回 NACK(最后一个)
localparam STEP_RD_STOP = 6'd43; // 停止,一次采集结束
// ---------------------------------------------------------------------
// 内部寄存器
// ---------------------------------------------------------------------
reg [ 5: 0] step; // 当前步骤
reg [21: 0] wait_cnt; // 等待计数器
reg cmd_req; // 给 i2c_master 的启动脉冲(防重发锁)
reg busy; // 整个流程忙标志
reg init_done; // 初始化做完了,后面 start 直接读数据
reg [ 7: 0] id_data; // 读回的器件 ID(期望 0x44)
reg [ 7: 0] rx0; // 8 个字节缓存,低字节在前
reg [ 7: 0] rx1;
reg [ 7: 0] rx2;
reg [ 7: 0] rx3;
reg [ 7: 0] rx4;
reg [ 7: 0] rx5;
reg [ 7: 0] rx6;
reg [ 7: 0] rx7;
// ---------------------------------------------------------------------
// 主流程:一步一步发命令,i2c_done 表示这一步的 I2C 动作做完了
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
step <= STEP_IDLE;
wait_cnt <= 22'd0;
cmd_req <= 1'b0;
busy <= 1'b0;
init_done <= 1'b0;
id_data <= 8'd0;
rx0 <= 8'd0;
rx1 <= 8'd0;
rx2 <= 8'd0;
rx3 <= 8'd0;
rx4 <= 8'd0;
rx5 <= 8'd0;
rx6 <= 8'd0;
rx7 <= 8'd0;
clear_data <= 16'd0;
red_data <= 16'd0;
green_data <= 16'd0;
blue_data <= 16'd0;
data_valid <= 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 靠这一拍启动)
data_valid<= 1'b0; // 默认拉低,只在完成那拍拉高
case (step)
// =============================================================
// 空闲:等顶层打 start
// =============================================================
STEP_IDLE: begin
busy <= 1'b0;
if (start) begin
busy <= 1'b1;
// 初始化只做一次,做完了就直接去读数据
if (init_done) step <= STEP_RD_START;
else step <= STEP_ID_START;
end
end
// =============================================================
// 阶段〇:读 ID 验证(只在首次)
// START + 0x52 + 0x92 + Sr + 0x53 + 读(NACK) + STOP
// =============================================================
STEP_ID_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_ID_ADDR_W;
end
STEP_ID_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_ID_REG;
end
STEP_ID_REG: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= REG_ID; // 0x80|0x12,命令字节
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_ID_RSTART;
end
// ★ 重复起始:中间不发 STOP,再来一个 START(和普通 START 波形一样)
STEP_ID_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_ID_ADDR_R;
end
STEP_ID_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_ID_RX;
end
STEP_ID_RX: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_READ;
i2c_nack <= 1'b1; // 只读 1 个字节,回 NACK:"没有下一个了"
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
id_data <= i2c_rx; // done 那拍锁存,期望 0x44
step <= STEP_ID_STOP;
end
end
STEP_ID_STOP: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_STOP;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_PON_START;
end
// =============================================================
// 阶段一:PON 上电(只在首次)
// START + 0x52 + 0x80 + 0x01 + STOP
// =============================================================
STEP_PON_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_PON_ADDR;
end
STEP_PON_ADDR: 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_PON_REG;
end
STEP_PON_REG: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= REG_ENABLE; // 0x80|0x00,指向 ENABLE
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_PON_DATA;
end
STEP_PON_DATA: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= CFG_PON; // 0x01,只上电
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_PON_STOP;
end
STEP_PON_STOP: 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_PON;
wait_cnt <= 22'd0;
end
end
// ---- 中间:纯等 3ms,啥也不发 ----
STEP_WAIT_PON: begin
if (wait_cnt == WAIT_PON)
step <= STEP_AEN_START;
else
wait_cnt <= wait_cnt + 1'b1;
end
// =============================================================
// 阶段二:PON + AEN,开 ADC(只在首次)
// START + 0x52 + 0x80 + 0x03 + STOP
// =============================================================
STEP_AEN_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_AEN_ADDR;
end
STEP_AEN_ADDR: 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_AEN_REG;
end
STEP_AEN_REG: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= REG_ENABLE; // 0x80|0x00,还是指向 ENABLE
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_AEN_DATA;
end
STEP_AEN_DATA: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= CFG_AEN; // 0x03,PON + AEN,ADC 开转
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_AEN_STOP;
end
STEP_AEN_STOP: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_STOP;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_ATM_START;
end
// =============================================================
// 阶段三:写积分时间 ATIME(只在首次)
// START + 0x52 + 0x81 + 0xEB + STOP
// =============================================================
STEP_ATM_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_ATM_ADDR;
end
STEP_ATM_ADDR: 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_ATM_REG;
end
STEP_ATM_REG: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= REG_ATIME; // 0x80|0x01,指向 ATIME
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_ATM_DATA;
end
STEP_ATM_DATA: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= CFG_ATIME; // 0xEB,积分时间(见文件头说明)
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_ATM_STOP;
end
STEP_ATM_STOP: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_STOP;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_GAIN_START;
end
// =============================================================
// 阶段四:写增益 CONTROL(只在首次)
// START + 0x52 + 0x8F + 0x02 + STOP
// =============================================================
STEP_GAIN_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_GAIN_ADDR;
end
STEP_GAIN_ADDR: 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_GAIN_REG;
end
STEP_GAIN_REG: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= REG_CTRL; // 0x80|0x0F,指向 CONTROL
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_GAIN_DATA;
end
STEP_GAIN_DATA: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= CFG_GAIN; // 0x02,16x 增益
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_GAIN_STOP;
end
STEP_GAIN_STOP: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_STOP;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
init_done <= 1'b1; // ★ 初始化到此全部做完,以后不用再做
step <= STEP_WAIT_INT;
wait_cnt <= 22'd0;
end
end
// ---- 中间:等第一轮积分转完,60ms ----
STEP_WAIT_INT: begin
if (wait_cnt == WAIT_INT)
step <= STEP_RD_START;
else
wait_cnt <= wait_cnt + 1'b1;
end
// =============================================================
// 阶段五:读 8 字节数据(每次采样都走)
// START + 0x52 + 0x94 + Sr + 0x53 + 8 字节 + STOP
// =============================================================
STEP_RD_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_RD_ADDR_W;
end
STEP_RD_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_RD_REG;
end
STEP_RD_REG: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= REG_DATA; // 0x80|0x14,指向数据起始
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
// 指针切好了,不停顿,直接重复起始去读
if (i2c_done) step <= STEP_RD_RSTART;
end
STEP_RD_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_RD_ADDR_R;
end
STEP_RD_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_C_L;
end
// ---- 8 个字节:前 7 个回 ACK,最后一个回 NACK ----
// 小端:每个量都是低字节在前(C_L C_H R_L R_H G_L G_H B_L B_H)
STEP_RX_C_L: 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
rx0 <= i2c_rx; // C 低字节
step <= STEP_RX_C_H;
end
end
STEP_RX_C_H: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_READ;
i2c_nack <= 1'b0;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
rx1 <= i2c_rx; // C 高字节
step <= STEP_RX_R_L;
end
end
STEP_RX_R_L: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_READ;
i2c_nack <= 1'b0;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
rx2 <= i2c_rx; // R 低字节
step <= STEP_RX_R_H;
end
end
STEP_RX_R_H: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_READ;
i2c_nack <= 1'b0;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
rx3 <= i2c_rx; // R 高字节
step <= STEP_RX_G_L;
end
end
STEP_RX_G_L: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_READ;
i2c_nack <= 1'b0;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
rx4 <= i2c_rx; // G 低字节
step <= STEP_RX_G_H;
end
end
STEP_RX_G_H: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_READ;
i2c_nack <= 1'b0;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
rx5 <= i2c_rx; // G 高字节
step <= STEP_RX_B_L;
end
end
STEP_RX_B_L: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_READ;
i2c_nack <= 1'b0;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
rx6 <= i2c_rx; // B 低字节
step <= STEP_RX_B_H;
end
end
STEP_RX_B_H: 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
rx7 <= i2c_rx; // B 高字节
step <= STEP_RD_STOP;
end
end
STEP_RD_STOP: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_STOP;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
// 高字节在后(小端),拼回 16 位
clear_data <= {rx1, rx0};
red_data <= {rx3, rx2};
green_data <= {rx5, rx4};
blue_data <= {rx7, rx6};
data_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
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
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
`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
驱动的 44 步按阶段分组如下(步骤编号见 tcs34725_driver.v 里的 STEP_ 常量):
| 阶段 | 步骤范围 | 作用 |
|---|---|---|
| 〇 读 ID | 1 ~ 7 | START + 0x52 + 0x92 + 重复起始 + 0x53 + 读 1 字节(NACK)+ STOP,锁存 id_data(期望 0x44) |
| 一 PON 上电 | 8 ~ 12 | 写 ENABLE = 0x01,只上电 |
| 等待 | 13 | 等 3 ms,上电稳定 |
| 二 开 ADC | 14 ~ 18 | 写 ENABLE = 0x03,ADC 开始积分 |
| 三 积分时间 | 19 ~ 23 | 写 ATIME = 0xEB |
| 四 增益 | 24 ~ 28 | 写 CONTROL = 0x02(16x),完成后置 init_done |
| 等待 | 29 | 等 60 ms,让第一轮积分转完 |
| 五 读数据 | 30 ~ 43 | 连读 8 字节(前 7 字节回 ACK、最后回 NACK),小端拼成四个 16 位码值,打一拍 data_valid |
首次 start 从阶段〇一路走到阶段五(初始化 + 读数),之后每次 start 直接从阶段五读数据。底层 i2c_master 靠 i2c_start 打一拍启动、cmd_req 防止同一步命令重发,一步一步把字节搬完。
6、I/O 绑定
顶层模块 top 的端口与约束文件 TCS34725_color.cst 一一对应:
| 端口 | 方向 | FPGA 引脚 | 接线 |
|---|---|---|---|
sys_clk | input | T7 | 板载 50 MHz 系统时钟 |
rst_n | input | F10 | 复位按键(按下为低,异步复位) |
key | input | D11 | 手动补采按键(按下为低,20 ms 消抖) |
scl | output | R14 | 接 J5 排针空闲脚 → TCS34725 模块 SCL,需上拉 |
sda | inout | T15 | 接 J5 排针空闲脚 → TCS34725 模块 SDA,需上拉;双向,靠 sda_oe 切换 |
uart_tx | output | F12 | 接 USB 转串口 RX,115200-8-N-1 |
对应的约束文件内容如下:
// 引脚约束(立创·逻辑派 FPGA-G1 开发板)
// 模块: TCS34725_color
// 器件: GW2A-LV18PG256C8/I7
// 注意: G1 板 IO 为 3.3V(LVCMOS33)
// 系统时钟 50MHz
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;
// 手动触发按键
IO_LOC "key" D11;
IO_PORT "key" IO_TYPE=LVCMOS33 PULL_MODE=UP BANK_VCCIO=3.3;
// 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
19
20
21
22
23
24
25
26
27
实际接线请用杜邦线把模块接到 J5 排针:
7、程序下载
串口助手设置
波特率 115200、数据位 8、停止位 1、无校验,编码选择 GB2312 / GBK(本例输出是纯 ASCII,编码不影响显示;以后改成中文打印记得选对编码)。
常见问题排查
四个通道全是 0:多半是 ADC 没开或第一轮积分没转完,检查 ENABLE 是否写了 0x03、读数前是否等了足够久(本例等 60 ms)。 数值不随颜色变化:检查传感器上方有没有遮挡物、环境光够不够,可以把增益(CONTROL)调到 60x 或加长积分时间再试。 完全没有输出:检查 SCL、SDA 接线和上拉、模块供电是不是 3.3 V,串口线是否接到 F12。