Skip to content

Program Counter

Diogo Valadares Reis dos Santos edited this page Aug 18, 2025 · 4 revisions

[← Previous Page | Next Page →]

The Program Counter

The Program Counter (PC) is the component responsible for holding the memory address of the current and next instruction. It also handles jumps, load/store addresses, and system-level overrides.

PC Component in Logisim

The PC receives all of its regular inputs from the left and outputs its values on the right. The upper part contains the CSR interface, which provides overrides for certain control signals and supplies the CSR Controller with important information about the PC.

The Outputs

The PC contains four outputs of general use. The first one is the Pad Address, which connects directly to the IO Devices through the Address Bus. This output can contain values for different functions depending on the context. During the instruction fetch process, it outputs the address for the next instruction. During load and store instructions, it forwards an address calculated internally from its inputs. In case of a system load, an address coming from the CSR Controller is used instead.

Then there are two outputs, one for each register of the PC. The "next" one is used for storing the address after a JAL or JALR instruction, while the "current" one is used for the AUIPC instruction or for some operations of the CSR Controller.

Finally, there is the byte offset, which indicates the starting byte of the data in load operations. This is a remnant from the RISC I architecture, where the load instruction collects the whole word from memory, even if the instruction only requires a short or byte value. If the byte is at the start of the word, then the byte offset is 0; otherwise, it can be offset by up to 3 bytes for byte loads, and up to 2 bytes for short loads. This behavior will be further explained in the Input Buffer Chapter.

Inside the Program Counter

Internal PC Circuit

timescale 1s/1s
module program_counter(
    input reset,
    input clock,
    input write,
    input jump,
    input pc_relative,
    input use_offset,
    input forward_address,
    input [31:0] immediate,
    input [31:0] address_in,
    input system_jump,
    input system_load,
    input [31:0] system_address_target,
    output reg [1:0] data_offset,
    output [31:0] calculated_address,
    output [31:0] next,
    output [31:0] current,
    output [31:0] address_bus
    );

    reg [29:0] next_reg;
    reg [29:0] current_reg;

    assign calculated_address = pc_relative ? current + immediate : address_in + immediate;
    assign next = {next_reg, 2'b00};
    assign current = {current_reg, 2'b00};
    assign address_bus = forward_address ?
                        (system_load ? system_address_target :
                         use_offset ? calculated_address : {calculated_address[31:2], 2'b00}) :
                        {next_reg, 2'b00};

    always@(posedge clock) begin
        if(reset) begin
            current_reg <= 30'b0;
            next_reg <= 30'b0;
            data_offset <= 2'b0;
        end
        else if(write) begin
            if(system_jump) next_reg <= system_address_target[31:2];
            else if(jump) next_reg <= calculated_address[31:2];
            else next_reg <= next_reg + 1;

            current_reg <= next_reg;
        end

        if(forward_address) begin
            data_offset <= calculated_address[1:0];
        end
    end

endmodule```

Clone this wiki locally