📖 第10课:二维CA显示引擎

VGA HDMI 帧缓冲 实时渲染

🖥️ 从计算到可视化

二维CA的实时可视化是一个完整的系统工程——需要帧缓冲、颜色映射、时序控制、交互接口。本课构建一个完整的显示引擎,将CA计算结果呈现在屏幕上。

🏗️ 系统架构

完整的CA可视化系统由5个核心模块组成:

┌────────────────────────────────────────────┐
│           CA Display Engine                 │
│                                             │
│  ┌──────────┐     ┌──────────────┐         │
│  │ CA Core  │────▶│ Frame Buffer │         │
│  │ (计算)   │     │ (双Bank)     │         │
│  └──────────┘     └──────┬───────┘         │
│                          │                   │
│  ┌──────────┐     ┌──────▼───────┐         │
│  │ Control  │     │ Color Mapper │         │
│  │ (FSM)    │     │ (调色板)     │         │
│  └──────────┘     └──────┬───────┘         │
│                          │                   │
│  ┌──────────┐     ┌──────▼───────┐         │
│  │ Input    │     │ VGA/HDMI     │         │
│  │ Handler  │     │ Timing Gen   │         │
│  └──────────┘     └──────────────┘         │
└────────────────────────────────────────────┘

⚡ 双缓冲帧缓冲

双缓冲的核心思想:CA核心写入一个Bank,显示控制器读取另一个Bank。每帧完成后交换。这消除了"撕裂"现象——显示的是完整的一帧,而不是半新半旧的混合。

双缓冲帧缓冲控制器

// ============================================================================
// frame_buffer_dual.v - 双缓冲帧缓冲控制器
// 写端口连接CA核心,读端口连接显示控制器
// 两个Bank交替使用,消除画面撕裂
// ============================================================================
module frame_buffer_dual #(
    parameter WIDTH  = 160,
    parameter HEIGHT = 120,
    parameter DEPTH  = 4           // 每像素位深
)(
    input  wire              clk,
    input  wire              rst_n,
    // 写接口(CA核心)
    input  wire [7:0]        wr_x,
    input  wire [7:0]        wr_y,
    input  wire [DEPTH-1:0]  wr_data,
    input  wire              wr_en,
    input  wire              frame_swap,
    // 读接口(显示控制器)
    input  wire [7:0]        rd_x,
    input  wire [7:0]        rd_y,
    output wire [DEPTH-1:0]  rd_data,
    output wire              wr_bank
);

    localparam TOTAL = WIDTH * HEIGHT;
    localparam ADDR_W = $clog2(TOTAL);

    reg [DEPTH-1:0] bank_a [0:TOTAL-1];
    reg [DEPTH-1:0] bank_b [0:TOTAL-1];
    reg write_sel;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            write_sel <= 1'b0;
        else if (frame_swap)
            write_sel <= ~write_sel;
    end

    wire [ADDR_W-1:0] wr_addr = wr_y * WIDTH + wr_x;
    wire [ADDR_W-1:0] rd_addr = rd_y * WIDTH + rd_x;

    always @(posedge clk) begin
        if (wr_en) begin
            if (write_sel) bank_b[wr_addr] <= wr_data;
            else           bank_a[wr_addr] <= wr_data;
        end
    end

    assign rd_data = write_sel ? bank_a[rd_addr] : bank_b[rd_addr];
    assign wr_bank = write_sel;

endmodule

🎨 颜色映射器

简单的黑白显示不够有趣。我们可以根据元胞的年龄、邻居密度等属性映射颜色。

多模式颜色映射器

