diff --git a/examples/i2c_oled/.gdbinit b/examples/i2c_oled/.gdbinit
new file mode 100644
index 0000000000000000000000000000000000000000..193753f4f7e9b58eca31fc50ae6edb2bc868e39e
--- /dev/null
+++ b/examples/i2c_oled/.gdbinit
@@ -0,0 +1,2 @@
+file i2c_oled.elf
+target extended-remote localhost:3333
diff --git a/examples/i2c_oled/README.md b/examples/i2c_oled/README.md
index 625a07552f3d2e21353334f551caa36d46d824e9..3015423ddfa4536364173b310dfeddcc9386ab28 100644
--- a/examples/i2c_oled/README.md
+++ b/examples/i2c_oled/README.md
@@ -9,6 +9,23 @@ various drawing primitives.
 
 https://user-images.githubusercontent.com/1132011/230734071-dee305de-5aad-4ca0-a422-5fb31d2bb0e0.mp4
 
+## Build options
+There are a few build-time options in the i2c.h source:
+* I2C_CLKRATE - defines the I2C bus clock rate. Both 100kHz and 400kHz are supported.
+800kHz has been seen to work when I2C_PRERATE is 1000000, but 1MHz did not. To
+use higher bus rates you must increase I2C_PRERATE at the expense of higher power
+consumption.
+* I2C_PRERATE - defines the I2C logic clock rate. Must be higher than I2C_CLKRATE.
+Keep this value as low as possible (but not lower than 1000000) to ensure low power
+operaton.
+* I2C_DUTY - for I2C_CLKRATE > 100kHz this specifies the duty cycle, either 33% or 36%.
+* TIMEOUT_MAX - the amount of tries in busy-wait loops before giving up. This value
+depends on the I2C_CLKRATE and should not affect normal operation.
+* I2C_IRQ - chooses IRQ-based operation instead of busy-wait polling. Useful to
+free up CPU resources but should be used carefully since it has more potential
+mysterious effects and less error checking.
+* IRQ_DIAG - enables timing analysis via GPIO toggling. Don't enable this unless
+you know what you're doing.
 
 ## Use
 Connect an SSD1306-based OLED in I2C interface mode to pins PC1 (SCL) and PC2 (SDA)
diff --git a/examples/i2c_oled/debug.sh b/examples/i2c_oled/debug.sh
new file mode 100755
index 0000000000000000000000000000000000000000..bb05a9465959cb153fad95bba4b2175667830528
--- /dev/null
+++ b/examples/i2c_oled/debug.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+# before running this you should start OOCD server
+#../../../MRS_Toolchain_Linux_x64_V1.70/OpenOCD/bin/openocd -f ../../../MRS_Toolchain_Linux_x64_V1.70/OpenOCD/bin/wch-riscv.cfg
+ 
+../../../MRS_Toolchain_Linux_x64_V1.70/RISC-V\ Embedded\ GCC/bin/riscv-none-embed-gdb
diff --git a/examples/i2c_oled/i2c.h b/examples/i2c_oled/i2c.h
index 04b2e1903ff729473c1c123f9a7596f4532ee81c..92cfbdd58ec091024723f96c417a4a96860c0af7 100644
--- a/examples/i2c_oled/i2c.h
+++ b/examples/i2c_oled/i2c.h
@@ -6,12 +6,29 @@
 #ifndef _I2C_H
 #define _I2C_H
 
-// I2C clock rate
-#define I2C_CLKRATE 100000
+// I2C Bus clock rate - must be lower the Logic clock rate
+#define I2C_CLKRATE 1000000
+
+// I2C Logic clock rate - must be higher than Bus clock rate
+#define I2C_PRERATE 2000000
+
+// uncomment this for high-speed 36% duty cycle, otherwise 33%
+#define I2C_DUTY
 
 // I2C Timeout count
 #define TIMEOUT_MAX 100000
 
+// uncomment this to enable IRQ-driven operation
+#define I2C_IRQ
+
+#ifdef I2C_IRQ
+// some stuff that IRQ mode needs
+volatile uint8_t i2c_send_buffer[64], *i2c_send_ptr, i2c_send_sz, i2c_irq_state;
+
+// uncomment this to enable time diags in IRQ
+//#define IRQ_DIAG
+#endif
+
 /*
  * init just I2C
  */
@@ -26,7 +43,7 @@ void i2c_setup(void)
 	// set freq
 	tempreg = I2C1->CTLR2;
 	tempreg &= ~I2C_CTLR2_FREQ;
-	tempreg |= (APB_CLOCK/1000000)&I2C_CTLR2_FREQ;
+	tempreg |= (APB_CLOCK/I2C_PRERATE)&I2C_CTLR2_FREQ;
 	I2C1->CTLR2 = tempreg;
 	
 	// Set clock config
@@ -35,9 +52,26 @@ void i2c_setup(void)
 	// standard mode good to 100kHz
 	tempreg = (APB_CLOCK/(2*I2C_CLKRATE))&I2C_CKCFGR_CCR;
 #else
-	// fast mode not yet handled here
+	// fast mode over 100kHz
+#ifndef I2C_DUTY
+	// 33% duty cycle
+	tempreg = (APB_CLOCK/(3*I2C_CLKRATE))&I2C_CKCFGR_CCR;
+#else
+	// 36% duty cycle
+	tempreg = (APB_CLOCK/(25*I2C_CLKRATE))&I2C_CKCFGR_CCR;
+	tempreg |= I2C_CKCFGR_DUTY;
+#endif
+	tempreg |= I2C_CKCFGR_FS;
 #endif
 	I2C1->CKCFGR = tempreg;
