CLINT (Core Local Interruptor)
Author: Christian Zellinger, Alexander Beck 2025
The CLINT (Core Local Interruptor) module provides timer and software interrupt functionality for the PicoNut processor, following the RISC-V specification. It is responsible for generating machine timer interrupts (MTIP) and machine software interrupts (MSIP) for the processor core.
There are two implementations of the CLINT module in the PicoNut project:
The Wishbone CLINT: A hardware module connected to the Wishbone bus, intended for use on FPGAs.
The clint_soft: A software model for simulation, implementing the same register interface for use in the simulator.
CLINT Registers
The CLINT module exposes the following memory-mapped registers (all addresses are offsets from the CLINT base address, typically 0x2000000):
Register |
Offset |
Width |
Description |
|---|---|---|---|
MSIP |
0x0000 |
32 |
Machine Software Interrupt Pending |
MTIMECMP |
0x4000 |
64 |
Machine Timer Compare |
MTIME |
0xBFF8 |
64 |
Machine Timer |
MSIP: Writing a nonzero value to bit 0 sets the software interrupt pending flag.
MTIMECMP: When
MTIMEis greater than or equal toMTIMECMP, the timer interrupt is triggered. Important: When writing to the 64-bit MTIMECMP register using 32-bit accesses, always write the high 32 bits first, then the low 32 bits to avoid race conditions per RISC-V specification.MTIME: Continuously incrementing timer, typically incremented every clock cycle.
Wishbone CLINT
-
SC_MODULE(m_clint)
The Wishbone CLINT (Core Local Interruptor) module provides timer and software interrupt functionality for the PicoNut processor, following the RISC-V specification.
- Author
[Alexander Beck, Christian Zellinger]
It implements the following registers:
MSIP(0x0000): Machine Software Interrupt Pending (32-bit)MTIMECMP(0x4000): Machine Timer Compare (64-bit, split into two 32-bit words)MTIME(0xBFF8): Machine Timer (64-bit, split into two 32-bit words)
The module increments the
MTIMEregister on every clock cycle (unless a write is in progress) and asserts themtip_ooutput whenMTIME >= MTIMECMP. Themsip_ooutput is asserted when bit 0 of theMSIPregister is set.Register accesses are handled via the Wishbone bus, supporting both word and byte accesses. The module uses a state machine to ensure correct Wishbone protocol handling and safe multi-cycle writes.
- Ports:
- Parameters:
clk – [in] Clock input
reset – [in] Reset input
wb_stb_i – [in] Wishbone strobe
wb_cyc_i – [in] Wishbone cycle
wb_we_i – [in] Wishbone write enable
wb_adr_i – [in] Wishbone address input
wb_dat_i – [in] Wishbone data input
wb_sel_i – [in] Wishbone byte select
wb_dat_o – [out] Wishbone data output
wb_ack_o – [out] Wishbone acknowledge
wb_err_o – [out] Wishbone error
wb_rty_o – [out] Wishbone retry
msip_o – [out] Machine software interrupt output
mtip_o – [out] Machine timer interrupt output
The Wishbone CLINT module consists of the following components:
Wishbone Slave Interface: Handles Wishbone protocol transactions for register access.
Timer Logic: Increments the
MTIMEregister and compares it toMTIMECMPto generate timer interrupts.Interrupt Outputs: Generates the
msip_oandmtip_osignals for software and timer interrupts.
Register Access
Register access is handled via the Wishbone bus. The module supports both word and byte accesses, with proper masking and state machine handling for multi-cycle writes.
Interrupt Generation
Software Interrupt (MSIP): Set by writing to the MSIP register. The
msip_ooutput is asserted when bit 0 of MSIP is set.Timer Interrupt (MTIP): Asserted when
MTIME>=MTIMECMP. Themtip_ooutput reflects this condition.
Clint Soft
The clint_soft module implements the CLINT functionality for simulation, using the c_soft_peripheral interface.
- group clint_soft
soft peripheral implementation of the RISC-V Core Local Interruptor (CLINT) for simulation
This module implements the RISC-V Core Local Interruptor (CLINT). It is responsible for generating timer and software interrupts. The module is implemented as a soft peripheral and can be used in the simulation environment. The module has the same registers as the RISC-V CLINT specification. They are:
MSIP: 32-bit register for machine software interrupt pendingMTIMECMP: 64-bit register for machine timer compare (split into low/high 32-bit registers)MTIME: 64-bit register for machine timer (split into low/high 32-bit registers)
- Author
[Alexander Beck, Christian Zellinger]
The CLINT provides memory-mapped registers:
MTIMECMP (Machine Timer Compare): Used to trigger timer interrupts when mtime >= mtimecmp
MTIME (Machine Timer): Increments continuously with the system clock
MSIP (Machine Software Interrupt Pending): Used to trigger software interrupts
The module has a 32-bit memory interface and can be accessed by the soft peripheral interface.
Note: The module is not synthesizable and is only used for simulation purposes.
CSoftPeripheral Interface
The following methods are implemented from the c_soft_peripheral interface to enable the CLINT module to be connected to the simulation environment:
-
m_clint_soft::m_clint_soft(std::function<void(bool)> callback_signal_sw_interrupt = nullptr, std::function<void(bool)> callback_signal_timer_interrupt = nullptr)
Constructor.
- Parameters:
callback_signal_sw_interrupt – Callback function to trigger software interrupt
callback_signal_timer_interrupt – Callback function to trigger timer interrupt
-
const char *m_clint_soft::get_info() override
Get the peripheral info.
- Returns:
The peripheral info.
-
uint8_t m_clint_soft::read8(uint64_t adr) override
Read a single byte (8-bit) value from a register.
- Parameters:
adr – The address of the byte to read.
- Returns:
The 8-bit value read from the register.
-
void m_clint_soft::write8(uint64_t adr, uint8_t data) override
Write a single byte (8-bit) value to a register.
- Parameters:
adr – The address of the byte to write.
data – The 8-bit value to write to the register.
-
void m_clint_soft::write32(uint64_t adr, uint32_t data) override
Write a 32-bit value to a register.
- Parameters:
adr – The address of the register to write.
data – The 32-bit value to write to the register.
-
uint32_t m_clint_soft::read32(uint64_t adr) override
Read a 32-bit value from a register.
- Parameters:
adr – The address of the register to read.
- Returns:
The 32-bit value read from the register.
-
bool m_clint_soft::is_addressed(uint64_t adr) override
Check if the address is within this peripheral’s range.
- Parameters:
adr – The address to check.
- Returns:
True if the address is within this peripheral’s range.
CLINT Emulation
The following methods are implemented to emulate the CLINT’s functionality, including timer increment, interrupt generation, and register access:
-
void m_clint_soft::on_rising_edge_clock() override
Updates the timer and checks for interrupts This function is automatically called on every rising edge of the clock.
-
void m_clint_soft::update_timer_interrupt()
Update the timer interrupt based on mtime and mtimecmp values.
-
void m_clint_soft::register_msip_callback(std::function<void(bool)> callback)
Register a callback function for software interrupt changes.
- Parameters:
callback – The callback function
-
void m_clint_soft::register_mtip_callback(std::function<void(bool)> callback)
Register a callback function for timer interrupt changes.
- Parameters:
callback – The callback function
Usage in Reference Design (clint_soft)
The following example shows how to integrate the clint_soft in an reference design. Also see refdesign_clint_soft.
1. Instantiation and Configuration
#define CLINT_BASE_ADDR 0x2000000UL
// Declare signals for interrupt handling
sc_signal<bool> mtip_signal; // Timer interrupt signal
sc_signal<bool> msip_signal; // Software interrupt signal
sc_signal<bool> meip_signal; // External interrupt signal
// Create CLINT peripheral instance
std::unique_ptr<clint_soft> clint = std::make_unique<clint_soft>();
clint_soft* clint_ptr = clint.get();
2. Register Interrupt Callbacks
// Register software interrupt callback
clint_ptr->register_msip_callback([&msip_signal](bool state) {
msip_signal.write(state);
std::cout << "Software interrupt state changed: " << state << std::endl;
});
// Register timer interrupt callback
clint_ptr->register_mtip_callback([&mtip_signal](bool state) {
mtip_signal.write(state);
});
// Register external interrupt callback
peripheral_ptr->register_meip_callback([&meip_signal](bool state) {
meip_signal.write(state);
});
3. Add Peripheral to System
// Add CLINT to peripheral container at standard RISC-V address
dut_inst.piconut->membrana->add_peripheral(PN_CFG_CLINT_BASE_ADDRESS, std::move(clint));
// Connect interrupt signals to PicoNut processor
dut_inst.piconut->mtip_in(mtip_signal);
dut_inst.piconut->msip_in(msip_signal);
dut_inst.piconut->meip_in(meip_signal);
4. Makefile Configuration
In the Makefile, the CLINT module must be added to the peripheral modules:
# Include clint_soft as peripheral module
PERIPHERAL_MODULES = c_soft_uart clint_soft
Key Steps for Integration:
Create Instance:
std::make_unique<clint_soft>()Register Callbacks: Setup interrupt callback functions for timer and software interrupts
Add to Peripheral Container:
membrana_soft->add_peripheral(PN_CFG_CLINT_BASE_ADDRESS, std::move(clint))Connect Interrupt Signals: Connect MTIP, MSIP, and MEIP signals to processor
Configure Makefile: Add
clint_softtoPERIPHERAL_MODULES
Usage in Hardware Reference Design (m_clint)
The following example shows how to integrate the m_clint in a hardware reference design. Also see demo_clint.
1. Module Declaration and Instantiation
SC_MODULE(m_top)
{
// Submodules
m_piconut *piconut;
m_clint *clint;
m_uart *uart;
// Internal interrupt signals
sc_signal<bool> PN_NAME(mtip_signal); // Timer interrupt signal
sc_signal<bool> PN_NAME(msip_signal); // Software interrupt signal
sc_signal<bool> PN_NAME(meip_signal); // External interrupt signal
};
2. Clint Instantiation and Wishbone Connections
// ----------- WB_CLINT -----------
clint = sc_new<m_clint>("clint");
// Clock and reset connections
clint->reset(reset);
clint->clk(clk);
// Wishbone bus connections
clint->wb_ack_o(wb_ack_clint);
clint->wb_dat_i(wb_dat_o); // Data from master (PicoNut)
clint->wb_dat_o(wb_dat_i_clint); // Data to master
clint->wb_we_i(wb_we); // Write enable from master
clint->wb_stb_i(wb_stb); // Strobe from master
clint->wb_cyc_i(wb_cyc); // Cycle from master
clint->wb_sel_i(wb_sel_o); // Byte select from master
clint->wb_rty_o(wb_rty_clint); // Retry output
clint->wb_err_o(wb_err_clint); // Error output
clint->wb_adr_i(wb_adr); // Address from master
// Interrupt signal connections
clint->msip_o(msip_signal); // Software interrupt output
clint->mtip_o(mtip_signal); // Timer interrupt output
wb_peripheral->meip_o(meip_signal); // External interrupt output
3. Processor Interrupt Connections
// Connect the interrupt signals to the PicoNut processor
piconut->mtip_in(mtip_signal);
piconut->msip_in(msip_signal);
piconut->meip_in(meip_signal);
4. Address Decoding and Bus Arbitration
void m_top::proc_comb_wb()
{
if(wb_adr.read() >= PN_CFG_CLINT_BASE_ADDRESS && wb_adr.read() < (PN_CFG_CLINT_BASE_ADDRESS + (CLINT_SIZE)))
{
// Route to CLINT when address is in CLINT range
wb_dat_i_pn = wb_dat_i_clint.read();
wb_ack_pn = wb_ack_clint.read();
}
else
{
// Route to other peripherals for different addresses
wb_dat_i_pn = wb_dat_i_uart.read();
wb_ack_pn = wb_ack_uart.read();
}
meip_signal = 0;
}
5. Makefile Configuration
In the Makefile, include the clint module:
# PERIPHERAL_MODULES: List of peripheral modules to include into the system
PERIPHERAL_MODULES = clint uart
Key Steps for Hardware Integration:
Instantiate Module: Create
m_clintinstance in top-level moduleConnect Wishbone Bus: Connect all Wishbone signals (address, data, control, acknowledge)
Connect Interrupts: Connect
msip_oandmtip_oto processor interrupt inputsConfigure Address Decoding: Implement bus arbitration logic for CLINT address range
Configure Makefile: Add
clinttoPERIPHERAL_MODULESSet Base Address: Define
PN_CFG_CLINT_BASE_ADDRESSin configuration (if not already done)
Usage Notes
The CLINT is essential for timer-based scheduling and inter-processor software interrupts in RISC-V systems.
The hardware and simulation models are designed to be register-compatible, allowing seamless switching between simulation and FPGA targets.