← ブログ一覧へ
FPGAMicroLEDPWMSPII2CiCE40驱动ICASIC原型cocotb半导体

开源 FPGA 09|MicroLED 驱动控制器:从需求到 FPGA 实现(工程实战)

この記事は中国語で書かれ、Google 翻訳で自動翻訳されています。
中国語の原文を見る →
  __  __ _            ___ _     ___ ___  
 |  \/  (_)__ _ _ ___| __| |   |   \_ _| 
 | |\/| | / _| '_/ _ \ _|| |__ | |) | |  
 |_|  |_|_\__|_| \___/___|____|___/___| 

   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 的用武之地。

本篇目标:

  1. 明确 MicroLED 驱动 IC 的功能需求
  2. 为什么用 FPGA 做原型验证,而不是直接流片
  3. 设计完整的 FPGA 驱动控制器(SPI + PWM + I2C + 故障检测)
  4. 在 iCE40 上综合,验证资源可行性
  5. 用 cocotb 验证 PWM 精度
  6. 讨论 FPGA 原型到 ASIC 的路线

本篇不覆盖:

  • MicroLED 的制造工艺(Micro Transfer Printing、Mass Transfer 等)
  • ASIC 实际流片流程(涉及 PDK、DRC/LVS,另立专题)
  • 多路复用扫描驱动(Multiplex 驱动方案,更复杂)

1. MicroLED 驱动 IC 功能需求

MicroLED 驱动 IC 的核心规格(以 16-channel 小型驱动 IC 为例):

MicroLED Driver IC — 内部架构 16-channel · 12-bit PWM · SPI control · I²C temperature feedback Host MCU (ARM) MicroLED Driver IC (die boundary) SPI Slave Mode 0 · ≤ 10 MHz 8-bit addr / 16-bit data CS / SCK / MOSI / MISO 命令解析 I²C Master TMP117 ext. sensor 100 / 400 kHz 温度 → 控制环路 SCL / SDA Register File 256 × 16-bit brightness · phase · cfg fault flags / status memory-mapped Control Logic FSM 温度补偿曲线 故障检测 / 中断 PWM 相位调度 EMI 优化 16× PWM Channels 12-bit · 相位可配 CH00 CH01 CH02 CH03 CH04 CH05 CH06 CH07 CH08 CH09 CH10 CH11 CH12 CH13 CH14 CH15 → Current DAC (10-bit) Power / Bias / Bandgap Reference VDD 3.3V GND → analog rails → digital ground SPI commands write cfg temp duty[15:0] status PWM[15:0] → MOSFET / LED MicroLED Array (4 × 4)
16 通道 MicroLED 驱动 IC 内部架构:SPI 接收命令写入寄存器组,Control FSM 结合 I²C 温度反馈生成 12-bit PWM 占空比与相位,驱动 16 路电流 DAC,最终点亮 MicroLED 阵列。
参数规格说明
驱动通道数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 MHzCPOL=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)
模块LUTFFBRAM
SPI Slave 控制器~80~600
16 路 12-bit PWM~100~300
I2C Master~120~800
故障检测~80~400
顶层 + 寄存器组~118~1914 KB
合计~498~4014 KB
iCE40HX8K 总量76807680128 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-20K4-6 月
SMIC MPW180nm/130nm~5 mm²$3K-10K4-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 接口协议说明