diff --git a/components/badge23/audio.c b/components/badge23/audio.c
index c94a650423388303f553782992e2d2c60499653f..ef37031ff2421a6b83c8142881e34fed426db401 100644
--- a/components/badge23/audio.c
+++ b/components/badge23/audio.c
@@ -1,10 +1,9 @@
 #include "badge23/audio.h"
-#include "badge23/lock.h"
 
 #include "st3m_scope.h"
 
 #include "driver/i2s.h"
-#include "driver/i2c.h"
+#include "flow3r_bsp_i2c.h"
 
 #include <freertos/FreeRTOS.h>
 #include <freertos/task.h>
@@ -15,8 +14,6 @@
 
 #define TIMEOUT_MS 1000
 
-#define I2C_MASTER_NUM 0 /*!< I2C master i2c port number, the number of i2c peripheral interfaces available will depend on the chip */
-
 static void audio_player_task(void* arg);
 
 #define DMA_BUFFER_SIZE 64
@@ -74,29 +71,27 @@ static uint8_t max98091_i2c_read(const uint8_t reg)
 {
     const uint8_t tx[] = {reg};
     uint8_t rx[1];
-    xSemaphoreTake(mutex_i2c, portMAX_DELAY);
-    esp_err_t ret = i2c_master_write_read_device(I2C_MASTER_NUM, 0x10, tx, sizeof(tx), rx, sizeof(rx), TIMEOUT_MS / portTICK_PERIOD_MS);
-    xSemaphoreGive(mutex_i2c);
+    flow3r_bsp_i2c_write_read_device(flow3r_i2c_addresses.codec, tx, sizeof(tx), rx, sizeof(rx), TIMEOUT_MS / portTICK_PERIOD_MS);
+    // TODO(q3k): handle error
     return rx[0];
 }
 
 static esp_err_t max98091_i2c_write(const uint8_t reg, const uint8_t data)
 {
     const uint8_t tx[] = {reg, data};
-    xSemaphoreTake(mutex_i2c, portMAX_DELAY);
-    esp_err_t ret = i2c_master_write_to_device(I2C_MASTER_NUM, 0x10, tx, sizeof(tx), TIMEOUT_MS / portTICK_PERIOD_MS);
-    xSemaphoreGive(mutex_i2c);
-    return ret;
+    return flow3r_bsp_i2c_write_to_device(flow3r_i2c_addresses.codec, tx, sizeof(tx), TIMEOUT_MS / portTICK_PERIOD_MS);
 }
 
 static esp_err_t max98091_i2c_write_readback(const uint8_t reg, const uint8_t data)
 {
-    const uint8_t tx[] = {reg, data};
-    xSemaphoreTake(mutex_i2c, portMAX_DELAY);
-    esp_err_t ret = i2c_master_write_to_device(I2C_MASTER_NUM, 0x10, tx, sizeof(tx), TIMEOUT_MS / portTICK_PERIOD_MS);
-    xSemaphoreGive(mutex_i2c);
-    if(max98091_i2c_read(reg) != data) printf("readback of %04X to %02X write failed\n", data, reg);
-    return ret;
+    esp_err_t ret = max98091_i2c_write(reg, data);
+    if (ret != ESP_OK) {
+        return ret;
+    }
+    if (max98091_i2c_read(reg) != data) {
+        printf("readback of %04X to %02X write failed\n", data, reg);
+    }
+    return ESP_OK;
 }
 
 void audio_codec_i2c_write(const uint8_t reg, const uint8_t data) {
diff --git a/components/badge23/captouch.c b/components/badge23/captouch.c
index 93130ccf28c52032fc7ba055d0d67d08396a85d8..ff2c7b2e15ca85271fbc5094105db4100ea7d3ce 100644
--- a/components/badge23/captouch.c
+++ b/components/badge23/captouch.c
@@ -1,11 +1,10 @@
 //#include <stdio.h>
 //#include <string.h>
 #include "esp_log.h"
-#include "driver/i2c.h"
 #include <stdint.h>
 #include <freertos/FreeRTOS.h>
 #include <freertos/atomic.h>
-#include "badge23/lock.h"
+#include "flow3r_bsp_i2c.h"
 
 #define PETAL_PAD_TIP 0
 #define PETAL_PAD_CCW 1
@@ -38,33 +37,24 @@ static const uint8_t bot_segment_map[] = {3,0,3,0,3,0,0,3,0,3}; //idk
 static const uint8_t bot_stage_config[] = {0,1,2,3,4,5,6,7,8,9,10,11};
 #define DEFAULT_THRES_TOP 2000
 #define DEFAULT_THRES_BOT 12000
-#define AD7147_ADDR_TOP            0b101101
-#define AD7147_ADDR_BOT            0b101100
 
 #else
 #error "captouch not implemented for this badge generation"
 #endif
 
 #if defined(CONFIG_BADGE23_HW_GEN_P4)
-#define AD7147_ADDR_TOP            0b101100
-#define AD7147_ADDR_BOT            0b101101
 static const uint8_t top_segment_map[] = {1,3,2,2,3,1,1,3,2,1,3,2}; //PETAL_PAD_*
 static const uint8_t bot_segment_map[] = {3,0,3,0,0,0,3,0,3,1,2,3}; //PETAL_PAD_*
 #elif defined(CONFIG_BADGE23_HW_GEN_P6)
-#define AD7147_ADDR_TOP            0b101100
-#define AD7147_ADDR_BOT            0b101101
 static const uint8_t top_segment_map[] = {1,3,2,2,3,1,1,3,2,1,3,2}; //PETAL_PAD_*
 static const uint8_t bot_segment_map[] = {3,0,3,0,0,0,3,0,3,1,2,3}; //PETAL_PAD_*
 #elif defined(CONFIG_BADGE23_HW_GEN_P3)
-#define AD7147_ADDR_TOP            0b101101
-#define AD7147_ADDR_BOT            0b101100
 static const uint8_t top_segment_map[] = {0,1,2, 2,1,0, 0,1,2, 2,1,0}; //PETAL_PAD_*
 static const uint8_t bot_segment_map[] = {3,0,3,0,0,0,3,0,3, 0,2,1}; //PETAL_PAD_*
 #endif
 
 static const char *TAG = "captouch";
 
-#define I2C_MASTER_NUM              0                          /*!< I2C master i2c port number, the number of i2c peripheral interfaces available will depend on the chip */
 
 #define AD7147_REG_PWR_CONTROL              0x00
 #define AD7147_REG_STAGE_CAL_EN             0x01
@@ -89,7 +79,7 @@ typedef struct{
 static petal_t petals[10];
 
 struct ad714x_chip {
-    uint8_t addr;
+    flow3r_i2c_address *addr;
     uint8_t gpio;
     int pos_afe_offsets[13];
     int neg_afe_offsets[13];
@@ -97,29 +87,32 @@ struct ad714x_chip {
     int stages;
 };
 
-static struct ad714x_chip chip_top_rev5 = {.addr = AD7147_ADDR_TOP, .gpio = 15,
+static struct ad714x_chip chip_top_rev5 = {
+    .addr = &flow3r_i2c_addresses.touch_top,
+    .gpio = 15,
     .pos_afe_offsets = {4, 2, 2, 2, 2, 3, 4, 2, 2, 2, 2, 0},
-    .stages=top_stages};
-static struct ad714x_chip chip_bot_rev5 = {.addr = AD7147_ADDR_BOT, .gpio = 15,
+    .stages = top_stages,
+};
+
+static struct ad714x_chip chip_bot_rev5 = {
+    .addr = &flow3r_i2c_addresses.touch_bottom,
+    .gpio = 15,
     .pos_afe_offsets = {3, 2, 1, 1 ,1, 1, 1, 1, 2, 3, 3, 3},
-    .stages=bot_stages};
+    .stages = bot_stages,
+};
 
 static esp_err_t ad714x_i2c_write(const struct ad714x_chip *chip, const uint16_t reg, const uint16_t data)
 {
     const uint8_t tx[] = {reg >> 8, reg & 0xFF, data >> 8, data & 0xFF};
-    xSemaphoreTake(mutex_i2c, portMAX_DELAY);
     ESP_LOGI(TAG, "AD7147 write reg %X-> %X", reg, data);
-    xSemaphoreGive(mutex_i2c);
-    return i2c_master_write_to_device(I2C_MASTER_NUM, chip->addr, tx, sizeof(tx), TIMEOUT_MS / portTICK_PERIOD_MS);
+    return flow3r_bsp_i2c_write_to_device(*chip->addr, tx, sizeof(tx), TIMEOUT_MS / portTICK_PERIOD_MS);
 }
 
 static esp_err_t ad714x_i2c_read(const struct ad714x_chip *chip, const uint16_t reg, uint16_t *data, const size_t len)
 {
     const uint8_t tx[] = {reg >> 8, reg & 0xFF};
     uint8_t rx[len * 2];
-    xSemaphoreTake(mutex_i2c, portMAX_DELAY);
-    esp_err_t ret = i2c_master_write_read_device(I2C_MASTER_NUM, chip->addr, tx, sizeof(tx), rx, sizeof(rx), TIMEOUT_MS / portTICK_PERIOD_MS);
-    xSemaphoreGive(mutex_i2c);
+    esp_err_t ret = flow3r_bsp_i2c_write_read_device(*chip->addr, tx, sizeof(tx), rx, sizeof(rx), TIMEOUT_MS / portTICK_PERIOD_MS);
     for(int i = 0; i < len; i++) {
         data[i] = (rx[i * 2] << 8) | rx[i * 2 + 1];
     }
diff --git a/components/badge23/espan.c b/components/badge23/espan.c
index a261ca19b8307b04d1c17f7b02cb6bf44feebf08..0929b40b34c425e159417db8fe1a6c15bef65aa4 100644
--- a/components/badge23/espan.c
+++ b/components/badge23/espan.c
@@ -79,7 +79,6 @@ static void io_slow_task(void * data){
 }
 
 void locks_init(){
-    mutex_i2c = xSemaphoreCreateMutex();
     mutex_LED = xSemaphoreCreateMutex();
 }
 
diff --git a/components/badge23/include/badge23/lock.h b/components/badge23/include/badge23/lock.h
index 4e597f675651f207f26b11c4b883668957dd8c64..5f11a434df4a9ba4ed16abb708a5b92c13f051ee 100644
--- a/components/badge23/include/badge23/lock.h
+++ b/components/badge23/include/badge23/lock.h
@@ -3,5 +3,4 @@
 #include <freertos/FreeRTOS.h>
 #include <freertos/semphr.h>
 
-SemaphoreHandle_t mutex_i2c;
 SemaphoreHandle_t mutex_LED;
diff --git a/components/badge23/spio.c b/components/badge23/spio.c
index f57c80c865801a869a3fb6c9524356b8fc043943..1f1ecc040e8f7d4c851a1e60c9773495805d2144 100644
--- a/components/badge23/spio.c
+++ b/components/badge23/spio.c
@@ -5,8 +5,9 @@
 #include "badge23/lock.h"
 #include "badge23/audio.h"
 
+#include "flow3r_bsp_i2c.h"
+
 #include "driver/i2c.h"
-#define I2C_MASTER_NUM 0
 #define TIMEOUT_MS 1000
 
 #if defined(CONFIG_BADGE23_HW_GEN_P1)
@@ -77,7 +78,7 @@ static uint8_t badge_link_enabled = 0;
  * if there's an outside shunt.
  */
 typedef struct{
-    uint8_t address;
+    flow3r_i2c_address *addr;
     uint8_t is_output_pin;  // mask for pins we wish to use as outputs
     uint8_t read_state;     //
     uint8_t output_state;   // goal output state
@@ -89,10 +90,8 @@ void _max7321_update(max7321_t *max){
     uint8_t rx = 0;
     uint8_t tx = (~(max->is_output_pin)) | max->output_state;
 
-    xSemaphoreTake(mutex_i2c, portMAX_DELAY);
-    esp_err_t tx_error = i2c_master_write_to_device(I2C_MASTER_NUM, max->address, &tx, sizeof(tx), TIMEOUT_MS / portTICK_PERIOD_MS);
-    esp_err_t rx_error = i2c_master_read_from_device(I2C_MASTER_NUM, max->address, &rx, sizeof(rx), TIMEOUT_MS / portTICK_PERIOD_MS);
-    xSemaphoreGive(mutex_i2c);
+	flow3r_bsp_i2c_write_read_device(*max->addr, &tx, sizeof(tx), &rx, sizeof(rx), TIMEOUT_MS / portTICK_PERIOD_MS);
+    // TODO(q3k): handle error
     max->read_state = rx;
 }
 
@@ -200,8 +199,8 @@ void update_button_state(){
 
 #elif defined(CONFIG_BADGE23_HW_GEN_P3) || defined(CONFIG_BADGE23_HW_GEN_P4)
 
-max7321_t port_expanders[] = {  {0b01101110, 0, 255, 255}, 
-                                {0b01101101, 0, 255, 255}  };
+max7321_t port_expanders[] = {  {&flow3r_i2c_addresses.portexp[0], 0, 255, 255}, 
+                                {&flow3r_i2c_addresses.portexp[1], 0, 255, 255}  };
 
 static void _init_buttons(){
     //configure all buttons as pullup
@@ -257,8 +256,8 @@ void update_button_state(){
     leftbutton = new_leftbutton;
 }
 #elif defined(CONFIG_BADGE23_HW_GEN_P6)
-max7321_t port_expanders[] = {  {0b01101110, 0, 255, 255}, 
-                                {0b01101101, 0, 255, 255}  };
+max7321_t port_expanders[] = {  {&flow3r_i2c_addresses.portexp[0], 0, 255, 255}, 
+                                {&flow3r_i2c_addresses.portexp[1], 0, 255, 255}  };
 
 static void _init_buttons(){
     //configure all buttons as pullup
diff --git a/components/flow3r_bsp/CMakeLists.txt b/components/flow3r_bsp/CMakeLists.txt
index a15017d094bf85df30f677765b6d4235562999a0..fe5121c1321c8bd4130c5bdb7a6a8a6894dde08b 100644
--- a/components/flow3r_bsp/CMakeLists.txt
+++ b/components/flow3r_bsp/CMakeLists.txt
@@ -3,6 +3,7 @@ idf_component_register(
 		flow3r_bsp_display.c
 		flow3r_bsp_gc9a01.c
 		flow3r_bsp_hwconfig.c
+		flow3r_bsp_i2c.c
     INCLUDE_DIRS
 		.
 )
diff --git a/components/flow3r_bsp/flow3r_bsp.h b/components/flow3r_bsp/flow3r_bsp.h
index 58ff3056c2be94c85eb104faf540899b8079f5bb..a1dceb4702422f257d7cc494631be3327fc6d25a 100644
--- a/components/flow3r_bsp/flow3r_bsp.h
+++ b/components/flow3r_bsp/flow3r_bsp.h
@@ -1,5 +1,7 @@
 #pragma once
 
+#include "flow3r_bsp_i2c.h"
+
 #include <stdint.h>
 
 // Initialize badge display. An error will be reported if the initialization
@@ -32,4 +34,4 @@ void flow3r_bsp_display_set_backlight(uint8_t percent);
 #define FLOW3R_BSP_DISPLAY_HEIGHT 240
 
 // Badge hardware generation name, human-readable.
-const char *flow3r_bsp_hw_name;
\ No newline at end of file
+const char *flow3r_bsp_hw_name;
diff --git a/components/flow3r_bsp/flow3r_bsp_i2c.c b/components/flow3r_bsp/flow3r_bsp_i2c.c
new file mode 100644
index 0000000000000000000000000000000000000000..f1695b81ec086f5953a8a0bc349f0113b7b53471
--- /dev/null
+++ b/components/flow3r_bsp/flow3r_bsp_i2c.c
@@ -0,0 +1,99 @@
+#include "flow3r_bsp_i2c.h"
+
+#include "driver/i2c.h"
+#include "freertos/FreeRTOS.h"
+#include "freertos/semphr.h"
+
+static SemaphoreHandle_t mutex;
+
+static const char *TAG = "flow3r-bsp-i2c";
+
+#if defined(CONFIG_BADGE23_HW_GEN_P1)
+const flow3r_i2c_addressdef flow3r_i2c_addresses = {
+	.codec = 0, // p1 has no i2c control channel to codec.
+	.touch_top = 0x2d,
+	.touch_bottom = 0x2c,
+	.portexp = { 0x6e, 0x6d },
+};
+#elif defined(CONFIG_BADGE23_HW_GEN_P3)
+const flow3r_i2c_addressdef flow3r_i2c_addresses = {
+	.codec = 0x10,
+	.touch_top = 0x2d,
+	.touch_bottom = 0x2c,
+	.portexp = { 0x6e, 0x6d },
+};
+#elif defined(CONFIG_BADGE23_HW_GEN_P4)
+const flow3r_i2c_addressdef flow3r_i2c_addresses = {
+	.codec = 0x10,
+	.touch_top = 0x2c,
+	.touch_bottom = 0x2d,
+	.portexp = { 0x6e, 0x6d },
+};
+#elif defined(CONFIG_BADGE23_HW_GEN_P6)
+const flow3r_i2c_addressdef flow3r_i2c_addresses = {
+	.codec = 0x10,
+	.touch_top = 0x2c,
+	.touch_bottom = 0x2d,
+	.portexp = { 0x6e, 0x6d },
+};
+#else
+#error "i2c not implemented for this badge generation"
+#endif
+
+#if defined(CONFIG_BADGE23_HW_GEN_P3) || defined(CONFIG_BADGE23_HW_GEN_P4) || defined(CONFIG_BADGE23_HW_GEN_P6)
+static i2c_config_t i2c_conf = {
+    .mode = I2C_MODE_MASTER,
+    .sda_io_num = 2,
+    .scl_io_num = 1,
+    .sda_pullup_en = GPIO_PULLUP_ENABLE,
+    .scl_pullup_en = GPIO_PULLUP_ENABLE,
+    .master.clk_speed = 400000,
+};
+#elif defined(CONFIG_BADGE23_HW_GEN_P1)
+static i2c_config_t i2c_conf = {
+    .mode = I2C_MODE_MASTER,
+    .sda_io_num = 10,
+    .scl_io_num = 9,
+    .sda_pullup_en = GPIO_PULLUP_ENABLE,
+    .scl_pullup_en = GPIO_PULLUP_ENABLE,
+    .master.clk_speed = 400000,
+};
+#else
+#error "i2c not implemented for this badge generation"
+#endif
+
+void flow3r_bsp_i2c_init(void) {
+	if (mutex != NULL) {
+		return;
+	}
+
+	mutex = xSemaphoreCreateMutex();
+	assert(mutex != NULL);
+
+    assert(i2c_param_config(0, &i2c_conf) == ESP_OK);
+	assert(i2c_driver_install(0, i2c_conf.mode, 0, 0, 0) == ESP_OK);
+}
+
+// Take I2C bus lock.
+static void flow3r_bsp_i2c_get(void) {
+    xSemaphoreTake(mutex, portMAX_DELAY);
+}
+
+// Release I2C bus lock.
+static void flow3r_bsp_i2c_put(void) {
+    xSemaphoreGive(mutex);
+}
+
+esp_err_t flow3r_bsp_i2c_write_to_device(uint8_t address, const uint8_t *buffer, size_t write_size, TickType_t ticks_to_wait) {
+	flow3r_bsp_i2c_get();
+	esp_err_t res = i2c_master_write_to_device(0, address, buffer, write_size, ticks_to_wait);
+	flow3r_bsp_i2c_put();
+	return res;
+}
+
+esp_err_t flow3r_bsp_i2c_write_read_device(uint8_t address, const uint8_t *write_buffer, size_t write_size, uint8_t *read_buffer, size_t read_size, TickType_t ticks_to_wait) {
+	flow3r_bsp_i2c_get();
+	esp_err_t res = i2c_master_write_read_device(0, address, write_buffer, write_size, read_buffer, read_size, ticks_to_wait);
+	flow3r_bsp_i2c_put();
+	return res;
+}
\ No newline at end of file
diff --git a/components/flow3r_bsp/flow3r_bsp_i2c.h b/components/flow3r_bsp/flow3r_bsp_i2c.h
new file mode 100644
index 0000000000000000000000000000000000000000..f54ec6428e5cf89b267b8a2e8534015770e769ee
--- /dev/null
+++ b/components/flow3r_bsp/flow3r_bsp_i2c.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include "freertos/FreeRTOS.h"
+
+// Type definition for device addresses on the main I2C bus of flow3r badges.
+// This is a 7-bit address, which does not contain an R/W bit.
+typedef uint8_t flow3r_i2c_address;
+
+// I2C addresses of different devices on a flow3r badge. Not all fields are
+// applicable to all badge generations.
+typedef struct {
+	// Address of the audio codec.
+	flow3r_i2c_address codec;
+	// Address of the captouch controller on the top board.
+	flow3r_i2c_address touch_top;
+	// Address of the captouch controller on the bottom board.
+	flow3r_i2c_address touch_bottom;
+	// Addresses of the port expander chips.
+	flow3r_i2c_address portexp[2];
+} flow3r_i2c_addressdef;
+
+// Global constant defining addresses for the badge generation that this
+// firmware has been built for.
+extern const flow3r_i2c_addressdef flow3r_i2c_addresses;
+
+// Initialize the badge's I2C subsystem. Must be called before any other
+// flow3r_bsp_i2c_* calls.
+void flow3r_bsp_i2c_init(void);
+
+// Perform a write transaction to a I2C device.
+//
+// This can be called concurrently from different tassks.
+esp_err_t flow3r_bsp_i2c_write_to_device(flow3r_i2c_address address, const uint8_t *buffer, size_t write_size, TickType_t ticks_to_wait);
+
+// Perform a write-then read transaction on an I2C device.
+//
+// This can be called concurrently from different tassks.
+esp_err_t flow3r_bsp_i2c_write_read_device(flow3r_i2c_address address, const uint8_t *write_buffer, size_t write_size, uint8_t *read_buffer, size_t read_size, TickType_t ticks_to_wait);
\ No newline at end of file
diff --git a/components/st3m/st3m_board_startup.c b/components/st3m/st3m_board_startup.c
index 54f55de76ddac4b654231b3f1db31e4c9f389b4b..256d6966d45bc47d40bf73fef7282fd8a0393f4f 100644
--- a/components/st3m/st3m_board_startup.c
+++ b/components/st3m/st3m_board_startup.c
@@ -17,8 +17,9 @@ void st3m_board_startup(void) {
     }
     flow3r_bsp_display_set_backlight(100);
 
+    flow3r_bsp_i2c_init();
     st3m_fs_init();
 
-	// Handoff to badge23.
-	badge23_main();
+    // Handoff to badge23.
+    badge23_main();
 }
\ No newline at end of file