Specializing in RTL verification, protocol compliance, and coverage-driven validation. From silicon bring-up to UVM testbench architecture — building the bridge between design and silicon.
I am Shaik Khaja Mohiddeen, a passionate Design Verification Engineer with deep expertise in SystemVerilog, UVM methodology, and protocol-level verification. My focus is on building robust, reusable testbench architectures that ensure silicon correctness from RTL to tape-out.
I work across the full DV spectrum — from writing constrained-random sequences and functional coverage models to implementing SVA assertions and closing coverage goals on complex SoC designs involving multiple bus protocols.
My domain expertise spans AMBA protocols (APB, AHB, AXI), MIPI DSI, QSPI flash interfaces, GDMA controllers, GPIO, PLL, and processor boot sequences — all verified using industry-standard UVM methodology.
A complete 3-stage SoC boot flow: Primary Boot (ROM) → Secondary Bootloader (SBL) → Application. This is the industry-standard approach used in ARM-based SoCs.
// Memory Map #define QSPI_BASE 0x40000000 #define SRAM_BASE 0x20000000 #define DDR_BASE 0x80000000 #define FLASH_BOOT_ADDR 0x00000000 #define FLASH_APP_ADDR 0x00100000 #define SBL_SIZE 0x00010000 #define APP_SIZE 0x00020000 // Image Header Structure (Industry Standard) typedef struct { uint32_t magic; // 0xDEADBEEF uint32_t size; uint32_t load_addr; uint32_t entry_point; uint32_t crc; } image_header_t; // PRIMARY BOOT (Boot ROM) void primary_boot() { init_stack_pointer(); // MUST be first int mode = read_boot_mode(); if(mode != 1) error_handler(); pll_init(); qspi_init(); uint8_t *sbl_dst = (uint8_t *)SRAM_BASE; qspi_read(FLASH_BOOT_ADDR, sbl_dst, SBL_SIZE); if(!validate_crc(sbl_dst, SBL_SIZE)) error_handler(); void (*sbl_entry)() = (void (*)())SRAM_BASE; sbl_entry(); } // SECONDARY BOOT (SBL) void secondary_boot() { ddr_init(); // PHY training + calibration uart_init(); gdma_init(); image_header_t hdr; qspi_read(FLASH_APP_ADDR, &hdr, sizeof(hdr)); if(hdr.magic != 0xDEADBEEF) error_handler(); uint8_t *app_src = (uint8_t *)(QSPI_BASE + FLASH_APP_ADDR); uint8_t *app_dst = (uint8_t *)(hdr.load_addr); gdma_config(app_src, app_dst, hdr.size); gdma_start(); wait_for_completion(); if(!validate_crc(app_dst, hdr.size)) error_handler(); cache_invalidate_all(); void (*app_entry)() = (void (*)())(hdr.entry_point); app_entry(); } // APPLICATION (Final Stage) void application_main() { while(1) { /* Main program */ } } int main() { primary_boot(); // Reset vector entry point return 0; }
interface qspi_if(input bit clk); logic cs_n; logic sclk; logic [3:0] io; modport drv (output cs_n, sclk, io); modport mon (input cs_n, sclk, io); endinterface class qspi_seq_item extends uvm_sequence_item; `uvm_object_utils(qspi_seq_item) rand bit [7:0] cmd; rand bit [23:0] addr; rand bit [7:0] data[]; rand int dummy_cycles; constraint c_cmd { cmd inside {8'h06, 8'h02, 8'h32, 8'h0B, 8'hEB, 8'hD8}; } constraint c_dummy { dummy_cycles inside {[2:10]}; } constraint c_data_len { data.size() inside {[1:256]}; } endclass class qspi_scoreboard extends uvm_scoreboard; `uvm_component_utils(qspi_scoreboard) uvm_analysis_imp #(qspi_seq_item, qspi_scoreboard) ap; bit [7:0] flash_model [bit [23:0]]; int pass_cnt, fail_cnt; function void write(qspi_seq_item item); if(item.cmd == 8'h02 || item.cmd == 8'h32) begin foreach(item.data[i]) flash_model[item.addr + i] = item.data[i]; end else if(item.cmd == 8'h0B || item.cmd == 8'hEB) begin foreach(item.data[i]) begin if(item.data[i] === flash_model[item.addr + i]) pass_cnt++; else begin fail_cnt++; `uvm_error("SB", $sformatf("MISMATCH")) end end end endfunction endclass
A Phase Locked Loop (PLL) is a control system used to generate a stable high-frequency clock from a reference clock. It is widely used in SoC clock generation, CPUs, GPUs, DDR controllers, and communication systems.
Phase Detector (PD) → Charge Pump → Loop Filter → VCO → Feedback Divider → back to PD
#define PLL_CTRL (*(volatile uint32_t*)0x4000A000) #define PLL_DIV (*(volatile uint32_t*)0x4000A004) #define PLL_STATUS (*(volatile uint32_t*)0x4000A008) #define PLL_BYPASS (*(volatile uint32_t*)0x4000A00C) // TC1: PLL Enable void test_pll_enable() { PLL_CTRL |= (1 << 0); if(PLL_CTRL & (1 << 0)) printf("[PASS] PLL Enable\n"); else printf("[FAIL] PLL Enable\n"); } // TC2: PLL Lock Detection void test_pll_lock() { int timeout = 10000; while(!(PLL_STATUS & (1 << 0)) && timeout--); if(timeout > 0) printf("[PASS] PLL LOCK achieved\n"); else printf("[FAIL] PLL LOCK timeout\n"); } // TC3: Divider Configuration (M=100, N=2, P=1 → Fout=1200MHz) void test_pll_divider() { PLL_DIV = (100 << 16) | (2 << 8) | (1 << 0); if(PLL_DIV == ((100 << 16) | (2 << 8) | 1)) printf("[PASS] Divider Config: Fout=1200MHz\n"); else printf("[FAIL] Divider Config\n"); } // TC4: Bypass Mode (Fout = Fref = 24MHz) void test_pll_bypass() { PLL_BYPASS = 1; if(PLL_BYPASS == 1) printf("[PASS] PLL Bypass: Fout=Fref=24MHz\n"); else printf("[FAIL] PLL Bypass\n"); } // TC5: Disable void test_pll_disable() { PLL_CTRL &= ~(1 << 0); if(!(PLL_CTRL & (1 << 0))) printf("[PASS] PLL Disabled\n"); else printf("[FAIL] PLL Disable\n"); } // TC6: Lock Time Measurement void test_pll_lock_time() { uint32_t start = get_timer_us(); PLL_CTRL |= (1 << 0); while(!(PLL_STATUS & (1 << 0))); uint32_t lock_time = get_timer_us() - start; printf("PLL Lock Time: %u us\n", lock_time); if(lock_time < 500) printf("[PASS] Lock time within spec\n"); else printf("[FAIL] Lock time exceeded\n"); } // TC7: Stress Test — 100 enable/disable cycles void test_pll_stress() { for(int i = 0; i < 100; i++) { PLL_CTRL |= (1 << 0); while(!(PLL_STATUS & (1 << 0))); PLL_CTRL &= ~(1 << 0); } printf("[PASS] Stress: 100 enable/disable cycles\n"); }
GDMA (Generic Direct Memory Access) is a hardware engine that transfers data between memory and peripherals without CPU intervention. It reduces processor bottleneck and enables high-speed data movement in SoC designs.
typedef enum logic [1:0] { DMA_M2M=2'b00, DMA_M2P=2'b01, DMA_P2M=2'b10 } dma_dir_e; class dma_seq_item extends uvm_sequence_item; `uvm_object_utils_begin(dma_seq_item) `uvm_field_int(src_addr, UVM_ALL_ON) `uvm_field_int(dst_addr, UVM_ALL_ON) `uvm_field_int(length, UVM_ALL_ON) `uvm_field_int(burst_len, UVM_ALL_ON) `uvm_field_int(channel_id, UVM_ALL_ON) `uvm_object_utils_end rand logic [31:0] src_addr; rand logic [31:0] dst_addr; rand logic [15:0] length; rand dma_dir_e direction; rand logic [3:0] burst_len; rand int unsigned channel_id; rand logic irq_en; constraint c_align { src_addr[1:0] == 2'b00; dst_addr[1:0] == 2'b00; } constraint c_burst { burst_len inside {1, 4, 8, 16}; } constraint c_channel { channel_id inside {[0:7]}; } constraint c_no_overlap { (src_addr + (length << 2)) <= dst_addr || (dst_addr + (length << 2)) <= src_addr; } constraint c_sram { src_addr inside {[32'h2000_0000:32'h200F_FFFF]}; dst_addr inside {[32'h2010_0000:32'h201F_FFFF]}; } endclass
APB (Advanced Peripheral Bus) is an AMBA protocol for low-bandwidth peripheral access. 2-cycle non-pipelined transfer: SETUP phase + ACCESS phase.
class apb_seq_item extends uvm_sequence_item; `uvm_object_utils_begin(apb_seq_item) `uvm_field_int(addr, UVM_ALL_ON) `uvm_field_int(data, UVM_ALL_ON) `uvm_field_int(wr, UVM_ALL_ON) `uvm_field_int(delay, UVM_ALL_ON) `uvm_object_utils_end rand logic [31:0] addr; rand logic [31:0] data; rand logic wr; rand int unsigned delay; constraint c_align { addr[1:0] == 2'b00; } constraint c_delay { delay inside {[0:5]}; } endclass class apb_driver extends uvm_driver #(apb_seq_item); `uvm_component_utils(apb_driver) virtual apb_if vif; task drive_item(apb_seq_item item); @(posedge vif.pclk); vif.paddr <= item.addr; vif.pwrite <= item.wr; vif.pwdata <= item.wr ? item.data : 'x; vif.psel <= 1; vif.penable <= 0; @(posedge vif.pclk); vif.penable <= 1; do @(posedge vif.pclk); while(!vif.pready); vif.psel <= 0; vif.penable <= 0; endtask endclass // SVA Assertions property apb_setup_phase; @(posedge PCLK) disable iff(!PRESETn) $rose(PSEL) |-> !PENABLE; endproperty a_setup: assert property(apb_setup_phase) else $error("APB: PENABLE high on PSEL assertion"); property apb_addr_stable; @(posedge PCLK) disable iff(!PRESETn) (PSEL && PENABLE) |-> $stable(PADDR) && $stable(PWRITE); endproperty a_stable: assert property(apb_addr_stable);
`define GPIO_DIR 8'h00 `define GPIO_DATA_OUT 8'h04 `define GPIO_INT_EN 8'h0C `define GPIO_INT_TYPE 8'h10 `define GPIO_INT_STATUS 8'h14 class gpio_rise_int_seq extends uvm_sequence #(gpio_seq_item); `uvm_object_utils(gpio_rise_int_seq) rand int pin_num; constraint c_pin { pin_num inside {[0:31]}; } task body(); gpio_seq_item item; // 1. Set direction as INPUT item = gpio_seq_item::type_id::create("dir"); start_item(item); void'(item.randomize() with { is_write==1; reg_addr==`GPIO_DIR; reg_data[pin_num]==0; }); finish_item(item); // 2. Set rising edge interrupt type item = gpio_seq_item::type_id::create("type"); start_item(item); void'(item.randomize() with { is_write==1; reg_addr==`GPIO_INT_TYPE; }); finish_item(item); // 3. Enable interrupt on pin item = gpio_seq_item::type_id::create("en"); start_item(item); void'(item.randomize() with { is_write==1; reg_addr==`GPIO_INT_EN; reg_data[pin_num]==1; }); finish_item(item); // 4. Clear INT_STATUS (W1C) item = gpio_seq_item::type_id::create("clr"); start_item(item); void'(item.randomize() with { is_write==1; reg_addr==`GPIO_INT_STATUS; reg_data[pin_num]==1; }); finish_item(item); endtask endclass
A uvm_barrier blocks multiple processes until a specified count of participants reach a common synchronization point, after which all are released together. Think of it as a checkpoint gate — nobody passes until everyone arrives.
A uvm_event is a one-to-many communication mechanism where one process triggers an event and any number of processes waiting on it are released simultaneously. Like ringing a bell — one rings, many hear.
wait_ptrigger() instead of wait_trigger() when the event may fire before the waiter arrives — avoids missed-trigger bugs that are very hard to debug.b = new("barrier", 4) but only 3 processes call wait_for(), simulation hangs forever. Always match count to actual process count.`include "uvm_macros.svh" import uvm_pkg::*; // 3 parallel processes synchronize at a barrier class barrier_test extends uvm_test; `uvm_component_utils(barrier_test) uvm_barrier b; function void build_phase(uvm_phase phase); super.build_phase(phase); b = new("barrier", 3); // ⚠ Must match process count! endfunction task run_phase(uvm_phase phase); phase.raise_objection(this); fork process1(); // reaches at time 10 process2(); // reaches at time 20 process3(); // reaches at time 30 — slowest join // ALL cross together at time 30 phase.drop_objection(this); endtask task process1(); `uvm_info("P1", "Started", UVM_LOW) #10; `uvm_info("P1", "Reached barrier", UVM_LOW) b.wait_for(); `uvm_info("P1", "Crossed barrier", UVM_LOW) endtask task process2(); `uvm_info("P2", "Started", UVM_LOW) #20; `uvm_info("P2", "Reached barrier", UVM_LOW) b.wait_for(); `uvm_info("P2", "Crossed barrier", UVM_LOW) endtask task process3(); `uvm_info("P3", "Started", UVM_LOW) #30; // Slowest — all other processes wait for this `uvm_info("P3", "Reached barrier", UVM_LOW) b.wait_for(); `uvm_info("P3", "Crossed barrier", UVM_LOW) endtask endclass module top; initial run_test("barrier_test"); endmodule
// uvm_event: One producer triggers, many consumers react class event_test extends uvm_test; `uvm_component_utils(event_test) uvm_event ev; function void build_phase(uvm_phase phase); super.build_phase(phase); ev = new("ev"); endfunction task run_phase(uvm_phase phase); phase.raise_objection(this); fork producer(); consumer1(); consumer2(); join phase.drop_objection(this); endtask task producer(); #20; `uvm_info("PROD", "Triggering event", UVM_LOW) ev.trigger(); // Single trigger unblocks ALL waiters endtask task consumer1(); `uvm_info("CONS1", "Waiting...", UVM_LOW) ev.wait_trigger(); `uvm_info("CONS1", "Event received!", UVM_LOW) endtask task consumer2(); `uvm_info("CONS2", "Waiting...", UVM_LOW) ev.wait_ptrigger(); // Safe: works even if already triggered `uvm_info("CONS2", "Event received!", UVM_LOW) endtask endclass module top; initial run_test("event_test"); endmodule // Key uvm_event API: // ev.trigger() → fires the event // ev.wait_trigger() → blocks until triggered (may miss if early) // ev.wait_ptrigger() → safe: works even if triggered before wait // ev.is_on() → returns 1 if event has been triggered // ev.reset() → clears event state for reuse
A clock glitch is an unexpected short pulse, extra edge, or duty-cycle distortion in a clock signal. Glitches cause flip-flop metastability, incorrect data capture, and hard-to-debug functional failures.
$display("CLK transition at %0t", $time) and inspect waveform in DVE/Verdi.`define MIN_PULSE_WIDTH 4 // ns — adjust to your clock spec `define CLK_PERIOD 10 // ns — expected clock period `define JITTER_DELTA 1 // ns — allowed jitter tolerance `define MIN_HIGH_TIME 4 // ns — minimum HIGH duration // ─── TC1: Minimum Pulse Width Check ────────────── time prev_edge; always @(posedge clk or negedge clk) begin if (prev_edge != 0) begin assert (($time - prev_edge) >= `MIN_PULSE_WIDTH) else $error("GLITCH: Pulse width %0t ns at %0t", ($time - prev_edge), $time); end prev_edge = $time; end // ─── TC2: Clock Period Stability Check ─────────── time last_posedge; always @(posedge clk) begin if (last_posedge != 0) begin assert (($time - last_posedge) inside {[(`CLK_PERIOD-`JITTER_DELTA): (`CLK_PERIOD+`JITTER_DELTA)]}) else $error("PERIOD GLITCH: %0t ns at %0t", ($time - last_posedge), $time); end last_posedge = $time; end // ─── TC3: No Extra Edge Detection ──────────────── bit expected_clk; always @(posedge ref_clk) expected_clk <= ~expected_clk; always @(clk) begin assert (clk === expected_clk) else $error("EXTRA EDGE: Unexpected transition at %0t", $time); end // ─── TC4: Duty Cycle Check (50% expected) ──────── time rise_time, fall_time; always @(posedge clk) rise_time = $time; always @(negedge clk) begin fall_time = $time; assert ((fall_time - rise_time) >= `MIN_HIGH_TIME) else $error("DUTY CYCLE: HIGH=%0t ns at %0t", (fall_time - rise_time), $time); end // ─── TC5: Clock Gating Glitch (SVA Property) ───── property no_glitch_on_gating; @(posedge clk) disable iff (!reset) $stable(gated_clk) || $rose(gated_clk) || $fell(gated_clk); endproperty a_no_gating_glitch: assert property(no_glitch_on_gating) else $error("GATING GLITCH detected on gated_clk"); // ─── TC6: Random Stress Test ───────────────────── task run_clk_stress_test(); repeat(1000) begin clk_enable = $urandom_range(0, 1); @(posedge clk); end $display("[PASS] Stress: 1000 random enable cycles"); endtask
In AMBA AHB, when a slave needs a long time to prepare data (slow memory, busy peripheral), it must release the bus so other masters can use it. This is the purpose of SPLIT — true bus sharing for long-latency transactions.
HSPLIT is a multi-bit signal (one bit per master) sent from the slave to the arbiter. When bit N is asserted, it means "Master N is now permitted to retry its transaction." Without HSPLIT assertion, the arbiter will NOT re-grant the bus to that master.
// AHB Response Encoding (HRESP[1:0]) typedef enum logic [1:0] { OKAY = 2'b00, ERROR = 2'b01, RETRY = 2'b10, SPLIT = 2'b11 } hresp_e; // AHB Interface (relevant signals) interface ahb_if(input bit HCLK); logic [31:0] HADDR; logic [1:0] HTRANS; // IDLE/BUSY/NONSEQ/SEQ logic HWRITE; logic [31:0] HWDATA, HRDATA; logic [1:0] HRESP; // OKAY/ERROR/RETRY/SPLIT logic HREADY; logic [15:0] HSPLIT; // Bit per master — re-grant signal endinterface // SVA: Both SPLIT and RETRY are 2-cycle responses // Cycle 1: HRESP asserted, HREADY=0 // Cycle 2: HRESP still asserted, HREADY=1 property ahb_split_two_cycle; @(posedge HCLK) disable iff(!HRESETn) (HRESP == SPLIT && !HREADY) |=> (HRESP == SPLIT && HREADY); endproperty a_split_two_cycle: assert property(ahb_split_two_cycle) else $error("AHB: SPLIT must be a 2-cycle response"); property ahb_retry_two_cycle; @(posedge HCLK) disable iff(!HRESETn) (HRESP == RETRY && !HREADY) |=> (HRESP == RETRY && HREADY); endproperty a_retry_two_cycle: assert property(ahb_retry_two_cycle) else $error("AHB: RETRY must be a 2-cycle response"); // SVA: After SPLIT, master must NOT get bus until HSPLIT asserted property ahb_hsplit_required(master_id); @(posedge HCLK) disable iff(!HRESETn) $rose(HGRANT[master_id]) |-> $past(HSPLIT[master_id], 1); endproperty generate for(genvar i=0; i<16; i++) begin : g_hsplit a_hsplit: assert property(ahb_hsplit_required(i)) else $error("AHB: Bus granted to M%0d before HSPLIT", i); end endgenerate
If functional coverage is 100% and code coverage is 95%, you do not blindly proceed to sign-off. The critical question is: what does that missing 5% represent? Not all uncovered code is equally important.
The exclude mechanism intentionally removes certain RTL code from the coverage calculation. When RTL is confirmed unreachable by the design team, excluding it prevents falsely low coverage numbers — but exclusions require proper justification and documentation.
if (addr > MAX_ADDR) — constraints ensure this is never reached// ─── Coverage Exclusion Pragmas (Tool-Specific) ── // VCS / Questa / Xcelium style pragma: // pragma coverage off if (state == 4'hF) begin // Unreachable — only states 0,1,2 are valid // RTL designer confirmed: never reachable // Waiver ID: WAV-PLL-001 error_out <= 1; end // pragma coverage on // ─── Functional Coverage Model — QSPI Example ──── covergroup qspi_cg @(posedge clk); // All QSPI commands cp_cmd: coverpoint cmd { bins write_en = {8'h06}; // Write Enable bins page_prog = {8'h02}; // Page Program bins quad_prog = {8'h32}; // Quad Page Program bins fast_read = {8'h0B}; // Fast Read bins quad_read = {8'hEB}; // Quad I/O Read bins sect_erase = {8'hD8}; // Sector Erase // cmd[7:6]=2'b10 → EXCLUDED (reserved, unreachable) illegal_bins reserved = {[8'h80:8'hBF]}; } // Address coverage cp_addr: coverpoint addr { bins low = {[0:24'h0FFFFF]}; bins mid = {[24'h100000:24'hEFFFFF]}; bins high = {[24'hF00000:24'hFFFFFF]}; } // Dummy cycles cp_dummy: coverpoint dummy_cycles { bins min_d = {2}; bins mid_d = {[3:9]}; bins max_d = {10}; } // Cross coverage: command × address region cx_cmd_addr: cross cp_cmd, cp_addr; endgroup // ─── Coverage Waiver Log (Documentation) ───────── // Item | Coverage | Action | Waiver ID // ─────────────────────┼──────────┼─────────┼────────── // FSM default state | 0% | EXCLUDE | WAV-001 // cmd[7:6]=2b10 | 0% | EXCLUDE | WAV-002 // Error inject path | 0% | ADD TEST| N/A // Interrupt handler | 0% | ADD TEST| N/A // Debug SFR access | 0% | EXCLUDE | WAV-003