+
+#ifdef I2C_IRQ
+	// enable IRQ driven operation
+	NVIC_EnableIRQ(I2C1_EV_IRQn);
+	
+	// initialize the state
+	i2c_irq_state = 0;
+#endif
 	
 	// Enable I2C
 	I2C1->CTLR1 |= I2C_CTLR1_PE;
@@ -63,6 +97,16 @@ void i2c_init(void)
 					GPIO_CFGLR_CNF2_1 | GPIO_CFGLR_CNF2_0 |
 					GPIO_CFGLR_MODE2_1 | GPIO_CFGLR_MODE2_0;
 	
+#ifdef IRQ_DIAG
+	// GPIO diags on PC3/PC4
+	GPIOC->CFGLR &= ~(0xf<<(4*3));
+	GPIOC->CFGLR |= (GPIO_Speed_10MHz | GPIO_CNF_OUT_PP)<<(4*3);
+	GPIOC->BSHR = (1<<(16+3));
+	GPIOC->CFGLR &= ~(0xf<<(4*4));
+	GPIOC->CFGLR |= (GPIO_Speed_10MHz | GPIO_CNF_OUT_PP)<<(4*4);
+	GPIOC->BSHR = (1<<(16+4));
+#endif
+
 	// load I2C regs
 	i2c_setup();
 }
@@ -108,8 +152,118 @@ uint8_t i2c_chk_evt(uint32_t event_mask)
 	return (status & event_mask) == event_mask;
 }
 
+#ifdef I2C_IRQ
 /*
- * packet send
+ * packet send for IRQ-driven operation
+ */
+uint8_t i2c_send(uint8_t addr, uint8_t *data, uint8_t sz)
+{
+	int32_t timeout;
+	
+#ifdef IRQ_DIAG
+	GPIOC->BSHR = (1<<(3));
+#endif
+	
+	// error out if buffer under/overflow
+	if((sz > sizeof(i2c_send_buffer)) || !sz)
+		return 2;
+	
+	// wait for previous packet to finish
+	while(i2c_irq_state);
+	
+#ifdef IRQ_DIAG
+	GPIOC->BSHR = (1<<(16+3));
+	GPIOC->BSHR = (1<<(4));
+#endif
+	
+	// init buffer for sending
+	i2c_send_sz = sz;
+	i2c_send_ptr = i2c_send_buffer;
+	memcpy((uint8_t *)i2c_send_buffer, data, sz);
+	
+	// wait for not busy
+	timeout = TIMEOUT_MAX;
+	while((I2C1->STAR2 & I2C_STAR2_BUSY) && (timeout--));
+	if(timeout==-1)
+		return i2c_error(0);
+
+	// Set START condition
+	I2C1->CTLR1 |= I2C_CTLR1_START;
+
+	// wait for master mode select
+	timeout = TIMEOUT_MAX;
+	while((!i2c_chk_evt(I2C_EVENT_MASTER_MODE_SELECT)) && (timeout--));
+	if(timeout==-1)
+		return i2c_error(1);
+	
+	// send 7-bit address + write flag
+	I2C1->DATAR = addr<<1;
+
+	// wait for transmit condition
+	timeout = TIMEOUT_MAX;
+	while((!i2c_chk_evt(I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) && (timeout--));
+	if(timeout==-1)
+		return i2c_error(2);
+
+	// Enable TXE interrupt
+	I2C1->CTLR2 |= I2C_CTLR2_ITBUFEN | I2C_CTLR2_ITEVTEN;
+	i2c_irq_state = 1;
+
+#ifdef IRQ_DIAG
+	GPIOC->BSHR = (1<<(16+4));
+#endif
+	
+	// exit
+	return 0;
+}
+
+/*
+ * IRQ handler for I2C events
+ */
+void I2C1_EV_IRQHandler(void) __attribute__((interrupt));
+void I2C1_EV_IRQHandler(void)
+{
+	uint16_t STAR1, STAR2 __attribute__((unused));
+	
+#ifdef IRQ_DIAG
+	GPIOC->BSHR = (1<<(4));
+#endif
+
+	// read status, clear any events
+	STAR1 = I2C1->STAR1;
+	STAR2 = I2C1->STAR2;
+	
+	/* check for TXE */
+	if(STAR1 & I2C_STAR1_TXE)
+	{
+		/* check for remaining data */
+		if(i2c_send_sz--)
+			I2C1->DATAR = *i2c_send_ptr++;
+
+		/* was that the last byte? */
+		if(!i2c_send_sz)
+		{
+			// disable TXE interrupt
+			I2C1->CTLR2 &= ~(I2C_CTLR2_ITBUFEN | I2C_CTLR2_ITEVTEN);
+			
+			// reset IRQ state
+			i2c_irq_state = 0;
+			
+			// wait for tx complete
+			while(!i2c_chk_evt(I2C_EVENT_MASTER_BYTE_TRANSMITTED));
+
+			// set STOP condition
+			I2C1->CTLR1 |= I2C_CTLR1_STOP;
+		}
+	}
+
+#ifdef IRQ_DIAG
+	GPIOC->BSHR = (1<<(16+4));
+#endif
+}
+#else
+/*
+ * packet send for polled operation
  */
 uint8_t i2c_send(uint8_t addr, uint8_t *data, uint8_t sz)
 {
@@ -164,6 +318,6 @@ uint8_t i2c_send(uint8_t addr, uint8_t *data, uint8_t sz)
 	// we're happy
 	return 0;
 }
-
+#endif
 
 #endif