diff --git a/hdl/peri/hazard3_riscv_timer.v b/hdl/peri/hazard3_riscv_timer.v
index 99df9dd..5856fbf 100644
--- a/hdl/peri/hazard3_riscv_timer.v
+++ b/hdl/peri/hazard3_riscv_timer.v
@@ -39,8 +39,7 @@ module hazard3_riscv_timer (
 	output reg                timer_irq
 );
 
-wire bus_write = pwrite && psel && penable;
-wire bus_read = !pwrite && psel && penable;
+wire bus_write = pwrite && psel && penable && pready;
 
 localparam W_ADDR = 8;
 localparam W_DATA = 32;
@@ -74,46 +73,85 @@ always @ (posedge clk or negedge rst_n)
 	else if (bus_write && paddr == ADDR_CTRL)
 		ctrl_en <= pwdata[0];
 
-wire tick = ctrl_en && !dbg_halt && tick_nrz_prev != tick_nrz_sync;
+wire tick = tick_nrz_prev != tick_nrz_sync;
+wire tick_and_increment = ctrl_en && !dbg_halt && tick;
+
+// The 64-bit TIME and TIMECMP registers are processed serially, over the
+// course of 64 cycles.
+
+reg [5:0] serial_ctr;
+
+always @ (posedge clk or negedge rst_n) begin
+	if (!rst_n) begin
+		serial_ctr <= 6'h00;
+	end else if (tick) begin
+		serial_ctr <= serial_ctr + 1'b1;
+	end
+end
 
 reg [63:0] mtime;
+reg        mtime_carry;
 
 always @ (posedge clk or negedge rst_n) begin
 	if (!rst_n) begin
 		mtime <= 64'h0;
+		mtime_carry <= 1'b1;
 	end else begin
-		if (tick)
-			mtime <= mtime + 1'b1;
-		if (bus_write && paddr == ADDR_MTIME)
+		if (tick) begin
+			if (tick_and_increment) begin
+				// Serially increment mtime
+				{mtime_carry, mtime[63]} <= mtime_carry + mtime[0];
+				mtime[62:0] <= mtime[63:1];
+			end else begin
+				// Still keep rotating the register, so writes can take place,
+				// and so we can continuously compare with mtimecmp.
+				mtime <= {mtime[0], mtime[63:1]};
+			end
+			// Preload carry for increment
+			if (serial_ctr == 6'h3f)
+				mtime_carry <= 1'b1;
+		end
+		// Only the lower half is written; pready is driven so that the write
+		// occurs at the correct time and hence bit-alignment.
+		if (bus_write && (paddr == ADDR_MTIME || paddr == ADDR_MTIMEH))
 			mtime[31:0] <= pwdata;
-		if (bus_write && paddr == ADDR_MTIMEH)
-			mtime[63:32] <= pwdata;
 	end
 end
 
 reg [63:0] mtimecmp;
+reg        mtimecmp_borrow;
+
+wire mtimecmp_borrow_next = (!mtimecmp[0] && (mtime[0] || mtimecmp_borrow)) || (mtime[0] && mtimecmp_borrow);
 
 always @ (posedge clk or negedge rst_n) begin
 	if (!rst_n) begin
 		mtimecmp <= 64'h0;
+		mtimecmp_borrow <= 1'b0;
 		timer_irq <= 1'b0;
 	end else begin
-		if (bus_write && paddr == ADDR_MTIMECMP)
+		// Serially subtract mtime from mtimecmp. If there is no borrow from
+		// bit 63 (i.e. if mtimecmp was greater or equal) then assert IRQ.
+		if (tick) begin
+			mtimecmp_borrow <= mtimecmp_borrow_next;
+			mtimecmp <= {mtimecmp[0], mtimecmp[63:1]};
+			if (serial_ctr == 6'h3f) begin
+				mtimecmp_borrow <= 1'b0;
+				timer_irq <= !mtimecmp_borrow_next;
+			end
+		end
+		if (bus_write && (paddr == ADDR_MTIMECMP || paddr == ADDR_MTIMECMPH))
 			mtimecmp[31:0] <= pwdata;
-		if (bus_write && paddr == ADDR_MTIMECMPH)
-			mtimecmp[63:32] <= pwdata;
-		timer_irq <= mtime >= mtimecmp; // oof
 	end
 end
 
 always @ (*) begin
 	case (paddr)
-	ADDR_CTRL:      prdata = {{W_DATA-1{1'b0}}, ctrl_en};
-	ADDR_MTIME:     prdata = mtime[31:0];
-	ADDR_MTIMEH:    prdata = mtime[63:32];
-	ADDR_MTIMECMP:  prdata = mtimecmp[31:0];
-	ADDR_MTIMECMPH: prdata = mtimecmp[63:32];
-	default:        prdata = {W_DATA{1'b0}};
+	ADDR_CTRL:      begin  prdata = {{W_DATA-1{1'b0}}, ctrl_en}; pready = 1'b1;                end
+	ADDR_MTIME:     begin  prdata = mtime[31:0];                 pready = serial_ctr == 6'h00; end
+	ADDR_MTIMEH:    begin  prdata = mtime[31:0];                 pready = serial_ctr == 6'h20; end
+	ADDR_MTIMECMP:  begin  prdata = mtimecmp[31:0];              pready = serial_ctr == 6'h00; end
+	ADDR_MTIMECMPH: begin  prdata = mtimecmp[63:32];             pready = serial_ctr == 6'h20; end
+	default:        begin  prdata = {W_DATA{1'b0}};              pready = 1'b1;                end
 	endcase
 end