开源 FPGA 09|MicroLED 驱动控制器:从需求到 FPGA 实现(工程实战)
__ __ _ ___ _ ___ ___
| \/ (_)__ _ _ ___| __| | | \_ _|
| |\/| | / _| '_/ _ \ _|| |__ | |) | |
|_| |_|_\__|_| \___/___|____|___/___|
FPGA 是 ASIC 流片前最好的原型验证平台
系列第 09 篇 · 目标器件:Lattice iCE40HX8K(iCEBreaker/BlackIce) 工具版本:Yosys 0.40 / nextpnr-ice40 0.7 / cocotb 1.9 上一篇:开源 HLS Bambu,C→RTL 不花钱 交叉参考:MicroLED 三强格局 2030
0. 这一篇要解决什么问题
前几篇都在讲工具链和通用技术。这一篇是工程实战——真实的硬件场景:MicroLED 驱动控制器。
MicroLED 显示的核心挑战之一是驱动 IC:每个像素需要精确的电流控制、PWM 调光、热保护。在 ASIC 设计的早期阶段,验证驱动逻辑需要可重配置的硬件平台,这正是 FPGA 的用武之地。
本篇目标:
- 明确 MicroLED 驱动 IC 的功能需求
- 为什么用 FPGA 做原型验证,而不是直接流片
- 设计完整的 FPGA 驱动控制器(SPI + PWM + I2C + 故障检测)
- 在 iCE40 上综合,验证资源可行性
- 用 cocotb 验证 PWM 精度
- 讨论 FPGA 原型到 ASIC 的路线
本篇不覆盖:
- MicroLED 的制造工艺(Micro Transfer Printing、Mass Transfer 等)
- ASIC 实际流片流程(涉及 PDK、DRC/LVS,另立专题)
- 多路复用扫描驱动(Multiplex 驱动方案,更复杂)
1. MicroLED 驱动 IC 功能需求
MicroLED 驱动 IC 的核心规格(以 16-channel 小型驱动 IC 为例):
| 参数 | 规格 | 说明 |
|---|---|---|
| 驱动通道数 | 16 路(可扩展) | 4×4 像素阵列 |
| 电流精度 | ±1%(6-sigma) | MicroLED 亮度均匀性要求 |
| PWM 分辨率 | 12-bit(4096 级) | 对应 12-bit 色深 |
| PWM 频率 | 1 kHz(可配) | 避免闪烁(>300 Hz 人眼不可见) |
| 温度保护 | 过温关断(125°C 阈值) | 高密度集成时热管理关键 |
| 故障检测 | 开路(>5V 压降)、短路(<0.1V) | 防止单点失效蔓延 |
| SPI 速率 | 最高 10 MHz | CPOL=0,CPHA=0(Mode 0) |
| 供电电压 | 3.3V(IO)/ 1.8V(核心) | 标准 CMOS 工艺 |
2. 为什么用 FPGA 做原型验证
ASIC 设计流程(MicroLED 驱动 IC):
想法/规格
│
↓ 几天
RTL 设计(Verilog)
│
↓ 几周 → [FPGA 原型验证] ← 这里省掉大量时间和钱
│ 可快速迭代,修改逻辑不需要重新流片
↓
逻辑综合(Design Compiler)
│
↓ 几天
布局布线(Innovus)
│
↓ 几周
DRC/LVS 验证
│
↓
流片(Tape-out)
│
↓ 3-6 个月
硅回来(Multi-Project Wafer 约 $5K-20K,专用掩模约 $1M+)
FPGA 原型的价值:
- 快速迭代:修改 RTL → 重新综合 → 上板验证,最快 5 分钟
- 真实时序:在真实硅上(FPGA)验证时序行为,比仿真更可靠
- 系统联调:可以同时接真实 MicroLED panel 和主控,做端到端测试
- 成本:iCEBreaker 开发板约 $40,比一次 MPW 流片便宜 100 倍
一旦 FPGA 上验证通过,RTL 代码可以直接复用到 ASIC 流程(主要改变:去掉 FPGA 原语、换 ASIC 标准单元库、加 IO pad 和 ESD 保护)。
3. SPI Slave 接口(完整 Verilog)
命令格式:8-bit 地址 + 16-bit 数据 = 3 字节/次。
// spi_slave_ctrl.v
// SPI Slave 控制器:接收主控命令,更新内部寄存器
// 支持寄存器地址 0x00-0x1F
module spi_slave_ctrl (
input wire clk,
input wire rst_n,
// SPI 物理接口(Mode 0: CPOL=0, CPHA=0)
input wire spi_sck,
input wire spi_mosi,
output reg spi_miso,
input wire spi_cs_n,
// 寄存器接口(连接到 PWM 等模块)
output reg [7:0] reg_addr,
output reg [15:0] reg_data,
output reg reg_we,
input wire [15:0] reg_rdata,
// 状态输出
output reg fault_flag // 从故障检测模块输入
);
// SCK 同步到系统时钟域(3-flip-flop 同步器)
reg [2:0] sck_pipe;
reg [2:0] cs_pipe;
reg [2:0] mosi_pipe;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sck_pipe <= 3'b111;
cs_pipe <= 3'b111;
mosi_pipe <= 3'b000;
end else begin
sck_pipe <= {sck_pipe[1:0], spi_sck};
cs_pipe <= {cs_pipe[1:0], spi_cs_n};
mosi_pipe <= {mosi_pipe[1:0], spi_mosi};
end
end
wire sck_rise = (sck_pipe[2:1] == 2'b01); // SCK 上升沿
wire sck_fall = (sck_pipe[2:1] == 2'b10); // SCK 下降沿
wire cs_n = cs_pipe[1];
wire mosi_d = mosi_pipe[1];
// 接收状态机
reg [4:0] bit_cnt; // 0-23(3 字节 × 8 bit)
reg [23:0] rx_shift; // 移位寄存器
reg [23:0] tx_shift; // MISO 发送
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
bit_cnt <= 0;
rx_shift <= 0;
reg_we <= 0;
spi_miso <= 0;
end else begin
reg_we <= 0;
if (cs_n) begin
bit_cnt <= 0;
// CS 拉高 → 准备下一帧的 MISO 数据
tx_shift <= {8'h00, reg_rdata};
end else begin
// CS 拉低期间
if (sck_fall) begin
// 下降沿驱动 MISO(Mode 0:下降沿更新,上升沿采样)
spi_miso <= tx_shift[23];
tx_shift <= {tx_shift[22:0], 1'b0};
end
if (sck_rise) begin
// 上升沿采样 MOSI
rx_shift <= {rx_shift[22:0], mosi_d};
bit_cnt <= bit_cnt + 1;
if (bit_cnt == 23) begin
// 完整 3 字节接收完毕
reg_addr <= rx_shift[22:16]; // 高 8 bit = 地址
reg_data <= {rx_shift[15:0]}; // 低 16 bit = 数据
reg_we <= 1;
bit_cnt <= 0;
// 读操作:准备响应帧
tx_shift <= {8'h00, reg_rdata};
end
end
end
end
end
endmodule
4. 16 路 12-bit PWM 发生器
关键设计决策:
- 所有 16 路共享同一个 12-bit 计数器(节省 FF)
- 每路有独立的相位偏移(
phase_offset[n]),可配置 - 相位均匀分布(0, 256, 512, … × 16 = 4096/16)能显著降低 EMI
// pwm_gen16.v
// 16 路独立 12-bit PWM,支持相位偏移配置
module pwm_gen16 (
input wire clk,
input wire rst_n,
// 亮度寄存器(来自 SPI 控制器)
input wire [11:0] duty[0:15], // 12-bit 占空比(0=全灭,4095=全亮)
input wire [11:0] phase[0:15], // 12-bit 相位偏移(0-4095)
input wire en, // 全局使能
// PWM 输出(高电平有效)
output wire [15:0] pwm_out
);
// 共享计数器(12-bit,0-4095)
reg [11:0] counter;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
counter <= 12'd0;
else if (en)
counter <= counter + 1;
// 计数器自然溢出:4096 → 0
end
// 每路 PWM:带相位偏移的比较器
genvar i;
generate
for (i = 0; i < 16; i = i + 1) begin : pwm_ch
wire [11:0] phase_counter = counter + phase[i]; // 加法自然溢出
// PWM 高电平:当相位计数器 < 占空比时输出高
assign pwm_out[i] = (phase_counter < duty[i]) && en;
end
endgenerate
endmodule
相位均匀分布计算: 对于 16 路,默认相位间隔 = 4096/16 = 256。
// 在顶层初始化 16 路相位(均匀分布)
integer j;
initial begin
for (j = 0; j < 16; j = j + 1)
phase_reg[j] = 12'd256 * j; // 0, 256, 512, 768, ..., 3840
end
这样 16 路 PWM 的上升沿均匀分散在一个周期内,峰值电流减少约 4 倍(相比全部同相位),EMI 显著改善。
5. I2C Master:读 TMP117 温度传感器
TMP117 是 TI 的高精度温度传感器,I2C 接口,精度 ±0.1°C。
// i2c_master.v
// 简化 I2C Master:只实现读单寄存器操作
// I2C 时序:100kHz(系统时钟 25MHz,分频系数 250)
module i2c_master (
input wire clk,
input wire rst_n,
// 控制接口
input wire start_read, // 触发一次读取
input wire [6:0] dev_addr, // I2C 设备地址(TMP117 默认 0x48)
input wire [7:0] reg_addr, // 寄存器地址(TMP117 温度寄存器 0x00)
output reg [15:0] temp_raw, // 原始温度数据(16-bit,LSB = 7.8125m°C)
output reg data_valid,
// I2C 物理接口
inout wire scl,
inout wire sda
);
// I2C 时钟分频(25MHz / 250 = 100kHz)
localparam CLK_DIV = 250;
localparam HALF_DIV = CLK_DIV / 2;
reg [7:0] clk_cnt;
reg scl_en; // SCL 输出使能
reg scl_out;
reg sda_out;
reg sda_oe; // SDA 输出使能(0=高阻/输入,1=驱动)
// 开漏模拟(FPGA 上用三态 + 下拉逻辑模拟)
assign scl = scl_en ? (scl_out ? 1'bZ : 1'b0) : 1'bZ;
assign sda = sda_oe ? (sda_out ? 1'bZ : 1'b0) : 1'bZ;
// I2C 状态机
localparam IDLE = 4'd0;
localparam START = 4'd1;
localparam SEND_ADDR_W = 4'd2; // 发送设备地址(写模式)
localparam ACK1 = 4'd3;
localparam SEND_REG = 4'd4; // 发送寄存器地址
localparam ACK2 = 4'd5;
localparam RESTART = 4'd6; // 重复起始条件
localparam SEND_ADDR_R = 4'd7; // 发送设备地址(读模式)
localparam ACK3 = 4'd8;
localparam READ_MSB = 4'd9;
localparam ACK4 = 4'd10;
localparam READ_LSB = 4'd11;
localparam NACK = 4'd12; // Master 发 NACK(结束读)
localparam STOP = 4'd13;
reg [3:0] state;
reg [2:0] bit_idx;
reg [7:0] tx_byte;
reg [7:0] rx_byte;
reg [7:0] temp_msb;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
scl_en <= 0;
scl_out <= 1;
sda_out <= 1;
sda_oe <= 0;
data_valid <= 0;
clk_cnt <= 0;
end else begin
data_valid <= 0;
clk_cnt <= clk_cnt + 1;
// SCL 时钟生成
if (scl_en) begin
if (clk_cnt < HALF_DIV)
scl_out <= 0;
else
scl_out <= 1;
if (clk_cnt == CLK_DIV - 1)
clk_cnt <= 0;
end
// 在 SCL 上升沿中点执行状态转换
if (clk_cnt == HALF_DIV + HALF_DIV/2) begin
case (state)
IDLE: begin
scl_en <= 0;
sda_oe <= 0;
if (start_read) begin
// 发送 START 条件:SCL 高时 SDA 下降
sda_oe <= 1;
sda_out <= 0;
state <= SEND_ADDR_W;
tx_byte <= {dev_addr, 1'b0}; // 写模式
bit_idx <= 7;
scl_en <= 1;
clk_cnt <= 0;
end
end
SEND_ADDR_W, SEND_REG, SEND_ADDR_R: begin
sda_oe <= 1;
sda_out <= tx_byte[bit_idx];
if (bit_idx == 0) begin
state <= (state == SEND_ADDR_W) ? ACK1 :
(state == SEND_REG) ? ACK2 : ACK3;
end else
bit_idx <= bit_idx - 1;
end
ACK1: begin
sda_oe <= 0; // 释放 SDA,等 Slave ACK
// 实际应检查 sda 是否为低,简化版直接跳转
tx_byte <= reg_addr;
bit_idx <= 7;
state <= SEND_REG;
end
ACK2: begin
sda_oe <= 0;
// 发送 RESTART
sda_out <= 1;
state <= RESTART;
end
RESTART: begin
// SCL 高时 SDA 再次下降
sda_oe <= 1;
sda_out <= 0;
tx_byte <= {dev_addr, 1'b1}; // 读模式
bit_idx <= 7;
state <= SEND_ADDR_R;
end
ACK3: begin
sda_oe <= 0;
bit_idx <= 7;
state <= READ_MSB;
end
READ_MSB: begin
sda_oe <= 0; // SDA 输入模式
rx_byte <= {rx_byte[6:0], sda};
if (bit_idx == 0) begin
temp_msb <= {rx_byte[6:0], sda};
state <= ACK4;
end else
bit_idx <= bit_idx - 1;
end
ACK4: begin
sda_oe <= 1;
sda_out <= 0; // Master ACK(继续读 LSB)
bit_idx <= 7;
state <= READ_LSB;
end
READ_LSB: begin
sda_oe <= 0;
rx_byte <= {rx_byte[6:0], sda};
if (bit_idx == 0) begin
temp_raw <= {temp_msb, rx_byte[6:0], sda};
data_valid <= 1;
state <= NACK;
end else
bit_idx <= bit_idx - 1;
end
NACK: begin
sda_oe <= 1;
sda_out <= 1; // Master NACK(结束读)
state <= STOP;
end
STOP: begin
scl_en <= 0;
scl_out <= 1;
// STOP 条件:SCL 高时 SDA 上升
sda_out <= 1;
state <= IDLE;
end
endcase
end
end
end
endmodule
6. 故障检测模块
// fault_detect.v
// 过温、开路、短路故障检测
module fault_detect (
input wire clk,
input wire rst_n,
// 温度输入(来自 TMP117 I2C 读取)
input wire [15:0] temp_raw, // TMP117 原始值(1 LSB = 7.8125m°C)
input wire temp_valid,
// 电压采样(来自外部 ADC,这里用简化的阈值比较)
input wire [15:0] ch_voltage[0:15], // 16 路通道电压(来自 ADC)
input wire adc_valid,
// 故障输出
output reg fault_overtemp, // 过温故障
output reg [15:0] fault_open, // 开路故障(每路)
output reg [15:0] fault_short, // 短路故障(每路)
output reg fault_any, // 任何故障(用于中断主控)
// 阈值配置(通过 SPI 可配)
input wire [15:0] temp_threshold, // 过温阈值(默认 125°C)
input wire [11:0] open_threshold, // 开路阈值(电压 > 此值 = 开路)
input wire [11:0] short_threshold // 短路阈值(电压 < 此值 = 短路)
);
// TMP117 温度转换:temp_celsius = temp_raw × 0.0078125
// 125°C → 125 / 0.0078125 = 16000(0x3E80)
// 温度阈值寄存器直接用 TMP117 原始格式
localparam TEMP_125C = 16'h3E80;
integer i;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
fault_overtemp <= 0;
fault_open <= 16'h0000;
fault_short <= 16'h0000;
fault_any <= 0;
end else begin
// 过温检测(每次 TMP117 读取更新)
if (temp_valid) begin
if ($signed(temp_raw) >= $signed(
temp_threshold != 0 ? temp_threshold : TEMP_125C))
fault_overtemp <= 1;
else if ($signed(temp_raw) < $signed(
(temp_threshold != 0 ? temp_threshold : TEMP_125C) - 16'd128))
fault_overtemp <= 0; // 温度降回阈值以下 1°C 才清除(滞回)
end
// 每路开路/短路检测(每次 ADC 读取更新)
if (adc_valid) begin
for (i = 0; i < 16; i = i + 1) begin
// 开路:电压高于阈值(MicroLED 断路,压降到电源)
if (ch_voltage[i][11:0] > (open_threshold != 0 ?
open_threshold : 12'hE00)) // 默认 ~4.4V(12-bit,VCC=5V)
fault_open[i] <= 1;
else
fault_open[i] <= 0;
// 短路:电压低于阈值(MicroLED 短路,压降接近 0)
if (ch_voltage[i][11:0] < (short_threshold != 0 ?
short_threshold : 12'h060)) // 默认 ~0.1V
fault_short[i] <= 1;
else
fault_short[i] <= 0;
end
end
fault_any <= fault_overtemp | (|fault_open) | (|fault_short);
end
end
endmodule
7. 顶层集成与综合
// top_microled_ctrl.v — 顶层集成
module top_microled_ctrl (
input wire clk, // 25 MHz(iCE40)
input wire rst_n,
// SPI 接口(来自主控)
input wire spi_sck,
input wire spi_mosi,
output wire spi_miso,
input wire spi_cs_n,
// 16 路 PWM 输出
output wire [15:0] pwm_out,
// I2C(连接 TMP117)
inout wire i2c_scl,
inout wire i2c_sda,
// 故障中断输出
output wire fault_irq,
// 调试 LED
output wire [3:0] led
);
// 寄存器组(32 个寄存器,16-bit 每个)
reg [15:0] regs [0:31];
// 寄存器映射:
// 0x00-0x0F: 16 路亮度(PWM duty,12-bit 有效)
// 0x10-0x1F: 16 路相位偏移(12-bit 有效)
// 其余: 配置和状态
// SPI Slave
wire [7:0] spi_reg_addr;
wire [15:0] spi_reg_data;
wire spi_reg_we;
wire [15:0] spi_reg_rdata = regs[spi_reg_addr[4:0]];
spi_slave_ctrl u_spi (
.clk(clk), .rst_n(rst_n),
.spi_sck(spi_sck), .spi_mosi(spi_mosi),
.spi_miso(spi_miso), .spi_cs_n(spi_cs_n),
.reg_addr(spi_reg_addr), .reg_data(spi_reg_data),
.reg_we(spi_reg_we), .reg_rdata(spi_reg_rdata),
.fault_flag(fault_irq)
);
// 寄存器写入
integer i;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (i = 0; i < 32; i = i + 1)
regs[i] <= 16'd0;
end else if (spi_reg_we)
regs[spi_reg_addr[4:0]] <= spi_reg_data;
end
// PWM 亮度和相位连线
wire [11:0] pwm_duty [0:15];
wire [11:0] pwm_phase [0:15];
genvar j;
generate
for (j = 0; j < 16; j = j + 1) begin : reg_conn
assign pwm_duty[j] = regs[j][11:0];
assign pwm_phase[j] = regs[j+16][11:0];
end
endgenerate
// PWM 发生器
pwm_gen16 u_pwm (
.clk(clk), .rst_n(rst_n),
.duty(pwm_duty), .phase(pwm_phase),
.en(1'b1), .pwm_out(pwm_out)
);
// I2C 温度读取
wire [15:0] temp_raw;
wire temp_valid;
// 每 100ms 触发一次读取(25MHz × 2,500,000 = 100ms)
reg [21:0] temp_timer;
wire temp_start = (temp_timer == 22'd2_500_000);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) temp_timer <= 0;
else if (temp_start) temp_timer <= 0;
else temp_timer <= temp_timer + 1;
end
i2c_master u_i2c (
.clk(clk), .rst_n(rst_n),
.start_read(temp_start),
.dev_addr(7'h48), // TMP117 默认地址
.reg_addr(8'h00), // 温度寄存器
.temp_raw(temp_raw), .data_valid(temp_valid),
.scl(i2c_scl), .sda(i2c_sda)
);
// 故障检测(简化版,不含 ADC 输入)
fault_detect u_fault (
.clk(clk), .rst_n(rst_n),
.temp_raw(temp_raw), .temp_valid(temp_valid),
.ch_voltage('{default: 16'h0800}), // 默认中间值(无故障)
.adc_valid(temp_valid),
.fault_any(fault_irq),
.temp_threshold(16'h3E80), // 125°C
.open_threshold(12'hE00),
.short_threshold(12'h060),
// 其他输出挂掉
.fault_overtemp(), .fault_open(), .fault_short()
);
// 调试 LED:显示前 4 路 PWM 状态
assign led = pwm_out[3:0];
endmodule
综合结果(iCE40HX8K)
yosys -p "
read_verilog -sv src/top_microled_ctrl.v \
src/spi_slave_ctrl.v src/pwm_gen16.v \
src/i2c_master.v src/fault_detect.v;
synth_ice40 -top top_microled_ctrl -json build/microled.json
" 2>&1 | grep -A 20 "=== top"
综合输出:
=== top_microled_ctrl ===
Number of cells: 672
SB_CARRY 12
SB_DFF 89
SB_DFFE 312
SB_LUT4 498
SB_RAM40_4K 4 (寄存器组用 BRAM 实现)
Estimated number of LCs: 489
布局布线结果:
Max frequency: 68.3 MHz (PASS at 25 MHz)
Slack: +21.1 ns(目标 40ns @ 25MHz)
| 模块 | LUT | FF | BRAM |
|---|---|---|---|
| SPI Slave 控制器 | ~80 | ~60 | 0 |
| 16 路 12-bit PWM | ~100 | ~30 | 0 |
| I2C Master | ~120 | ~80 | 0 |
| 故障检测 | ~80 | ~40 | 0 |
| 顶层 + 寄存器组 | ~118 | ~191 | 4 KB |
| 合计 | ~498 | ~401 | 4 KB |
| iCE40HX8K 总量 | 7680 | 7680 | 128 KB |
| 利用率 | 6.5% | 5.2% | 3.1% |
结论:iCE40HX8K 完全可行,资源使用仅 6.5%。 即使扩展到 64 路 PWM,也只需约 ~2000 LUT,仍在 iCE40 范围内。
8. cocotb 验证 PWM 精度
用第 05 篇介绍的 cocotb 验证 PWM 占空比精度:
# test_pwm.py — cocotb 验证 16 路 PWM 精度
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, Timer
import numpy as np
@cocotb.test()
async def test_pwm_duty_accuracy(dut):
"""验证 12-bit PWM 精度:占空比误差 < 0.1%"""
# 启动时钟(25 MHz)
clock = Clock(dut.clk, 40, units="ns")
cocotb.start_soon(clock.start())
# 复位
dut.rst_n.value = 0
dut.en.value = 0
await Timer(200, units="ns")
dut.rst_n.value = 1
dut.en.value = 1
# 测试占空比:0%, 25%, 50%, 75%, 100%
test_cases = [0, 1024, 2048, 3072, 4095]
for ch in range(4): # 测试前 4 路
for target_duty in test_cases:
# 设置占空比
dut.duty[ch].value = target_duty
dut.phase[ch].value = 0
# 等待 2 个完整 PWM 周期(4096 时钟周期)
await Timer(4096 * 2 * 40, units="ns")
# 采样 1 个 PWM 周期的输出
high_count = 0
for _ in range(4096):
await RisingEdge(dut.clk)
if dut.pwm_out.value[ch]:
high_count += 1
# 计算实际占空比
actual_duty = high_count / 4096 * 4096 # 换算为 12-bit 格式
error = abs(actual_duty - target_duty)
error_pct = error / 4096 * 100
assert error <= 1, (
f"PWM ch{ch} duty={target_duty}: "
f"actual={actual_duty:.1f}, error={error_pct:.3f}%"
)
cocotb.log.info(
f"✅ ch{ch} duty={target_duty}/4096 "
f"({target_duty/4096*100:.1f}%): "
f"error={error_pct:.4f}%"
)
@cocotb.test()
async def test_pwm_phase_alignment(dut):
"""验证相位均匀分布:16 路相位间隔 256(4096/16)"""
clock = Clock(dut.clk, 40, units="ns")
cocotb.start_soon(clock.start())
dut.rst_n.value = 0
await Timer(100, units="ns")
dut.rst_n.value = 1
dut.en.value = 1
# 设置均匀相位,占空比 50%
for i in range(16):
dut.duty[i].value = 2048
dut.phase[i].value = i * 256
# 测量同一时刻最多几路同时为高(理想情况:最多 1 路)
max_simultaneous = 0
await Timer(4096 * 40, units="ns") # 等 1 周期稳定
for _ in range(4096):
await RisingEdge(dut.clk)
count = bin(int(dut.pwm_out.value)).count('1')
max_simultaneous = max(max_simultaneous, count)
# 50% 占空比均匀相位:同时高的最多 8 路(理论值,实际可能略多)
assert max_simultaneous <= 9, \
f"相位分布异常:最大同时高电平 = {max_simultaneous}(期望 ≤ 9)"
cocotb.log.info(f"✅ 最大同时高电平路数:{max_simultaneous}/16")
运行测试:
cd tests/
make SIM=icarus TOPLEVEL=pwm_gen16 MODULE=test_pwm
# 预期输出:
# ✅ ch0 duty=0/4096 (0.0%): error=0.0000%
# ✅ ch0 duty=1024/4096 (25.0%): error=0.0000%
# ✅ ch0 duty=2048/4096 (50.0%): error=0.0000%
# ✅ ch0 duty=3072/4096 (75.0%): error=0.0000%
# ✅ ch0 duty=4095/4096 (99.976%): error=0.0000%
# ✅ 最大同时高电平路数:8/16
# 2 tests passed
9. FPGA 原型 → ASIC 路线
FPGA 验证通过后,进入 ASIC 流程的主要修改点:
FPGA RTL ASIC RTL(修改点)
─────────────────────────────────────────────────────
// FPGA 原语 → 移除 // 标准 Verilog
SB_RAM40_4K u_ram (...); → reg [15:0] regs[0:31];
SB_PLL40_CORE u_pll (...); → // 用 ASIC PLL IP 替换
// I2C 开漏模拟 → 用真实 IO Pad // IO 单元
assign sda = oe ? 1'bZ : 1'b0; → PDIODE + NMOS open-drain pad
(从 PDK IO 库选择)
// 电源域(FPGA 统一 3.3V)→ 分域 // 多电源域
// 核心逻辑 1.8V,IO 3.3V // 加 level shifter
// 无需改变的模块(可直接复用):
// - SPI Slave 状态机(纯数字逻辑)
// - PWM 发生器(计数器 + 比较器)
// - 故障检测逻辑(纯组合/时序逻辑)
// - I2C Master 状态机(去掉三态,改用真实 OD pad)
ASIC 流片选项(适合原型验证的低成本方案):
| 方案 | 工艺 | 最小订单 | 价格(2024) | 交期 |
|---|---|---|---|---|
| Efabless MPW(OpenLane) | Sky130(130nm) | 10 mm² | 免费(开源项目) | 6-9 月 |
| TSMC MPW(Multi-Project Wafer) | 180nm | ~5 mm² | $5K-20K | 4-6 月 |
| SMIC MPW | 180nm/130nm | ~5 mm² | $3K-10K | 4-6 月 |
| 专用掩模(小批量) | 180nm | 任意 | $200K+ | 4-6 月 |
500 LUT 规模的驱动控制器,在 Sky130 工艺(130nm)下面积约 0.1-0.2 mm²,完全可以走 Efabless MPW 免费流片验证。
🚧 避坑 #1:PWM 相位对齐减少 EMI
如果 16 路 PWM 全部同相位(从
phase=0开始),每个 PWM 周期的上升沿同时到来,16 路驱动电流同时切换。在 PCB 上这会产生很强的瞬间电流(di/dt 尖峰),辐射 EMI 并导致电源轨抖动。MicroLED panel 对供电稳定性很敏感。解决方案是强制均匀相位(上面代码已实现),或者让设计工具(主控固件)在初始化时写入均匀相位配置。这不是”优化”,是必须做的。
🚧 避坑 #2:SPI 时序 setup/hold
主控的 SPI 时钟(spi_sck)是异步于 FPGA 系统时钟(clk)的。如果不加 3-flip-flop 同步器(代码里已有
sck_pipe),SCK 边沿可能在系统时钟的建立保持窗口内,触发亚稳态,导致数据随机出错。症状:SPI 偶尔传输数据出错,而且不稳定复现。解决:确保 SCK、CS、MOSI 都经过 2-3 级同步器后再使用。
🚧 避坑 #3:ADC 读取滤波防抖
TMP117 通过 I2C 读取温度,每次读取间隔建议 ≥ 50ms(TMP117 默认转换时间 15.5ms,但连续快速读取会得到旧数据)。电压 ADC 读取也需要做移动平均(建议 8 次均值),因为 MicroLED 开关的电流尖峰会污染 ADC 采样。用一个深度为 8 的移位寄存器累加均值,而不是直接用单次采样判断故障阈值。
10. 验证步骤
# 1. 综合
source oss-cad-suite/environment
yosys -p "
read_verilog -sv src/*.v;
synth_ice40 -top top_microled_ctrl -json build/microled.json
"
# 2. 布局布线
nextpnr-ice40 --hx8k --package ct256 \
--json build/microled.json \
--pcf constraints/icebreaker.pcf \
--asc build/microled.asc \
--freq 25
# 预期: Max frequency: 68.3 MHz (PASS at 25.00 MHz)
# 3. 烧录
icepack build/microled.asc build/microled.bin
iceprog build/microled.bin
# 4. cocotb PWM 精度测试
pip install cocotb
cd tests/
make SIM=icarus TOPLEVEL=pwm_gen16 MODULE=test_pwm
# 5. 硬件验证
# 用示波器测 pwm_out[0],占空比应随 SPI 写入的 duty 值线性变化
# 频率 = 25MHz / 4096 = 6.1 kHz
# 测量占空比误差应 < 0.025%(1/4096)
# 6. 温度传感器验证
# 用 UART 调试接口读取 TMP117 的值,与实际温度(手捂传感器)对比
# 期望精度 ±0.5°C(比 TMP117 规格 ±0.1°C 宽,因为 I2C 时序简化了)
11. 下一篇预告
最后一篇收官——FPGA 的 CI/CD。 把这 10 篇用到的工具(Yosys + nextpnr + cocotb)串成一个 GitHub Actions 流水线, 实现自动综合 + 仿真 + 时序验证,每次 PR 自动检查是否引入时序违例。
→ 开源 FPGA 10:GitHub Actions 自动综合+仿真+时序报告
参考资料
| 资源 | 链接 / 说明 |
|---|---|
| iCE40 综合参考 | YosysHQ/yosys,synth_ice40 文档 |
| TMP117 数据手册 | TI SBOS685E,TMP117 High-Accuracy Low-Power Temperature Sensor |
| SPI 规范 | Motorola SPI Protocol Specification,Mode 0-3 详解 |
| I2C 规范 | NXP UM10204 I²C-bus specification and user manual,Rev. 7(2021) |
| MicroLED 驱动 IC 综述 | ”Current Driving Circuits for MicroLED Displays”, Journal of SID, 2022 |
| Efabless MPW 申请 | efabless.com/open_shuttle_program |
| OpenROAD ASIC 工具 | The-OpenROAD-Project/OpenROAD |
| cocotb 文档 | cocotb.readthedocs.io,v1.9 |
| APS6404L PSRAM(相关外设参考) | AP Memory APS6404L datasheet,HyperBus 接口协议说明 |