// ============================================================================
// color_mapper.v - CA颜色映射器
// 支持:黑白、热力图、年龄渐变、密度着色、自定义调色板
// ============================================================================
module color_mapper #(
    parameter MODE = 0     // 0=黑白 1=热力图 2=年龄 3=密度 4=调色板
)(
    input  wire [3:0]  cell_age,
    input  wire [3:0]  neighbor_cnt,
    input  wire        cell_alive,
    input  wire [11:0] palette [0:15],  // 16色调色板(仅MODE=4)
    output wire [11:0] rgb              // 12位RGB (4-4-4)
);

    reg [3:0] r, g, b;

    always @(*) begin
        r = 4'd0; g = 4'd0; b = 4'd0;
        case (MODE)
            0: begin  // 黑白模式
                r = cell_alive ? 4'hF : 4'h0;
                g = cell_alive ? 4'hF : 4'h0;
                b = cell_alive ? 4'hF : 4'h0;
            end
            1: begin  // 热力图(年龄→温度色)
                if (cell_alive) begin
                    if (cell_age < 4) begin
                        r = cell_age * 4; g = 4'd0; b = 4'd0;
                    end else if (cell_age < 8) begin
                        r = 4'hF; g = (cell_age - 4) * 4; b = 4'd0;
                    end else begin
                        r = 4'hF; g = 4'hF; b = (cell_age - 8) * 5;
                    end
                end
            end
            2: begin  // 粉红渐变(匹配课程主色#ec4899)
                if (cell_alive) begin
                    r = 4'hF;
                    g = {1'b0, cell_age};
                    b = {2'b01, cell_age[3:2]};
                end
            end
            3: begin  // 密度着色
                if (cell_alive) begin
                    r = 4'hF;
                    g = {neighbor_cnt, 1'b0};
                end else begin
                    b = {neighbor_cnt, 1'b0};
                end
            end
            4: begin  // 调色板模式
                {r, g, b} = cell_alive ? palette[cell_age] : 12'd0;
            end
        endcase
    end

    assign rgb = {r, g, b};

endmodule

VGA时序控制器

// ============================================================================
// vga_controller.v - VGA 640x480 @60Hz 完整时序控制器
// 像素时钟: 25.175MHz (实际使用25MHz,误差<1%)
// ============================================================================
module vga_controller (
    input  wire        clk25,
    input  wire        rst_n,
    output reg  [9:0]  hcount,     // 水平像素计数
    output reg  [9:0]  vcount,     // 垂直行计数
    output wire        hsync,      // 水平同步
    output wire        vsync,      // 垂直同步
    output wire        blank,      // 消隐(1=不显示)
    output wire        active      // 有效显示区域
);

    localparam H_VIS = 640, H_FP = 16, H_SYNC = 96, H_BP = 48, H_TOTAL = 800;
    localparam V_VIS = 480, V_FP = 10, V_SYNC = 2,  V_BP = 33, V_TOTAL = 525;

    always @(posedge clk25 or negedge rst_n) begin
        if (!rst_n) begin
            hcount <= 10'd0;
            vcount <= 10'd0;
        end else begin
            if (hcount == H_TOTAL - 1) begin
                hcount <= 10'd0;
                if (vcount == V_TOTAL - 1)
                    vcount <= 10'd0;
                else
                    vcount <= vcount + 10'd1;
            end else begin
                hcount <= hcount + 10'd1;
            end
        end
    end

    assign hsync  = ~((hcount >= H_VIS + H_FP) && (hcount < H_VIS + H_FP + H_SYNC));
    assign vsync  = ~((vcount >= V_VIS + V_FP) && (vcount < V_VIS + V_FP + V_SYNC));
    assign blank  = (hcount >= H_VIS) || (vcount >= V_VIS);
    assign active = ~blank;

endmodule

📊 显示分辨率与CA网格映射

VGA分辨率像素时钟CA网格映射方式帧率
640×48025MHz640×4801:1直接映射60Hz
640×48025MHz160×1204×4像素/元胞60Hz
800×60040MHz200×1504×4像素/元胞60Hz
1024×76865MHz128×968×8像素/元胞60Hz
💡 缩放显示:当CA网格小于显示分辨率时,每个元胞用N×N像素块显示。这需要缩放器——可以用简单的重复像素(最近邻插值),也可以用双线性插值使图案更平滑。硬件实现中,最近邻插值只需一个计数器分频。

🏋️ 练习

练习10.1:将CA核心、帧缓冲和VGA控制器集成到顶层模块,实现完整的"CA-on-VGA"系统。在FPGA开发板上运行。
练习10.2:添加交互控制——通过按钮实现:暂停/继续、单步、重置、切换规则。通过UART串口接收更复杂的命令。
练习10.3:实现"年龄追踪"——每个元胞额外存储4位年龄计数器。新生的元胞年龄=0,每步+1。将年龄映射为颜色。
练习10.4:实现4级缩放——从全屏显示到4×4放大。用BRAM作为缩放缓冲,支持方向键平移。
练习10.5(挑战):实现HDMI输出接口(使用TMDS编码),替代VGA。对比两者的资源开销和信号质量。

🏆 成就解锁

🏅 可视化架构师

二维CA阶段完成!你已经构建了完整的可视化系统,从双缓冲帧缓冲到VGA时序到颜色映射。

📐 深入分析:显示系统的时序约束

VGA时序约束分析

640×480 @60Hz需要25MHz像素时钟

每像素时间 = 40ns

在这个时间内必须完成:地址计算 + 存储器读取 + 颜色映射 + 输出

关键路径

地址生成(2ns) + BRAM读取(3ns) + 颜色映射(5ns) + 输出寄存器(1ns) = 11ns

裕量 = 40 - 11 = 29ns,非常充足

更高分辨率的挑战

1024×768 @60Hz需要65MHz像素时钟

每像素时间 = 15.4ns,关键路径11ns → 裕量仅4.4ns

需要优化:流水线化颜色映射,使用更快的BRAM

补充实现

// 帧率控制器 - 自适应帧率
module frame_rate_ctrl #(
    parameter TARGET_FPS = 60
)(
    input  wire        clk,
    input  wire        rst_n,
    input  wire        ca_done,     // CA一步完成
    output wire        display_en,  // 显示使能
    output wire        ca_step,     // CA步进信号
    output wire [31:0] actual_fps   // 实际帧率
);
    localparam CLK_PER_FRAME = 100_000_000 / TARGET_FPS;
    reg [31:0] frame_timer;
    reg [31:0] fps_counter;
    reg [31:0] fps_timer;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            frame_timer <= 32'd0;
            fps_counter <= 32'd0;
            fps_timer   <= 32'd0;
        end else begin
            frame_timer <= frame_timer + 32'd1;
            fps_timer   <= fps_timer + 32'd1;
            if (frame_timer >= CLK_PER_FRAME) begin
                frame_timer <= 32'd0;
                fps_counter <= fps_counter + 32'd1;
            end
            if (fps_timer >= 100_000_000) begin
                fps_timer <= 32'd0;
            end
        end
    end
    assign display_en = (frame_timer < CLK_PER_FRAME);
    assign ca_step = ca_done;
    assign actual_fps = fps_counter;
endmodule

性能与优化分析

双缓冲vs三缓冲

双缓冲:CA写Bank A,显示读Bank B。每帧交换。问题:如果CA写完一帧时显示还在读同一帧,需要等待。

三缓冲:CA写Bank C,等待显示读完Bank A后,Bank A变为新的写入目标。避免了等待,但多用了50%存储。

推荐:对于60Hz显示,双缓冲通常够用(CA计算远快于显示刷新)。对于高分辨率+慢CA,用三缓冲。

📖 扩展阅读

元胞自动机课程 · 从Conway到Langton到Lattice Gas